added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
38
bundles/org.openhab.binding.loxone/.classpath
Normal file
38
bundles/org.openhab.binding.loxone/.classpath
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.loxone/.project
Normal file
23
bundles/org.openhab.binding.loxone/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.loxone</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
20
bundles/org.openhab.binding.loxone/NOTICE
Normal file
20
bundles/org.openhab.binding.loxone/NOTICE
Normal file
@@ -0,0 +1,20 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
|
||||
== Third-party Content
|
||||
|
||||
harmony-client
|
||||
* License: EPL 2.0 License
|
||||
* Project: https://github.com/digitaldan/harmony-client
|
||||
* Source: https://github.com/digitaldan/harmony-client
|
||||
317
bundles/org.openhab.binding.loxone/README.md
Normal file
317
bundles/org.openhab.binding.loxone/README.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Loxone Binding
|
||||
|
||||
This binding integrates [Loxone Miniserver](https://www.loxone.com/enen/products/miniserver-extensions/) with [openHAB](https://www.openhab.org/).
|
||||
Miniserver is represented as a [Thing](https://www.openhab.org/docs/configuration/things.html). Miniserver controls, that are visible in the Loxone [UI](https://www.loxone.com/enen/kb/user-interface-configuration/), are exposed as openHAB channels.
|
||||
|
||||
## Features
|
||||
|
||||
The following features are currently supported:
|
||||
|
||||
* [Discovery](https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol) of Miniservers available on the local network
|
||||
* Creation of channels for Loxone controls that are exposed in the Loxone [UI](https://www.loxone.com/enen/kb/user-interface-configuration/)
|
||||
* Tagging of channels and [items](https://www.openhab.org/docs/configuration/items.html) with tags that can be recognized by [Alexa](https://en.wikipedia.org/wiki/Amazon_Alexa) openHAB [skill](https://www.amazon.com/openHAB-Foundation/dp/B01MTY7Z5L), so voice can be used to command Loxone controls
|
||||
* Management of a Websocket connection to the Miniserver and updating Thing status accordingly
|
||||
* Updates of openHAB channel's state in runtime according to control's state changes on the Miniserver
|
||||
* Passing channel commands to the Miniserver's controls
|
||||
* Hash-based and token-based authentication methods
|
||||
* Command encryption and response decryption
|
||||
|
||||
## Things
|
||||
|
||||
This binding supports [Loxone Miniservers](https://www.loxone.com/enen/products/miniserver-extensions/) for accessing controls that are configured in their UI.
|
||||
|
||||
The Thing UID of automatically discovered Miniservers is: `loxone:miniserver:<serial>`, where `<serial>` is a serial number of the Miniserver (effectively this is the MAC address of its network interface).
|
||||
|
||||
### Discovery
|
||||
|
||||
[Loxone Miniservers](https://www.loxone.com/enen/products/miniserver-extensions/) are automatically discovered by the binding and put in the Inbox. [Discovery](https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol) is performed using [UPnP](https://en.wikipedia.org/wiki/Universal_Plug_and_Play) protocol.
|
||||
|
||||
Before a Miniserver Thing can go online, it must be configured with a user name and a password of an account available on the Miniserver.
|
||||
Please set them manually in Thing configuration after you add a new Miniserver Thing from your Inbox.
|
||||
|
||||
### Manual configuration
|
||||
|
||||
As an alternative to the automatic discovery process, Miniservers can be configured manually, through an entry in [.things file](https://www.openhab.org/docs/configuration/things.html#defining-things-using-files).
|
||||
The entry should have the following syntax:
|
||||
|
||||
`loxone:miniserver:<thing-id> [ user="<user>", password="<password>", host="<host>", port=<port>, ... ]`
|
||||
|
||||
Where:
|
||||
|
||||
* `<thing-id>` is a unique ID for your Miniserver (you can but do not have to use Miniserver's MAC address here)
|
||||
* `<user>` and `<password>` are the credentials used to log into the Miniserver
|
||||
* `<host>` is a host name or IP of the Miniserver
|
||||
* `<port>` is a port of web services on the Miniserver (please notice that port, as a number, is not surrounded by quotation marks, while the other values described above are)
|
||||
* `...` are optional advanced parameters - please refer to _Advanced parameters_ section at the end of this instruction for a list of available options
|
||||
|
||||
Example 1 - minimal required configuration:
|
||||
|
||||
`loxone:miniserver:504F2414780F [ user="kryten", password="jmc2017", host="loxone.local", port=80 ]`
|
||||
|
||||
Example 2 - additionally keep alive period is set to 2 minutes and Websocket maximum binary message size to 8MB:
|
||||
|
||||
`loxone:miniserver:504F2414780F [ user="kryten", password="jmc2017", host="192.168.0.210", port=80, keepAlivePeriod=120, maxBinMsgSize=8192 ]`
|
||||
|
||||
### Thing Offline Reasons
|
||||
|
||||
There can be following reasons why Miniserver status is `OFFLINE`:
|
||||
|
||||
* __Configuration Error__
|
||||
* _Unknown host_
|
||||
* Miniserver host/ip address can't be resolved. No connection attempt will be made.
|
||||
* _User authentication error_
|
||||
* Invalid user name or password or user not authorized to connect to the Miniserver. Binding will make another attempt to connect after some time.
|
||||
* _Too many failed login attempts - stopped trying_
|
||||
* Miniserver locked out user for too many failed login attempts. In this case binding will stop trying to connect to the Miniserver. A new connection will be attempted only when user corrects user name or password in the configuration parameters.
|
||||
* _Enter password to generate a new token_
|
||||
* Authentication using stored token failed - either token is wrong or it. A password must be reentered in the binding settings to acquire a new token.
|
||||
* _Internal error_
|
||||
* Probably a code defect, collect debug data and submit an issue. Binding will try to reconnect, but with unknown chance for success.
|
||||
* _Other_
|
||||
* An exception occured and its details will be displayed
|
||||
* __Communication Error__
|
||||
* _Error communicating with Miniserver_
|
||||
* I/O error occurred during established communication with the Miniserver, most likely due to network connectivity issues, Miniserver going offline or Loxone Config is uploading a new configuration. A reconnect attempt will be made soon. Please consult detailed message against one of the following:
|
||||
* _"Text message size &lsqbXX&rsqb exceeds maximum size &lsqbYY&rsqb"_ - adjust text message size in advanced parameters to be above XX value
|
||||
* _"Binary message size &lsqbXX&rsqb exceeds maximum size &lsqbYY&rsqb"_ - adjust binary message size in advanced parameters to be above XX value
|
||||
* _User authentication timeout_
|
||||
* Authentication procedure took too long time and Miniserver closed connection. It should not occur under normal conditions and may indicate performance issue on binding's OS side.
|
||||
* _Timeout due to no activity_
|
||||
* Miniserver closed connection because there was no activity from binding. It should not occur under normal conditions, as it is prevented by sending keep-alive messages from the binding to the Miniserver. By default Miniserver's timeout is 5 minutes and period between binding's keep-alive messages is 4 minutes. If you see this error, try changing the keep-alive period in binding's configuration to a smaller value.
|
||||
* _Other_
|
||||
* An exception occured and its details will be displayed
|
||||
|
||||
### Security
|
||||
|
||||
The binding supports the following authentication methods, which are selected automatically based on the firmware version. They can be also chosen manually in the advanced settings.
|
||||
|
||||
| Method | Miniserver Firmware | Authentication | Encryption | Requirements |
|
||||
|-------------|---------------------|--------------------------------------------------------------------------------|------------|-------------------------------------------------------|
|
||||
| Hash-based | 8.x | HMAC-SHA1 hash on user and password | None | None |
|
||||
| Token-based | 9.x | Token acquired on the first connection and used later instead of the password. | AES-256 | JRE must have unrestricted security policy configured |
|
||||
|
||||
For the token-based authentication, the password is required only for the first login and acquiring the token. After the token is acquired, the password is cleared in the binding configuration.
|
||||
|
||||
The acquired token will remain active for several weeks following the last succesful authentication with this token. If the connection is not established used during that period and the token expires, a user password has to be re-entered in the binding settings to acquire a new token.
|
||||
|
||||
In case a websocket connection to the Miniserver remains active for the whole duration of the token's life span, the binding will refresh the token one day before token expiration, without the need of providing the password.
|
||||
|
||||
|
||||
A method to enable unrestricted security policy depends on the JRE version and vendor, some examples can be found [here](https://www.petefreitag.com/item/844.cfm) and [here](https://stackoverflow.com/questions/41580489/how-to-install-unlimited-strength-jurisdiction-policy-files).
|
||||
|
||||
## Channels
|
||||
|
||||
This binding creates channels for controls that are [used in Loxone's user interface](https://www.loxone.com/enen/kb/user-interface-configuration/).
|
||||
Currently supported controls are presented in the table below.
|
||||
|
||||
| [Loxone API Control](https://www.loxone.com/enen/kb/api/) | Loxone Block-Functions | [Item Types](https://www.openhab.org/docs/concepts/items.html) | Supported Commands |
|
||||
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Alarm | [Burglar Alarm](https://www.loxone.com/enen/kb/burglar-alarm/) | `Switch` - arm the alarm | `OnOffType.*` |
|
||||
| | | `Switch` - arm with delay - pushbuton | `OnOffType.ON` - armes the alarm with delay |
|
||||
| | | `Number` - ID of the next alarm level | Read-only channel |
|
||||
| | | `Number` - delay of the next alarm level | Read-only channel |
|
||||
| | | `Number` - total delay of the next alarm level | Read-only channel |
|
||||
| | | `Number` - current alarm level | Read-only channel |
|
||||
| | | `DateTime` - time when alarm started | Read-only channel |
|
||||
| | | `Number` - delay of the alarm being armed | Read-only channel |
|
||||
| | | `Number` - total delay of the alarm being armed | Read-only channel |
|
||||
| | | `String` - list of alarm sensors separated with `|` | Read-only channel |
|
||||
| | | `Switch` - acknowledge the alarm - pushbutton | `OnOffType.ON` - acknowledge alarm |
|
||||
| ColorPickerV2 | [RGBW 24v Dimmer Tree](https://www.loxone.com/enen/kb/rgbw-24v-dimmer-tree/) | `Color` | `HSBType` - sets the color of the light, `DecimalType` and `PercentType` - sets the brightness, `IncreaseDecreaseType.*` - increases/decreases the brightness, `OnOffType.*` - switches light on/off |
|
||||
| Dimmer | [Dimmer](https://www.loxone.com/enen/kb/dimmer/) | `Dimmer` | `OnOffType.*`, `PercentType` |
|
||||
| InfoOnlyAnalog | Analog [virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) (virtual state) | `Number` | Read-only channel |
|
||||
| InfoOnlyDigital | Digital [virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) (virtual state) | `String` | Read-only channel |
|
||||
| IRoomControllerV2 | [Intelligent Room Controller V2](https://www.loxone.com/enen/kb/irc-v2/) | `Number` - active mode | Read-only channel |
|
||||
| | | `Number` - operating mode | `DecimalType` - operating mode |
|
||||
| | | `Number` - prepare state | Read-only channel |
|
||||
| | | `Switch` - open window | Read-only channel |
|
||||
| | | `Number` - current temperature | Read-only channel |
|
||||
| | | `Number` - target temperature | `DecimalType` |
|
||||
| | | `Number` - comfort temperature | `DecimalType` |
|
||||
| | | `Number` - comfort temperature offset | `DecimalType` |
|
||||
| | | `Number` - comfort tolerance | `DecimalType` |
|
||||
| | | `Number` - absent minimum temperature offset | `DecimalType` |
|
||||
| | | `Number` - absent maximum temperature offset | `DecimalType` |
|
||||
| | | `Number` - frost protect temperature | Read-only channel |
|
||||
| | | `Number` - heat protect temperature | Read-only channel |
|
||||
| Jalousie | Blinds, [Automatic Blinds](https://www.loxone.com/enen/kb/automatic-blinds/), Automatic Blinds Integrated | `Rollershutter` - main control element | `UpDownType.*`, `StopMoveType.*`, `PercentType` |
|
||||
| | | `Switch` - shading | `OnOffType.ON` - shade |
|
||||
| | | `Switch` - automatic shading | `OnOffType.*` - automatic shading enabled/disabled |
|
||||
| LeftRightAnalog | Analog [Virtual input](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of left-right buttons type | `Number` | `DecimalType` |
|
||||
| LeftRightDigital | Digital [Virtual input](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of left-right buttons type | `Switch` (left button) | `OnOffType.*` - left on/off, sets right to off |
|
||||
| | | `Switch` (right button) | `OnOffType.*` - right on/off, sets left to off |
|
||||
| LightController | [Lighting controller V1 (obsolete)](https://www.loxone.com/enen/kb/lighting-controller/), [Hotel lighting controller](https://www.loxone.com/enen/kb/hotel-lighting-controller/)<br>Additionally, for each configured output of a lighting controller, a new independent control (with own channel/item) will be created. | `Number` | `DecimalType` - select lighting scene, `UpDownType.*` - swipe through scenes, `OnOffType.*` - select all off or all on scene |
|
||||
| LightControllerV2 | [Lighting controller](https://www.loxone.com/enen/kb/lighting-controller-v2/)<br>Additionally, for each configured output and for each mood of a lighting controller, a new independent control (with own channel/item) will be created. | `Number` | `DecimalType` - select mood, `UpDownType.*` - swipe through moods |
|
||||
| LightControllerV2 Mood | A mood defined for a [Lighting controller](https://www.loxone.com/enen/kb/lighting-controller-v2/). Each mood will have own channel and can be operated independently in order to allow mixing of moods. | `Switch` | `OnOffType.*` - mixes mood in or out of the controller |
|
||||
| Meter | [Utility meter](https://www.loxone.com/enen/kb/utility-meter/) | `Number` | `DecimalType` - current meter value |
|
||||
| | | `Number` | `DecimalType` - total meter value |
|
||||
| Pushbutton | [Virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of pushbutton type | `Switch` | `OnOffType.ON` - generates Pulse command |
|
||||
| Radio | [Radio button 8x and 16x](https://www.loxone.com/enen/kb/radio-buttons/) | `Number` | `DecimalType` - select output number 1-8/16 or 0 for all outputs off, `OnOffType.OFF` - all outputs off |
|
||||
| Slider | [Virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of slider type | `Number` | `DecimalType` |
|
||||
| Switch | [Virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of switch type<br>[Push-button](https://www.loxone.com/enen/kb/push-button/) | `Switch` | `OnOffType.*` |
|
||||
| TextState | [State](https://www.loxone.com/enen/kb/state/) | `String` | Read-only channel |
|
||||
| TimedSwitch | [Stairwell light switch](https://www.loxone.com/enen/kb/stairwell-light-switch/) or [Multifunction switch](https://www.loxone.com/enen/kb/multifunction-switch/) | `Switch` | `OnOffType.*` - ON sends pulse to Loxone |
|
||||
| | | `Number` | Read-only countdown value to off |
|
||||
| Tracker | [Tracker](https://www.loxone.com/enen/kb/tracker/) | `String` | Read-only channel |
|
||||
| UpDownAnalog | Analog [Virtual input](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of up-down buttons type | `Number` | `DecimalType` |
|
||||
| UpDownDigital | Digital [Virtual input](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) of up-down buttons type | `Switch` - up button | `OnOffType.*` - up on/off, sets down to off |
|
||||
| | | `Switch` - down button | `OnOffType.*` - down on/off, sets up to off |
|
||||
| ValueSelector | [Selection Switch+](https://www.loxone.com/enen/kb/selection-switch-plus/), [Selection Switch+/-](https://www.loxone.com/enen/kb/selection-switch-plus-minus/) | `Dimmer` - selected value will be scaled to min/max | `OnOffType.*` - sets selector to max or min value, `PercentType` - sets selector to % of min-max range, `IncreaseDecreaseType` - adds or subtracts step value from the selector |
|
||||
| | | `Number` - direct input of selected value | `DecimalType` - must be between min-max range |
|
||||
| Webpage | [Web Page](https://www.loxone.com/enen/kb/webpage/) | `String` - low-res URL | Read-only channel |
|
||||
| | | `String` - high-res URL | Read-only channel |
|
||||
|
||||
If your control is supported, but binding does not recognize it, please check if it is exposed in Loxone UI using [Loxone Config](https://www.loxone.com/enen/kb-cat/loxone-config/) application.
|
||||
|
||||
Most controls have a single channel. Such channel ID is defined in the following way:
|
||||
|
||||
* `loxone:miniserver:<serial>:<control-UUID>`
|
||||
|
||||
Controls, which have more than one channel, define the channel ID of the extra channels in the following way:
|
||||
|
||||
* `loxone:miniserver:<serial>:<control-UUID>-<channel-index>`, where `<channel-index>` is equal to 1, 2, ...
|
||||
|
||||
Channel label is defined in the following way:
|
||||
|
||||
* For controls that belong to a room: `<Room name> / <Control name>`
|
||||
* For controls without a room: `<Control name>`
|
||||
|
||||
Channels have the default tags as follows:
|
||||
|
||||
* **Dimmer**: when it belongs to a category of _Lights_ type, the channel will be tagged with _Lighting_ tag.
|
||||
* **InfoOnlyAnalog**: when it belongs to a category of _Indoor Temperature_ type, it will be tagger with _CurrentTemperature_ tag.
|
||||
* **Jalousie**: main rollershutter channel will be tagged with _Blinds_ tag. Shade and automatic shade switch channels will be tagged with _Switchable_ tag.
|
||||
* **LightController (V1 and V2)**: main channel with selected scene will be tagged with _Scene_ tag.
|
||||
* **Switch**, **TimedSwitch** and **Pushbutton** controls: when it belongs to a category that is of a _Lights_ type, the channel will be tagged with _Lighting_ tag. Otherwise it will be tagged with _Switchable_ tag.
|
||||
|
||||
## Advanced Parameters
|
||||
|
||||
This section describes the optional advanced parameters that can be configured for a Miniserver. They can be set using UI (e.g. Paper UI) or in a .things file.
|
||||
If a parameter is not explicitly defined, binding will use its default value.
|
||||
|
||||
To define a parameter value in a .things file, please refer to it by parameter's ID, for example:
|
||||
|
||||
keepAlivePeriod=120
|
||||
|
||||
### Security
|
||||
|
||||
| ID | Name | Values | Default | Description |
|
||||
|--------------|-----------------------|-------------------------------------------------|--------------|-------------------------------------------------------|
|
||||
| `authMethod` | Authentication method | 0: Automatic<br>1: Hash-based<br>2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. |
|
||||
|
||||
### Timeouts
|
||||
|
||||
Timeout values control various parts of Websocket connection management.
|
||||
They can be tuned, when abnormal behavior of the binding is observed, which can be attributed to timing.
|
||||
<br>
|
||||
|
||||
| ID | Name | Range | Default | Description |
|
||||
|-------------------|-----------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `firstConDelay` | First connection delay | 0-120 s | 1 s | Time in seconds between binding initialization with all necessary parameters and first connection attempt. |
|
||||
| `keepAlivePeriod` | Period between connection keep-alive messages | 1-600 s | 240 s | Time in seconds between sending two consecutive keep-alive messages, in order to inform Miniserver about active connection and prevent it from disconnecting. Miniserver default connection timeout is 5 minutes, so default is set to 4 minutes. |
|
||||
| `connectErrDelay` | Connect error delay | 0-600 s | 10 s | Time in seconds between failed Websocket connect attempt and another attempt to connect. Websocket connection is established before authentication and data transfer. It can usually fail due to unreachable Miniserver. |
|
||||
| `responseTimeout` | Response timeout | 0-60 s | 4 s | Time to wait for a response from Miniserver to a request sent from the binding. A request can be any of: websocket connect request, credentials hashing key request, configuration request, enabling of state updates (until initial states are received). If this time passed without the expected reaction from the Miniserver, the connection will be closed. A new connection attempt may be made, depending on the situation. |
|
||||
| `userErrorDelay` | Authentication error delay | 0-3600 s | 60 s | Time in seconds between user authentication error and another connection attempt. User authentication error can be a result of a wrong name or password, or no authority granted to the user on the Miniserver. If this time is too short, Miniserver will eventually lock out the user for a longer period of time due to too many failed login attempts. This time should allow the administrator to fix the authentication issue without being locked out. Connection retry is required, because very rarely Miniserver seems to reject correct credentials, which are successful on a subsequent identical attempt. |
|
||||
| `comErrorDelay` | Communication error delay | 0-3600 s | 30 s | Time in seconds between an active connection closes, as a result of a communication error, and next connection attempt. This relates to all types of network communication issues, which can occur and cease to exist randomly to the binding. It is desired that the binding monitors the situation and brings things back to online as soon as Miniserver is accessible. |
|
||||
|
||||
### Sizes
|
||||
|
||||
| ID | Name | Range | Default | Description |
|
||||
|------------------|----------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `maxBinMsgSize` | Maximum binary message size (kB) | 0-100 MB | 3072 (3 MB) | For Websocket client, a maximum size of a binary message that can be received from the Miniserver. If you get communication errors with a message indicating there are too long binary messages received, you may need to adjust this parameter. |
|
||||
| `maxTextMsgSize` | Maximum text message size (kB) | 0-100 MB | 512 KB | For Websocket client, a maximum size of a text message that can be received from the Miniserver. If you get communication errors with a message indicating there are too long text messages received, you may need to adjust this parameter. |
|
||||
|
||||
## Limitations
|
||||
|
||||
* As there is no push button item type in openHAB, Loxone's push button is an openHAB's switch, which always generates a short pulse on changing its state to on. If you use simple UI mode and framework generates items for you, switches for push buttons will still be toggle switches. To change it to the push button style, you have to create item manually with `autoupdate=false` parameter. An example of such item definition is given in the _Items_ section above.
|
||||
|
||||
## Automatic Configuration Example
|
||||
|
||||
The simplest and quickest way of configuring a Loxone Miniserver with openHAB is to use automatic configuration features:
|
||||
|
||||
* Make sure your Miniserver is up and running and on the same network segment as openHAB server.
|
||||
* Add Loxone binding from the available `Add-ons`.
|
||||
* In `Configuration/System` page, set `Item Linking` to `Simple Mode` (don't forget to save your choice).
|
||||
* Add your Miniserver Thing from the `Inbox`, after automatic discovery is performed by the framework during binding initialization.
|
||||
* Configure your Miniserver by editing Miniserver Thing in `Configuration/Things` page and providing user name and password.
|
||||
* Miniserver Thing should go online. Channels and Items will be automatically created and configured.
|
||||
* On the `Control` page, you can test Miniserver Items and interact with them.
|
||||
* As the user interface, you may use [HABPanel](https://www.openhab.org/docs/configuration/habpanel.html), where all Miniserver's items are ready for picking up, using entirely the graphical user interface.
|
||||
|
||||
## Manual Configuration Example
|
||||
|
||||
A more advanced setup requires manual creation and editing of openHAB configuration files, according to the instructions provided in [configuration user guide](https://www.openhab.org/docs/configuration/).
|
||||
In this example we will manually configure:
|
||||
|
||||
* A Miniserver with serial number 504F2414780F, available at IP 192.168.0.220 and with web services port 80
|
||||
* A Miniserver's user named "kryten" and password "jmc2017"
|
||||
* Items for:
|
||||
* Temperature of the Miniserver - a Virtual Analog State functional block
|
||||
* State of a garage door - a Virtual Digital State funtional block (ON=door open, OFF=door closed)
|
||||
* Kitchen lights switch - a Switch Subcontrol at the AI1 output of a Lighting Controller functional block (with a tag recognizable by Alexa service)
|
||||
* Pushbutton to switch all lights off - a Virtual Input of Pushbutton type functional block (pushbutton realized by adding `autoupdate="false"` parameter)
|
||||
* Kitchen blinds - a Jalousie functional block
|
||||
* Lighting scene - a Lighting Controller functional block
|
||||
* Output valve selection for garden watering - 8x Radio Button functional block, where only one valve can be open at a time
|
||||
* A text displaying current alarm's state - a State functional block
|
||||
|
||||
### things/loxone.things:
|
||||
|
||||
```
|
||||
loxone:miniserver:504F2414780F [ user="kryten", password="jmc2017", host="192.168.0.220", port=80
|
||||
```
|
||||
|
||||
### items/loxone.items:
|
||||
|
||||
```
|
||||
// Type ID Label Icon Tags Settings
|
||||
|
||||
Number Miniserver_Temp "Miniserver temperature: [%.1f °C]" <temperature> {channel="loxone:miniserver:504F2414780F:0F2F2133-017D-3C82-FFFF203EB0C34B9E"}
|
||||
Switch Garage_Door "Garage door [MAP(garagedoor.map):%s]" <garagedoor> {channel="loxone:miniserver:504F2414780F:0F2F2133-017D-3C82-FFFF203EB0C34B9E"}
|
||||
Switch Kitchen_Lights "Kitchen Lights" <switch> ["lighting"] {channel="loxone:miniserver:504F2414780F:0EC5E0CF-0255-6ABD-FFFF402FB0C24B9E_AI1"}
|
||||
Switch Stair_Lights "Stair Lights" <switch> ["lighting"] {channel="loxone:miniserver:504F2414780F:0EC5E0CF-0255-31BD-FFFF402FB0C24B9E"}
|
||||
Number Stair_Lights-1 "Stair Lights Deactivation Delay" <clock> ["lighting"] {channel="loxone:miniserver:504F2414780F:0EC5E0CF-0255-31BD-FFFF402FB0C24B9E-1"}
|
||||
Switch Reset_Lights "Switch all lights off" <switch> ["lighting"] {channel="loxone:miniserver:504F2414780F:0F2F2133-01AD-3282-FFFF201EB0C24B9E",autoupdate="false"}
|
||||
Rollershutter Kitchen_Blinds "Kitchen blinds" <blinds> {channel="loxone:miniserver:504F2414780F:0F2E2123-014D-3232-FFEF204EB3C24B9E"}
|
||||
Dimmer Kitchen_Dimmer "Kitchen dimmer" <slider> ["lighting"] {channel="loxone:miniserver:504F2414780F:0F2E2123-014D-3232-FFEF207EB3C24B9E"}
|
||||
Number Light_Scene "Lighting scene" <light> {channel="loxone:miniserver:504F2414780F:0FC4E0DF-0255-6ABD-FFFE403FB0C34B9E"}
|
||||
Number Mood_Selector "Lighting mood" <light> {channel="loxone:miniserver:504F2414780F:0FC4E0DF-0255-6ABD-FFFE203EA0C34B9E"}
|
||||
Switch Mood_Enter_Home "Entering home" <light> {channel="loxone:miniserver:504F2414780F:0FC4E0DF-0255-6ABD-FFFE203EA0C34B9E-M1"}
|
||||
Switch Mood_Read_Book "Reading book" <light> {channel="loxone:miniserver:504F2414780F:0FC4E0DF-0255-6ABD-FFFE203EA0C34B9E-M2"}
|
||||
Switch Mood_Evening "Evening setup" <light> {channel="loxone:miniserver:504F2414780F:0FC4E0DF-0255-6ABD-FFFE203EA0C34B9E-M3"}
|
||||
Number Garden_Valve "Garden watering section" <garden> {channel="loxone:miniserver:504F2414780F:0FC5E0DF-0355-6AAD-FFFE403FB0C34B9E"}
|
||||
String Alarm_State "Alarm state [%s]" <alarm> {channel="loxone:miniserver:504F2414780F:0F2E2134-017D-3E82-FFFF433FB4A34B9E"}
|
||||
```
|
||||
|
||||
### sitemaps/loxone.sitemap:
|
||||
|
||||
```
|
||||
sitemap loxone label="Loxone Example Menu"
|
||||
{
|
||||
Frame label="Demo Controls" {
|
||||
Text item=Miniserver_Temp
|
||||
Text item=Garage_Door
|
||||
Switch item=Kitchen_Lights
|
||||
Switch item=Reset_Lights
|
||||
Switch item=Kitchen_Blinds
|
||||
Slider item=Kitchen_Dimmer switchSupport
|
||||
Switch item=Stairs_Light
|
||||
Text item=Stairs_Light-1
|
||||
Selection item=Light_Scene mappings=[0="All off", 1="My scene 1", 2="My scene 2", 9="All on"]
|
||||
Selection item=Mood_Selector
|
||||
Switch item=Mood_Enter_Home
|
||||
Switch item=Mood_Read_Book
|
||||
Switch item=Mood_Evening
|
||||
Setpoint item=Garden_Valve minValue=0 maxValue=8 step=1
|
||||
Text item=Alarm_State
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### transform/garagedoor.map:
|
||||
|
||||
```java
|
||||
OFF=Closed
|
||||
ON=Open
|
||||
-=Unknown
|
||||
```
|
||||
17
bundles/org.openhab.binding.loxone/pom.xml
Normal file
17
bundles/org.openhab.binding.loxone/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.loxone</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Loxone Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.loxone-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-loxone" description="Loxone Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-upnp</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.loxone/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -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.loxone.internal;
|
||||
|
||||
/**
|
||||
* Configuration of a Loxone Miniserver ({@link LxServerHandler})
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxBindingConfiguration {
|
||||
/**
|
||||
* Host address or IP of the Miniserver
|
||||
*/
|
||||
public String host;
|
||||
/**
|
||||
* Port of web service of the Miniserver
|
||||
*/
|
||||
public int port;
|
||||
/**
|
||||
* User name used to log into the Miniserver
|
||||
*/
|
||||
public String user;
|
||||
/**
|
||||
* Password used to log into the Miniserver
|
||||
*/
|
||||
public String password;
|
||||
/**
|
||||
* Authentication token acquired from the Miniserver
|
||||
*/
|
||||
public String authToken;
|
||||
/**
|
||||
* Time in seconds between binding initialization and first connection attempt
|
||||
*/
|
||||
public int firstConDelay;
|
||||
/**
|
||||
* Time in seconds between sending two consecutive keep-alive messages
|
||||
*/
|
||||
public int keepAlivePeriod;
|
||||
/**
|
||||
* Time in seconds between failed websocket connect attempts
|
||||
*/
|
||||
public int connectErrDelay;
|
||||
/**
|
||||
* Time to wait for Miniserver response to a request sent from the binding
|
||||
*/
|
||||
public int responseTimeout;
|
||||
/**
|
||||
* Time in seconds between user login error as a result of wrong name/password or no authority and next connection
|
||||
* attempt
|
||||
*/
|
||||
public int userErrorDelay;
|
||||
/**
|
||||
* Time in seconds between connection close (as a result of some communication error) and next connection attempt
|
||||
*/
|
||||
public int comErrorDelay;
|
||||
/**
|
||||
* Websocket client's max binary message size in kB
|
||||
*/
|
||||
public int maxBinMsgSize;
|
||||
/**
|
||||
* Websocket client's max text message size in kB
|
||||
*/
|
||||
public int maxTextMsgSize;
|
||||
/**
|
||||
* Authentication method (0-auto, 1-hash, 2-token)
|
||||
*/
|
||||
public int authMethod;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* Common constants used across the whole binding.
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LxBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "loxone";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_MINISERVER = new ThingTypeUID(BINDING_ID, "miniserver");
|
||||
|
||||
// Channel Type IDs - read/write
|
||||
public static final String MINISERVER_CHANNEL_TYPE_SWITCH = "switchTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_LIGHT_CTRL = "lightCtrlTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_RADIO_BUTTON = "radioButtonTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_ROLLERSHUTTER = "rollerShutterTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_DIMMER = "dimmerTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_NUMBER = "numberTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_COLORPICKER = "colorPickerTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_IROOM_V2_ACTIVE_MODE = "iRoomV2ActiveModeTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_IROOM_V2_OPERATING_MODE = "iRoomV2OperatingModeTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_IROOM_V2_PREPARE_STATE = "iRoomV2PrepareStateTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_IROOM_V2_COMFORT_TOLERANCE = "iRoomV2ComfortToleranceTypeId";
|
||||
// Channel Type IDs - read only
|
||||
public static final String MINISERVER_CHANNEL_TYPE_RO_TEXT = "roTextTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_RO_SWITCH = "roSwitchTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_RO_ANALOG = "roAnalogTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_RO_NUMBER = "roNumberTypeId";
|
||||
public static final String MINISERVER_CHANNEL_TYPE_RO_DATETIME = "roDateTimeTypeId";
|
||||
|
||||
// Miniserver properties and parameters
|
||||
public static final String MINISERVER_PARAM_HOST = "host";
|
||||
public static final String MINISERVER_PARAM_PORT = "port";
|
||||
public static final String MINISERVER_PROPERTY_MINISERVER_NAME = "name";
|
||||
public static final String MINISERVER_PROPERTY_PROJECT_NAME = "project";
|
||||
public static final String MINISERVER_PROPERTY_CLOUD_ADDRESS = "cloudAddress";
|
||||
|
||||
// Location as configured on the Miniserver - it may be different to the Thing location property, which is user
|
||||
// defined and influences the grouping of items in the UI
|
||||
public static final String MINISERVER_PROPERTY_PHYSICAL_LOCATION = "physicalLocation";
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jupnp.model.meta.DeviceDetails;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link LxDiscoveryParticipant} class creates Miniserver things.
|
||||
* It analyzes UPNP devices discovered by the framework and if Loxone Miniserver is found,
|
||||
* a new thing discovery is reported, which in turn will result in creating a {@link Thing}
|
||||
* and subsequently a new {@link LxServerHandler} object.
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
public class LxDiscoveryParticipant implements UpnpDiscoveryParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LxDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return LxServerHandler.SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveryResult createResult(RemoteDevice device) {
|
||||
ThingUID uid = getThingUID(device);
|
||||
if (uid != null) {
|
||||
Map<String, Object> properties = new HashMap<>(2);
|
||||
|
||||
// After correct Thing UID is created, we have confidence that all following parameters exist and we don't
|
||||
// need to check for null objects here in the device details
|
||||
DeviceDetails details = device.getDetails();
|
||||
String serial = details.getSerialNumber();
|
||||
String host = details.getPresentationURI().getHost();
|
||||
String label = details.getFriendlyName() + " @ " + host;
|
||||
int port = details.getPresentationURI().getPort();
|
||||
String vendor = details.getManufacturerDetails().getManufacturer();
|
||||
String model = details.getModelDetails().getModelName();
|
||||
|
||||
logger.debug("Creating discovery result for serial {} label {} port {}", serial, label, port);
|
||||
properties.put(LxBindingConstants.MINISERVER_PARAM_HOST, host);
|
||||
properties.put(LxBindingConstants.MINISERVER_PARAM_PORT, port);
|
||||
properties.put(Thing.PROPERTY_VENDOR, vendor);
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serial);
|
||||
|
||||
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label)
|
||||
.withRepresentationProperty(serial).build();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThingUID getThingUID(RemoteDevice device) {
|
||||
String manufacturer = device.getDetails().getManufacturerDetails().getManufacturer();
|
||||
if (manufacturer != null && manufacturer.toLowerCase().contains("loxone")) {
|
||||
String model = device.getDetails().getModelDetails().getModelName();
|
||||
if (model != null && model.toLowerCase().contentEquals("loxone miniserver")) {
|
||||
String serial = device.getDetails().getSerialNumber();
|
||||
if (serial == null) {
|
||||
serial = device.getIdentity().getUdn().getIdentifierString();
|
||||
}
|
||||
if (serial != null) {
|
||||
return new ThingUID(LxBindingConstants.THING_TYPE_MINISERVER, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Dynamic channel state description provider.
|
||||
* Overrides the state description for the controls, which receive its configuration in the runtime.
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, LxDynamicStateDescriptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class LxDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
|
||||
|
||||
private Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
|
||||
private Logger logger = LoggerFactory.getLogger(LxDynamicStateDescriptionProvider.class);
|
||||
|
||||
/**
|
||||
* Set a state description for a channel. This description will be used when preparing the channel state by
|
||||
* the framework for presentation. A previous description, if existed, will be replaced.
|
||||
*
|
||||
* @param channelUID channel UID
|
||||
* @param description state description for the channel
|
||||
*/
|
||||
void setDescription(ChannelUID channelUID, StateDescription description) {
|
||||
logger.debug("Adding state description for channel {}", channelUID);
|
||||
descriptions.put(channelUID, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered state descriptions
|
||||
*/
|
||||
void removeAllDescriptions() {
|
||||
logger.debug("Removing all state descriptions");
|
||||
descriptions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a state description for a given channel ID
|
||||
*
|
||||
* @param channelUID channel ID to remove description for
|
||||
*/
|
||||
void removeDescription(ChannelUID channelUID) {
|
||||
logger.debug("Removing state description for channel {}", channelUID);
|
||||
descriptions.remove(channelUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable StateDescription getStateDescription(Channel channel,
|
||||
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
|
||||
StateDescription description = descriptions.get(channel.getUID());
|
||||
logger.trace("Providing state description for channel {}", channel.getUID());
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Factory responsible for creating Loxone things (Miniservers) and their handlers ({@link LxServerHandler}
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.loxone")
|
||||
public class LxHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.singleton(LxBindingConstants.THING_TYPE_MINISERVER);
|
||||
|
||||
private LxDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID uid = thing.getThingTypeUID();
|
||||
if (uid.equals(LxBindingConstants.THING_TYPE_MINISERVER)) {
|
||||
LxServerHandler handler = new LxServerHandler(thing, dynamicStateDescriptionProvider);
|
||||
return handler;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setDynamicStateDescriptionProvider(LxDynamicStateDescriptionProvider provider) {
|
||||
this.dynamicStateDescriptionProvider = provider;
|
||||
}
|
||||
|
||||
protected void unsetDynamicStateDescriptionProvider(LxDynamicStateDescriptionProvider provider) {
|
||||
this.dynamicStateDescriptionProvider = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
import org.openhab.binding.loxone.internal.types.LxConfig;
|
||||
import org.openhab.binding.loxone.internal.types.LxConfig.LxServerInfo;
|
||||
import org.openhab.binding.loxone.internal.types.LxErrorCode;
|
||||
import org.openhab.binding.loxone.internal.types.LxResponse;
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxStateUpdate;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* Representation of a Loxone Miniserver. It is an openHAB {@link Thing}, which is used to communicate with
|
||||
* objects (controls) configured in the Miniserver over channels.
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*/
|
||||
public class LxServerHandler extends BaseThingHandler implements LxServerHandlerApi {
|
||||
|
||||
private static final String SOCKET_URL = "/ws/rfc6455";
|
||||
private static final String CMD_CFG_API = "jdev/cfg/api";
|
||||
|
||||
private static final Gson GSON;
|
||||
|
||||
private LxBindingConfiguration bindingConfig;
|
||||
private InetAddress host;
|
||||
|
||||
// initial delay to initiate connection
|
||||
private AtomicInteger reconnectDelay = new AtomicInteger();
|
||||
|
||||
// Map of state UUID to a map of control UUID and state objects
|
||||
// State with a unique UUID can be configured in many controls and each control can even have a different name of
|
||||
// the state. It must be ensured that updates received for this state UUID are passed to all controls that have this
|
||||
// state UUID configured.
|
||||
private Map<LxUuid, Map<LxUuid, LxState>> states = new HashMap<>();
|
||||
|
||||
private LxWebSocket socket;
|
||||
private WebSocketClient wsClient;
|
||||
|
||||
private int debugId = 0;
|
||||
private Thread monitorThread;
|
||||
private final Lock threadLock = new ReentrantLock();
|
||||
private final Lock queueUpdatedLock = new ReentrantLock();
|
||||
private final Condition queueUpdated = queueUpdatedLock.newCondition();
|
||||
private AtomicBoolean sessionActive = new AtomicBoolean(false);
|
||||
|
||||
// Data structures
|
||||
private final Map<LxUuid, LxControl> controls = new HashMap<>();
|
||||
private final Map<ChannelUID, LxControl> channels = new HashMap<>();
|
||||
private final ConcurrentLinkedQueue<LxStateUpdate> stateUpdateQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private LxDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
|
||||
private final Logger logger = LoggerFactory.getLogger(LxServerHandler.class);
|
||||
private static AtomicInteger staticDebugId = new AtomicInteger(1);
|
||||
|
||||
static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.singleton(LxBindingConstants.THING_TYPE_MINISERVER);
|
||||
|
||||
private QueuedThreadPool jettyThreadPool;
|
||||
|
||||
static {
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
builder.registerTypeAdapter(LxUuid.class, LxUuid.DESERIALIZER);
|
||||
builder.registerTypeAdapter(LxControl.class, LxControl.DESERIALIZER);
|
||||
GSON = builder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link LxServerHandler} object
|
||||
*
|
||||
* @param thing Thing object that creates the handler
|
||||
* @param provider state description provider service
|
||||
*/
|
||||
public LxServerHandler(Thing thing, LxDynamicStateDescriptionProvider provider) {
|
||||
super(thing);
|
||||
logger.debug("[{}] Constructing thing object", debugId);
|
||||
if (provider != null) {
|
||||
dynamicStateDescriptionProvider = provider;
|
||||
} else {
|
||||
logger.warn("Dynamic state description provider is null");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Methods from BaseThingHandler
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
updateChannelState(channelUID);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LxControl control = channels.get(channelUID);
|
||||
if (control != null) {
|
||||
control.handleCommand(channelUID, command);
|
||||
} else {
|
||||
logger.error("[{}] Received command {} for unknown control.", debugId, command);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
setOffline(LxErrorCode.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
logger.debug("[{}] Channel linked: {}", debugId, channelUID.getAsString());
|
||||
updateChannelState(channelUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
threadLock.lock();
|
||||
try {
|
||||
debugId = staticDebugId.getAndIncrement();
|
||||
|
||||
logger.debug("[{}] Initializing thing instance", debugId);
|
||||
bindingConfig = getConfig().as(LxBindingConfiguration.class);
|
||||
try {
|
||||
this.host = InetAddress.getByName(bindingConfig.host);
|
||||
} catch (UnknownHostException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unknown host");
|
||||
return;
|
||||
}
|
||||
reconnectDelay.set(bindingConfig.firstConDelay);
|
||||
|
||||
jettyThreadPool = new QueuedThreadPool();
|
||||
jettyThreadPool.setName(LxServerHandler.class.getSimpleName() + "-" + debugId);
|
||||
jettyThreadPool.setDaemon(true);
|
||||
|
||||
socket = new LxWebSocket(debugId, this, bindingConfig, host);
|
||||
wsClient = new WebSocketClient();
|
||||
wsClient.setExecutor(jettyThreadPool);
|
||||
if (debugId > 1) {
|
||||
reconnectDelay.set(0);
|
||||
}
|
||||
if (monitorThread == null) {
|
||||
monitorThread = new LxServerThread(debugId);
|
||||
monitorThread.start();
|
||||
}
|
||||
} finally {
|
||||
threadLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("[{}] Disposing of thing", debugId);
|
||||
Thread thread;
|
||||
threadLock.lock();
|
||||
try {
|
||||
sessionActive.set(false);
|
||||
stateUpdateQueue.clear();
|
||||
thread = monitorThread;
|
||||
if (monitorThread != null) {
|
||||
monitorThread.interrupt();
|
||||
monitorThread = null;
|
||||
}
|
||||
clearConfiguration();
|
||||
} finally {
|
||||
threadLock.unlock();
|
||||
}
|
||||
if (thread != null) {
|
||||
try {
|
||||
thread.join(5000);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("[{}] Waiting for thread termination interrupted.", debugId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Public methods that are called by {@link LxControl} child classes
|
||||
*/
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.loxone.internal.LxServerHandlerApi#sendAction(org.openhab.binding.loxone.internal.types.
|
||||
* LxUuid, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public void sendAction(LxUuid id, String operation) throws IOException {
|
||||
socket.sendAction(id, operation);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.openhab.binding.loxone.internal.LxServerHandlerApi#addControl(org.openhab.binding.loxone.internal.controls.
|
||||
* LxControl)
|
||||
*/
|
||||
@Override
|
||||
public void addControl(LxControl control) {
|
||||
addControlStructures(control);
|
||||
addThingChannels(control.getChannelsWithSubcontrols(), false);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.openhab.binding.loxone.internal.LxServerHandlerApi#removeControl(org.openhab.binding.loxone.internal.controls
|
||||
* .LxControl)
|
||||
*/
|
||||
@Override
|
||||
public void removeControl(LxControl control) {
|
||||
logger.debug("[{}] Removing control: {}", debugId, control.getName());
|
||||
control.getSubControls().values().forEach(subControl -> removeControl(subControl));
|
||||
LxUuid controlUuid = control.getUuid();
|
||||
control.getStates().values().forEach(state -> {
|
||||
LxUuid stateUuid = state.getUuid();
|
||||
Map<LxUuid, LxState> perUuid = states.get(stateUuid);
|
||||
if (perUuid != null) {
|
||||
perUuid.remove(controlUuid);
|
||||
if (perUuid.isEmpty()) {
|
||||
states.remove(stateUuid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ThingBuilder builder = editThing();
|
||||
control.getChannels().forEach(channel -> {
|
||||
ChannelUID id = channel.getUID();
|
||||
builder.withoutChannel(id);
|
||||
dynamicStateDescriptionProvider.removeDescription(id);
|
||||
channels.remove(id);
|
||||
});
|
||||
updateThing(builder.build());
|
||||
controls.remove(controlUuid);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.loxone.internal.LxServerHandlerApi#setChannelState(org.openhab.core.thing.
|
||||
* ChannelUID, org.openhab.core.types.State)
|
||||
*/
|
||||
@Override
|
||||
public void setChannelState(ChannelUID channelId, State state) {
|
||||
updateState(channelId, state);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.openhab.binding.loxone.internal.LxServerHandlerApi#setChannelStateDescription(org.openhab.core.
|
||||
* thing.ChannelUID, org.openhab.core.types.StateDescription)
|
||||
*/
|
||||
@Override
|
||||
public void setChannelStateDescription(ChannelUID channelId, StateDescription description) {
|
||||
logger.debug("[{}] State description update for channel {}", debugId, channelId);
|
||||
dynamicStateDescriptionProvider.setDescription(channelId, description);
|
||||
}
|
||||
|
||||
/*
|
||||
* Public methods called by {@link LxWsSecurity} child classes.
|
||||
*/
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.loxone.internal.LxServerHandlerApi#getSetting(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public String getSetting(String name) {
|
||||
Object value = getConfig().get(name);
|
||||
return (value instanceof String) ? (String) value : null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.loxone.internal.LxServerHandlerApi#setSettings(java.util.Map)
|
||||
*/
|
||||
@Override
|
||||
public void setSettings(Map<String, String> properties) {
|
||||
Configuration config = getConfig();
|
||||
properties.forEach((name, value) -> config.put(name, value));
|
||||
updateConfiguration(config);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.loxone.internal.LxServerHandlerApi#getGson()
|
||||
*/
|
||||
@Override
|
||||
public Gson getGson() {
|
||||
return GSON;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.loxone.internal.LxServerHandlerApi#getThingId()
|
||||
*/
|
||||
@Override
|
||||
public ThingUID getThingId() {
|
||||
return getThing().getUID();
|
||||
}
|
||||
|
||||
/*
|
||||
* Methods called by {@link LxWebSocket} class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose of all objects created from the Miniserver configuration.
|
||||
*/
|
||||
void clearConfiguration() {
|
||||
controls.clear();
|
||||
channels.clear();
|
||||
states.clear();
|
||||
dynamicStateDescriptionProvider.removeAllDescriptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new configuration received from the Miniserver and creates all required channels.
|
||||
*
|
||||
* @param config Miniserver's configuration
|
||||
*/
|
||||
void setMiniserverConfig(LxConfig config) {
|
||||
logger.debug("[{}] Setting configuration from Miniserver", debugId);
|
||||
|
||||
if (config.msInfo == null) {
|
||||
logger.warn("[{}] missing global configuration msInfo on Loxone", debugId);
|
||||
config.msInfo = config.new LxServerInfo();
|
||||
}
|
||||
Thing thing = getThing();
|
||||
LxServerInfo info = config.msInfo;
|
||||
thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_MINISERVER_NAME, buildName(info.msName));
|
||||
thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_PROJECT_NAME, buildName(info.projectName));
|
||||
thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_CLOUD_ADDRESS, buildName(info.remoteUrl));
|
||||
thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_PHYSICAL_LOCATION, buildName(info.location));
|
||||
thing.setProperty(Thing.PROPERTY_FIRMWARE_VERSION, buildName(info.swVersion));
|
||||
thing.setProperty(Thing.PROPERTY_SERIAL_NUMBER, buildName(info.serialNr));
|
||||
thing.setProperty(Thing.PROPERTY_MAC_ADDRESS, buildName(info.macAddress));
|
||||
|
||||
List<Channel> list = new ArrayList<>();
|
||||
if (config.controls != null) {
|
||||
logger.trace("[{}] creating control structures.", debugId);
|
||||
config.controls.values().forEach(ctrl -> {
|
||||
addControlStructures(ctrl);
|
||||
list.addAll(ctrl.getChannelsWithSubcontrols());
|
||||
});
|
||||
} else {
|
||||
logger.warn("[{}] no controls received in Miniserver configuration.", debugId);
|
||||
}
|
||||
addThingChannels(list, true);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set thing status to offline and start attempts to establish a new connection to the Miniserver after a delay
|
||||
* depending of the reason for going offline.
|
||||
*
|
||||
* @param code error code
|
||||
* @param reason reason for going offline
|
||||
*/
|
||||
void setOffline(LxErrorCode code, String reason) {
|
||||
logger.debug("[{}] set offline code={} reason={}", debugId, code, reason);
|
||||
switch (code) {
|
||||
case TOO_MANY_FAILED_LOGIN_ATTEMPTS:
|
||||
// assume credentials are wrong, do not re-attempt connections any time soon
|
||||
// expect a new instance will have to be initialized with corrected configuration
|
||||
reconnectDelay.set(60 * 60 * 24 * 7);
|
||||
updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Too many failed login attempts - stopped trying");
|
||||
break;
|
||||
case USER_UNAUTHORIZED:
|
||||
reconnectDelay.set(bindingConfig.userErrorDelay);
|
||||
updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
reason != null ? reason : "User authentication error (invalid user name or password)");
|
||||
break;
|
||||
case USER_AUTHENTICATION_TIMEOUT:
|
||||
updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, "User authentication timeout");
|
||||
break;
|
||||
case COMMUNICATION_ERROR:
|
||||
reconnectDelay.set(bindingConfig.comErrorDelay);
|
||||
String text = "Error communicating with Miniserver";
|
||||
if (reason != null) {
|
||||
text += " (" + reason + ")";
|
||||
}
|
||||
updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, text);
|
||||
break;
|
||||
case INTERNAL_ERROR:
|
||||
updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
reason != null ? "Internal error (" + reason + ")" : "Internal error");
|
||||
break;
|
||||
case WEBSOCKET_IDLE_TIMEOUT:
|
||||
logger.warn("Idle timeout from Loxone Miniserver - adjust keepalive settings");
|
||||
updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Timeout due to no activity");
|
||||
break;
|
||||
case ERROR_CODE_MISSING:
|
||||
logger.warn("No error code available from the Miniserver");
|
||||
updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason - error code missing");
|
||||
break;
|
||||
default:
|
||||
updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Unknown reason");
|
||||
break;
|
||||
}
|
||||
sessionActive.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a new state update event to the queue for processing and signal thread to process it
|
||||
*
|
||||
* @param uuid state uuid (null indicates websocket session should be closed)
|
||||
* @param value new state value
|
||||
*/
|
||||
void queueStateUpdate(LxUuid uuid, Object value) {
|
||||
stateUpdateQueue.add(new LxStateUpdate(uuid, value));
|
||||
queueUpdatedLock.lock();
|
||||
try {
|
||||
queueUpdated.signalAll();
|
||||
} finally {
|
||||
queueUpdatedLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update to the new value of a state received from Miniserver. This method will go through all instances of this
|
||||
* state UUID and update their value, which will trigger corresponding control state update method in each control
|
||||
* that has this state.
|
||||
*
|
||||
* @param update Miniserver's update event
|
||||
*/
|
||||
private void updateStateValue(LxStateUpdate update) {
|
||||
Map<LxUuid, LxState> perStateUuid = states.get(update.getUuid());
|
||||
if (perStateUuid != null) {
|
||||
perStateUuid.forEach((controlUuid, state) -> {
|
||||
state.setStateValue(update.getValue());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new control, its states, subcontrols and channels to the handler structures.
|
||||
* Handler maintains maps of all controls (main controls + subcontrols), all channels for all controls and all
|
||||
* states to match received openHAB commands and state updates from the Miniserver. States also contain links to
|
||||
* possibly multiple control objects, as many controls can share the same state with the same state uuid.
|
||||
* To create channels, {@link LxServerHandler#addThingChannels} method should be called separately. This allows
|
||||
* creation of all channels for all controls with a single thing update.
|
||||
*
|
||||
* @param control a created control object to be added
|
||||
*/
|
||||
private void addControlStructures(LxControl control) {
|
||||
LxUuid uuid = control.getUuid();
|
||||
logger.debug("[{}] Adding control to handler: {}, {}", debugId, uuid, control.getName());
|
||||
control.getStates().values().forEach(state -> {
|
||||
Map<LxUuid, LxState> perUuid = states.get(state.getUuid());
|
||||
if (perUuid == null) {
|
||||
perUuid = new HashMap<>();
|
||||
states.put(state.getUuid(), perUuid);
|
||||
}
|
||||
perUuid.put(uuid, state);
|
||||
});
|
||||
controls.put(control.getUuid(), control);
|
||||
control.getChannels().forEach(channel -> channels.put(channel.getUID(), control));
|
||||
control.getSubControls().values().forEach(subControl -> addControlStructures(subControl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds channels to the thing, to make them available to the framework and user.
|
||||
* This method will sort the channels according to their label.
|
||||
* It is expected that input list contains no duplicate channel IDs.
|
||||
*
|
||||
* @param newChannels a list of channels to add to the thing
|
||||
* @param purge if true, old channels will be removed, otherwise merged
|
||||
*/
|
||||
private void addThingChannels(List<Channel> newChannels, boolean purge) {
|
||||
List<Channel> channels = newChannels;
|
||||
if (!purge) {
|
||||
channels.addAll(getThing().getChannels());
|
||||
}
|
||||
channels.sort((c1, c2) -> {
|
||||
String label = c1.getLabel();
|
||||
return label == null ? 1 : label.compareTo(c2.getLabel());
|
||||
});
|
||||
ThingBuilder builder = editThing();
|
||||
builder.withChannels(channels);
|
||||
updateThing(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the websocket.
|
||||
* Attempts to connect to the websocket on a remote Miniserver. If a connection is established, a
|
||||
* {@link LxWebSocket#onConnect} method will be called from a parallel websocket thread.
|
||||
*
|
||||
* @return true if connection request initiated correctly, false if not
|
||||
*/
|
||||
private boolean connect() {
|
||||
logger.debug("[{}] connect() websocket", debugId);
|
||||
/*
|
||||
* Try to read CfgApi structure from the miniserver. It contains serial number and firmware version. If it can't
|
||||
* be read this is not a fatal issue, we will assume most recent version running.
|
||||
*/
|
||||
String message = socket.httpGet(CMD_CFG_API);
|
||||
if (message != null) {
|
||||
LxResponse resp = socket.getResponse(message);
|
||||
if (resp != null) {
|
||||
socket.setFwVersion(GSON.fromJson(resp.getValueAsString(), LxResponse.LxResponseCfgApi.class).version);
|
||||
}
|
||||
} else {
|
||||
logger.debug("[{}] Http get failed for API config request.", debugId);
|
||||
}
|
||||
|
||||
try {
|
||||
wsClient.start();
|
||||
|
||||
// Following the PR github.com/eclipse/smarthome/pull/6636
|
||||
// without this zero timeout, jetty will wait 30 seconds for stopping the client to eventually fail
|
||||
// with the timeout it is immediate and all threads end correctly
|
||||
jettyThreadPool.setStopTimeout(0);
|
||||
URI target = new URI("ws://" + host.getHostAddress() + ":" + bindingConfig.port + SOCKET_URL);
|
||||
ClientUpgradeRequest request = new ClientUpgradeRequest();
|
||||
request.setSubProtocols("remotecontrol");
|
||||
|
||||
socket.startResponseTimeout();
|
||||
logger.debug("[{}] Connecting to server : {} ", debugId, target);
|
||||
wsClient.connect(socket, target, request);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.debug("[{}] Error starting websocket client: {}", debugId, e.getMessage());
|
||||
try {
|
||||
wsClient.stop();
|
||||
} catch (Exception e2) {
|
||||
logger.debug("[{}] Error stopping websocket client: {}", debugId, e2.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Private methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Disconnect websocket session - initiated from this end.
|
||||
*
|
||||
* @param code error code for disconnecting the websocket
|
||||
* @param reason reason for disconnecting the websocket
|
||||
*/
|
||||
private void disconnect(LxErrorCode code, String reason) {
|
||||
logger.debug("[{}] disconnect the websocket: {}, {}", debugId, code, reason);
|
||||
socket.disconnect(code, reason);
|
||||
try {
|
||||
logger.debug("[{}] client stop", debugId);
|
||||
wsClient.stop();
|
||||
logger.debug("[{}] client stopped", debugId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("[{}] Exception disconnecting the websocket: ", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread that maintains connection to the Miniserver.
|
||||
* It will periodically attempt to connect and if failed, wait a configured amount of time.
|
||||
* If connection succeeds, it will sleep until the session is terminated. Then it will wait and try to reconnect
|
||||
* again.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
private class LxServerThread extends Thread {
|
||||
private int debugId = 0;
|
||||
private long elapsed = 0;
|
||||
private Instant lastKeepAlive;
|
||||
|
||||
LxServerThread(int id) {
|
||||
debugId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.debug("[{}] Thread starting", debugId);
|
||||
try {
|
||||
while (!isInterrupted()) {
|
||||
sessionActive.set(connectSession());
|
||||
processStateUpdates();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
logger.debug("[{}] Thread interrupted", debugId);
|
||||
}
|
||||
disconnect(LxErrorCode.OK, "Thing is going down.");
|
||||
logger.debug("[{}] Thread ending", debugId);
|
||||
}
|
||||
|
||||
private boolean connectSession() throws InterruptedException {
|
||||
int delay = reconnectDelay.get();
|
||||
if (delay > 0) {
|
||||
logger.debug("[{}] Delaying connect request by {} seconds.", debugId, reconnectDelay);
|
||||
TimeUnit.SECONDS.sleep(delay);
|
||||
}
|
||||
logger.debug("[{}] Server connecting to websocket", debugId);
|
||||
if (!connect()) {
|
||||
updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Failed to connect to Miniserver's WebSocket");
|
||||
reconnectDelay.set(bindingConfig.connectErrDelay);
|
||||
return false;
|
||||
}
|
||||
lastKeepAlive = Instant.now();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void processStateUpdates() throws InterruptedException {
|
||||
while (sessionActive.get()) {
|
||||
logger.debug("[{}] Sleeping for {} seconds.", debugId, bindingConfig.keepAlivePeriod - elapsed);
|
||||
queueUpdatedLock.lock();
|
||||
try {
|
||||
if (!queueUpdated.await(bindingConfig.keepAlivePeriod - elapsed, TimeUnit.SECONDS)) {
|
||||
sendKeepAlive();
|
||||
continue;
|
||||
}
|
||||
} finally {
|
||||
queueUpdatedLock.unlock();
|
||||
}
|
||||
elapsed = Duration.between(lastKeepAlive, Instant.now()).getSeconds();
|
||||
if (elapsed >= bindingConfig.keepAlivePeriod) {
|
||||
sendKeepAlive();
|
||||
}
|
||||
LxStateUpdate update;
|
||||
while ((update = stateUpdateQueue.poll()) != null && sessionActive.get()) {
|
||||
updateStateValue(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendKeepAlive() {
|
||||
socket.sendKeepAlive();
|
||||
lastKeepAlive = Instant.now();
|
||||
elapsed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the thing status to offline, if it is not already offline. This will preserve he first reason of going
|
||||
* offline in case there were multiple reasons.
|
||||
*
|
||||
* @param code error code
|
||||
* @param reason reason for going offline
|
||||
*/
|
||||
private void updateStatusToOffline(ThingStatusDetail code, String reason) {
|
||||
ThingStatus status = getThing().getStatus();
|
||||
if (status == ThingStatus.OFFLINE) {
|
||||
logger.debug("[{}] received offline request with code {}, but thing already offline.", debugId, code);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, code, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an actual state of a channel.
|
||||
* Determines control for the channel and retrieves the state from the control.
|
||||
*
|
||||
* @param channelId channel ID to update its state
|
||||
*/
|
||||
private void updateChannelState(ChannelUID channelId) {
|
||||
LxControl control = channels.get(channelId);
|
||||
if (control != null) {
|
||||
State state = control.getChannelState(channelId);
|
||||
if (state != null) {
|
||||
updateState(channelId, state);
|
||||
}
|
||||
} else {
|
||||
logger.error("[{}] Received state update request for unknown control (channelId={}).", debugId, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and convert null string to empty string.
|
||||
*
|
||||
* @param name string to check
|
||||
* @return string guaranteed to be not null
|
||||
*/
|
||||
private String buildName(String name) {
|
||||
if (name == null) {
|
||||
return "";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
import org.openhab.binding.loxone.internal.security.LxWsSecurity;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Representation of a Loxone Miniserver. It is an openHAB {@link Thing}, which is used to communicate with
|
||||
* objects (controls) configured in the Miniserver over channels.
|
||||
*
|
||||
* @author Pawel Pieczul - Initial contribution
|
||||
*/
|
||||
public interface LxServerHandlerApi {
|
||||
|
||||
/**
|
||||
* Sends an action to a Loxone Miniserver's control.
|
||||
*
|
||||
* @param id identifier of the control
|
||||
* @param operation identifier of the operation
|
||||
* @throws IOException when communication error with Miniserver occurs
|
||||
*/
|
||||
void sendAction(LxUuid id, String operation) throws IOException;
|
||||
|
||||
/**
|
||||
* Add a control - creates internal data structures and channels in the framework.
|
||||
* This method should be used for all dynamically created controls, usually as a result of Miniserver's state update
|
||||
* messages, after the static configuration is setup.
|
||||
*
|
||||
* @param control control to be added
|
||||
*/
|
||||
void addControl(LxControl control);
|
||||
|
||||
/**
|
||||
* Remove a control - removes internal data structures and channels from the framework
|
||||
* This method should be used for all dynamically created controls, usually as a result of Miniserver's state update
|
||||
* messages, after the static configuration is setup.
|
||||
*
|
||||
* @param control control to remove
|
||||
*/
|
||||
void removeControl(LxControl control);
|
||||
|
||||
/**
|
||||
* Sets channel's state to a new value
|
||||
*
|
||||
* @param channelId channel ID
|
||||
* @param state new state value
|
||||
*/
|
||||
void setChannelState(ChannelUID channelId, State state);
|
||||
|
||||
/**
|
||||
* Sets a new channel state description. This method is called to dynamically change the way the channel state is
|
||||
* interpreted and displayed. It is called when a dynamic state update is received from the Miniserver with a new
|
||||
* way of displaying control's state.
|
||||
*
|
||||
* @param channelId channel ID
|
||||
* @param description a new state description
|
||||
*/
|
||||
void setChannelStateDescription(ChannelUID channelId, StateDescription description);
|
||||
|
||||
/**
|
||||
* Get configuration parameter from the thing configuration. This method is called by the {@link LxWsSecurity} class
|
||||
* to dynamically retrieve previously stored login token and its parameters.
|
||||
*
|
||||
* @param name parameter name
|
||||
* @return parameter value
|
||||
*/
|
||||
String getSetting(String name);
|
||||
|
||||
/**
|
||||
* Set configuration parameters in the thing configuration. This method is called by the {@link LxWsSecurity} class
|
||||
* to dynamically stored login token and its parameters received from the Miniserver.
|
||||
*
|
||||
* @param properties pairs of parameter names and values
|
||||
*/
|
||||
void setSettings(Map<String, String> properties);
|
||||
|
||||
/**
|
||||
* Get GSON object for reuse
|
||||
*
|
||||
* @return GSON object
|
||||
*/
|
||||
Gson getGson();
|
||||
|
||||
/**
|
||||
* Get ID of the Miniserver's Thing
|
||||
*
|
||||
* @return ID of the Thing
|
||||
*/
|
||||
ThingUID getThingId();
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.StatusCode;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.openhab.binding.loxone.internal.security.LxWsSecurity;
|
||||
import org.openhab.binding.loxone.internal.types.LxConfig;
|
||||
import org.openhab.binding.loxone.internal.types.LxErrorCode;
|
||||
import org.openhab.binding.loxone.internal.types.LxResponse;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.binding.loxone.internal.types.LxWsBinaryHeader;
|
||||
import org.openhab.binding.loxone.internal.types.LxWsSecurityType;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* Implementation of jetty websocket client
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
@WebSocket
|
||||
public class LxWebSocket {
|
||||
private static final String CMD_ACTION = "jdev/sps/io/";
|
||||
private static final String CMD_KEEPALIVE = "keepalive";
|
||||
private static final String CMD_ENABLE_UPDATES = "jdev/sps/enablebinstatusupdate";
|
||||
private static final String CMD_GET_APP_CONFIG = "data/LoxAPP3.json";
|
||||
|
||||
private final int debugId;
|
||||
private final Gson gson;
|
||||
private final LxServerHandler thingHandler;
|
||||
|
||||
private long responseTimeout = 4; // 4 seconds to wait for Miniserver response
|
||||
private int maxBinMsgSize = 3 * 1024; // 3 MB
|
||||
private int maxTextMsgSize = 512; // 512 KB
|
||||
private final LxWsSecurityType securityType;
|
||||
private final InetAddress host;
|
||||
private final int port;
|
||||
private final String user;
|
||||
private final String password;
|
||||
|
||||
private Session session;
|
||||
private String fwVersion;
|
||||
private ScheduledFuture<?> timeout;
|
||||
private LxWsBinaryHeader header;
|
||||
private LxWsSecurity security;
|
||||
private boolean awaitingConfiguration = false;
|
||||
private final Lock webSocketLock = new ReentrantLock();
|
||||
private final Lock responseLock = new ReentrantLock();
|
||||
private final Condition responseAvailable = responseLock.newCondition();
|
||||
private String awaitingCommand;
|
||||
private LxResponse awaitedResponse;
|
||||
private boolean syncRequest;
|
||||
|
||||
private LxErrorCode offlineCode;
|
||||
private String offlineReason;
|
||||
|
||||
private static final ScheduledExecutorService SCHEDULER = ThreadPoolManager
|
||||
.getScheduledPool(LxWebSocket.class.getSimpleName());
|
||||
private final Logger logger = LoggerFactory.getLogger(LxWebSocket.class);
|
||||
|
||||
/**
|
||||
* Create websocket object.
|
||||
*
|
||||
* @param debugId instance of the client used for debugging purposes only
|
||||
* @param thingHandler API to the thing handler
|
||||
* @param cfg binding configuration
|
||||
* @param host IP address of the Miniserver
|
||||
*/
|
||||
LxWebSocket(int debugId, LxServerHandler thingHandler, LxBindingConfiguration cfg, InetAddress host) {
|
||||
this.debugId = debugId;
|
||||
this.thingHandler = thingHandler;
|
||||
this.host = host;
|
||||
this.port = cfg.port;
|
||||
this.user = cfg.user;
|
||||
this.password = cfg.password;
|
||||
this.gson = thingHandler.getGson();
|
||||
|
||||
securityType = LxWsSecurityType.getType(cfg.authMethod);
|
||||
if (cfg.responseTimeout > 0 && cfg.responseTimeout != responseTimeout) {
|
||||
logger.debug("[{}] Changing responseTimeout to {}", debugId, cfg.responseTimeout);
|
||||
responseTimeout = cfg.responseTimeout;
|
||||
}
|
||||
if (cfg.maxBinMsgSize > 0 && cfg.maxBinMsgSize != maxBinMsgSize) {
|
||||
logger.debug("[{}] Changing maxBinMsgSize to {}", debugId, cfg.maxBinMsgSize);
|
||||
maxBinMsgSize = cfg.maxBinMsgSize;
|
||||
}
|
||||
if (cfg.maxTextMsgSize > 0 && cfg.maxTextMsgSize != maxTextMsgSize) {
|
||||
logger.debug("[{}] Changing maxTextMsgSize to {}", debugId, cfg.maxTextMsgSize);
|
||||
maxTextMsgSize = cfg.maxTextMsgSize;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Jetty websocket methods
|
||||
*/
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(Session session) {
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
offlineCode = null;
|
||||
offlineReason = null;
|
||||
WebSocketPolicy policy = session.getPolicy();
|
||||
policy.setMaxBinaryMessageSize(maxBinMsgSize * 1024);
|
||||
policy.setMaxTextMessageSize(maxTextMsgSize * 1024);
|
||||
|
||||
logger.debug("[{}] Websocket connected (maxBinMsgSize={}, maxTextMsgSize={})", debugId,
|
||||
policy.getMaxBinaryMessageSize(), policy.getMaxTextMessageSize());
|
||||
this.session = session;
|
||||
|
||||
security = LxWsSecurity.create(securityType, fwVersion, debugId, thingHandler, this, user, password);
|
||||
security.authenticate((result, details) -> {
|
||||
if (result == LxErrorCode.OK) {
|
||||
authenticated();
|
||||
} else {
|
||||
disconnect(result, details);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onClose(int statusCode, String reason) {
|
||||
String reasonToPass;
|
||||
LxErrorCode codeToPass;
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
logger.debug("[{}] Websocket connection closed with code {} reason : {}", debugId, statusCode, reason);
|
||||
if (security != null) {
|
||||
security.cancel();
|
||||
}
|
||||
session = null;
|
||||
// This callback is called when connection is terminated by either end.
|
||||
// If there is already a reason for disconnection, pass it unchanged.
|
||||
// Otherwise try to interpret the remote end reason.
|
||||
if (offlineCode != null) {
|
||||
codeToPass = offlineCode;
|
||||
reasonToPass = offlineReason;
|
||||
} else {
|
||||
codeToPass = LxErrorCode.getErrorCode(statusCode);
|
||||
reasonToPass = reason;
|
||||
}
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
|
||||
// Release any requester waiting for message response
|
||||
responseLock.lock();
|
||||
try {
|
||||
if (awaitedResponse != null) {
|
||||
awaitedResponse.subResponse = null;
|
||||
}
|
||||
responseAvailable.signalAll();
|
||||
} finally {
|
||||
responseLock.unlock();
|
||||
}
|
||||
thingHandler.setOffline(codeToPass, reasonToPass);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onError(Throwable error) {
|
||||
logger.debug("[{}] Websocket error : {}", debugId, error.getMessage());
|
||||
// We do nothing. This callback may be called at various connection stages and indicates something wrong
|
||||
// with the connection mostly on the protocol level. It will be caught by other activities - connection will
|
||||
// be closed of timeouts will detect its inactivity.
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onBinaryMessage(byte data[], int msgOffset, int msgLength) {
|
||||
int offset = msgOffset;
|
||||
int length = msgLength;
|
||||
if (logger.isTraceEnabled()) {
|
||||
String s = HexUtils.bytesToHex(data);
|
||||
logger.trace("[{}] Binary message: length {}: {}", debugId, length, s);
|
||||
}
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
// websocket will receive header and data in turns as two separate binary messages
|
||||
if (header == null) {
|
||||
// header expected now
|
||||
header = new LxWsBinaryHeader(data, offset);
|
||||
switch (header.getType()) {
|
||||
// following header types precede data in next message
|
||||
case BINARY_FILE:
|
||||
case EVENT_TABLE_OF_VALUE_STATES:
|
||||
case EVENT_TABLE_OF_TEXT_STATES:
|
||||
case EVENT_TABLE_OF_DAYTIMER_STATES:
|
||||
case EVENT_TABLE_OF_WEATHER_STATES:
|
||||
break;
|
||||
// other header types have no data and next message will be header again
|
||||
default:
|
||||
header = null;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// data expected now
|
||||
switch (header.getType()) {
|
||||
case EVENT_TABLE_OF_VALUE_STATES:
|
||||
stopResponseTimeout();
|
||||
while (length > 0) {
|
||||
Double value = ByteBuffer.wrap(data, offset + 16, 8).order(ByteOrder.LITTLE_ENDIAN)
|
||||
.getDouble();
|
||||
thingHandler.queueStateUpdate(new LxUuid(data, offset), value);
|
||||
offset += 24;
|
||||
length -= 24;
|
||||
}
|
||||
break;
|
||||
case EVENT_TABLE_OF_TEXT_STATES:
|
||||
while (length > 0) {
|
||||
// unused today at (offset + 16): iconUuid
|
||||
int textLen = ByteBuffer.wrap(data, offset + 32, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
||||
String value = new String(data, offset + 36, textLen);
|
||||
int size = 36 + (textLen % 4 > 0 ? textLen + 4 - (textLen % 4) : textLen);
|
||||
thingHandler.queueStateUpdate(new LxUuid(data, offset), value);
|
||||
offset += size;
|
||||
length -= size;
|
||||
}
|
||||
break;
|
||||
case KEEPALIVE_RESPONSE:
|
||||
case TEXT_MESSAGE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// header will be next
|
||||
header = null;
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
logger.debug("[{}] malformed binary message received, discarded", debugId);
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onMessage(String msg) {
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
if (logger.isTraceEnabled()) {
|
||||
String trace = msg;
|
||||
if (trace.length() > 100) {
|
||||
trace = msg.substring(0, 100);
|
||||
}
|
||||
logger.trace("[{}] received message: {}", debugId, trace);
|
||||
}
|
||||
if (!awaitingConfiguration) {
|
||||
processResponse(msg);
|
||||
return;
|
||||
}
|
||||
awaitingConfiguration = false;
|
||||
stopResponseTimeout();
|
||||
thingHandler.clearConfiguration();
|
||||
|
||||
LxConfig config = gson.fromJson(msg, LxConfig.class);
|
||||
config.finalize(thingHandler);
|
||||
|
||||
thingHandler.setMiniserverConfig(config);
|
||||
|
||||
if (sendCmdWithResp(CMD_ENABLE_UPDATES, false, false) == null) {
|
||||
disconnect(LxErrorCode.COMMUNICATION_ERROR, "Failed to enable state updates.");
|
||||
}
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Public methods, called by {@link LxControl} and {@link LxWsSecurity} child classes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse received message into a response structure. Check basic correctness of the response.
|
||||
*
|
||||
* @param msg received response message
|
||||
* @return parsed response message
|
||||
*/
|
||||
public LxResponse getResponse(String msg) {
|
||||
try {
|
||||
LxResponse resp = gson.fromJson(msg, LxResponse.class);
|
||||
if (!resp.isResponseOk()) {
|
||||
logger.debug("[{}] Miniserver response is not ok: {}", debugId, msg);
|
||||
return null;
|
||||
}
|
||||
return resp;
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("[{}] Miniserver response JSON parsing error: {}, {}", debugId, msg, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to the Miniserver and encrypts it if command can be encrypted and encryption is available.
|
||||
* Request can be synchronous or asynchronous. There is always a response expected to the command, and it is a
|
||||
* standard command response as defined in {@link LxResponse}. Such commands are the majority of commands
|
||||
* used for performing actions on the controls and for executing authentication procedure.
|
||||
* A synchronous command must not be sent from the websocket thread (from websocket callback methods) or it will
|
||||
* cause a deadlock.
|
||||
* An asynchronous command request returns immediately, but the returned value will not contain valid data in
|
||||
* the subResponse structure until a response is received. Asynchronous request can be sent from the websocket
|
||||
* thread. There can be only one command sent which awaits response per websocket connection,
|
||||
* whether this is synchronous or asynchronous command (this seems how Loxone Miniserver behaves, as it does not
|
||||
* have any unique identifier to match commands to responses).
|
||||
* For synchronous commands this is ensured naturally, for asynchronous the caller must manage it.
|
||||
* If this method is called before a response to the previous command is received, it will return error and not
|
||||
* send the command.
|
||||
*
|
||||
* @param command command to send to the Miniserver
|
||||
* @param sync true is synchronous request, false if ansynchronous
|
||||
* @param encrypt true if command can be encrypted (does not mean it will)
|
||||
* @return response received (for sync command) or to be received (for async), null if error occurred
|
||||
*/
|
||||
public LxResponse sendCmdWithResp(String command, boolean sync, boolean encrypt) {
|
||||
responseLock.lock();
|
||||
try {
|
||||
if (awaitedResponse != null || awaitingCommand != null) {
|
||||
logger.warn("[{}] Command not sent, previous command not finished: {}", debugId, command);
|
||||
return null;
|
||||
}
|
||||
if (!sendCmdNoResp(command, encrypt)) {
|
||||
return null;
|
||||
}
|
||||
LxResponse resp = new LxResponse();
|
||||
awaitingCommand = command;
|
||||
awaitedResponse = resp;
|
||||
syncRequest = sync;
|
||||
if (sync) {
|
||||
if (!responseAvailable.await(responseTimeout, TimeUnit.SECONDS)) {
|
||||
awaitedResponse = null;
|
||||
awaitingCommand = null;
|
||||
responseTimeout();
|
||||
return null;
|
||||
}
|
||||
awaitedResponse = null;
|
||||
awaitingCommand = null;
|
||||
}
|
||||
return resp;
|
||||
} catch (InterruptedException e) {
|
||||
logger.debug("[{}] Interrupted waiting for response: {}", debugId, command);
|
||||
awaitedResponse = null;
|
||||
awaitingCommand = null;
|
||||
return null;
|
||||
} finally {
|
||||
responseLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP GET request and return server's response.
|
||||
*
|
||||
* @param request request content
|
||||
* @return response received
|
||||
*/
|
||||
public String httpGet(String request) {
|
||||
HttpURLConnection con = null;
|
||||
try {
|
||||
URL url = new URL("http", host.getHostAddress(), port, request.startsWith("/") ? request : "/" + request);
|
||||
con = (HttpURLConnection) url.openConnection();
|
||||
con.setRequestMethod("GET");
|
||||
StringBuilder result = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
|
||||
String l;
|
||||
while ((l = reader.readLine()) != null) {
|
||||
result.append(l);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (con != null) {
|
||||
con.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Methods used by {@link LxServerHandler}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sends an action to a Loxone Miniserver's control.
|
||||
*
|
||||
* @param id identifier of the control
|
||||
* @param operation identifier of the operation
|
||||
* @throws IOException when communication error with Miniserver occurs
|
||||
*/
|
||||
void sendAction(LxUuid id, String operation) throws IOException {
|
||||
String command = CMD_ACTION + id.getOriginalString() + "/" + operation;
|
||||
logger.debug("[{}] Sending command {}", debugId, command);
|
||||
LxResponse response = sendCmdWithResp(command, true, true);
|
||||
if (response == null) {
|
||||
throw new IOException("Error sending command " + command);
|
||||
}
|
||||
if (!response.isResponseOk()) {
|
||||
if (response.getResponseCode() == LxErrorCode.USER_UNAUTHORIZED) {
|
||||
// we don't support per-control passwords, because the controls should have been filtered to remove
|
||||
// secured ones, it is an unexpected situation to receive this error code, but generally we can continue
|
||||
// operation
|
||||
logger.warn("[{}] User not authorised to operate on control {}", debugId, id);
|
||||
} else {
|
||||
throw new IOException("Received response is not ok to command " + command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send keep-alive message to the Miniserver
|
||||
*/
|
||||
void sendKeepAlive() {
|
||||
sendCmdNoResp(CMD_KEEPALIVE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Miniserver firmware version, if known.
|
||||
*
|
||||
* @param fwVersion Miniserver firmware version
|
||||
*/
|
||||
void setFwVersion(String fwVersion) {
|
||||
this.fwVersion = fwVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a timer to wait for a Miniserver response to an action sent from the binding.
|
||||
* When timer expires, connection is removed and server error is reported. Further connection attempt can be made
|
||||
* later by the upper layer.
|
||||
* If a previous timer is running, it will be stopped before a new timer is started.
|
||||
* The caller must take care of thread synchronization.
|
||||
*/
|
||||
void startResponseTimeout() {
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
stopResponseTimeout();
|
||||
timeout = SCHEDULER.schedule(this::responseTimeout, responseTimeout, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect websocket session - initiated from this end.
|
||||
*
|
||||
* @param code error code for disconnecting the websocket
|
||||
* @param reason reason for disconnecting the websocket
|
||||
*/
|
||||
void disconnect(LxErrorCode code, String reason) {
|
||||
logger.trace("[{}] disconnect the websocket: {}, {}", debugId, code, reason);
|
||||
// in case the disconnection happens from both connection ends, store and pass only the first reason
|
||||
if (offlineCode == null) {
|
||||
offlineCode = code;
|
||||
offlineReason = reason;
|
||||
}
|
||||
stopResponseTimeout();
|
||||
if (session != null) {
|
||||
logger.debug("[{}] Closing session", debugId);
|
||||
session.close(StatusCode.NORMAL, reason);
|
||||
logger.debug("[{}] Session closed", debugId);
|
||||
} else {
|
||||
logger.debug("[{}] Disconnecting websocket, but no session, reason : {}", debugId, reason);
|
||||
thingHandler.setOffline(LxErrorCode.COMMUNICATION_ERROR, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Private methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Stops scheduled timeout waiting for a Miniserver response
|
||||
* The caller must take care of thread synchronization.
|
||||
*/
|
||||
private void stopResponseTimeout() {
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
logger.trace("[{}] stopping response timeout", debugId);
|
||||
if (timeout != null) {
|
||||
timeout.cancel(true);
|
||||
timeout = null;
|
||||
}
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to the Miniserver and encrypts it if command can be encrypted and encryption is available.
|
||||
* The request is asynchronous and no response is expected (but it can arrive). It can be used to send commands
|
||||
* from the websocket thread or commands for which the responses are not following the standard format defined
|
||||
* in {@link LxResponse}.
|
||||
* If the caller expects the non-standard response it should manage its reception and the response timeout.
|
||||
*
|
||||
* @param command command to send to the Miniserver
|
||||
* @param encrypt true if command can be encrypted (does not mean it will)
|
||||
* @return true if command was sent (no information if it was received by the remote end)
|
||||
*/
|
||||
private boolean sendCmdNoResp(String command, boolean encrypt) {
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
if (session != null) {
|
||||
String encrypted;
|
||||
if (encrypt) {
|
||||
encrypted = security.encrypt(command);
|
||||
logger.debug("[{}] Sending encrypted string: {}", debugId, command);
|
||||
logger.debug("[{}] Encrypted: {}", debugId, encrypted);
|
||||
} else {
|
||||
logger.debug("[{}] Sending unencrypted string: {}", debugId, command);
|
||||
encrypted = command;
|
||||
}
|
||||
try {
|
||||
session.getRemote().sendString(encrypted);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
logger.debug("[{}] Error sending command: {}, {}", debugId, command, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
logger.debug("[{}] NOT sending command: {}", debugId, command);
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Miniserver's response to a command. The response is in plain text format as received from the
|
||||
* websocket, but is expected to follow the standard format defined in {@link LxResponse}.
|
||||
* If there is a thread waiting for the response (on a synchronous command request), the thread will be
|
||||
* released. Otherwise the response will be copied into the response object provided to the asynchronous
|
||||
* requester when the command was sent.
|
||||
* Only one requester is expected to wait for the response at a time - commands must be sent sequentially - a
|
||||
* command can be sent only after a response to the previous command was received, whether it was sent
|
||||
* synchronously or asynchronously.
|
||||
* If the received message is encrypted, it will be decrypted before processing.
|
||||
*
|
||||
* @param message websocket message with the response
|
||||
*/
|
||||
private void processResponse(String message) {
|
||||
LxResponse resp = getResponse(message);
|
||||
if (resp == null) {
|
||||
return;
|
||||
}
|
||||
logger.debug("[{}] Response: {}", debugId, message.trim());
|
||||
String control = resp.getCommand().trim();
|
||||
control = security.decryptControl(control);
|
||||
// for some reason the responses to some commands starting with jdev begin with dev, not jdev
|
||||
// this seems to be a bug in the Miniserver
|
||||
if (control.startsWith("dev/")) {
|
||||
control = "j" + control;
|
||||
}
|
||||
responseLock.lock();
|
||||
try {
|
||||
if (awaitedResponse == null || awaitingCommand == null) {
|
||||
logger.warn("[{}] Received response, but awaiting none.", debugId);
|
||||
return;
|
||||
}
|
||||
if (!awaitingCommand.equals(control)) {
|
||||
logger.warn("[{}] Waiting for another response: {}", debugId, awaitingCommand);
|
||||
return;
|
||||
}
|
||||
awaitedResponse.subResponse = resp.subResponse;
|
||||
if (syncRequest) {
|
||||
logger.debug("[{}] Releasing command sender with response: {}, {}", debugId, control,
|
||||
resp.getResponseCodeNumber());
|
||||
responseAvailable.signal();
|
||||
} else {
|
||||
logger.debug("[{}] Reponse to asynchronous request: {}, {}", debugId, control,
|
||||
resp.getResponseCodeNumber());
|
||||
awaitedResponse = null;
|
||||
awaitingCommand = null;
|
||||
}
|
||||
} finally {
|
||||
responseLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform actions after user authentication is successfully completed.
|
||||
* This method sends a request to receive Miniserver configuration.
|
||||
*/
|
||||
private void authenticated() {
|
||||
logger.debug("[{}] Websocket authentication successfull.", debugId);
|
||||
webSocketLock.lock();
|
||||
try {
|
||||
awaitingConfiguration = true;
|
||||
if (sendCmdNoResp(CMD_GET_APP_CONFIG, false)) {
|
||||
startResponseTimeout();
|
||||
} else {
|
||||
disconnect(LxErrorCode.INTERNAL_ERROR, "Error sending get config command.");
|
||||
}
|
||||
} finally {
|
||||
webSocketLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when response timeout occurred.
|
||||
*/
|
||||
private void responseTimeout() {
|
||||
logger.debug("[{}] Miniserver response timeout", debugId);
|
||||
disconnect(LxErrorCode.COMMUNICATION_ERROR, "Miniserver response timeout occured");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.binding.loxone.internal.LxServerHandlerApi;
|
||||
import org.openhab.binding.loxone.internal.types.LxCategory;
|
||||
import org.openhab.binding.loxone.internal.types.LxConfig;
|
||||
import org.openhab.binding.loxone.internal.types.LxContainer;
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* A control of Loxone Miniserver.
|
||||
* <p>
|
||||
* It represents a control object on the Miniserver. Controls can represent an input, functional block or an output of
|
||||
* the Miniserver, that is marked as visible in the Loxone UI. Controls can belong to a {@link LxContainer} room and a
|
||||
* {@link LxCategory} category.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControl {
|
||||
|
||||
/**
|
||||
* This class contains static configuration of the control and is used to make the fields transparent to the child
|
||||
* classes that implement specific controls.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public static class LxControlConfig {
|
||||
private final LxServerHandlerApi thingHandler;
|
||||
private final LxContainer room;
|
||||
private final LxCategory category;
|
||||
|
||||
LxControlConfig(LxControlConfig config) {
|
||||
this(config.thingHandler, config.room, config.category);
|
||||
}
|
||||
|
||||
public LxControlConfig(LxServerHandlerApi thingHandler, LxContainer room, LxCategory category) {
|
||||
this.room = room;
|
||||
this.category = category;
|
||||
this.thingHandler = thingHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to instantiate a particular control object by the {@link LxControlFactory}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
abstract static class LxControlInstance {
|
||||
/**
|
||||
* Creates an instance of a particular control class.
|
||||
*
|
||||
* @param uuid UUID of the control object to be created
|
||||
* @return a newly created control object
|
||||
*/
|
||||
abstract LxControl create(LxUuid uuid);
|
||||
|
||||
/**
|
||||
* Return a type name for this control.
|
||||
*
|
||||
* @return type name (as used on the Miniserver)
|
||||
*/
|
||||
abstract String getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* This class describes additional parameters of a control received from the Miniserver and is used during JSON
|
||||
* deserialization.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlDetails {
|
||||
Double min;
|
||||
Double max;
|
||||
Double step;
|
||||
String format;
|
||||
String actualFormat;
|
||||
String totalFormat;
|
||||
Boolean increaseOnly;
|
||||
String allOff;
|
||||
String url;
|
||||
String urlHd;
|
||||
Map<String, String> outputs;
|
||||
Boolean presenceConnected;
|
||||
Integer connectedInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback that should be implemented by child classes to process received commands. This callback can be
|
||||
* provided for each channel created by the controls.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface CommandCallback {
|
||||
abstract void handleCommand(Command cmd) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback that should be implemented by child classes to return current channel state. This callback can be
|
||||
* provided for each channel created by the controls.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface StateCallback {
|
||||
abstract State getChannelState();
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of callbacks registered per each channel by the child classes.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
private class Callbacks {
|
||||
private CommandCallback commandCallback;
|
||||
private StateCallback stateCallback;
|
||||
|
||||
private Callbacks(CommandCallback cC, StateCallback sC) {
|
||||
commandCallback = cC;
|
||||
stateCallback = sC;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Parameters parsed from the JSON configuration file during deserialization
|
||||
*/
|
||||
LxUuid uuid;
|
||||
LxControlDetails details;
|
||||
private String name;
|
||||
private LxUuid roomUuid;
|
||||
private Boolean isSecured;
|
||||
private LxUuid categoryUuid;
|
||||
private Map<LxUuid, LxControl> subControls;
|
||||
private final Map<String, LxState> states;
|
||||
|
||||
/*
|
||||
* Parameters set when finalizing {@link LxConfig} object setup. They will be null right after constructing object.
|
||||
*/
|
||||
transient String defaultChannelLabel;
|
||||
private transient LxControlConfig config;
|
||||
|
||||
/*
|
||||
* Parameters set when object is connected to the openHAB by the binding handler
|
||||
*/
|
||||
final transient Set<String> tags = new HashSet<>();
|
||||
private final transient List<Channel> channels = new ArrayList<>();
|
||||
private final transient Map<ChannelUID, Callbacks> callbacks = new HashMap<>();
|
||||
|
||||
private final transient Logger logger;
|
||||
private int numberOfChannels = 0;
|
||||
|
||||
/*
|
||||
* JSON deserialization routine, called during parsing configuration by the GSON library
|
||||
*/
|
||||
public static final JsonDeserializer<LxControl> DESERIALIZER = new JsonDeserializer<LxControl>() {
|
||||
@Override
|
||||
public LxControl deserialize(JsonElement json, Type type, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
JsonObject parent = json.getAsJsonObject();
|
||||
String controlName = LxConfig.deserializeString(parent, "name");
|
||||
String controlType = LxConfig.deserializeString(parent, "type");
|
||||
LxUuid uuid = LxConfig.deserializeObject(parent, "uuidAction", LxUuid.class, context);
|
||||
if (controlName == null || controlType == null || uuid == null) {
|
||||
throw new JsonParseException("Control name/type/uuid is null.");
|
||||
}
|
||||
LxControl control = LxControlFactory.createControl(uuid, controlType);
|
||||
if (control == null) {
|
||||
return null;
|
||||
}
|
||||
control.name = controlName;
|
||||
control.isSecured = LxConfig.deserializeObject(parent, "isSecured", Boolean.class, context);
|
||||
control.roomUuid = LxConfig.deserializeObject(parent, "room", LxUuid.class, context);
|
||||
control.categoryUuid = LxConfig.deserializeObject(parent, "cat", LxUuid.class, context);
|
||||
control.details = LxConfig.deserializeObject(parent, "details", LxControlDetails.class, context);
|
||||
control.subControls = LxConfig.deserializeObject(parent, "subControls",
|
||||
new TypeToken<Map<LxUuid, LxControl>>() {
|
||||
}.getType(), context);
|
||||
|
||||
JsonObject states = parent.getAsJsonObject("states");
|
||||
if (states != null) {
|
||||
states.entrySet().forEach(entry -> {
|
||||
// temperature state of intelligent home controller object is the only
|
||||
// one that has state represented as an array, as this is not implemented
|
||||
// yet, we will skip this state
|
||||
JsonElement element = entry.getValue();
|
||||
if (element != null && !(element instanceof JsonArray)) {
|
||||
String value = element.getAsString();
|
||||
if (value != null) {
|
||||
String name = entry.getKey().toLowerCase();
|
||||
control.states.put(name, new LxState(new LxUuid(value), name, control));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return control;
|
||||
}
|
||||
};
|
||||
|
||||
LxControl(LxUuid uuid) {
|
||||
logger = LoggerFactory.getLogger(LxControl.class);
|
||||
this.uuid = uuid;
|
||||
states = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that executes commands by the control. It delegates command execution to a registered callback method.
|
||||
*
|
||||
* @param channelId channel Id for the command
|
||||
* @param command value of the command to perform
|
||||
* @throws IOException in case of communication error with the Miniserver
|
||||
*/
|
||||
public final void handleCommand(ChannelUID channelId, Command command) throws IOException {
|
||||
Callbacks c = callbacks.get(channelId);
|
||||
if (c != null && c.commandCallback != null) {
|
||||
c.commandCallback.handleCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides actual state value for the specified channel. It delegates execution to a registered callback method.
|
||||
*
|
||||
* @param channelId channel ID to get state for
|
||||
* @return state if the channel value or null if no value available
|
||||
*/
|
||||
public final State getChannelState(ChannelUID channelId) {
|
||||
Callbacks c = callbacks.get(channelId);
|
||||
if (c != null && c.stateCallback != null) {
|
||||
try {
|
||||
return c.stateCallback.getChannelState();
|
||||
} catch (NumberFormatException e) {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain control's name
|
||||
*
|
||||
* @return Human readable name of control
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control's UUID as defined on the Miniserver
|
||||
*
|
||||
* @return UUID of the control
|
||||
*/
|
||||
public LxUuid getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subcontrols of this control
|
||||
*
|
||||
* @return subcontrols of the control
|
||||
*/
|
||||
public Map<LxUuid, LxControl> getSubControls() {
|
||||
return subControls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control's channels
|
||||
*
|
||||
* @return channels
|
||||
*/
|
||||
public List<Channel> getChannels() {
|
||||
return channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control's and its subcontrols' channels
|
||||
*
|
||||
* @return channels
|
||||
*/
|
||||
public List<Channel> getChannelsWithSubcontrols() {
|
||||
final List<Channel> list = new ArrayList<>(channels);
|
||||
subControls.values().forEach(c -> list.addAll(c.getChannelsWithSubcontrols()));
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control's Miniserver states
|
||||
*
|
||||
* @return control's Miniserver states
|
||||
*/
|
||||
public Map<String, LxState> getStates() {
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information is password is required to operate on this control object
|
||||
*
|
||||
* @return true is control is secured
|
||||
*/
|
||||
public Boolean isSecured() {
|
||||
return isSecured != null && isSecured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UUID's of two controls -
|
||||
*
|
||||
* @param object Object to compare with
|
||||
* @return true if UUID of two objects are equal
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (object == null) {
|
||||
return false;
|
||||
}
|
||||
if (object.getClass() != getClass()) {
|
||||
return false;
|
||||
}
|
||||
LxControl c = (LxControl) object;
|
||||
return Objects.equals(c.uuid, uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash code of the control is equal to its UUID's hash code
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uuid.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Miniserver's control in runtime. Each class that implements {@link LxControl} should override this
|
||||
* method and call it as a first step in the overridden implementation. Then it should add all runtime data, like
|
||||
* channels and any fields that derive their value from the parsed JSON configuration.
|
||||
* Before this method is called during configuration parsing, the control object must not be used.
|
||||
*
|
||||
* @param configToSet control's configuration
|
||||
*/
|
||||
public void initialize(LxControlConfig configToSet) {
|
||||
logger.debug("Initializing LxControl: {}", uuid);
|
||||
|
||||
if (config != null) {
|
||||
logger.error("Error, attempt to initialize control that is already initialized: {}", uuid);
|
||||
return;
|
||||
}
|
||||
config = configToSet;
|
||||
|
||||
if (subControls == null) {
|
||||
subControls = new HashMap<>();
|
||||
} else {
|
||||
subControls.values().removeIf(Objects::isNull);
|
||||
}
|
||||
|
||||
if (config.room != null) {
|
||||
config.room.addControl(this);
|
||||
}
|
||||
|
||||
if (config.category != null) {
|
||||
config.category.addControl(this);
|
||||
}
|
||||
|
||||
String label = getLabel();
|
||||
if (label == null) {
|
||||
// Each control on a Miniserver must have a name defined, but in case this is a subject
|
||||
// of some malicious data attack, we'll prevent null pointer exception
|
||||
label = "Undefined name";
|
||||
}
|
||||
String roomName = config.room != null ? config.room.getName() : null;
|
||||
if (roomName != null) {
|
||||
label = roomName + " / " + label;
|
||||
}
|
||||
defaultChannelLabel = label;
|
||||
|
||||
// Propagate to all subcontrols of this control object
|
||||
subControls.values().forEach(c -> c.initialize(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will be called from {@link LxState}, when Miniserver state value is updated.
|
||||
* By default it will query all channels of the control and update their state accordingly.
|
||||
* This method will not handle channel state descriptions, as they must be prepared individually.
|
||||
* It can be overridden in child class to handle particular states differently.
|
||||
*
|
||||
* @param state changed Miniserver state or null if not specified (any/all)
|
||||
*/
|
||||
public void onStateChange(LxState state) {
|
||||
if (config == null) {
|
||||
logger.error("Attempt to change state with not finalized configuration!: {}", state.getUuid());
|
||||
} else {
|
||||
channels.forEach(channel -> {
|
||||
ChannelUID channelId = channel.getUID();
|
||||
State channelState = getChannelState(channelId);
|
||||
if (channelState != null) {
|
||||
config.thingHandler.setChannelState(channelId, channelState);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets room UUID after it was deserialized by GSON
|
||||
*
|
||||
* @return room UUID
|
||||
*/
|
||||
public LxUuid getRoomUuid() {
|
||||
return roomUuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets category UUID after it was deserialized by GSON
|
||||
*
|
||||
* @return category UUID
|
||||
*/
|
||||
public LxUuid getCategoryUuid() {
|
||||
return categoryUuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a GSON object for reuse
|
||||
*
|
||||
* @return GSON object
|
||||
*/
|
||||
Gson getGson() {
|
||||
if (config == null) {
|
||||
logger.error("Attempt to get GSON from not finalized configuration!");
|
||||
return null;
|
||||
}
|
||||
return config.thingHandler.getGson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new control in the framework. Called when a control is dynamically created based on some control's state
|
||||
* changes from the Miniserver.
|
||||
*
|
||||
* @param control a new control to be created
|
||||
*/
|
||||
static void addControl(LxControl control) {
|
||||
control.config.thingHandler.addControl(control);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a control from the framework. Called when a control is dynamically deleted based on some control's state
|
||||
* changes from the Miniserver.
|
||||
*
|
||||
* @param control a control to be removed
|
||||
*/
|
||||
static void removeControl(LxControl control) {
|
||||
control.config.thingHandler.removeControl(control);
|
||||
control.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets control's configuration
|
||||
*
|
||||
* @return configuration
|
||||
*/
|
||||
LxControlConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control's room.
|
||||
*
|
||||
* @return control's room object
|
||||
*/
|
||||
LxContainer getRoom() {
|
||||
return config.room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control's category.
|
||||
*
|
||||
* @return control's category object
|
||||
*/
|
||||
LxCategory getCategory() {
|
||||
return config.category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the channel state in the framework.
|
||||
*
|
||||
* @param id channel ID
|
||||
* @param state new state value
|
||||
*/
|
||||
void setChannelState(ChannelUID id, State state) {
|
||||
if (config == null) {
|
||||
logger.error("Attempt to set channel state with not finalized configuration!: {}", id);
|
||||
} else {
|
||||
if (state != null) {
|
||||
config.thingHandler.setChannelState(id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns control label that will be used for building channel name. This allows for customizing the label per
|
||||
* control by overriding this method, but keeping {@link LxControl#getName()} intact.
|
||||
*
|
||||
* @return control channel label
|
||||
*/
|
||||
String getLabel() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets value of a state object of given name, if exists
|
||||
*
|
||||
* @param name name of state object
|
||||
* @return state object's value
|
||||
*/
|
||||
Double getStateDoubleValue(String name) {
|
||||
LxState state = states.get(name);
|
||||
if (state != null) {
|
||||
Object value = state.getStateValue();
|
||||
if (value instanceof Double) {
|
||||
return (Double) value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets value of a state object of given name, if exists, and converts it to decimal type value.
|
||||
*
|
||||
* @param name state name
|
||||
* @return state value
|
||||
*/
|
||||
State getStateDecimalValue(String name) {
|
||||
Double value = getStateDoubleValue(name);
|
||||
if (value != null) {
|
||||
return new DecimalType(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets text value of a state object of given name, if exists
|
||||
*
|
||||
* @param name name of state object
|
||||
* @return state object's text value
|
||||
*/
|
||||
String getStateTextValue(String name) {
|
||||
LxState state = states.get(name);
|
||||
if (state != null) {
|
||||
Object value = state.getStateValue();
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets text value of a state object of given name, if exists and converts it to string type
|
||||
*
|
||||
* @param name name of state object
|
||||
* @return state object's text value
|
||||
*/
|
||||
State getStateStringValue(String name) {
|
||||
String value = getStateTextValue(name);
|
||||
if (value != null) {
|
||||
return new StringType(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets double value of a state object of given name, if exists and converts it to switch type
|
||||
*
|
||||
* @param name name of state object
|
||||
* @return state object's text value
|
||||
*/
|
||||
State getStateOnOffValue(String name) {
|
||||
Double value = getStateDoubleValue(name);
|
||||
if (value != null) {
|
||||
if (value == 1.0) {
|
||||
return OnOffType.ON;
|
||||
}
|
||||
return OnOffType.OFF;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new channel and add it to the control. Channel ID is assigned automatically in the order of calls to
|
||||
* this method, see (@link LxControl#getChannelId}.
|
||||
*
|
||||
* @param itemType item type for the channel
|
||||
* @param typeId channel type ID for the channel
|
||||
* @param channelLabel channel label
|
||||
* @param channelDescription channel description
|
||||
* @param tags tags for the channel or null if no tags needed
|
||||
* @param commandCallback {@link LxControl} child class method that will be called when command is received
|
||||
* @param stateCallback {@link LxControl} child class method that will be called to get state value
|
||||
* @return channel ID of the added channel (can be used to later set state description to it)
|
||||
*/
|
||||
ChannelUID addChannel(String itemType, ChannelTypeUID typeId, String channelLabel, String channelDescription,
|
||||
Set<String> tags, CommandCallback commandCallback, StateCallback stateCallback) {
|
||||
if (channelLabel == null || channelDescription == null) {
|
||||
logger.error("Attempt to add channel with not finalized configuration!: {}", channelLabel);
|
||||
return null;
|
||||
}
|
||||
ChannelUID channelId = getChannelId(numberOfChannels++);
|
||||
ChannelBuilder builder = ChannelBuilder.create(channelId, itemType).withType(typeId).withLabel(channelLabel)
|
||||
.withDescription(channelDescription + " : " + channelLabel);
|
||||
if (tags != null) {
|
||||
builder.withDefaultTags(tags);
|
||||
}
|
||||
channels.add(builder.build());
|
||||
if (commandCallback != null || stateCallback != null) {
|
||||
callbacks.put(channelId, new Callbacks(commandCallback, stateCallback));
|
||||
}
|
||||
return channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link StateDescriptionFragment} for a channel that has multiple options to select from or a custom
|
||||
* format string.
|
||||
*
|
||||
* @param channelId channel ID to add the description for
|
||||
* @param descriptionFragment channel state description fragment
|
||||
*/
|
||||
void addChannelStateDescriptionFragment(ChannelUID channelId, StateDescriptionFragment descriptionFragment) {
|
||||
if (config == null) {
|
||||
logger.error("Attempt to set channel state description with not finalized configuration!: {}", channelId);
|
||||
} else {
|
||||
if (channelId != null && descriptionFragment != null) {
|
||||
config.thingHandler.setChannelStateDescription(channelId, descriptionFragment.toStateDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an action command to the Miniserver using active socket connection
|
||||
*
|
||||
* @param action string with action command
|
||||
* @throws IOException when communication error with Miniserver occurs
|
||||
*/
|
||||
void sendAction(String action) throws IOException {
|
||||
if (config == null) {
|
||||
logger.error("Attempt to send command with not finalized configuration!: {}", action);
|
||||
} else {
|
||||
config.thingHandler.sendAction(uuid, action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all channels from the control. This method is used by child classes that may decide to stop exposing any
|
||||
* channels, for example by {@link LxControlMood}, which is based on {@link LxControlSwitch}, but sometime does not
|
||||
* expose anything to the user.
|
||||
*/
|
||||
void removeAllChannels() {
|
||||
channels.clear();
|
||||
callbacks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when control is no more needed - unlink it from containers
|
||||
*/
|
||||
private void dispose() {
|
||||
if (config.room != null) {
|
||||
config.room.removeControl(this);
|
||||
}
|
||||
if (config.category != null) {
|
||||
config.category.removeControl(this);
|
||||
}
|
||||
subControls.values().forEach(control -> control.dispose());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build channel ID for the control, based on control's UUID, thing's UUID and index of the channel for the control
|
||||
*
|
||||
* @param index index of a channel within control (0 for primary channel) all indexes greater than 0 will have
|
||||
* -index added to the channel ID
|
||||
* @return channel ID for the control and index
|
||||
*/
|
||||
private ChannelUID getChannelId(int index) {
|
||||
if (config == null) {
|
||||
logger.error("Attempt to get control's channel ID with not finalized configuration!: {}", index);
|
||||
return null;
|
||||
}
|
||||
String controlId = uuid.toString();
|
||||
if (index > 0) {
|
||||
controlId += "-" + index;
|
||||
}
|
||||
return new ChannelUID(config.thingHandler.getThingId(), controlId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Loxone Control that controls the Burglar Alarm
|
||||
*
|
||||
* @author Michael Mattan - Initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlAlarm extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlAlarm(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "alarm";
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CMD_ON = "on";
|
||||
private static final String CMD_ON_WITH_MOVEMENT = "on/1";
|
||||
private static final String CMD_ON_WITHOUT_MOVEMENT = "on/0";
|
||||
private static final String CMD_DELAYED_ON = "delayedon";
|
||||
private static final String CMD_DELAYED_ON_WITH_MOVEMENT = "delayedon/1";
|
||||
private static final String CMD_DELAYED_ON_WITHOUT_MOVEMENT = "delayedon/0";
|
||||
private static final String CMD_OFF = "off";
|
||||
private static final String CMD_QUIT = "quit";
|
||||
private static final String CMD_DISABLE_MOVEMENT = "dismv/1";
|
||||
private static final String CMD_ENABLE_MOVEMENT = "dismv/0";
|
||||
|
||||
/**
|
||||
* If the alarm control is armed
|
||||
*/
|
||||
private static final String STATE_ARMED = "armed";
|
||||
|
||||
/**
|
||||
* The id of the next alarm level
|
||||
*/
|
||||
private static final String STATE_NEXT_LEVEL = "nextlevel";
|
||||
|
||||
/**
|
||||
* The delay of the next level in seconds
|
||||
*/
|
||||
private static final String STATE_NEXT_LEVEL_DELAY = "nextleveldelay";
|
||||
|
||||
/**
|
||||
* The total delay of the next level in seconds
|
||||
*/
|
||||
private static final String STATE_NEXT_LEVEL_DELAY_TOTAL = "nextleveldelaytotal";
|
||||
|
||||
/**
|
||||
* The id of the current alarm level
|
||||
*/
|
||||
private static final String STATE_LEVEL = "level";
|
||||
|
||||
/**
|
||||
* Timestamp when alarm started
|
||||
*/
|
||||
private static final String STATE_START_TIME = "starttime";
|
||||
|
||||
/**
|
||||
* The delay of the alarm control being armed
|
||||
*/
|
||||
private static final String STATE_ARMED_DELAY = "armeddelay";
|
||||
|
||||
/**
|
||||
* The total delay of the alarm control being armed
|
||||
*/
|
||||
private static final String STATE_ARMED_DELAY_TOTAL = "armeddelaytotal";
|
||||
|
||||
/**
|
||||
* A string of sensors separated by a pipe
|
||||
*/
|
||||
private static final String STATE_SENSOR = "sensors";
|
||||
|
||||
/**
|
||||
* If the movement is disabled or not
|
||||
*/
|
||||
private static final String STATE_DISABLED_MOVE = "disabledmove";
|
||||
|
||||
private ChannelUID startTimeId;
|
||||
private ChannelUID ackChannelId;
|
||||
private State startTime = UnDefType.UNDEF;
|
||||
private boolean presenceConnected = false;
|
||||
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private LxControlAlarm(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
if (details.presenceConnected != null) {
|
||||
presenceConnected = details.presenceConnected;
|
||||
}
|
||||
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH), defaultChannelLabel,
|
||||
"Alarm armed", tags, this::handleArmAlarm, () -> getStateOnOffValue(STATE_ARMED));
|
||||
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + " / Arm Delayed", "Arm with delay", tags, this::handleArmDelayedAlarm,
|
||||
() -> OnOffType.OFF);
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Next Level", "ID of the next alarm level", tags, null,
|
||||
() -> getStateDecimalValue(STATE_NEXT_LEVEL));
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Next Level Delay", "Delay of the next level", tags, null,
|
||||
() -> getStateDecimalValue(STATE_NEXT_LEVEL_DELAY));
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Next Level Delay Total", "Total delay of the next level", tags, null,
|
||||
() -> getStateDecimalValue(STATE_NEXT_LEVEL_DELAY_TOTAL));
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Level", "Current alarm level", tags, null,
|
||||
() -> getStateDecimalValue(STATE_LEVEL));
|
||||
|
||||
startTimeId = addChannel("DateTime", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_DATETIME),
|
||||
defaultChannelLabel + " / Start Time", "Time when alarm started", tags, null, () -> startTime);
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Armed Delay", "Delay of the alarm being armed", tags, null,
|
||||
() -> getStateDecimalValue(STATE_ARMED_DELAY));
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Armed Total Delay", "Total delay of the alarm being armed", tags, null,
|
||||
() -> getStateDecimalValue(STATE_ARMED_DELAY_TOTAL));
|
||||
|
||||
addChannel("String", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_TEXT),
|
||||
defaultChannelLabel + " / Sensors", "Alarm sensors", tags, null,
|
||||
() -> getStateStringValue(STATE_SENSOR));
|
||||
|
||||
ackChannelId = addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + " / Acknowledge", "Acknowledge alarm", tags, this::handleQuitAlarm,
|
||||
() -> OnOffType.OFF);
|
||||
addChannelStateDescriptionFragment(ackChannelId,
|
||||
StateDescriptionFragmentBuilder.create().withReadOnly(true).build());
|
||||
|
||||
if (presenceConnected) {
|
||||
// this channel has reversed logic - we show state of enabled option, but receive state updates if disabled
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + " / Motion Sensors", "Motion sensors enabled", tags,
|
||||
this::handleMotionSensors, this::getStateMotionSensors);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateChange(LxState state) {
|
||||
String stateName = state.getName();
|
||||
if (STATE_START_TIME.equals(stateName)) {
|
||||
startTime = UnDefType.UNDEF;
|
||||
Object obj = state.getStateValue();
|
||||
if (obj instanceof String && !((String) obj).isEmpty()) {
|
||||
try {
|
||||
LocalDateTime ldt = LocalDateTime.parse((String) obj, dateTimeFormatter);
|
||||
ZonedDateTime dt = ldt.atZone(ZoneId.systemDefault());
|
||||
startTime = new DateTimeType(dt);
|
||||
} catch (DateTimeParseException e) {
|
||||
startTime = null;
|
||||
}
|
||||
}
|
||||
setChannelState(startTimeId, startTime);
|
||||
} else if (STATE_LEVEL.equals(stateName)) {
|
||||
Object obj = state.getStateValue();
|
||||
addChannelStateDescriptionFragment(ackChannelId, StateDescriptionFragmentBuilder.create()
|
||||
.withReadOnly(obj instanceof Double && ((Double) obj) == 0.0).build());
|
||||
super.onStateChange(state);
|
||||
} else {
|
||||
super.onStateChange(state);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleArming(Command command, String onAction, String onWithMovementAction,
|
||||
String onWithoutMovementAction) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
if (presenceConnected) {
|
||||
Double value = getStateDoubleValue(STATE_DISABLED_MOVE);
|
||||
if (value == null || value == 1.0) {
|
||||
sendAction(onWithoutMovementAction);
|
||||
} else {
|
||||
sendAction(onWithMovementAction);
|
||||
}
|
||||
} else {
|
||||
sendAction(onAction);
|
||||
}
|
||||
} else {
|
||||
sendAction(CMD_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleArmAlarm(Command command) throws IOException {
|
||||
handleArming(command, CMD_ON, CMD_ON_WITH_MOVEMENT, CMD_ON_WITHOUT_MOVEMENT);
|
||||
}
|
||||
|
||||
private void handleArmDelayedAlarm(Command command) throws IOException {
|
||||
handleArming(command, CMD_DELAYED_ON, CMD_DELAYED_ON_WITH_MOVEMENT, CMD_DELAYED_ON_WITHOUT_MOVEMENT);
|
||||
}
|
||||
|
||||
private void handleQuitAlarm(Command command) throws IOException {
|
||||
if (command instanceof OnOffType && command == OnOffType.ON) {
|
||||
sendAction(CMD_QUIT);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMotionSensors(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
sendAction(CMD_ENABLE_MOVEMENT);
|
||||
} else {
|
||||
sendAction(CMD_DISABLE_MOVEMENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private State getStateMotionSensors() {
|
||||
Double value = getStateDoubleValue(STATE_DISABLED_MOVE);
|
||||
if (value != null) {
|
||||
if (value == 1.0) {
|
||||
return OnOffType.OFF;
|
||||
}
|
||||
return OnOffType.ON;
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxTemperatureHSBType;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* A Color Picker V2 type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a color picker control covers:
|
||||
* <ul>
|
||||
* <li>Color (Hue/Saturation/Brightness)
|
||||
* </ul>
|
||||
*
|
||||
* @author Michael Mattan - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlColorPickerV2 extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlColorPickerV2(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "colorpickerv2";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color state
|
||||
*/
|
||||
private static final String STATE_COLOR = "color";
|
||||
|
||||
private LxControlColorPickerV2(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
addChannel("Color", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_COLORPICKER), defaultChannelLabel,
|
||||
"Color Picker", tags, this::handleCommands, this::getColor);
|
||||
}
|
||||
|
||||
private void handleCommands(Command command) throws IOException {
|
||||
if (command instanceof HSBType) {
|
||||
setColor((HSBType) command);
|
||||
} else if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
on();
|
||||
} else {
|
||||
off();
|
||||
}
|
||||
} else if (command instanceof DecimalType) {
|
||||
setBrightness((DecimalType) command);
|
||||
} else if (command instanceof PercentType) {
|
||||
setBrightness((PercentType) command);
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
|
||||
increaseDecreaseBrightness(1);
|
||||
} else {
|
||||
increaseDecreaseBrightness(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current Loxone color in HSBType format
|
||||
*
|
||||
* @return the HSBType color
|
||||
*/
|
||||
private HSBType getColor() {
|
||||
HSBType hsbColor = null;
|
||||
String color = getStateTextValue(STATE_COLOR);
|
||||
if (color != null) {
|
||||
hsbColor = this.mapLoxoneToOH(color);
|
||||
}
|
||||
return hsbColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of the color picker
|
||||
*
|
||||
* @param hsb the color to set
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void setColor(HSBType hsb) throws IOException {
|
||||
HSBType currentColor = getColor();
|
||||
if (currentColor == null || !currentColor.toString().equals(hsb.toString())) {
|
||||
// only update the color when it changed
|
||||
// this prevents a mood switch in the Light Controller when the color did not change anyway
|
||||
sendAction("hsv(" + hsb.toString() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color picker to on
|
||||
*
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void on() throws IOException {
|
||||
HSBType currentColor = getColor();
|
||||
if (currentColor != null) {
|
||||
setColor(new HSBType(currentColor.getHue(), currentColor.getSaturation(), PercentType.HUNDRED));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color picker to off
|
||||
*
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void off() throws IOException {
|
||||
HSBType currentColor = getColor();
|
||||
if (currentColor != null) {
|
||||
setColor(new HSBType(currentColor.getHue(), currentColor.getSaturation(), PercentType.ZERO));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set the brightness level
|
||||
*
|
||||
* @param p the brightness percentage
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void setBrightness(PercentType p) throws IOException {
|
||||
HSBType currentColor = this.getColor();
|
||||
if (currentColor != null) {
|
||||
setColor(new HSBType(currentColor.getHue(), currentColor.getSaturation(), p));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set the brightness level from a decimal type
|
||||
*
|
||||
* @param d the brightness in decimal
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void setBrightness(DecimalType d) throws IOException {
|
||||
setBrightness(new PercentType(d.toBigDecimal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases/decreases the brightness with a given step
|
||||
*
|
||||
* @param step the amount to increase/decrease
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void increaseDecreaseBrightness(int step) throws IOException {
|
||||
HSBType currentColor = this.getColor();
|
||||
if (currentColor != null) {
|
||||
setBrightness(new PercentType(currentColor.getBrightness().intValue() + step));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Loxone color to OpenHab HSBType
|
||||
*
|
||||
* @param color color in format hsb(h,s,v) or temp(brightness,kelvin)
|
||||
* @return HSBType
|
||||
*/
|
||||
private HSBType mapLoxoneToOH(String color) {
|
||||
HSBType hsbColor = null;
|
||||
|
||||
try {
|
||||
Pattern hsvPattern = Pattern.compile("^hsv\\([0-9]\\d{0,2},[0-9]\\d{0,2},[0-9]\\d{0,2}\\)");
|
||||
Pattern tempPattern = Pattern.compile("^temp\\([0-9]\\d{0,2},[0-9]\\d{0,4}\\)");
|
||||
Matcher valueMatcher = Pattern.compile("\\((.*?)\\)").matcher(color);
|
||||
|
||||
if (hsvPattern.matcher(color).matches() && valueMatcher.find()) {
|
||||
// we have a hsv(hue,saturation,value) pattern
|
||||
hsbColor = new HSBType(valueMatcher.group(1));
|
||||
} else if (tempPattern.matcher(color).matches() && valueMatcher.find()) {
|
||||
// we have a temp(brightness,kelvin) pattern
|
||||
hsbColor = LxTemperatureHSBType.fromBrightnessTemperature(valueMatcher.group(1));
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// an error happened during HSBType creation, we return null
|
||||
hsbColor = null;
|
||||
}
|
||||
return hsbColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxCategory;
|
||||
import org.openhab.binding.loxone.internal.types.LxTags;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* A dimmer type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a dimmer control is:
|
||||
* <ul>
|
||||
* <li>a virtual input of dimmer type
|
||||
* </ul>
|
||||
*
|
||||
* @author Stephan Brunner - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlDimmer extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlDimmer(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "dimmer";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* States
|
||||
*/
|
||||
private static final String STATE_POSITION = "position";
|
||||
private static final String STATE_MIN = "min";
|
||||
private static final String STATE_MAX = "max";
|
||||
private static final String STATE_STEP = "step";
|
||||
|
||||
/**
|
||||
* Command string used to set the dimmer ON
|
||||
*/
|
||||
private static final String CMD_ON = "On";
|
||||
/**
|
||||
* Command string used to set the dimmer to OFF
|
||||
*/
|
||||
private static final String CMD_OFF = "Off";
|
||||
|
||||
private LxControlDimmer(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
LxCategory category = getCategory();
|
||||
if (category != null && category.getType() == LxCategory.CategoryType.LIGHTS) {
|
||||
tags.addAll(LxTags.LIGHTING);
|
||||
}
|
||||
addChannel("Dimmer", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_DIMMER), defaultChannelLabel,
|
||||
"Dimmer", tags, this::handleCommands, this::getChannelState);
|
||||
}
|
||||
|
||||
private void handleCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
sendAction(CMD_ON);
|
||||
} else {
|
||||
sendAction(CMD_OFF);
|
||||
}
|
||||
} else if (command instanceof PercentType) {
|
||||
PercentType percentCmd = (PercentType) command;
|
||||
setPosition(percentCmd.doubleValue());
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
Double value = getStateDoubleValue(STATE_POSITION);
|
||||
Double min = getStateDoubleValue(STATE_MIN);
|
||||
Double max = getStateDoubleValue(STATE_MAX);
|
||||
Double step = getStateDoubleValue(STATE_STEP);
|
||||
if (value != null && max != null && min != null && step != null && min >= 0 && max >= 0 && max > min) {
|
||||
if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) {
|
||||
value += step;
|
||||
if (value > max) {
|
||||
value = max;
|
||||
}
|
||||
} else {
|
||||
value -= step;
|
||||
if (value < min) {
|
||||
value = min;
|
||||
}
|
||||
}
|
||||
sendAction(value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PercentType getChannelState() {
|
||||
Double value = mapLoxoneToOH(getStateDoubleValue(STATE_POSITION));
|
||||
if (value != null && value >= 0 && value <= 100) {
|
||||
return new PercentType(value.intValue());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current position of the dimmer
|
||||
*
|
||||
* @param position position to move to (0-100, 0 - full off, 100 - full on)
|
||||
* @throws IOException error communicating with the Miniserver
|
||||
*/
|
||||
private void setPosition(Double position) throws IOException {
|
||||
Double loxonePosition = mapOHToLoxone(position);
|
||||
if (loxonePosition != null) {
|
||||
sendAction(loxonePosition.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private Double mapLoxoneToOH(Double loxoneValue) {
|
||||
if (loxoneValue != null) {
|
||||
// 0 means turn dimmer off, any value above zero should be mapped from min-max range
|
||||
if (Double.compare(loxoneValue, 0.0) == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
Double max = getStateDoubleValue(STATE_MAX);
|
||||
Double min = getStateDoubleValue(STATE_MIN);
|
||||
if (max != null && min != null && max > min && min >= 0 && max >= 0) {
|
||||
return 100 * (loxoneValue - min) / (max - min);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Double mapOHToLoxone(Double ohValue) {
|
||||
if (ohValue != null) {
|
||||
// 0 means turn dimmer off, any value above zero should be mapped to min-max range
|
||||
if (Double.compare(ohValue, 0.0) == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
Double max = getStateDoubleValue(STATE_MAX);
|
||||
Double min = getStateDoubleValue(STATE_MIN);
|
||||
if (max != null && min != null) {
|
||||
double value = min + ohValue * (max - min) / 100;
|
||||
return value; // no rounding to integer value is needed as loxone is accepting floating point values
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl.LxControlInstance;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
|
||||
/**
|
||||
* A factory of controls of Loxone Miniserver.
|
||||
* It creates various types of control objects based on control type received from Miniserver.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlFactory {
|
||||
static {
|
||||
CONTROLS = new HashMap<>();
|
||||
add(new LxControlAlarm.Factory());
|
||||
add(new LxControlColorPickerV2.Factory());
|
||||
add(new LxControlDimmer.Factory());
|
||||
add(new LxControlInfoOnlyAnalog.Factory());
|
||||
add(new LxControlInfoOnlyDigital.Factory());
|
||||
add(new LxControlIRoomControllerV2.Factory());
|
||||
add(new LxControlJalousie.Factory());
|
||||
add(new LxControlLeftRightAnalog.Factory());
|
||||
add(new LxControlLeftRightDigital.Factory());
|
||||
add(new LxControlLightController.Factory());
|
||||
add(new LxControlLightControllerV2.Factory());
|
||||
add(new LxControlMeter.Factory());
|
||||
add(new LxControlPushbutton.Factory());
|
||||
add(new LxControlRadio.Factory());
|
||||
add(new LxControlSlider.Factory());
|
||||
add(new LxControlSwitch.Factory());
|
||||
add(new LxControlTextState.Factory());
|
||||
add(new LxControlTimedSwitch.Factory());
|
||||
add(new LxControlTracker.Factory());
|
||||
add(new LxControlUpDownAnalog.Factory());
|
||||
add(new LxControlUpDownDigital.Factory());
|
||||
add(new LxControlValueSelector.Factory());
|
||||
add(new LxControlWebPage.Factory());
|
||||
}
|
||||
|
||||
private static final Map<String, LxControlInstance> CONTROLS;
|
||||
|
||||
/**
|
||||
* Create a {@link LxControl} object for a control received from the Miniserver
|
||||
*
|
||||
* @param uuid UUID of the control to create
|
||||
* @param type control type
|
||||
* @return created control object or null if error
|
||||
*/
|
||||
static LxControl createControl(LxUuid uuid, String type) {
|
||||
LxControlInstance control = CONTROLS.get(type.toLowerCase());
|
||||
if (control != null) {
|
||||
return control.create(uuid);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void add(LxControlInstance control) {
|
||||
CONTROLS.put(control.getType(), control);
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxTags;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* An Intelligent Room Controller V2.
|
||||
* This implementation does not support the timers which describe temperature zones during the day.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlIRoomControllerV2 extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlIRoomControllerV2(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "iroomcontrollerv2";
|
||||
}
|
||||
}
|
||||
|
||||
private static final String STATE_ACTIVE_MODE = "activemode";
|
||||
private static final String STATE_OPERATING_MODE = "operatingmode";
|
||||
private static final String STATE_PREPARE_STATE = "preparestate";
|
||||
private static final String STATE_OPEN_WINDOW = "openwindow";
|
||||
private static final String STATE_TEMP_ACTUAL = "tempactual";
|
||||
private static final String STATE_TEMP_TARGET = "temptarget";
|
||||
private static final String STATE_COMFORT_TEMPERATURE = "comforttemperature";
|
||||
private static final String STATE_COMFORT_TEMPERATURE_OFFSET = "comforttemperatureoffset";
|
||||
private static final String STATE_COMFORT_TOLERANCE = "comforttolerance";
|
||||
private static final String STATE_ABSENT_MIN_OFFSET = "absentminoffset";
|
||||
private static final String STATE_ABSENT_MAX_OFFSET = "absentmaxoffset";
|
||||
private static final String STATE_FROST_PROTECT_TEMPERATURE = "frostprotecttemperature";
|
||||
private static final String STATE_HEAT_PROTECT_TEMPERATURE = "heatprotecttemperature";
|
||||
|
||||
private static final String CMD_SET_OPERATING_MODE = "setOperatingMode/";
|
||||
private static final String CMD_SET_COMFORT_TOLERANCE = "setComfortTolerance/";
|
||||
private static final String CMD_SET_COMFORT_TEMPERATURE = "setComfortTemperature/";
|
||||
private static final String CMD_SET_COMFORT_TEMPERATURE_OFFSET = "setComfortModeTemp/";
|
||||
private static final String CMD_SET_ABSENT_MIN_TEMPERATURE = "setAbsentMinTemperature/";
|
||||
private static final String CMD_SET_ABSENT_MAX_TEMPERATURE = "setAbsentMaxTemperature/";
|
||||
private static final String CMD_SET_MANUAL_TEMPERATURE = "setManualTemperature/";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LxControlIRoomControllerV2.class);
|
||||
|
||||
private LxControlIRoomControllerV2(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
ChannelUID cid;
|
||||
Set<String> tempTags = new HashSet<>(tags);
|
||||
tempTags.addAll(LxTags.TEMPERATURE);
|
||||
String format = details.format;
|
||||
StateDescriptionFragment patternRoFragment = null;
|
||||
StateDescriptionFragment patternRwFragment = null;
|
||||
if (format != null) {
|
||||
patternRoFragment = StateDescriptionFragmentBuilder.create().withPattern(format).withReadOnly(true).build();
|
||||
patternRwFragment = StateDescriptionFragmentBuilder.create().withPattern(format).withReadOnly(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_IROOM_V2_ACTIVE_MODE),
|
||||
defaultChannelLabel + "/ Active Mode", "Active mode", tags, null,
|
||||
() -> getStateDecimalValue(STATE_ACTIVE_MODE));
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_IROOM_V2_OPERATING_MODE),
|
||||
defaultChannelLabel + "/ Operating Mode", "Operating mode", tags, this::setOperatingMode,
|
||||
() -> getStateDecimalValue(STATE_OPERATING_MODE));
|
||||
|
||||
addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_IROOM_V2_PREPARE_STATE),
|
||||
defaultChannelLabel + "/ Prepare State", "Prepare state", tags, null,
|
||||
() -> getStateDecimalValue(STATE_PREPARE_STATE));
|
||||
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_SWITCH),
|
||||
defaultChannelLabel + "/ Open Window", "Open window", tags, null,
|
||||
() -> getSwitchState(STATE_OPEN_WINDOW));
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_ANALOG),
|
||||
defaultChannelLabel + "/ Current Temperature", "Current temperature", tempTags, null,
|
||||
() -> getStateDecimalValue(STATE_TEMP_ACTUAL));
|
||||
addChannelStateDescriptionFragment(cid, patternRoFragment);
|
||||
|
||||
// manual temperature will affect value of target temperature only in manual operating modes
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel + "/ Target Temperature", "Target temperature", tempTags,
|
||||
(c) -> setTemperature(c, CMD_SET_MANUAL_TEMPERATURE), () -> getStateDecimalValue(STATE_TEMP_TARGET));
|
||||
addChannelStateDescriptionFragment(cid, patternRwFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel + "/ Comfort Temperature", "Comfort temperature", tempTags,
|
||||
(c) -> setTemperature(c, CMD_SET_COMFORT_TEMPERATURE),
|
||||
() -> getStateDecimalValue(STATE_COMFORT_TEMPERATURE));
|
||||
addChannelStateDescriptionFragment(cid, patternRwFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel + "/ Comfort Temperature Offset", "Comfort temperature offset", tempTags,
|
||||
(c) -> setTemperature(c, CMD_SET_COMFORT_TEMPERATURE_OFFSET),
|
||||
() -> getStateDecimalValue(STATE_COMFORT_TEMPERATURE_OFFSET));
|
||||
addChannelStateDescriptionFragment(cid, patternRwFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_IROOM_V2_COMFORT_TOLERANCE),
|
||||
defaultChannelLabel + "/ Comfort Tolerance", "Comfort tolerance", tempTags,
|
||||
(c) -> setTemperature(c, CMD_SET_COMFORT_TOLERANCE),
|
||||
() -> getStateDecimalValue(STATE_COMFORT_TOLERANCE));
|
||||
addChannelStateDescriptionFragment(cid, patternRwFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel + "/ Absent Min Offset", "Absent minimum temperature offset", tempTags,
|
||||
(c) -> setTemperature(c, CMD_SET_ABSENT_MIN_TEMPERATURE),
|
||||
() -> getStateDecimalValue(STATE_ABSENT_MIN_OFFSET));
|
||||
addChannelStateDescriptionFragment(cid, patternRwFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel + "/ Absent Max Offset", "Absent maximum temperature offset", tempTags,
|
||||
(c) -> setTemperature(c, CMD_SET_ABSENT_MAX_TEMPERATURE),
|
||||
() -> getStateDecimalValue(STATE_ABSENT_MAX_OFFSET));
|
||||
addChannelStateDescriptionFragment(cid, patternRwFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_ANALOG),
|
||||
defaultChannelLabel + "/ Frost Protect Temperature", "Frost protect temperature", tempTags, null,
|
||||
() -> getStateDecimalValue(STATE_FROST_PROTECT_TEMPERATURE));
|
||||
addChannelStateDescriptionFragment(cid, patternRoFragment);
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_ANALOG),
|
||||
defaultChannelLabel + "/ Heat Protect Temperature", "Heat protect temperature", tempTags, null,
|
||||
() -> getStateDecimalValue(STATE_HEAT_PROTECT_TEMPERATURE));
|
||||
addChannelStateDescriptionFragment(cid, patternRoFragment);
|
||||
}
|
||||
|
||||
private State getSwitchState(String name) {
|
||||
return LxControlSwitch.convertSwitchState(getStateDoubleValue(name));
|
||||
}
|
||||
|
||||
private void setOperatingMode(Command command) throws IOException {
|
||||
if (command instanceof DecimalType) {
|
||||
DecimalType mode = (DecimalType) command;
|
||||
sendAction(CMD_SET_OPERATING_MODE + String.valueOf(mode.intValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private void setTemperature(Command command, String prefix) throws IOException {
|
||||
if (command instanceof DecimalType) {
|
||||
DecimalType temp = (DecimalType) command;
|
||||
sendAction(prefix + String.valueOf(temp.doubleValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxCategory;
|
||||
import org.openhab.binding.loxone.internal.types.LxTags;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
|
||||
/**
|
||||
* An InfoOnlyAnalog type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, this control covers analog virtual states only. This control does not send any
|
||||
* commands to the Miniserver. It can be used to read a formatted representation of an analog virtual state.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlInfoOnlyAnalog extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlInfoOnlyAnalog(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "infoonlyanalog";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* InfoOnlyAnalog state with current value
|
||||
*/
|
||||
private static final String STATE_VALUE = "value";
|
||||
|
||||
private LxControlInfoOnlyAnalog(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
LxCategory category = getCategory();
|
||||
if (category != null && category.getType() == LxCategory.CategoryType.TEMPERATURE) {
|
||||
tags.addAll(LxTags.TEMPERATURE);
|
||||
}
|
||||
ChannelUID cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_ANALOG),
|
||||
defaultChannelLabel, "Analog virtual state", tags, null, () -> getStateDecimalValue(STATE_VALUE));
|
||||
String format;
|
||||
if (details != null && details.format != null) {
|
||||
format = details.format;
|
||||
} else {
|
||||
format = "%.1f";
|
||||
}
|
||||
addChannelStateDescriptionFragment(cid,
|
||||
StateDescriptionFragmentBuilder.create().withPattern(format).withReadOnly(true).build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* An InfoOnlyDigital type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, this control covers digital virtual states only. This control does not send
|
||||
* any commands to the Miniserver. It can be used to read a formatted representation of a digital virtual state.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlInfoOnlyDigital extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlInfoOnlyDigital(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "infoonlydigital";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* InfoOnlyDigital has one state that can be on/off
|
||||
*/
|
||||
private static final String STATE_ACTIVE = "active";
|
||||
|
||||
private LxControlInfoOnlyDigital(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_SWITCH), defaultChannelLabel,
|
||||
"Digital virtual state", tags, null, this::getChannelState);
|
||||
}
|
||||
|
||||
private State getChannelState() {
|
||||
Double value = getStateDoubleValue(STATE_ACTIVE);
|
||||
if (value != null) {
|
||||
if (value == 0) {
|
||||
return OnOffType.OFF;
|
||||
} else if (value == 1.0) {
|
||||
return OnOffType.ON;
|
||||
} else {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A jalousie type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a jalousie control covers:
|
||||
* <ul>
|
||||
* <li>Blinds</li>
|
||||
* <li>Automatic blinds</li>
|
||||
* <li>Automatic blinds integrated</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Jalousie control has three channels:
|
||||
* <ul>
|
||||
* <li>0 (default) - rollershutter position</li>
|
||||
* <li>1 - shading command (always off switch, sending on triggers shading)</li>
|
||||
* <li>2 - automatic shading (on/off switch)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlJalousie extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlJalousie(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "jalousie";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jalousie is moving up
|
||||
*/
|
||||
private static final String STATE_UP = "up";
|
||||
/**
|
||||
* Jalousie is moving down
|
||||
*/
|
||||
private static final String STATE_DOWN = "down";
|
||||
/**
|
||||
* The position of the Jalousie, a number from 0 to 1
|
||||
* Jalousie upper position = 0
|
||||
* Jalousie lower position = 1
|
||||
*/
|
||||
private static final String STATE_POSITION = "position";
|
||||
/**
|
||||
* Only used by ones with Autopilot
|
||||
*/
|
||||
private static final String STATE_AUTO_ACTIVE = "autoactive";
|
||||
/**
|
||||
* Command string used to set control's state to Full Down
|
||||
*/
|
||||
private static final String CMD_FULL_DOWN = "FullDown";
|
||||
/**
|
||||
* Command string used to set control's state to Full Up
|
||||
*/
|
||||
private static final String CMD_FULL_UP = "FullUp";
|
||||
/**
|
||||
* Command string used to stop rollershutter
|
||||
*/
|
||||
private static final String CMD_STOP = "Stop";
|
||||
/**
|
||||
* Command to shade the jalousie
|
||||
*/
|
||||
private static final String CMD_SHADE = "shade";
|
||||
/**
|
||||
* Command to enable automatic shading
|
||||
*/
|
||||
private static final String CMD_AUTO = "auto";
|
||||
/**
|
||||
* Command to disable automatic shading
|
||||
*/
|
||||
private static final String CMD_NO_AUTO = "NoAuto";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LxControlJalousie.class);
|
||||
private Double targetPosition;
|
||||
|
||||
private LxControlJalousie(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
Set<String> blindsTags = new HashSet<>(tags);
|
||||
Set<String> switchTags = new HashSet<>(tags);
|
||||
blindsTags.add("Blinds");
|
||||
switchTags.add("Switchable");
|
||||
addChannel("Rollershutter", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_ROLLERSHUTTER),
|
||||
defaultChannelLabel, "Rollershutter", blindsTags, this::handleOperateCommands, this::getOperateState);
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + " / Shade", "Rollershutter shading", switchTags, this::handleShadeCommands,
|
||||
() -> OnOffType.OFF);
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + " / Auto Shade", "Rollershutter automatic shading", switchTags,
|
||||
this::handleAutoShadeCommands, this::getAutoShadeState);
|
||||
}
|
||||
|
||||
private void handleOperateCommands(Command command) throws IOException {
|
||||
logger.debug("Command input {}", command);
|
||||
if (command instanceof PercentType) {
|
||||
if (PercentType.ZERO.equals(command)) {
|
||||
sendAction(CMD_FULL_UP);
|
||||
} else if (PercentType.HUNDRED.equals(command)) {
|
||||
sendAction(CMD_FULL_DOWN);
|
||||
} else {
|
||||
moveToPosition(((PercentType) command).doubleValue() / 100);
|
||||
}
|
||||
} else if (command instanceof UpDownType) {
|
||||
if ((UpDownType) command == UpDownType.UP) {
|
||||
sendAction(CMD_FULL_UP);
|
||||
} else {
|
||||
sendAction(CMD_FULL_DOWN);
|
||||
}
|
||||
} else if (command instanceof StopMoveType) {
|
||||
if ((StopMoveType) command == StopMoveType.STOP) {
|
||||
sendAction(CMD_STOP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PercentType getOperateState() {
|
||||
Double value = getStateDoubleValue(STATE_POSITION);
|
||||
if (value != null && value >= 0 && value <= 1) {
|
||||
// state UP or DOWN from Loxone indicates blinds are moving up or down
|
||||
// state UP in openHAB means blinds are fully up (0%) and DOWN means fully down (100%)
|
||||
// so we will update only position and not up or down states
|
||||
// a basic calculation like (value * 100.0) will give significant errors for some fractions, e.g.
|
||||
// 0.29 * 100 = 28.xxxxx which in turn if converted to integer will cause the position to take same value
|
||||
// for two different positions and later jump skipping one value (29 in this example)
|
||||
// for that reason we need to round the results of multiplication to avoid this skipping of percentages
|
||||
return new PercentType((int) Math.round(value * 100.0));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleShadeCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON) {
|
||||
sendAction(CMD_SHADE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAutoShadeCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON) {
|
||||
sendAction(CMD_AUTO);
|
||||
} else {
|
||||
sendAction(CMD_NO_AUTO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OnOffType getAutoShadeState() {
|
||||
Double value = getStateDoubleValue(STATE_AUTO_ACTIVE);
|
||||
if (value != null) {
|
||||
return value == 1.0 ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor jalousie position against desired target position and stop it if target position is reached.
|
||||
*/
|
||||
@Override
|
||||
public void onStateChange(LxState state) {
|
||||
// check position changes
|
||||
if (STATE_POSITION.equals(state.getName()) && targetPosition != null && targetPosition >= 0
|
||||
&& targetPosition <= 1) {
|
||||
// see in which direction jalousie is moving
|
||||
Object value = state.getStateValue();
|
||||
if (value instanceof Double) {
|
||||
Double currentPosition = (Double) value;
|
||||
Double upValue = getStateDoubleValue(STATE_UP);
|
||||
Double downValue = getStateDoubleValue(STATE_DOWN);
|
||||
if (upValue != null && downValue != null) {
|
||||
if (((upValue == 1) && (currentPosition <= targetPosition))
|
||||
|| ((downValue == 1) && (currentPosition >= targetPosition))) {
|
||||
targetPosition = null;
|
||||
try {
|
||||
sendAction(CMD_STOP);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error stopping jalousie when meeting target position.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onStateChange(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the rollershutter (jalousie) to a desired position.
|
||||
* <p>
|
||||
* The jalousie will start moving in the desired direction based on the current position. It will stop moving once
|
||||
* there is a state update event received with value above/below (depending on direction) or equal to the set
|
||||
* position.
|
||||
*
|
||||
* @param position end position to move jalousie to, floating point number from 0..1 (0-fully closed to 1-fully
|
||||
* open)
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
private void moveToPosition(Double position) throws IOException {
|
||||
Double currentPosition = getStateDoubleValue(STATE_POSITION);
|
||||
if (currentPosition != null && currentPosition >= 0 && currentPosition <= 1) {
|
||||
if (currentPosition > position) {
|
||||
logger.debug("Moving jalousie up from {} to {}", currentPosition, position);
|
||||
targetPosition = position;
|
||||
sendAction(CMD_FULL_UP);
|
||||
} else if (currentPosition < position) {
|
||||
logger.debug("Moving jalousie down from {} to {}", currentPosition, position);
|
||||
targetPosition = position;
|
||||
sendAction(CMD_FULL_DOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
|
||||
/**
|
||||
* An LeftRightAnalog type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, LeftRightAnalog control is a virtual input that is analog and has an input
|
||||
* type
|
||||
* up-down buttons. The analog buttons are simulated as a single analog number value. This control behaves exactly the
|
||||
* same as {@link LxControlUpDownAnalog} but has a different name.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlLeftRightAnalog extends LxControlUpDownAnalog {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlLeftRightAnalog(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "leftrightanalog";
|
||||
}
|
||||
}
|
||||
|
||||
private LxControlLeftRightAnalog(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config, "Left/Right Analog");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
|
||||
/**
|
||||
* An LeftRightDigital type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, LeftRightDigital control is a virtual input that is digital and has an input
|
||||
* type left-right buttons. It has no states and can only accept commands. Only left/right (which are actually equal to
|
||||
* up/down commands of {@link LxControlUpDownDigital}) on/off commands are generated. Pulse commands are not supported,
|
||||
* because of lack of corresponding feature in openHAB. Pulse can be emulated by quickly alternating between ON and OFF
|
||||
* commands.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlLeftRightDigital extends LxControlUpDownDigital {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlLeftRightDigital(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "leftrightdigital";
|
||||
}
|
||||
}
|
||||
|
||||
private LxControlLeftRightDigital(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config, " / Left", "Left/Right Digital: Left", " / Right", "Left/Right Digital: Right");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxTags;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
/**
|
||||
* A Light Controller type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a light controller is one of following functional blocks:
|
||||
* <ul>
|
||||
* <li>Lighting Controller
|
||||
* <li>Hotel Lighting Controller
|
||||
* </ul>
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlLightController extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlLightController(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "lightcontroller";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of scenes supported by the Miniserver. Indexing starts with 0 to NUM_OF_SCENES-1.
|
||||
*/
|
||||
private static final int NUM_OF_SCENES = 10;
|
||||
|
||||
/**
|
||||
* Current active scene number (0-9)
|
||||
*/
|
||||
private static final String STATE_ACTIVE_SCENE = "activescene";
|
||||
/**
|
||||
* List of available scenes (public state, so user can monitor scene list updates)
|
||||
*/
|
||||
private static final String STATE_SCENE_LIST = "scenelist";
|
||||
/**
|
||||
* Command string used to set control's state to ON
|
||||
*/
|
||||
private static final String CMD_ON = "On";
|
||||
/**
|
||||
* Command string used to set control's state to OFF
|
||||
*/
|
||||
private static final String CMD_OFF = "Off";
|
||||
/**
|
||||
* Command string used to go to the next scene
|
||||
*/
|
||||
private static final String CMD_NEXT_SCENE = "plus";
|
||||
/**
|
||||
* Command string used to go to the previous scene
|
||||
*/
|
||||
private static final String CMD_PREVIOUS_SCENE = "minus";
|
||||
private static final int SCENE_ALL_ON = 9;
|
||||
|
||||
private List<StateOption> sceneNames = new ArrayList<>();
|
||||
private ChannelUID channelId;
|
||||
|
||||
private LxControlLightController(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
tags.addAll(LxTags.SCENE);
|
||||
// add only channel, state description will be added later when a control state update message is received
|
||||
channelId = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_LIGHT_CTRL),
|
||||
defaultChannelLabel, "Light controller", tags, this::handleCommands, this::getChannelState);
|
||||
}
|
||||
|
||||
private void handleCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON) {
|
||||
sendAction(CMD_ON);
|
||||
} else {
|
||||
sendAction(CMD_OFF);
|
||||
}
|
||||
} else if (command instanceof UpDownType) {
|
||||
if ((UpDownType) command == UpDownType.UP) {
|
||||
sendAction(CMD_NEXT_SCENE);
|
||||
} else {
|
||||
sendAction(CMD_PREVIOUS_SCENE);
|
||||
}
|
||||
} else if (command instanceof DecimalType) {
|
||||
int scene = ((DecimalType) command).intValue();
|
||||
if (scene == SCENE_ALL_ON) {
|
||||
sendAction(CMD_ON);
|
||||
} else if (scene >= 0 && scene < NUM_OF_SCENES) {
|
||||
sendAction(Long.toString(scene));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DecimalType getChannelState() {
|
||||
Double value = getStateDoubleValue(STATE_ACTIVE_SCENE);
|
||||
if (value != null && value >= 0 && value < NUM_OF_SCENES) {
|
||||
return new DecimalType(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scene names from new state value received from the Miniserver
|
||||
*/
|
||||
@Override
|
||||
public void onStateChange(LxState state) {
|
||||
if (STATE_SCENE_LIST.equals(state.getName()) && channelId != null) {
|
||||
Object value = state.getStateValue();
|
||||
if (value instanceof String) {
|
||||
sceneNames.clear();
|
||||
String[] scenes = ((String) value).split(",");
|
||||
for (String line : scenes) {
|
||||
line = line.replaceAll("\"", "");
|
||||
String[] params = line.split("=");
|
||||
if (params.length == 2) {
|
||||
sceneNames.add(new StateOption(params[0], params[1]));
|
||||
}
|
||||
}
|
||||
addChannelStateDescriptionFragment(channelId,
|
||||
StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO)
|
||||
.withMaximum(new BigDecimal(NUM_OF_SCENES - 1)).withStep(BigDecimal.ONE)
|
||||
.withReadOnly(false).withOptions(sceneNames).build());
|
||||
}
|
||||
} else {
|
||||
super.onStateChange(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* A Light Controller V2 type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* This control has been introduced in Loxone Config 9 in 2017 and it makes the {@link LxControlLightController}
|
||||
* obsolete. Both controls will exist for some time together.
|
||||
* <p>
|
||||
* Light controller V2 can have N outputs named AQ1...AQN that can function as Switch, Dimmer, RGB, Lumitech or Smart
|
||||
* Actuator functional blocks. Individual controls will be created for these outputs so they can be operated directly
|
||||
* and independently from the controller.
|
||||
* <p>
|
||||
* Controller can also have M moods configured. Each mood defines own subset of outputs and their settings, which will
|
||||
* be engaged when the mood is active. A dedicated switch control object will be created for each mood.
|
||||
* This effectively will allow for mixing various moods by individually enabling/disabling them.
|
||||
* <p>
|
||||
* It seems there is no imposed limitation for the number of outputs and moods.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlLightControllerV2 extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlLightControllerV2(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "lightcontrollerv2";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State with list of active moods
|
||||
*/
|
||||
private static final String STATE_ACTIVE_MOODS_LIST = "activemoods";
|
||||
/**
|
||||
* State with list of available moods
|
||||
*/
|
||||
private static final String STATE_MOODS_LIST = "moodlist";
|
||||
|
||||
/**
|
||||
* Command string used to set a given mood
|
||||
*/
|
||||
private static final String CMD_CHANGE_TO_MOOD = "changeTo";
|
||||
/**
|
||||
* Command string used to change to the next mood
|
||||
*/
|
||||
private static final String CMD_NEXT_MOOD = "plus";
|
||||
/**
|
||||
* Command string used to change to the previous mood
|
||||
*/
|
||||
private static final String CMD_PREVIOUS_MOOD = "minus";
|
||||
/**
|
||||
* Command string used to add mood to the active moods (mix it in)
|
||||
*/
|
||||
private static final String CMD_ADD_MOOD = "addMood";
|
||||
/**
|
||||
* Command string used to remove mood from the active moods (mix it out)
|
||||
*/
|
||||
private static final String CMD_REMOVE_MOOD = "removeMood";
|
||||
|
||||
private final transient Logger logger = LoggerFactory.getLogger(LxControlLightControllerV2.class);
|
||||
|
||||
// Following commands are not supported:
|
||||
// moveFavoriteMood, moveAdditionalMood, moveMood, addToFavoriteMood, removeFromFavoriteMood, learn, delete
|
||||
|
||||
private Map<Integer, LxControlMood> moodList = new HashMap<>();
|
||||
private List<Integer> activeMoods = new ArrayList<>();
|
||||
private ChannelUID channelId;
|
||||
|
||||
private LxControlLightControllerV2(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
tags.add("Scene");
|
||||
// add only channel, state description will be added later when a control state update message is received
|
||||
channelId = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_LIGHT_CTRL),
|
||||
defaultChannelLabel, "Light controller V2", tags, this::handleCommands, this::getChannelState);
|
||||
}
|
||||
|
||||
private void handleCommands(Command command) throws IOException {
|
||||
if (command instanceof UpDownType) {
|
||||
if ((UpDownType) command == UpDownType.UP) {
|
||||
sendAction(CMD_NEXT_MOOD);
|
||||
} else {
|
||||
sendAction(CMD_PREVIOUS_MOOD);
|
||||
}
|
||||
} else if (command instanceof DecimalType) {
|
||||
int moodId = ((DecimalType) command).intValue();
|
||||
if (isMoodOk(moodId)) {
|
||||
sendAction(CMD_CHANGE_TO_MOOD + "/" + moodId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private State getChannelState() {
|
||||
// update the single mood channel state
|
||||
if (activeMoods.size() == 1) {
|
||||
Integer id = activeMoods.get(0);
|
||||
if (isMoodOk(id)) {
|
||||
return new DecimalType(id);
|
||||
}
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured and active moods from a new state value received from the Miniserver
|
||||
*
|
||||
* @param state state update from the Miniserver
|
||||
*/
|
||||
@Override
|
||||
public void onStateChange(LxState state) {
|
||||
String stateName = state.getName();
|
||||
Object value = state.getStateValue();
|
||||
try {
|
||||
if (STATE_MOODS_LIST.equals(stateName) && value instanceof String) {
|
||||
onMoodsListChange((String) value);
|
||||
} else if (STATE_ACTIVE_MOODS_LIST.equals(stateName) && value instanceof String) {
|
||||
// this state can be received before list of moods, but it contains a valid list of IDs
|
||||
Integer[] array = getGson().fromJson((String) value, Integer[].class);
|
||||
activeMoods = Arrays.asList(array).stream().filter(id -> isMoodOk(id)).collect(Collectors.toList());
|
||||
// update all moods states - this will force update of channels too
|
||||
moodList.values().forEach(mood -> mood.onStateChange(null));
|
||||
// finally we update controller's state based on the active moods list
|
||||
super.onStateChange(state);
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.debug("Error parsing state {}: {}", stateName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix a mood into currently active moods.
|
||||
*
|
||||
* @param moodId ID of the mood to add
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
void addMood(Integer moodId) throws IOException {
|
||||
if (isMoodOk(moodId)) {
|
||||
sendAction(CMD_ADD_MOOD + "/" + moodId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mood is currently active.
|
||||
*
|
||||
* @param moodId mood ID to check
|
||||
* @return true if mood is currently active
|
||||
*/
|
||||
boolean isMoodActive(Integer moodId) {
|
||||
return activeMoods.contains(moodId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mood ID is within allowed range
|
||||
*
|
||||
* @param moodId mood ID to check
|
||||
* @return true if mood ID is within allowed range or range is not configured
|
||||
*/
|
||||
boolean isMoodOk(Integer moodId) {
|
||||
return moodId != null && moodList.containsKey(moodId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix a mood out of currently active moods.
|
||||
*
|
||||
* @param moodId ID of the mood to remove
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
void removeMood(Integer moodId) throws IOException {
|
||||
if (isMoodOk(moodId)) {
|
||||
sendAction(CMD_REMOVE_MOOD + "/" + moodId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the list of configured moods
|
||||
*
|
||||
* @param text json structure with new moods
|
||||
* @throws JsonSyntaxException error parsing json structure
|
||||
*/
|
||||
private void onMoodsListChange(String text) throws JsonSyntaxException {
|
||||
LxControlMood[] array = getGson().fromJson(text, LxControlMood[].class);
|
||||
Map<Integer, LxControlMood> newMoodList = new HashMap<>();
|
||||
Integer minMoodId = null;
|
||||
Integer maxMoodId = null;
|
||||
for (LxControlMood mood : array) {
|
||||
Integer id = mood.getId();
|
||||
if (id != null && mood.getName() != null) {
|
||||
logger.debug("Adding mood (id={}, name={})", id, mood.getName());
|
||||
// mood-UUID = <controller-UUID>-M<mood-ID>
|
||||
LxUuid moodUuid = new LxUuid(getUuid().toString() + "-M" + id);
|
||||
mood.initialize(getConfig(), this, moodUuid);
|
||||
newMoodList.put(id, mood);
|
||||
if (minMoodId == null || minMoodId > id) {
|
||||
minMoodId = id;
|
||||
}
|
||||
if (maxMoodId == null || maxMoodId < id) {
|
||||
maxMoodId = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId != null && minMoodId != null && maxMoodId != null) {
|
||||
// convert all moods to options list for state description
|
||||
List<StateOption> optionsList = newMoodList.values().stream()
|
||||
.map(mood -> new StateOption(mood.getId().toString(), mood.getName())).collect(Collectors.toList());
|
||||
addChannelStateDescriptionFragment(channelId,
|
||||
StateDescriptionFragmentBuilder.create().withMinimum(new BigDecimal(minMoodId))
|
||||
.withMaximum(new BigDecimal(maxMoodId)).withStep(BigDecimal.ONE).withReadOnly(false)
|
||||
.withOptions(optionsList).build());
|
||||
}
|
||||
|
||||
moodList.values().forEach(m -> removeControl(m));
|
||||
newMoodList.values().forEach(m -> addControl(m));
|
||||
moodList = newMoodList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
|
||||
/**
|
||||
* A meter type of control on Loxone Miniserver.
|
||||
* According to Loxone API documentation, a meter control covers Utility Meter functional block in Loxone Config.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlMeter extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlMeter(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "meter";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value for actual consumption
|
||||
*/
|
||||
private static final String STATE_ACTUAL = "actual";
|
||||
|
||||
/**
|
||||
* Value for total consumption
|
||||
*/
|
||||
private static final String STATE_TOTAL = "total";
|
||||
|
||||
/**
|
||||
* Command string used to reset the meter
|
||||
*/
|
||||
private static final String CMD_RESET = "reset";
|
||||
|
||||
LxControlMeter(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
ChannelUID cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Current", "Current meter value", tags, null,
|
||||
() -> getStateDecimalValue(STATE_ACTUAL));
|
||||
String format;
|
||||
if (details != null && details.actualFormat != null) {
|
||||
format = details.actualFormat;
|
||||
} else {
|
||||
format = "%.3f"; // Loxone default for this format
|
||||
}
|
||||
addChannelStateDescriptionFragment(cid,
|
||||
StateDescriptionFragmentBuilder.create().withPattern(format).withReadOnly(true).build());
|
||||
|
||||
cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Total", "Total meter consumption", tags, null,
|
||||
() -> getStateDecimalValue(STATE_TOTAL));
|
||||
if (details != null && details.totalFormat != null) {
|
||||
format = details.totalFormat;
|
||||
} else {
|
||||
format = "%.1f"; // Loxone default for this format
|
||||
}
|
||||
addChannelStateDescriptionFragment(cid,
|
||||
StateDescriptionFragmentBuilder.create().withPattern(format).withReadOnly(true).build());
|
||||
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + " / Reset", "Reset meter", tags, this::handleResetCommands, () -> OnOffType.OFF);
|
||||
}
|
||||
|
||||
private void handleResetCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType && (OnOffType) command == OnOffType.ON) {
|
||||
sendAction(CMD_RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* This class represents a mood belonging to a {@link LxControlMood} object.
|
||||
* A mood is effectively a switch. When the switch is set to ON, mood is active and mixed into a set of active
|
||||
* moods.
|
||||
* A mood is deserialized using a default gson, not like {@link LxControl} which has a proprietary deserialization
|
||||
* method.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlMood extends LxControlSwitch {
|
||||
|
||||
/**
|
||||
* An ID that uniquely identifies this mood (e.g. inside activeMoods)
|
||||
*/
|
||||
@SerializedName("id")
|
||||
private Integer moodId;
|
||||
|
||||
/**
|
||||
* Bitmask that tells if the mood is used for a specific purpose in the logic.
|
||||
* If it’s not used, it can be removed without affecting the logic on the Miniserver.
|
||||
* 0: not used
|
||||
* 1: this mood is activated by a movement event
|
||||
* 2: a T5 or other inputs activate/deactivate this mood
|
||||
*/
|
||||
@SerializedName("used")
|
||||
private Integer isUsed;
|
||||
|
||||
/**
|
||||
* Whether or not this mood can be controlled with a t5 input
|
||||
*/
|
||||
@SerializedName("t5")
|
||||
private Boolean isT5Controlled;
|
||||
|
||||
/**
|
||||
* If a mood is marked as static it cannot be deleted or modified in any way.
|
||||
* But it can be moved within and between favorite and additional lists.
|
||||
*/
|
||||
@SerializedName("static")
|
||||
private Boolean isStatic;
|
||||
|
||||
private LxControlLightControllerV2 controller;
|
||||
|
||||
/**
|
||||
* This constructor will be called by the default JSON deserialization
|
||||
*/
|
||||
LxControlMood() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
}
|
||||
|
||||
public void initialize(LxControlConfig config, LxControlLightControllerV2 controller, LxUuid uuid) {
|
||||
this.uuid = uuid;
|
||||
this.controller = controller;
|
||||
super.initialize(config);
|
||||
// the 'all off' mood can't be operated as a switch, but needs to be present on the moods list for the
|
||||
// lighting controller
|
||||
// currently the API does not give a hint how to figure out the 'all off' mood
|
||||
// empirically this is the only mood that is not editable by the user and has a static flag set on
|
||||
// we will assume that the only static mood is 'all off' mood
|
||||
if (isStatic != null && isStatic) {
|
||||
removeAllChannels();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
String getLabel() {
|
||||
return "Mood / " + super.getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ID of this mood. ID identifies the mood within a light controller.
|
||||
* It is equal to the mood ID received from the Miniserver.
|
||||
*
|
||||
* @return mood ID
|
||||
*/
|
||||
Integer getId() {
|
||||
return moodId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix the mood into active moods.
|
||||
*
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
@Override
|
||||
void on() throws IOException {
|
||||
if (controller != null) {
|
||||
controller.addMood(moodId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix the mood out of active moods.
|
||||
*
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
@Override
|
||||
void off() throws IOException {
|
||||
if (controller != null) {
|
||||
controller.removeMood(moodId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the mood is active of not.
|
||||
*
|
||||
* @return 1 if mood is active and 0 otherwise
|
||||
*/
|
||||
@Override
|
||||
OnOffType getSwitchState() {
|
||||
if (controller != null && controller.isMoodOk(moodId)) {
|
||||
if (controller.isMoodActive(moodId)) {
|
||||
return OnOffType.ON;
|
||||
}
|
||||
return OnOffType.OFF;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* A pushbutton type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a pushbutton control covers virtual input of type pushbutton
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlPushbutton extends LxControlSwitch {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlPushbutton(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "pushbutton";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command string used to set control's state to ON and OFF (tap)
|
||||
*/
|
||||
private static final String CMD_PULSE = "Pulse";
|
||||
|
||||
LxControlPushbutton(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
void handleSwitchCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON) {
|
||||
sendAction(CMD_PULSE);
|
||||
} else {
|
||||
off();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
/**
|
||||
* A radio-button type of control on Loxone Miniserver.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlRadio extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlRadio(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "radio";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of outputs a radio controller may have
|
||||
*/
|
||||
private static final int MAX_RADIO_OUTPUTS = 16;
|
||||
|
||||
/**
|
||||
* Radio-button has one state that is a number representing current active output
|
||||
*/
|
||||
private static final String STATE_ACTIVE_OUTPUT = "activeoutput";
|
||||
|
||||
/**
|
||||
* Command string used to set radio button to all outputs off
|
||||
*/
|
||||
private static final String CMD_RESET = "reset";
|
||||
|
||||
private Map<String, String> outputsMap;
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
// add both channel and state description (all needed configuration is available)
|
||||
ChannelUID cid = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RADIO_BUTTON),
|
||||
defaultChannelLabel, "Radio button", tags, this::handleCommands, this::getChannelState);
|
||||
|
||||
if (details != null) {
|
||||
List<StateOption> outputs = new ArrayList<>();
|
||||
if (details.outputs != null) {
|
||||
outputsMap = details.outputs;
|
||||
outputs = details.outputs.entrySet().stream().map(e -> new StateOption(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (details.allOff != null && !details.allOff.isEmpty()) {
|
||||
outputs.add(new StateOption("0", details.allOff));
|
||||
outputsMap.put("0", details.allOff);
|
||||
}
|
||||
addChannelStateDescriptionFragment(cid,
|
||||
StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO)
|
||||
.withMaximum(new BigDecimal(MAX_RADIO_OUTPUTS)).withStep(BigDecimal.ONE).withReadOnly(false)
|
||||
.withOptions(outputs).build());
|
||||
}
|
||||
}
|
||||
|
||||
private LxControlRadio(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
private void handleCommands(Command command) throws IOException {
|
||||
if (((command instanceof OnOffType && (OnOffType) command == OnOffType.OFF) || DecimalType.ZERO.equals(command))
|
||||
&& outputsMap.containsKey("0")) {
|
||||
sendAction(CMD_RESET);
|
||||
} else if (command instanceof DecimalType) {
|
||||
DecimalType output = (DecimalType) command;
|
||||
if (outputsMap.containsKey(output.toString())) {
|
||||
sendAction(String.valueOf(output.intValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DecimalType getChannelState() {
|
||||
Double output = getStateDoubleValue(STATE_ACTIVE_OUTPUT);
|
||||
if (output != null && output % 1 == 0 && outputsMap.containsKey(String.valueOf(output.intValue()))) {
|
||||
return new DecimalType(output);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
|
||||
/**
|
||||
* A slider type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a slider control is a virtual input of slider type.
|
||||
* It behaves exactly the same as {@link LxControlUpDownAnalog}.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlSlider extends LxControlUpDownAnalog {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlSlider(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "slider";
|
||||
}
|
||||
}
|
||||
|
||||
private LxControlSlider(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config, "Slider");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxCategory;
|
||||
import org.openhab.binding.loxone.internal.types.LxTags;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* A switch type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a switch control is:
|
||||
* <ul>
|
||||
* <li>a virtual input of switch type
|
||||
* <li>a push button function block
|
||||
* </ul>
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlSwitch extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlSwitch(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "switch";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch has one state that can be on/off
|
||||
*/
|
||||
private static final String STATE_ACTIVE = "active";
|
||||
|
||||
/**
|
||||
* Command string used to set control's state to ON
|
||||
*/
|
||||
private static final String CMD_ON = "On";
|
||||
/**
|
||||
* Command string used to set control's state to OFF
|
||||
*/
|
||||
private static final String CMD_OFF = "Off";
|
||||
|
||||
LxControlSwitch(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
LxCategory category = getCategory();
|
||||
if (category != null && category.getType() == LxCategory.CategoryType.LIGHTS) {
|
||||
tags.addAll(LxTags.LIGHTING);
|
||||
} else {
|
||||
tags.addAll(LxTags.SWITCHABLE);
|
||||
}
|
||||
addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH), defaultChannelLabel,
|
||||
"Switch", tags, this::handleSwitchCommands, this::getSwitchState);
|
||||
}
|
||||
|
||||
void handleSwitchCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON) {
|
||||
on();
|
||||
} else {
|
||||
off();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set switch to ON.
|
||||
* <p>
|
||||
* Sends a command to operate the switch.
|
||||
* This method is separated, so {@link LxControlMood} can inherit from this class, but handle 'on' commands
|
||||
* differently.
|
||||
*
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
void on() throws IOException {
|
||||
sendAction(CMD_ON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set switch to OFF.
|
||||
* <p>
|
||||
* Sends a command to operate the switch.
|
||||
* This method is separated, so {@link LxControlMood} and {@link LxControlPushbutton} can inherit from this class,
|
||||
* but handle 'off' commands differently.
|
||||
*
|
||||
* @throws IOException when something went wrong with communication
|
||||
*/
|
||||
void off() throws IOException {
|
||||
sendAction(CMD_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current value of the switch'es state.
|
||||
* This method is separated, so it can be overridden by {@link LxControlTimedSwitch}, which inherits from the switch
|
||||
* class, but has a different way of handling states.
|
||||
*
|
||||
* @return ON/OFF or null if undefined
|
||||
*/
|
||||
State getSwitchState() {
|
||||
return convertSwitchState(getStateDoubleValue(STATE_ACTIVE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert double value of switch into ON/OFF state value
|
||||
*
|
||||
* @param value state value as double
|
||||
* @return state value as ON/OFF
|
||||
*/
|
||||
static State convertSwitchState(Double value) {
|
||||
if (value != null) {
|
||||
if (value == 1.0) {
|
||||
return OnOffType.ON;
|
||||
} else if (value == 0.0) {
|
||||
return OnOffType.OFF;
|
||||
} else {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
|
||||
/**
|
||||
* A Text State type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a text state represents a State functional block on the Miniserver
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlTextState extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlTextState(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "textstate";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A state which will receive an update of possible Text State values)
|
||||
*/
|
||||
private static final String STATE_TEXT_AND_ICON = "textandicon";
|
||||
|
||||
private LxControlTextState(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
ChannelUID id = addChannel("String", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_TEXT),
|
||||
defaultChannelLabel, "Text state", tags, null, () -> getStateStringValue(STATE_TEXT_AND_ICON));
|
||||
addChannelStateDescriptionFragment(id, StateDescriptionFragmentBuilder.create().withReadOnly(true).build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
|
||||
/**
|
||||
* A timed switch type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a switch control is:
|
||||
* <ul>
|
||||
* <li>a virtual input of switch type
|
||||
* <li>a push button function block
|
||||
* </ul>
|
||||
*
|
||||
* @author Stephan Brunner - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlTimedSwitch extends LxControlPushbutton {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlTimedSwitch(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "timedswitch";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* deactivationDelay - countdown until the output is deactivated.
|
||||
* 0 = the output is turned off
|
||||
* -1 = the output is permanently on
|
||||
* otherwise it will count down from deactivationDelayTotal
|
||||
*/
|
||||
private static final String STATE_DEACTIVATION_DELAY = "deactivationdelay";
|
||||
|
||||
private LxControlTimedSwitch(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
ChannelUID id = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
|
||||
defaultChannelLabel + " / Deactivation Delay", "Deactivation Delay", null, null,
|
||||
this::getDeactivationState);
|
||||
addChannelStateDescriptionFragment(id,
|
||||
StateDescriptionFragmentBuilder.create().withMinimum(new BigDecimal(-1)).withReadOnly(true).build());
|
||||
}
|
||||
|
||||
private State getDeactivationState() {
|
||||
Double deactivationValue = getStateDoubleValue(STATE_DEACTIVATION_DELAY);
|
||||
if (deactivationValue != null) {
|
||||
if (deactivationValue.equals(-1.0)) {
|
||||
// we don't show the special value of -1 to the user, this means switch is on and delay is zero
|
||||
deactivationValue = 0.0;
|
||||
}
|
||||
return new DecimalType(deactivationValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
OnOffType getSwitchState() {
|
||||
/**
|
||||
* 0 = the output is turned off
|
||||
* -1 = the output is permanently on
|
||||
* otherwise it will count down from deactivationDelayTotal
|
||||
**/
|
||||
Double value = getStateDoubleValue(STATE_DEACTIVATION_DELAY);
|
||||
if (value != null) {
|
||||
if (value == -1.0 || value > 0.0) { // mapping
|
||||
return OnOffType.ON;
|
||||
} else if (value == 0) {
|
||||
return OnOffType.OFF;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
|
||||
/**
|
||||
* A Tracker type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, a Tracker control represents a Tracker functional block on the Miniserver
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlTracker extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlTracker(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "tracker";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A state which will receive an update of possible Text State values)
|
||||
*/
|
||||
private static final String STATE_ENTRIES = "entries";
|
||||
|
||||
private LxControlTracker(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
ChannelUID id = addChannel("String", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_TEXT),
|
||||
defaultChannelLabel, "Tracker", tags, null, () -> getStateStringValue(STATE_ENTRIES));
|
||||
addChannelStateDescriptionFragment(id, StateDescriptionFragmentBuilder.create().withReadOnly(true).build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* An UpDownAnalog type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, UpDownAnalog control is a virtual input that is analog and has an input type
|
||||
* up-down buttons. The analog buttons are simulated as a single analog number value.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlUpDownAnalog extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlUpDownAnalog(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "updownanalog";
|
||||
}
|
||||
}
|
||||
|
||||
private static final String STATE_VALUE = "value";
|
||||
private static final String STATE_ERROR = "error";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LxControlUpDownAnalog.class);
|
||||
|
||||
private Double minValue;
|
||||
private Double maxValue;
|
||||
private ChannelUID channelId;
|
||||
|
||||
LxControlUpDownAnalog(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
initialize(config, "Up/Down Analog");
|
||||
}
|
||||
|
||||
void initialize(LxControlConfig config, String channelDescription) {
|
||||
super.initialize(config);
|
||||
channelId = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel, channelDescription, tags, this::handleCommands, this::getChannelState);
|
||||
if (details != null && details.min != null && details.max != null) {
|
||||
if (details.min <= details.max) {
|
||||
minValue = details.min;
|
||||
maxValue = details.max;
|
||||
if (details.step != null) {
|
||||
addChannelStateDescriptionFragment(channelId,
|
||||
StateDescriptionFragmentBuilder.create().withMinimum(new BigDecimal(minValue))
|
||||
.withMaximum(new BigDecimal(maxValue)).withStep(new BigDecimal(details.step))
|
||||
.withPattern(details.format != null ? details.format : "%.1f").withReadOnly(false)
|
||||
.build());
|
||||
}
|
||||
} else {
|
||||
logger.warn("Received min value > max value: {}, {}", minValue, maxValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCommands(Command command) throws IOException {
|
||||
if (command instanceof DecimalType) {
|
||||
Double value = ((DecimalType) command).doubleValue();
|
||||
if (minValue != null && maxValue != null && value >= minValue && value <= maxValue) {
|
||||
sendAction(value.toString());
|
||||
} else {
|
||||
// we'll update the state value to reflect current real value that has not been changed
|
||||
setChannelState(channelId, getChannelState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private State getChannelState() {
|
||||
Double error = getStateDoubleValue(STATE_ERROR);
|
||||
if (error == null || error == 0.0) {
|
||||
Double value = getStateDoubleValue(STATE_VALUE);
|
||||
if (value != null) {
|
||||
if (minValue != null && maxValue != null && (minValue > value || maxValue < value)) {
|
||||
return null;
|
||||
}
|
||||
return new DecimalType(value);
|
||||
}
|
||||
} else {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* An UpDownDigital type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, UpDownDigital control is a virtual input that is digital and has an input type
|
||||
* up-down buttons. Buttons act like on an integrated up/down arrows switch - only one direction can be active at a
|
||||
* time. Pushing button in one direction will automatically set the other direction to off.
|
||||
* This control has no states and can only accept commands. Only up/down on/off commands are generated. Pulse
|
||||
* commands are not supported, because of lack of corresponding feature in openHAB. Pulse can be emulated by quickly
|
||||
* alternating between ON and OFF commands. Because this control has no states, there will be no openHAB state changes
|
||||
* triggered by the Miniserver and we need to take care of updating the states inside this class.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlUpDownDigital extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlUpDownDigital(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "updowndigital";
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CMD_UP_ON = "UpOn";
|
||||
private static final String CMD_UP_OFF = "UpOff";
|
||||
private static final String CMD_DOWN_ON = "DownOn";
|
||||
private static final String CMD_DOWN_OFF = "DownOff";
|
||||
|
||||
private OnOffType upState = OnOffType.OFF;
|
||||
private OnOffType downState = OnOffType.OFF;
|
||||
private ChannelUID upChannelId;
|
||||
private ChannelUID downChannelId;
|
||||
|
||||
LxControlUpDownDigital(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
initialize(config, " / Up", "Up/Down Digital: Up", " / Down", "Up/Down Digital: Down");
|
||||
}
|
||||
|
||||
void initialize(LxControlConfig config, String upChannelLabel, String upChannelDescription, String downChannelLabel,
|
||||
String downChannelDescription) {
|
||||
super.initialize(config);
|
||||
upChannelId = addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + upChannelLabel, upChannelDescription, tags, this::handleUpCommands,
|
||||
() -> upState);
|
||||
downChannelId = addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
|
||||
defaultChannelLabel + downChannelLabel, downChannelDescription, tags, this::handleDownCommands,
|
||||
() -> downState);
|
||||
}
|
||||
|
||||
private void handleUpCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON && upState == OnOffType.OFF) {
|
||||
setStates(OnOffType.ON, OnOffType.OFF);
|
||||
sendAction(CMD_UP_ON);
|
||||
} else if (upState == OnOffType.ON) {
|
||||
setStates(OnOffType.OFF, OnOffType.OFF);
|
||||
sendAction(CMD_UP_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownCommands(Command command) throws IOException {
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON && downState == OnOffType.OFF) {
|
||||
setStates(OnOffType.OFF, OnOffType.ON);
|
||||
sendAction(CMD_DOWN_ON);
|
||||
} else if (downState == OnOffType.ON) {
|
||||
setStates(OnOffType.OFF, OnOffType.OFF);
|
||||
sendAction(CMD_DOWN_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStates(OnOffType upState, OnOffType downState) {
|
||||
this.upState = upState;
|
||||
this.downState = downState;
|
||||
setChannelState(upChannelId, upState);
|
||||
setChannelState(downChannelId, downState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A ValueSelector type of control on Loxone Miniserver.
|
||||
* <p>
|
||||
* According to Loxone API documentation, this control covers: Push-button +/- and Push-button + functional blocks.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlValueSelector extends LxControl {
|
||||
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlValueSelector(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "valueselector";
|
||||
}
|
||||
}
|
||||
|
||||
private static final String STATE_VALUE = "value";
|
||||
private static final String STATE_MIN = "min";
|
||||
private static final String STATE_MAX = "max";
|
||||
private static final String STATE_STEP = "step";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LxControlValueSelector.class);
|
||||
|
||||
private Boolean increaseOnly = false;
|
||||
private String format = "%.1f";
|
||||
private Double minValue;
|
||||
private Double maxValue;
|
||||
private Double stepValue;
|
||||
private ChannelUID channelId;
|
||||
private ChannelUID numberChannelId;
|
||||
|
||||
private LxControlValueSelector(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
channelId = addChannel("Dimmer", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_DIMMER),
|
||||
defaultChannelLabel, "Value Selector", tags, this::handleCommand, this::getChannelState);
|
||||
numberChannelId = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
|
||||
defaultChannelLabel + " / Number", "Value Selector by number", tags, this::handleNumberCommand,
|
||||
this::getChannelNumberState);
|
||||
|
||||
if (details != null) {
|
||||
if (details.format != null) {
|
||||
this.format = details.format;
|
||||
}
|
||||
if (details.increaseOnly != null) {
|
||||
this.increaseOnly = details.increaseOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCommand(Command command) throws IOException {
|
||||
if (minValue == null || maxValue == null || minValue >= maxValue) {
|
||||
logger.debug("Value selector min or max value missing or min>max.");
|
||||
return;
|
||||
}
|
||||
if (command instanceof OnOffType) {
|
||||
if ((OnOffType) command == OnOffType.ON) {
|
||||
sendAction(maxValue.toString());
|
||||
} else {
|
||||
sendAction(minValue.toString());
|
||||
}
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
if (stepValue == null) {
|
||||
logger.debug("Value selector step value missing.");
|
||||
return;
|
||||
}
|
||||
IncreaseDecreaseType type = (IncreaseDecreaseType) command;
|
||||
if (increaseOnly != null && type == IncreaseDecreaseType.DECREASE && increaseOnly) {
|
||||
logger.debug("Value selector configured to allow increase only.");
|
||||
return;
|
||||
}
|
||||
Double currentValue = getStateDoubleValue(STATE_VALUE);
|
||||
if (currentValue != null) {
|
||||
Double nextValue = currentValue + (type == IncreaseDecreaseType.INCREASE ? stepValue : -stepValue);
|
||||
if (nextValue > maxValue) {
|
||||
nextValue = maxValue;
|
||||
}
|
||||
if (nextValue < minValue) {
|
||||
nextValue = minValue;
|
||||
}
|
||||
sendAction(nextValue.toString());
|
||||
}
|
||||
} else if (command instanceof PercentType) {
|
||||
Double value = ((PercentType) command).doubleValue() * (maxValue - minValue) / 100.0 + minValue;
|
||||
sendAction(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNumberCommand(Command command) throws IOException {
|
||||
if (minValue == null || maxValue == null || minValue >= maxValue) {
|
||||
logger.debug("Value selector min or max value missing or min>max.");
|
||||
return;
|
||||
}
|
||||
if (command instanceof DecimalType) {
|
||||
Double value = ((DecimalType) command).doubleValue();
|
||||
if (value < minValue || value > maxValue) {
|
||||
logger.debug("Value {} out of {}-{} range", value, minValue, maxValue);
|
||||
return;
|
||||
}
|
||||
sendAction(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private PercentType getChannelState() {
|
||||
Double value = getStateDoubleValue(STATE_VALUE);
|
||||
if (value != null && minValue != null && maxValue != null && minValue <= value && value <= maxValue) {
|
||||
value = ((value - minValue) * 100.0) / (maxValue - minValue);
|
||||
return new PercentType(value.intValue());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DecimalType getChannelNumberState() {
|
||||
Double value = getStateDoubleValue(STATE_VALUE);
|
||||
if (value != null && minValue != null && maxValue != null && minValue <= value && value <= maxValue) {
|
||||
return new DecimalType(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dynamic updates to the minimum, maximum and step values. Sets a new state description if these values are
|
||||
* available. If no updates to the min/max/step, calls parent class method to update selector value in the
|
||||
* framework.
|
||||
*
|
||||
* @param state state update from the Miniserver
|
||||
*/
|
||||
@Override
|
||||
public void onStateChange(LxState state) {
|
||||
String stateName = state.getName();
|
||||
Object value = state.getStateValue();
|
||||
try {
|
||||
if (value instanceof Double) {
|
||||
if (STATE_MIN.equals(stateName)) {
|
||||
minValue = (Double) value;
|
||||
} else if (STATE_MAX.equals(stateName)) {
|
||||
maxValue = (Double) value;
|
||||
} else if (STATE_STEP.equals(stateName)) {
|
||||
stepValue = (Double) value;
|
||||
if (stepValue <= 0) {
|
||||
logger.warn("Value selector step value <= 0: {}", stepValue);
|
||||
stepValue = null;
|
||||
}
|
||||
} else {
|
||||
super.onStateChange(state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
logger.debug("Error parsing value for state {}: {}", stateName, e.getMessage());
|
||||
}
|
||||
if (minValue != null && maxValue != null && stepValue != null && minValue < maxValue) {
|
||||
StateDescriptionFragment fragment = StateDescriptionFragmentBuilder.create()
|
||||
.withMinimum(new BigDecimal(minValue)).withMaximum(new BigDecimal(maxValue))
|
||||
.withStep(new BigDecimal(stepValue)).withPattern(format).withReadOnly(false).build();
|
||||
addChannelStateDescriptionFragment(channelId, fragment);
|
||||
addChannelStateDescriptionFragment(numberChannelId, fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
|
||||
/**
|
||||
* A web page type of control on Loxone Miniserver.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlWebPage extends LxControl {
|
||||
static class Factory extends LxControlInstance {
|
||||
@Override
|
||||
LxControl create(LxUuid uuid) {
|
||||
return new LxControlWebPage(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getType() {
|
||||
return "webpage";
|
||||
}
|
||||
}
|
||||
|
||||
private StringType url;
|
||||
private StringType urlHd;
|
||||
|
||||
private LxControlWebPage(LxUuid uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LxControlConfig config) {
|
||||
super.initialize(config);
|
||||
if (details != null) {
|
||||
if (details.url != null) {
|
||||
url = new StringType(details.url);
|
||||
}
|
||||
if (details.urlHd != null) {
|
||||
urlHd = new StringType(details.urlHd);
|
||||
}
|
||||
}
|
||||
StateDescriptionFragment fragment = StateDescriptionFragmentBuilder.create().withReadOnly(true).build();
|
||||
ChannelUID c1 = addChannel("String", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_TEXT),
|
||||
defaultChannelLabel + " / URL", "Low resolution URL", tags, null, () -> url);
|
||||
addChannelStateDescriptionFragment(c1, fragment);
|
||||
|
||||
ChannelUID c2 = addChannel("String", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_TEXT),
|
||||
defaultChannelLabel + " / URL HD", "High resolution URL", tags, null, () -> urlHd);
|
||||
addChannelStateDescriptionFragment(c2, fragment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.security;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.openhab.binding.loxone.internal.LxServerHandler;
|
||||
import org.openhab.binding.loxone.internal.LxServerHandlerApi;
|
||||
import org.openhab.binding.loxone.internal.LxWebSocket;
|
||||
import org.openhab.binding.loxone.internal.types.LxErrorCode;
|
||||
import org.openhab.binding.loxone.internal.types.LxResponse;
|
||||
import org.openhab.binding.loxone.internal.types.LxWsSecurityType;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
|
||||
/**
|
||||
* Security abstract class providing authentication and encryption services.
|
||||
* Used by the {@link LxServerHandler} during connection establishment to authenticate user and during message exchange
|
||||
* for encryption and decryption or the messages.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public abstract class LxWsSecurity {
|
||||
final int debugId;
|
||||
final String user;
|
||||
final String password;
|
||||
final LxWebSocket socket;
|
||||
final LxServerHandlerApi thingHandler;
|
||||
|
||||
LxErrorCode reason;
|
||||
|
||||
private String details;
|
||||
private boolean cancel = false;
|
||||
private final Lock authenticationLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* Create an authentication instance.
|
||||
*
|
||||
* @param debugId instance of the client used for debugging purposes only
|
||||
* @param thingHandler API to the thing handler
|
||||
* @param socket websocket to perform communication with Miniserver
|
||||
* @param user user to authenticate
|
||||
* @param password password to authenticate
|
||||
*/
|
||||
LxWsSecurity(int debugId, LxServerHandlerApi thingHandler, LxWebSocket socket, String user, String password) {
|
||||
this.debugId = debugId;
|
||||
this.thingHandler = thingHandler;
|
||||
this.socket = socket;
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate user authentication. This method will return immediately and authentication will be done in a separate
|
||||
* thread asynchronously. On successful or unsuccessful completion, a provided callback will be called with
|
||||
* information about failed reason and details of failure. In case of success, the reason value will be
|
||||
* {@link LxErrorCode#OK}
|
||||
* Only one authentication can run in parallel and must be performed sequentially (create no more threads).
|
||||
*
|
||||
* @param doneCallback callback to execute when authentication is finished or failed
|
||||
*/
|
||||
public void authenticate(BiConsumer<LxErrorCode, String> doneCallback) {
|
||||
Runnable init = () -> {
|
||||
authenticationLock.lock();
|
||||
try {
|
||||
execute();
|
||||
doneCallback.accept(reason, details);
|
||||
} finally {
|
||||
authenticationLock.unlock();
|
||||
}
|
||||
};
|
||||
new Thread(init).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform user authentication using a specific authentication algorithm.
|
||||
* This method will be executed in a dedicated thread to allow sending synchronous messages to the Miniserver.
|
||||
*
|
||||
* @return true when authentication granted
|
||||
*/
|
||||
abstract boolean execute();
|
||||
|
||||
/**
|
||||
* Cancel authentication procedure and any pending activities.
|
||||
* It is supposed to be overridden by implementing classes.
|
||||
*/
|
||||
public void cancel() {
|
||||
cancel = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a response received from the Miniserver for errors, interpret it and store the results in class fields.
|
||||
*
|
||||
* @param response response received from the Miniserver
|
||||
* @return {@link LxErrorCode#OK} when response is correct or a specific {@link LxErrorCode}
|
||||
*/
|
||||
boolean checkResponse(LxResponse response) {
|
||||
if (response == null || response.subResponse == null || cancel) {
|
||||
reason = LxErrorCode.COMMUNICATION_ERROR;
|
||||
return false;
|
||||
}
|
||||
reason = response.getResponseCode();
|
||||
return (reason == LxErrorCode.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash string (e.g. containing user name and password or token) according to the algorithm required by the
|
||||
* Miniserver.
|
||||
*
|
||||
* @param string string to be hashed
|
||||
* @param hashKeyHex hash key received from the Miniserver in hex format
|
||||
* @return hashed string or null if failed
|
||||
*/
|
||||
String hashString(String string, String hashKeyHex) {
|
||||
if (string == null || hashKeyHex == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] hashKeyBytes = HexUtils.hexToBytes(hashKeyHex);
|
||||
SecretKeySpec signKey = new SecretKeySpec(hashKeyBytes, "HmacSHA1");
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(signKey);
|
||||
byte[] rawData = mac.doFinal(string.getBytes());
|
||||
return HexUtils.bytesToHex(rawData);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt string using current encryption algorithm.
|
||||
*
|
||||
* @param string input string to encrypt
|
||||
* @return encrypted string
|
||||
*/
|
||||
public String encrypt(String string) {
|
||||
// by default no encryption
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if control is encrypted and decrypt it using current decryption algorithm.
|
||||
* If control is not encrypted or decryption is not available or not ready, the control should be returned in its
|
||||
* original form.
|
||||
*
|
||||
* @param control control to be decrypted
|
||||
* @return decrypted control or original control in case decryption is unavailable, control is not encrypted or
|
||||
* other issue occurred
|
||||
*/
|
||||
public String decryptControl(String control) {
|
||||
// by default no decryption
|
||||
return control;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set error code and return false. It is used to report detailed error information from inside the algorithms.
|
||||
*
|
||||
* @param reason reason for failure
|
||||
* @param details details of the failure
|
||||
* @return always false
|
||||
*/
|
||||
boolean setError(LxErrorCode reason, String details) {
|
||||
if (reason != null) {
|
||||
this.reason = reason;
|
||||
}
|
||||
if (details != null) {
|
||||
this.details = details;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authentication instance.
|
||||
*
|
||||
* @param type type of security algorithm
|
||||
* @param swVersion Miniserver's software version or null if unknown
|
||||
* @param debugId instance of the client used for debugging purposes only
|
||||
* @param thingHandler API to the thing handler
|
||||
* @param socket websocket to perform communication with Miniserver
|
||||
* @param user user to authenticate
|
||||
* @param password password to authenticate
|
||||
* @return created security object
|
||||
*/
|
||||
public static LxWsSecurity create(LxWsSecurityType type, String swVersion, int debugId,
|
||||
LxServerHandlerApi thingHandler, LxWebSocket socket, String user, String password) {
|
||||
LxWsSecurityType securityType = type;
|
||||
if (securityType == LxWsSecurityType.AUTO && swVersion != null) {
|
||||
String[] versions = swVersion.split("[.]");
|
||||
if (versions != null && versions.length > 0 && Integer.parseInt(versions[0]) <= 8) {
|
||||
securityType = LxWsSecurityType.HASH;
|
||||
} else {
|
||||
securityType = LxWsSecurityType.TOKEN;
|
||||
}
|
||||
}
|
||||
if (securityType == LxWsSecurityType.HASH) {
|
||||
return new LxWsSecurityHash(debugId, thingHandler, socket, user, password);
|
||||
} else {
|
||||
return new LxWsSecurityToken(debugId, thingHandler, socket, user, password);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.security;
|
||||
|
||||
import org.openhab.binding.loxone.internal.LxServerHandlerApi;
|
||||
import org.openhab.binding.loxone.internal.LxWebSocket;
|
||||
import org.openhab.binding.loxone.internal.types.LxErrorCode;
|
||||
import org.openhab.binding.loxone.internal.types.LxResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A hash-based authentication algorithm. No encryption and decryption supported.
|
||||
* The algorithm computes a HMAC-SHA1 hash from the user name and password, using a key received from the Miniserver.
|
||||
* This hash is sent to the Miniserver to authorize the user.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxWsSecurityHash extends LxWsSecurity {
|
||||
|
||||
private static final String CMD_GET_KEY = "jdev/sys/getkey";
|
||||
private static final String CMD_AUTHENTICATE = "authenticate/";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LxWsSecurityHash.class);
|
||||
|
||||
/**
|
||||
* Create a hash-based authentication instance.
|
||||
*
|
||||
* @param debugId instance of the client used for debugging purposes only
|
||||
* @param thingHandler API to the thing handler
|
||||
* @param socket websocket to perform communication with Miniserver
|
||||
* @param user user to authenticate
|
||||
* @param password password to authenticate
|
||||
*/
|
||||
LxWsSecurityHash(int debugId, LxServerHandlerApi thingHandler, LxWebSocket socket, String user, String password) {
|
||||
super(debugId, thingHandler, socket, user, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean execute() {
|
||||
logger.debug("[{}] Starting hash-based authentication.", debugId);
|
||||
if (password == null || password.isEmpty()) {
|
||||
return setError(LxErrorCode.USER_UNAUTHORIZED, "Enter password for hash-based authentication.");
|
||||
}
|
||||
LxResponse resp = socket.sendCmdWithResp(CMD_GET_KEY, true, false);
|
||||
if (!checkResponse(resp)) {
|
||||
return false;
|
||||
}
|
||||
String hash = hashString(user + ":" + password, resp.getValueAsString());
|
||||
if (hash == null) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR, "Error hashing credentials.");
|
||||
}
|
||||
String cmd = CMD_AUTHENTICATE + hash;
|
||||
if (!checkResponse(socket.sendCmdWithResp(cmd, true, false))) {
|
||||
return false;
|
||||
}
|
||||
logger.debug("[{}] Authenticated - hash based authentication.", debugId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.security;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Base64;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import org.openhab.binding.loxone.internal.LxServerHandlerApi;
|
||||
import org.openhab.binding.loxone.internal.LxWebSocket;
|
||||
import org.openhab.binding.loxone.internal.types.LxErrorCode;
|
||||
import org.openhab.binding.loxone.internal.types.LxResponse;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.id.InstanceUUID;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* A token-based authentication algorithm with AES-256 encryption and decryption.
|
||||
*
|
||||
* The encryption algorithm uses public Miniserver key to RSA-encrypt own AES-256 key and initialization vector into a
|
||||
* session key. The encrypted session key is sent to the Miniserver. From this point on encryption (and decryption) of
|
||||
* the communication is possible and all further commands sent to the Miniserver are encrypted. The encryption makes use
|
||||
* of an additional salt value injected into the commands and updated frequently.
|
||||
*
|
||||
* To get the token, a hash key and salt values that are specific to the user are received from the Miniserver. These
|
||||
* values are used to compute a hash over user name and password using Miniserver's salt and key values (combined SHA1
|
||||
* and HMAC-SHA1 algorithm). This hash is sent to the Miniserver in an encrypted message to authorize the user and
|
||||
* obtain a token.
|
||||
*
|
||||
* Once a token is obtained, it can be used in all future authorizations instead of hashed user name and password.
|
||||
* When a token expires, it is refreshed.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxWsSecurityToken extends LxWsSecurity {
|
||||
/**
|
||||
* A sub-response value structure that is received as a response to get key-salt request command sent to the
|
||||
* Miniserver during authentication procedure.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
private class LxResponseKeySalt {
|
||||
String key;
|
||||
String salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* A sub-response value structure that is received as a response to token request or token update command sent to
|
||||
* the Miniserver during authentication procedure.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
private class LxResponseToken {
|
||||
String token;
|
||||
Integer validUntil;
|
||||
Boolean unsecurePass;
|
||||
@SuppressWarnings("unused")
|
||||
String key;
|
||||
@SuppressWarnings("unused")
|
||||
Integer tokenRights;
|
||||
}
|
||||
|
||||
// length of salt used for encrypting commands
|
||||
private static final int SALT_BYTES = 16;
|
||||
// after salt aged or reached max use count, a new salt will be generated
|
||||
private static final int SALT_MAX_AGE_SECONDS = 60 * 60;
|
||||
private static final int SALT_MAX_USE_COUNT = 30;
|
||||
|
||||
// defined by Loxone API, value 4 gives longest token expiration time
|
||||
private static final int TOKEN_PERMISSION = 4; // 2=web, 4=app
|
||||
// number of attempts for token refresh and delay between them
|
||||
private static final int TOKEN_REFRESH_RETRY_COUNT = 5;
|
||||
private static final int TOKEN_REFRESH_RETRY_DELAY_SECONDS = 10;
|
||||
// token will be refreshed 1 day before its expiration date
|
||||
private static final int TOKEN_REFRESH_SECONDS_BEFORE_EXPIRY = 24 * 60 * 60; // 1 day
|
||||
// if can't determine token expiration date, it will be refreshed after 2 days
|
||||
private static final int TOKEN_REFRESH_DEFAULT_SECONDS = 2 * 24 * 60 * 60; // 2 days
|
||||
|
||||
// AES encryption random initialization vector length
|
||||
private static final int IV_LENGTH_BYTES = 16;
|
||||
|
||||
private static final String CMD_GET_KEY_AND_SALT = "jdev/sys/getkey2/";
|
||||
private static final String CMD_GET_PUBLIC_KEY = "jdev/sys/getPublicKey";
|
||||
private static final String CMD_KEY_EXCHANGE = "jdev/sys/keyexchange/";
|
||||
private static final String CMD_REQUEST_TOKEN = "jdev/sys/gettoken/";
|
||||
private static final String CMD_GET_KEY = "jdev/sys/getkey";
|
||||
private static final String CMD_AUTH_WITH_TOKEN = "authwithtoken/";
|
||||
private static final String CMD_REFRESH_TOKEN = "jdev/sys/refreshtoken/";
|
||||
private static final String CMD_ENCRYPT_CMD = "jdev/sys/enc/";
|
||||
|
||||
private static final String SETTINGS_TOKEN = "authToken";
|
||||
private static final String SETTINGS_PASSWORD = "password";
|
||||
|
||||
private SecretKey aesKey;
|
||||
private Cipher aesEncryptCipher;
|
||||
private Cipher aesDecryptCipher;
|
||||
private SecureRandom secureRandom;
|
||||
private String salt;
|
||||
private int saltUseCount;
|
||||
private long saltTimeStamp;
|
||||
private boolean encryptionReady = false;
|
||||
private String token;
|
||||
private int tokenRefreshRetryCount;
|
||||
private ScheduledFuture<?> tokenRefreshTimer;
|
||||
private final Lock tokenRefreshLock = new ReentrantLock();
|
||||
|
||||
private final byte[] initVector = new byte[IV_LENGTH_BYTES];
|
||||
private final Logger logger = LoggerFactory.getLogger(LxWsSecurityToken.class);
|
||||
private static final ScheduledExecutorService SCHEDULER = ThreadPoolManager
|
||||
.getScheduledPool(LxWsSecurityToken.class.getName());
|
||||
|
||||
/**
|
||||
* Create a token-based authentication instance.
|
||||
*
|
||||
* @param debugId instance of the client used for debugging purposes only
|
||||
* @param thingHandler API to the thing handler
|
||||
* @param socket websocket to perform communication with Miniserver
|
||||
* @param user user to authenticate
|
||||
* @param password password to authenticate
|
||||
*/
|
||||
LxWsSecurityToken(int debugId, LxServerHandlerApi thingHandler, LxWebSocket socket, String user, String password) {
|
||||
super(debugId, thingHandler, socket, user, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean execute() {
|
||||
logger.debug("[{}] Starting token-based authentication.", debugId);
|
||||
if (!initialize()) {
|
||||
return false;
|
||||
}
|
||||
if ((token == null || token.isEmpty()) && (password == null || password.isEmpty())) {
|
||||
return setError(LxErrorCode.USER_UNAUTHORIZED, "Enter password to acquire token.");
|
||||
}
|
||||
// Get Miniserver's public key - must be over http, not websocket
|
||||
String msg = socket.httpGet(CMD_GET_PUBLIC_KEY);
|
||||
LxResponse resp = socket.getResponse(msg);
|
||||
if (resp == null) {
|
||||
return setError(LxErrorCode.COMMUNICATION_ERROR, "Get public key failed - null response.");
|
||||
}
|
||||
// RSA cipher to encrypt our AES-256 key using Miniserver's public key
|
||||
Cipher rsaCipher = getRsaCipher(resp.getValueAsString());
|
||||
if (rsaCipher == null) {
|
||||
return false;
|
||||
}
|
||||
// Generate session key
|
||||
byte[] sessionKey = generateSessionKey(rsaCipher);
|
||||
if (sessionKey == null) {
|
||||
return false;
|
||||
}
|
||||
// Exchange keys
|
||||
resp = socket.sendCmdWithResp(CMD_KEY_EXCHANGE + Base64.getEncoder().encodeToString(sessionKey), true, false);
|
||||
if (!checkResponse(resp)) {
|
||||
return setError(null, "Key exchange failed.");
|
||||
}
|
||||
logger.debug("[{}] Keys exchanged.", debugId);
|
||||
encryptionReady = true;
|
||||
|
||||
if (token == null || token.isEmpty()) {
|
||||
if (!acquireToken()) {
|
||||
return false;
|
||||
}
|
||||
logger.debug("[{}] Authenticated - acquired new token.", debugId);
|
||||
} else {
|
||||
if (!useToken()) {
|
||||
return false;
|
||||
}
|
||||
logger.debug("[{}] Authenticated - used stored token.", debugId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encrypt(String command) {
|
||||
if (!encryptionReady) {
|
||||
return command;
|
||||
}
|
||||
String str;
|
||||
if (salt != null && newSaltNeeded()) {
|
||||
String prevSalt = salt;
|
||||
salt = generateSalt();
|
||||
str = "nextSalt/" + prevSalt + "/" + salt + "/" + command + "\0";
|
||||
} else {
|
||||
if (salt == null) {
|
||||
salt = generateSalt();
|
||||
}
|
||||
str = "salt/" + salt + "/" + command + "\0";
|
||||
}
|
||||
|
||||
logger.debug("[{}] Command for encryption: {}", debugId, str);
|
||||
try {
|
||||
String encrypted = Base64.getEncoder()
|
||||
.encodeToString(aesEncryptCipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
|
||||
try {
|
||||
encrypted = URLEncoder.encode(encrypted, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.warn("[{}] Unsupported encoding for encrypted command conversion to URL.", debugId);
|
||||
}
|
||||
return CMD_ENCRYPT_CMD + encrypted;
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
logger.warn("[{}] Command encryption failed: {}", debugId, e.getMessage());
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptControl(String control) {
|
||||
String string = control;
|
||||
if (!encryptionReady || !string.startsWith(CMD_ENCRYPT_CMD)) {
|
||||
return string;
|
||||
}
|
||||
string = string.substring(CMD_ENCRYPT_CMD.length());
|
||||
try {
|
||||
byte[] bytes = Base64.getDecoder().decode(string);
|
||||
bytes = aesDecryptCipher.doFinal(bytes);
|
||||
string = new String(bytes, "UTF-8");
|
||||
string = string.replaceAll("\0+.*$", "");
|
||||
string = string.replaceFirst("^salt/[^/]*/", "");
|
||||
string = string.replaceFirst("^nextSalt/[^/]*/[^/]*/", "");
|
||||
return string;
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("[{}] Failed to decode base64 string: {}", debugId, string);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
logger.warn("[{}] Command decryption failed: {}", debugId, e.getMessage());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.warn("[{}] Unsupported encoding for decrypted bytes to string conversion.", debugId);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
super.cancel();
|
||||
tokenRefreshLock.lock();
|
||||
try {
|
||||
if (tokenRefreshTimer != null) {
|
||||
logger.debug("[{}] Cancelling token refresh.", debugId);
|
||||
tokenRefreshTimer.cancel(true);
|
||||
}
|
||||
} finally {
|
||||
tokenRefreshLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean initialize() {
|
||||
try {
|
||||
encryptionReady = false;
|
||||
tokenRefreshRetryCount = TOKEN_REFRESH_RETRY_COUNT;
|
||||
if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR,
|
||||
"Enable Java cryptography unlimited strength (see binding doc).");
|
||||
}
|
||||
// generate a random key for the session
|
||||
KeyGenerator aesKeyGen = KeyGenerator.getInstance("AES");
|
||||
aesKeyGen.init(256);
|
||||
aesKey = aesKeyGen.generateKey();
|
||||
// generate an initialization vector
|
||||
secureRandom = new SecureRandom();
|
||||
secureRandom.nextBytes(initVector);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(initVector);
|
||||
// initialize aes cipher for command encryption
|
||||
aesEncryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
aesEncryptCipher.init(Cipher.ENCRYPT_MODE, aesKey, ivSpec);
|
||||
// initialize aes cipher for response decryption
|
||||
aesDecryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
aesDecryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivSpec);
|
||||
// get token value from configuration storage
|
||||
token = thingHandler.getSetting(SETTINGS_TOKEN);
|
||||
logger.debug("[{}] Retrieved token value: {}", debugId, token);
|
||||
} catch (InvalidParameterException e) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR, "Invalid parameter: " + e.getMessage());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR, "AES not supported on platform.");
|
||||
} catch (InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR, "AES cipher initialization failed.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Cipher getRsaCipher(String key) {
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
String keyString = key.replace("-----BEGIN CERTIFICATE-----", "").replace("-----END CERTIFICATE-----", "");
|
||||
byte[] keyData = Base64.getDecoder().decode(keyString);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyData);
|
||||
PublicKey publicKey = keyFactory.generatePublic(keySpec);
|
||||
logger.debug("[{}] Miniserver public key: {}", debugId, publicKey);
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
cipher.init(Cipher.PUBLIC_KEY, publicKey);
|
||||
logger.debug("[{}] Initialized RSA public key cipher", debugId);
|
||||
return cipher;
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException e) {
|
||||
setError(LxErrorCode.INTERNAL_ERROR, "Exception enabling RSA cipher: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateSessionKey(Cipher rsaCipher) {
|
||||
String key = HexUtils.bytesToHex(aesKey.getEncoded()) + ":" + HexUtils.bytesToHex(initVector);
|
||||
try {
|
||||
byte[] sessionKey = rsaCipher.doFinal(key.getBytes());
|
||||
logger.debug("[{}] Generated session key: {}", debugId, HexUtils.bytesToHex(sessionKey));
|
||||
return sessionKey;
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
setError(LxErrorCode.INTERNAL_ERROR, "Exception encrypting session key: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String hashCredentials(LxResponseKeySalt keySalt) {
|
||||
try {
|
||||
MessageDigest msgDigest = MessageDigest.getInstance("SHA-1");
|
||||
String pwdHashStr = password + ":" + keySalt.salt;
|
||||
byte[] rawData = msgDigest.digest(pwdHashStr.getBytes(StandardCharsets.UTF_8));
|
||||
String pwdHash = HexUtils.bytesToHex(rawData).toUpperCase();
|
||||
logger.debug("[{}] PWDHASH: {}", debugId, pwdHash);
|
||||
return hashString(user + ":" + pwdHash, keySalt.key);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.debug("[{}] Error hashing token credentials: {}", debugId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean acquireToken() {
|
||||
// Get Miniserver hash key and salt - this command should be encrypted
|
||||
LxResponse resp = socket.sendCmdWithResp(CMD_GET_KEY_AND_SALT + user, true, true);
|
||||
if (!checkResponse(resp)) {
|
||||
return setError(null, "Hash key/salt get failed.");
|
||||
}
|
||||
LxResponseKeySalt keySalt = resp.getValueAs(thingHandler.getGson(), LxResponseKeySalt.class);
|
||||
if (keySalt == null) {
|
||||
return setError(null, "Error parsing hash key/salt json: " + resp.getValueAsString());
|
||||
}
|
||||
|
||||
logger.debug("[{}] Hash key: {}, salt: {}", debugId, keySalt.key, keySalt.salt);
|
||||
// Hash user name, password, key and salt
|
||||
String hash = hashCredentials(keySalt);
|
||||
if (hash == null) {
|
||||
return false;
|
||||
}
|
||||
// Request token
|
||||
String uuid = InstanceUUID.get();
|
||||
resp = socket.sendCmdWithResp(CMD_REQUEST_TOKEN + hash + "/" + user + "/" + TOKEN_PERMISSION + "/"
|
||||
+ (uuid != null ? uuid : "098802e1-02b4-603c-ffffeee000d80cfd") + "/openHAB", true, true);
|
||||
if (!checkResponse(resp)) {
|
||||
return setError(null, "Request token failed.");
|
||||
}
|
||||
|
||||
try {
|
||||
LxResponseToken tokenResponse = parseTokenResponse(resp);
|
||||
if (tokenResponse == null) {
|
||||
return false;
|
||||
}
|
||||
token = tokenResponse.token;
|
||||
if (token == null) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR, "Received null token.");
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
return setError(LxErrorCode.INTERNAL_ERROR, "Error parsing token response: " + e.getMessage());
|
||||
}
|
||||
|
||||
persistToken();
|
||||
logger.debug("[{}] Token acquired.", debugId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean useToken() {
|
||||
String hash = hashToken();
|
||||
if (hash == null) {
|
||||
return false;
|
||||
}
|
||||
LxResponse resp = socket.sendCmdWithResp(CMD_AUTH_WITH_TOKEN + hash + "/" + user, true, true);
|
||||
if (!checkResponse(resp)) {
|
||||
if (reason == LxErrorCode.USER_UNAUTHORIZED) {
|
||||
token = null;
|
||||
persistToken();
|
||||
return setError(null, "Enter password to generate a new token.");
|
||||
}
|
||||
return setError(null, "Token-based authentication failed.");
|
||||
}
|
||||
parseTokenResponse(resp);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String hashToken() {
|
||||
LxResponse resp = socket.sendCmdWithResp(CMD_GET_KEY, true, true);
|
||||
if (!checkResponse(resp)) {
|
||||
setError(null, "Get key command failed.");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String hashKey = resp.getValueAsString();
|
||||
// here is a difference to the API spec, which says the string to hash is "user:token", but this is "token"
|
||||
String hash = hashString(token, hashKey);
|
||||
if (hash == null) {
|
||||
setError(null, "Error hashing token.");
|
||||
}
|
||||
return hash;
|
||||
} catch (ClassCastException | IllegalStateException e) {
|
||||
setError(LxErrorCode.INTERNAL_ERROR, "Error parsing Miniserver key.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistToken() {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put(SETTINGS_TOKEN, token);
|
||||
if (token != null) {
|
||||
properties.put(SETTINGS_PASSWORD, null);
|
||||
}
|
||||
thingHandler.setSettings(properties);
|
||||
}
|
||||
|
||||
private LxResponseToken parseTokenResponse(LxResponse response) {
|
||||
LxResponseToken tokenResponse = response.getValueAs(thingHandler.getGson(), LxResponseToken.class);
|
||||
if (tokenResponse == null) {
|
||||
setError(LxErrorCode.INTERNAL_ERROR, "Error parsing token response.");
|
||||
return null;
|
||||
}
|
||||
Boolean unsecurePass = tokenResponse.unsecurePass;
|
||||
if (unsecurePass != null && unsecurePass) {
|
||||
logger.warn("[{}] Unsecure user password on Miniserver.", debugId);
|
||||
}
|
||||
long secondsToExpiry;
|
||||
Integer validUntil = tokenResponse.validUntil;
|
||||
if (validUntil == null) {
|
||||
secondsToExpiry = TOKEN_REFRESH_DEFAULT_SECONDS;
|
||||
} else {
|
||||
// validUntil is the end of token life-span in seconds from 2009/01/01
|
||||
Calendar loxoneCalendar = Calendar.getInstance();
|
||||
loxoneCalendar.clear();
|
||||
loxoneCalendar.set(2009, Calendar.JANUARY, 1);
|
||||
loxoneCalendar.add(Calendar.SECOND, validUntil);
|
||||
Calendar ohCalendar = Calendar.getInstance();
|
||||
secondsToExpiry = (loxoneCalendar.getTimeInMillis() - ohCalendar.getTimeInMillis()) / 1000;
|
||||
if (logger.isDebugEnabled()) {
|
||||
try {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
logger.debug("[{}] Token will expire on: {}.", debugId, format.format(loxoneCalendar.getTime()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("[{}] Token will expire in {} days.", debugId,
|
||||
TimeUnit.SECONDS.toDays(secondsToExpiry));
|
||||
}
|
||||
}
|
||||
if (secondsToExpiry <= 0) {
|
||||
logger.warn("[{}] Time to token expiry is negative or zero: {}", debugId, secondsToExpiry);
|
||||
secondsToExpiry = TOKEN_REFRESH_DEFAULT_SECONDS;
|
||||
} else {
|
||||
int correction = TOKEN_REFRESH_SECONDS_BEFORE_EXPIRY;
|
||||
while (secondsToExpiry - correction < 0) {
|
||||
correction /= 2;
|
||||
}
|
||||
secondsToExpiry -= correction;
|
||||
}
|
||||
}
|
||||
scheduleTokenRefresh(secondsToExpiry);
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
private void refreshToken() {
|
||||
tokenRefreshLock.lock();
|
||||
try {
|
||||
tokenRefreshTimer = null;
|
||||
String hash = hashToken();
|
||||
if (hash != null) {
|
||||
LxResponse resp = socket.sendCmdWithResp(CMD_REFRESH_TOKEN + hash + "/" + user, true, true);
|
||||
if (checkResponse(resp)) {
|
||||
logger.debug("[{}] Successful token refresh.", debugId);
|
||||
parseTokenResponse(resp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.debug("[{}] Token refresh failed, retrying (retry={}).", debugId, tokenRefreshRetryCount);
|
||||
if (tokenRefreshRetryCount-- > 0) {
|
||||
scheduleTokenRefresh(TOKEN_REFRESH_RETRY_DELAY_SECONDS);
|
||||
} else {
|
||||
logger.warn("[{}] All token refresh attempts failed.", debugId);
|
||||
}
|
||||
} finally {
|
||||
tokenRefreshLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleTokenRefresh(long delay) {
|
||||
logger.debug("[{}] Setting token refresh in {} days.", debugId, TimeUnit.SECONDS.toDays(delay));
|
||||
tokenRefreshLock.lock();
|
||||
try {
|
||||
tokenRefreshTimer = SCHEDULER.schedule(this::refreshToken, delay, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
tokenRefreshLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private String generateSalt() {
|
||||
byte[] bytes = new byte[SALT_BYTES];
|
||||
secureRandom.nextBytes(bytes);
|
||||
String salt = HexUtils.bytesToHex(bytes);
|
||||
try {
|
||||
salt = URLEncoder.encode(salt, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.warn("[{}] Unsupported encoding for salt conversion to URL.", debugId);
|
||||
}
|
||||
saltTimeStamp = timeElapsedInSeconds();
|
||||
saltUseCount = 0;
|
||||
logger.debug("[{}] Generated salt: {}", debugId, salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
private boolean newSaltNeeded() {
|
||||
return (++saltUseCount > SALT_MAX_USE_COUNT || timeElapsedInSeconds() - saltTimeStamp > SALT_MAX_AGE_SECONDS);
|
||||
}
|
||||
|
||||
private long timeElapsedInSeconds() {
|
||||
return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
|
||||
/**
|
||||
* Category of Loxone Miniserver's {@link LxControl} object.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxCategory extends LxContainer {
|
||||
|
||||
/**
|
||||
* Various categories that Loxone Miniserver's control can belong to.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*/
|
||||
public enum CategoryType {
|
||||
/**
|
||||
* Category for lights
|
||||
*/
|
||||
LIGHTS,
|
||||
/**
|
||||
* Category for shading / rollershutter / blinds
|
||||
*/
|
||||
SHADING,
|
||||
/**
|
||||
* Category for temperatures
|
||||
*/
|
||||
TEMPERATURE,
|
||||
/**
|
||||
* Unknown category
|
||||
*/
|
||||
UNDEFINED
|
||||
}
|
||||
|
||||
private String type; // deserialized from JSON
|
||||
private CategoryType catType;
|
||||
|
||||
/**
|
||||
* Obtain the type of this category
|
||||
*
|
||||
* @return type of category
|
||||
*/
|
||||
public CategoryType getType() {
|
||||
if (catType == null && type != null) {
|
||||
String tl = type.toLowerCase();
|
||||
if (tl.equals("lights")) {
|
||||
catType = CategoryType.LIGHTS;
|
||||
} else if (tl.equals("shading")) {
|
||||
catType = CategoryType.SHADING;
|
||||
} else if (tl.equals("indoortemperature")) {
|
||||
catType = CategoryType.TEMPERATURE;
|
||||
} else {
|
||||
catType = CategoryType.UNDEFINED;
|
||||
}
|
||||
}
|
||||
return catType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openhab.binding.loxone.internal.LxServerHandlerApi;
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl.LxControlConfig;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* A structure of JSON file http://miniserver/data/LoxAPP3.json used for parsing it with Gson library.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxConfig {
|
||||
|
||||
private Map<LxUuid, LxContainer> rooms;
|
||||
@SerializedName("cats")
|
||||
private Map<LxUuid, LxCategory> categories;
|
||||
public Map<LxUuid, LxControl> controls;
|
||||
|
||||
public class LxServerInfo {
|
||||
public String serialNr;
|
||||
public String location;
|
||||
public String roomTitle;
|
||||
public String catTitle;
|
||||
public String msName;
|
||||
public String projectName;
|
||||
public String remoteUrl;
|
||||
public String swVersion;
|
||||
public String macAddress;
|
||||
}
|
||||
|
||||
public LxServerInfo msInfo;
|
||||
|
||||
public void finalize(LxServerHandlerApi thingHandler) {
|
||||
rooms.values().removeIf(o -> (o == null || o.getUuid() == null));
|
||||
categories.values().removeIf(o -> (o == null || o.getUuid() == null));
|
||||
controls.values().removeIf(c -> c == null || c.isSecured());
|
||||
controls.values().forEach(c -> c.initialize(
|
||||
new LxControlConfig(thingHandler, rooms.get(c.getRoomUuid()), categories.get(c.getCategoryUuid()))));
|
||||
}
|
||||
|
||||
public static <T> T deserializeObject(JsonObject parent, String name, Type type,
|
||||
JsonDeserializationContext context) {
|
||||
JsonElement element = parent.get(name);
|
||||
if (element != null) {
|
||||
return context.deserialize(element, type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String deserializeString(JsonObject parent, String name) {
|
||||
JsonElement element = parent.get(name);
|
||||
if (element != null) {
|
||||
return element.getAsString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
|
||||
/**
|
||||
* Container on Loxone Miniserver that groups {@link LxControl} objects.
|
||||
* <p>
|
||||
* Examples of containers are rooms and categories.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxContainer {
|
||||
private LxUuid uuid; // set by JSON deserialization
|
||||
private String name; // set by JSON deserialization
|
||||
private final Set<LxControl> controls;
|
||||
|
||||
/**
|
||||
* Create a new container with given uuid and name
|
||||
*/
|
||||
LxContainer() {
|
||||
controls = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain container's current name
|
||||
*
|
||||
* @return
|
||||
* container's current name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain container's UUID (assigned by Loxone Miniserver)
|
||||
*
|
||||
* @return
|
||||
* container's UUID
|
||||
*/
|
||||
public LxUuid getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new control to this container
|
||||
*
|
||||
* @param control control to be added
|
||||
*/
|
||||
public void addControl(LxControl control) {
|
||||
controls.add(control);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a control from the container
|
||||
*
|
||||
* @param control control object to remove from the container
|
||||
* @return true if control object existed in the container and was removed
|
||||
*/
|
||||
public boolean removeControl(LxControl control) {
|
||||
return controls.remove(control);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
/**
|
||||
* Reasons why Miniserver may be not reachable
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public enum LxErrorCode {
|
||||
/**
|
||||
* No error at all
|
||||
*/
|
||||
OK,
|
||||
/**
|
||||
* User name or password incorrect or user not authorized
|
||||
*/
|
||||
USER_UNAUTHORIZED,
|
||||
/**
|
||||
* Too many failed login attempts and server's temporary ban of the user
|
||||
*/
|
||||
TOO_MANY_FAILED_LOGIN_ATTEMPTS,
|
||||
/**
|
||||
* Communication error with the Miniserv
|
||||
*/
|
||||
COMMUNICATION_ERROR,
|
||||
/**
|
||||
* Timeout of user authentication procedure
|
||||
*/
|
||||
USER_AUTHENTICATION_TIMEOUT,
|
||||
/**
|
||||
* No activity from Miniserver's client
|
||||
*/
|
||||
WEBSOCKET_IDLE_TIMEOUT,
|
||||
/**
|
||||
* Internal error, sign of something wrong with the program
|
||||
*/
|
||||
INTERNAL_ERROR,
|
||||
/**
|
||||
* Error code is missing - reason for failure is unknown
|
||||
*/
|
||||
ERROR_CODE_MISSING;
|
||||
|
||||
/**
|
||||
* Converts Miniserver status code to enumerated error value
|
||||
*
|
||||
* @param code status code received in message response from the Miniserver
|
||||
* @return converted error code
|
||||
*/
|
||||
public static LxErrorCode getErrorCode(Integer code) {
|
||||
if (code == null) {
|
||||
return ERROR_CODE_MISSING;
|
||||
}
|
||||
switch (code) {
|
||||
case 420:
|
||||
return USER_AUTHENTICATION_TIMEOUT;
|
||||
case 401:
|
||||
case 500:
|
||||
return USER_UNAUTHORIZED;
|
||||
case 4003:
|
||||
return TOO_MANY_FAILED_LOGIN_ATTEMPTS;
|
||||
case 1001:
|
||||
return WEBSOCKET_IDLE_TIMEOUT;
|
||||
case 200:
|
||||
return OK;
|
||||
default:
|
||||
return COMMUNICATION_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Response to a command sent to Miniserver's control.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxResponse {
|
||||
|
||||
/**
|
||||
* A sub-response value structure that is received as a response to config API HTTP request sent to the Miniserver.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxResponseCfgApi {
|
||||
public String snr;
|
||||
public String version;
|
||||
}
|
||||
|
||||
/**
|
||||
* A sub-response structure that is part of a {@link LxResponse} class and contains a response to a command sent to
|
||||
* Miniserver's control.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
private class LxSubResponse {
|
||||
@SerializedName("control")
|
||||
private String command;
|
||||
@SerializedName(value = "Code", alternate = { "code" })
|
||||
private Integer code;
|
||||
private JsonElement value;
|
||||
|
||||
private boolean isSubResponseOk() {
|
||||
return (getResponseCode() == LxErrorCode.OK) && (command != null) && (value != null);
|
||||
}
|
||||
|
||||
private LxErrorCode getResponseCode() {
|
||||
return LxErrorCode.getErrorCode(code);
|
||||
}
|
||||
}
|
||||
|
||||
@SerializedName("LL")
|
||||
public LxSubResponse subResponse;
|
||||
private final Logger logger = LoggerFactory.getLogger(LxResponse.class);
|
||||
|
||||
/**
|
||||
* Return true when response has correct syntax and return code was successful
|
||||
*
|
||||
* @return true when response is ok
|
||||
*/
|
||||
public boolean isResponseOk() {
|
||||
return (subResponse != null && subResponse.isSubResponseOk());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets command to which this response relates
|
||||
*
|
||||
* @return command name
|
||||
*/
|
||||
public String getCommand() {
|
||||
return (subResponse != null ? subResponse.command : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets error code from the response in numerical form or null if absent
|
||||
*
|
||||
* @return error code value
|
||||
*/
|
||||
public Integer getResponseCodeNumber() {
|
||||
return (subResponse != null ? subResponse.code : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets error code from the response as an enumerated value
|
||||
*
|
||||
* @return error code
|
||||
*/
|
||||
public LxErrorCode getResponseCode() {
|
||||
return LxErrorCode.getErrorCode(getResponseCodeNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets response value as a string
|
||||
*
|
||||
* @return response value as string
|
||||
*/
|
||||
public String getValueAsString() {
|
||||
return (subResponse != null && subResponse.value != null ? subResponse.value.getAsString() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes response value as a given type
|
||||
*
|
||||
* @param gson GSON object used for deserialization
|
||||
* @param <T> class to deserialize response to
|
||||
* @param type class type to deserialize response to
|
||||
* @return deserialized response
|
||||
*/
|
||||
public <T> T getValueAs(Gson gson, Type type) {
|
||||
if (subResponse != null && subResponse.value != null) {
|
||||
try {
|
||||
return gson.fromJson(subResponse.value, type);
|
||||
} catch (NumberFormatException | JsonParseException e) {
|
||||
logger.debug("Error parsing response value as {}: {}", type, e.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A state of a Loxone control ({@link LxControl})
|
||||
* <p>
|
||||
* Each control object may have a number of states defined, that describe the overall condition of the control.
|
||||
* List of states is read from LoxApp3.json configuration file.
|
||||
* <p>
|
||||
* Each state is identified by its own UUID and a name of the state. Names are proprietary to a particular type of the
|
||||
* control and as such are defined in {@link LxControl} child classes implementation.
|
||||
* Objects of this class are used to bind state updates received from the Miniserver to a control object.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxState {
|
||||
private final LxUuid uuid;
|
||||
private final String name;
|
||||
private final LxControl control;
|
||||
private final Logger logger = LoggerFactory.getLogger(LxState.class);
|
||||
private Object stateValue;
|
||||
|
||||
/**
|
||||
* Create a control state object.
|
||||
*
|
||||
* @param uuid UUID of the state
|
||||
* @param name name of the state
|
||||
* @param control control to which this state belongs
|
||||
*/
|
||||
public LxState(LxUuid uuid, String name, LxControl control) {
|
||||
this.uuid = uuid;
|
||||
this.name = name;
|
||||
this.control = control;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets UUID of the state
|
||||
*
|
||||
* @return state's UUID
|
||||
*/
|
||||
public LxUuid getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current value of the control's state
|
||||
*
|
||||
* @param value current state's value to set
|
||||
*/
|
||||
public void setStateValue(Object value) {
|
||||
logger.debug("State set ({},{}) control ({},{}) value={}", uuid, name, control.getUuid(), control.getName(),
|
||||
value);
|
||||
if (value != null && !value.equals(this.stateValue)) {
|
||||
this.stateValue = value;
|
||||
control.onStateChange(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current value of the control's state
|
||||
*
|
||||
* @return current state's value
|
||||
*/
|
||||
public Object getStateValue() {
|
||||
return stateValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets state's name.
|
||||
* <p>
|
||||
* State's name is proprietary per control type.
|
||||
*
|
||||
* @return state's name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.types;
|
||||
|
||||
/**
|
||||
* A state update event. It is used to defer and queue processing of Loxone state updates, so they are not processed in
|
||||
* the websocket thread.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxStateUpdate {
|
||||
private final LxUuid uuid;
|
||||
private final Object value;
|
||||
|
||||
public LxStateUpdate(LxUuid uuid, Object value) {
|
||||
this.uuid = uuid;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public LxUuid getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.types;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.binding.loxone.internal.controls.LxControl;
|
||||
|
||||
/**
|
||||
* Channel tags for a {@link LxControl} object.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxTags {
|
||||
public static final Set<String> SCENE = Collections.singleton("Scene");
|
||||
public static final Set<String> LIGHTING = Collections.singleton("Lighting");
|
||||
public static final Set<String> SWITCHABLE = Collections.singleton("Switchable");
|
||||
public static final Set<String> TEMPERATURE = Collections.singleton("CurrentTemperature");
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.types;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
|
||||
/**
|
||||
* Temperature HSB Type which acceptss a color in the form brigthness,temperature (Kelvin)
|
||||
*
|
||||
* @author Michael Mattan - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxTemperatureHSBType extends HSBType {
|
||||
|
||||
private static final long serialVersionUID = -2821122730407485795L;
|
||||
|
||||
public static HSBType fromBrightnessTemperature(String value) {
|
||||
List<String> constituents = Arrays.stream(value.split(",")).map(in -> in.trim()).collect(Collectors.toList());
|
||||
|
||||
if (constituents.size() == 2) {
|
||||
int brightness = constrain(Integer.valueOf(constituents.get(0)), 0, 100);
|
||||
int temperature = constrain(Integer.valueOf(constituents.get(1)), 0, 65500);
|
||||
|
||||
int red = map(brightness, 0, 100, 0, calculateRed(temperature));
|
||||
int green = map(brightness, 0, 100, 0, calculateGreen(temperature));
|
||||
int blue = map(brightness, 0, 100, 0, calculateBlue(temperature));
|
||||
|
||||
return HSBType.fromRGB(red, green, blue);
|
||||
} else {
|
||||
throw new IllegalArgumentException(value + " is not a valid TemperatureHSBType syntax");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-maps a number from one range to another. That is, a value of fromLow would get mapped to toLow, a value of
|
||||
* fromHigh to toHigh, values in-between to values in-between, etc.
|
||||
*
|
||||
* @param x the number to map
|
||||
* @param fromLow the lower bound of the value's current range
|
||||
* @param fromHigh the upper bound of the value's current range
|
||||
* @param toLow the lower bound of the value's target range
|
||||
* @param toHigh the upper bound of the value's target range
|
||||
* @return the mapped value
|
||||
*/
|
||||
private static int map(int x, int fromLow, int fromHigh, int toLow, int toHigh) {
|
||||
return (x - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow;
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates the red value based on the Kelvin temperature
|
||||
*
|
||||
* @param temp the Kelvin temperature
|
||||
* @return the red value
|
||||
*/
|
||||
private static int calculateRed(int temp) {
|
||||
int red = 255;
|
||||
int temperature = temp / 100;
|
||||
|
||||
if (temperature > 66) {
|
||||
red = temperature - 60;
|
||||
red = ((Long) Math.round(329.698727466 * Math.pow(red, -0.1332047592))).intValue();
|
||||
}
|
||||
|
||||
return constrain(red, 0, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates the green value based on the Kelvin temperature
|
||||
*
|
||||
* @param temp green Kelvin temperature
|
||||
* @return the red value
|
||||
*/
|
||||
private static int calculateGreen(int temp) {
|
||||
int green;
|
||||
int temperature = temp / 100;
|
||||
|
||||
if (temperature <= 66) {
|
||||
green = temperature;
|
||||
green = ((Long) Math.round((99.4708025861 * Math.log(green)) - 161.1195681661)).intValue();
|
||||
} else {
|
||||
green = temperature - 60;
|
||||
green = ((Long) Math.round(288.1221695283 * Math.pow(green, -0.0755148492))).intValue();
|
||||
}
|
||||
|
||||
return constrain(green, 0, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates the blue value based on the Kelvin temperature
|
||||
*
|
||||
* @param temp the Kelvin temperature
|
||||
* @return the blue value
|
||||
*/
|
||||
private static int calculateBlue(int temp) {
|
||||
int blue = 255;
|
||||
int temperature = temp / 100;
|
||||
|
||||
if (temperature < 65) {
|
||||
if (temperature <= 19) {
|
||||
blue = 0;
|
||||
} else {
|
||||
blue = temperature - 10;
|
||||
blue = ((Long) Math.round((138.5177312231 * Math.log(blue)) - 305.0447927307)).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
return constrain(blue, 0, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains a number to be within a range.
|
||||
*
|
||||
* @param x the number to constrain
|
||||
* @param min the minimum value
|
||||
* @param max the maximum value
|
||||
* @return the constrained value
|
||||
*/
|
||||
private static int constrain(int x, int min, int max) {
|
||||
if (x >= min && x <= max) {
|
||||
return x;
|
||||
} else if (x < min) {
|
||||
return min;
|
||||
} else {
|
||||
return max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.types;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* Unique identifier of an object on Loxone Miniserver.
|
||||
* <p>
|
||||
* It is defined by the Miniserver. UUID can represent a control, room, category, etc. and provides a unique ID space
|
||||
* across all objects residing on the Miniserver.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxUuid {
|
||||
private final String uuid;
|
||||
private final String uuidOriginal;
|
||||
|
||||
public static final JsonDeserializer<LxUuid> DESERIALIZER = new JsonDeserializer<LxUuid>() {
|
||||
@Override
|
||||
public LxUuid deserialize(JsonElement json, Type type, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
String uuid = json.getAsString();
|
||||
if (uuid != null) {
|
||||
return new LxUuid(uuid);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new {@link LxUuid} object from an UUID on a Miniserver.
|
||||
*
|
||||
* @param uuid identifier retrieved from Loxone Miniserver
|
||||
*/
|
||||
public LxUuid(String uuid) {
|
||||
uuidOriginal = uuid;
|
||||
this.uuid = init(uuid);
|
||||
}
|
||||
|
||||
public LxUuid(byte data[], int offset) {
|
||||
String id = String.format("%08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
ByteBuffer.wrap(data, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(),
|
||||
ByteBuffer.wrap(data, offset + 4, 2).order(ByteOrder.LITTLE_ENDIAN).getShort(),
|
||||
ByteBuffer.wrap(data, offset + 6, 2).order(ByteOrder.LITTLE_ENDIAN).getShort(), data[offset + 8],
|
||||
data[offset + 9], data[offset + 10], data[offset + 11], data[offset + 12], data[offset + 13],
|
||||
data[offset + 14], data[offset + 15]);
|
||||
uuidOriginal = id;
|
||||
this.uuid = init(id);
|
||||
}
|
||||
|
||||
private String init(String uuid) {
|
||||
return uuidOriginal.replaceAll("[^a-zA-Z0-9-]", "-").toUpperCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null) {
|
||||
return false;
|
||||
}
|
||||
if (o.getClass() != getClass()) {
|
||||
return false;
|
||||
}
|
||||
LxUuid id = (LxUuid) o;
|
||||
return uuid.equals(id.uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uuid.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an original string that was used to create UUID.
|
||||
*
|
||||
* @return original string for the UUID
|
||||
*/
|
||||
public String getOriginalString() {
|
||||
return uuidOriginal;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.types;
|
||||
|
||||
/**
|
||||
* A header of a binary message received from Loxone Miniserver on a websocket connection.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxWsBinaryHeader {
|
||||
/**
|
||||
* Type of a binary message received from the Miniserver
|
||||
*
|
||||
* @author Pawel Pieczul
|
||||
*
|
||||
*/
|
||||
public enum LxWsMessageType {
|
||||
/**
|
||||
* Text message - jetty websocket client will pass it on automatically to a callback
|
||||
*/
|
||||
TEXT_MESSAGE,
|
||||
/**
|
||||
* Binary file
|
||||
*/
|
||||
BINARY_FILE,
|
||||
/**
|
||||
* A set of value states for controls that changed their state
|
||||
*/
|
||||
EVENT_TABLE_OF_VALUE_STATES,
|
||||
/**
|
||||
* A set of text states for controls that changed their state
|
||||
*/
|
||||
EVENT_TABLE_OF_TEXT_STATES,
|
||||
EVENT_TABLE_OF_DAYTIMER_STATES,
|
||||
OUT_OF_SERVICE_INDICATOR,
|
||||
/**
|
||||
* Response to keepalive request message
|
||||
*/
|
||||
KEEPALIVE_RESPONSE,
|
||||
EVENT_TABLE_OF_WEATHER_STATES,
|
||||
/**
|
||||
* Unknown header
|
||||
*/
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
private LxWsMessageType type = LxWsMessageType.UNKNOWN;
|
||||
|
||||
/**
|
||||
* Create header from binary buffer at a given offset
|
||||
*
|
||||
* @param buffer buffer with received message
|
||||
* @param offset offset in bytes at which header is expected
|
||||
*/
|
||||
public LxWsBinaryHeader(byte[] buffer, int offset) throws IndexOutOfBoundsException {
|
||||
if (buffer[offset] != 0x03) {
|
||||
return;
|
||||
}
|
||||
switch (buffer[offset + 1]) {
|
||||
case 0:
|
||||
type = LxWsMessageType.TEXT_MESSAGE;
|
||||
break;
|
||||
case 1:
|
||||
type = LxWsMessageType.BINARY_FILE;
|
||||
break;
|
||||
case 2:
|
||||
type = LxWsMessageType.EVENT_TABLE_OF_VALUE_STATES;
|
||||
break;
|
||||
case 3:
|
||||
type = LxWsMessageType.EVENT_TABLE_OF_TEXT_STATES;
|
||||
break;
|
||||
case 4:
|
||||
type = LxWsMessageType.EVENT_TABLE_OF_DAYTIMER_STATES;
|
||||
break;
|
||||
case 5:
|
||||
type = LxWsMessageType.OUT_OF_SERVICE_INDICATOR;
|
||||
break;
|
||||
case 6:
|
||||
type = LxWsMessageType.KEEPALIVE_RESPONSE;
|
||||
break;
|
||||
case 7:
|
||||
type = LxWsMessageType.EVENT_TABLE_OF_WEATHER_STATES;
|
||||
break;
|
||||
default:
|
||||
type = LxWsMessageType.UNKNOWN;
|
||||
break;
|
||||
}
|
||||
// These fields are not used today , but left it for future reference
|
||||
// estimated = ((buffer[offset + 2] & 0x01) != 0);
|
||||
// length = ByteBuffer.wrap(buffer, offset + 3, 4).getInt();
|
||||
}
|
||||
|
||||
public LxWsMessageType getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.types;
|
||||
|
||||
/**
|
||||
* Types of security authentication/encryption methods.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public enum LxWsSecurityType {
|
||||
/**
|
||||
* Method will be determined base on Miniserver software version
|
||||
*/
|
||||
AUTO,
|
||||
/**
|
||||
* Hash-based authentication with no command encryption
|
||||
*/
|
||||
HASH,
|
||||
/**
|
||||
* Token-based authentication with AES-256 command encryption
|
||||
*/
|
||||
TOKEN;
|
||||
|
||||
/**
|
||||
* Encode security type based on index
|
||||
*
|
||||
* @param index
|
||||
* 0 for auto, 1 for hash, 2 for token
|
||||
* @return
|
||||
* security type fo given index
|
||||
*/
|
||||
public static LxWsSecurityType getType(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return AUTO;
|
||||
case 1:
|
||||
return HASH;
|
||||
default:
|
||||
case 2:
|
||||
return TOKEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="loxone" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
<name>Loxone Binding</name>
|
||||
<description>This is the binding for Loxone Miniserver</description>
|
||||
<author>Pawel Pieczul</author>
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,262 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="loxone"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="miniserver">
|
||||
|
||||
<label>Loxone Miniserver</label>
|
||||
<description>IP gateway for Loxone Smarthome system</description>
|
||||
|
||||
<config-description>
|
||||
|
||||
<parameter-group name="miniserver">
|
||||
<label>Miniserver Settings</label>
|
||||
<description>Connection to Miniserver</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter-group name="security">
|
||||
<label>Security Settings</label>
|
||||
<description>Authentication and encryption parameters</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter-group name="timeouts">
|
||||
<label>Timeout Settings</label>
|
||||
<description>Various timeout parameters</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter-group name="sizes">
|
||||
<label>Size Settings</label>
|
||||
<description>Various sizing parameters</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="host" type="text" required="true" groupName="miniserver">
|
||||
<label>Host</label>
|
||||
<context>network-address</context>
|
||||
<description>Host address or IP of the Loxone Miniserver</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" min="1" max="65535" groupName="miniserver">
|
||||
<label>Port</label>
|
||||
<description>Web interface port of the Loxone Miniserver</description>
|
||||
<default>80</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="authMethod" type="integer" min="0" max="2" groupName="security">
|
||||
<label>Authorization Method</label>
|
||||
<description>Method used to authorize user in the Miniserver</description>
|
||||
<default>0</default>
|
||||
<options>
|
||||
<option value="0">Automatic</option>
|
||||
<option value="1">Hash-based</option>
|
||||
<option value="2">Token-based</option>
|
||||
</options>
|
||||
<limitToOptions>true</limitToOptions>
|
||||
</parameter>
|
||||
<parameter name="user" type="text" required="true" groupName="security">
|
||||
<label>User</label>
|
||||
<description>User name on the Loxone Miniserver</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="false" groupName="security">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description>User password on the Loxone Miniserver. In token-based authentication this password will be cleared
|
||||
after token is acquired.</description>
|
||||
</parameter>
|
||||
<parameter name="authToken" type="text" required="false" groupName="security">
|
||||
<label>Token</label>
|
||||
<description>Token acquired by the binding from the Miniserver. Applicable only in token-based authentication mode.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="firstConDelay" type="integer" min="0" max="120" groupName="timeouts">
|
||||
<label>First Connection Delay</label>
|
||||
<description>Time between binding initialization and first connection attempt (seconds, 0-120)</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="keepAlivePeriod" type="integer" min="1" max="600" groupName="timeouts">
|
||||
<label>Period Between Connection Keep-alive Messages</label>
|
||||
<description>Time between sending two consecutive keep-alive messages (seconds, 1-600)</description>
|
||||
<default>240</default>
|
||||
</parameter>
|
||||
<parameter name="connectErrDelay" type="integer" min="0" max="600" groupName="timeouts">
|
||||
<label>Connect Error Delay</label>
|
||||
<description>Time between failed websocket connect attempts (seconds, 0-600)</description>
|
||||
<default>10</default>
|
||||
</parameter>
|
||||
<parameter name="responseTimeout" type="integer" min="0" max="60" groupName="timeouts">
|
||||
<label>Miniserver Response Timeout</label>
|
||||
<description>Time to wait for a response from Miniserver to a request sent from the binding (seconds, 0-60)</description>
|
||||
<default>4</default>
|
||||
</parameter>
|
||||
<parameter name="userErrorDelay" type="integer" min="0" max="3600" groupName="timeouts">
|
||||
<label>Authentication Error Delay</label>
|
||||
<description>Time in seconds between user login error as a result of wrong name/password or no authority and next
|
||||
connection attempt (seconds, 0-3600)</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
<parameter name="comErrorDelay" type="integer" min="0" max="3600" groupName="timeouts">
|
||||
<label>Communication Error Delay</label>
|
||||
<description>Time between connection close (as a result of some communication error) and next connection attempt
|
||||
(seconds, 0-3600)</description>
|
||||
<default>30</default>
|
||||
</parameter>
|
||||
<parameter name="maxBinMsgSize" type="integer" min="0" max="102400" groupName="sizes">
|
||||
<label>Maximum Binary Message Size (kB)</label>
|
||||
<description>Websocket client's maximum binary message size in kB</description>
|
||||
<default>3072</default>
|
||||
</parameter>
|
||||
<parameter name="maxTextMsgSize" type="integer" min="0" max="102400" groupName="sizes">
|
||||
<label>Maximum Text Message Size (kB)</label>
|
||||
<description>Websocket client's maximum text message size in kB</description>
|
||||
<default>512</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="switchTypeId">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Loxone Switch</label>
|
||||
<description>Loxone's virtual input of switch type and push button controls.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="roSwitchTypeId">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Loxone Digital Read-only Information</label>
|
||||
<description>Loxone's digital information controls (InfoOnlyDigital, read-only).</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rollerShutterTypeId">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Loxone Jalousie</label>
|
||||
<description>Loxone's Jalousies (rollershutters, blinds).</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="roTextTypeId">
|
||||
<item-type>String</item-type>
|
||||
<label>Loxone State Read-only Information</label>
|
||||
<description>Loxone's state information controls (TextState, read-only).</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="numberTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Loxone Virtual Input Information</label>
|
||||
<description>Loxone's Virtual Inputs.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="roNumberTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Loxone State Read-only Information</label>
|
||||
<description>Loxone's time counter (TimedSwitch, read-only).</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="roAnalogTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Loxone Virtual Analog State Read-only Information</label>
|
||||
<description>Loxone's state information controls (InfoOnlyAnalog, read-only).</description>
|
||||
<state readOnly="true" pattern="%.1f"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lightCtrlTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Loxone Light Controller / Hotel Light Controller</label>
|
||||
<description>Loxone's light controllers (LightController).</description>
|
||||
<state pattern="%d" min="0" max="9" step="1">
|
||||
<options>
|
||||
<option value="0">All off</option>
|
||||
<option value="1">Scene 1</option>
|
||||
<option value="2">Scene 2</option>
|
||||
<option value="3">Scene 3</option>
|
||||
<option value="4">Scene 4</option>
|
||||
<option value="5">Scene 5</option>
|
||||
<option value="6">Scene 6</option>
|
||||
<option value="7">Scene 7</option>
|
||||
<option value="8">Scene 8</option>
|
||||
<option value="9">All on</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="radioButtonTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Loxone Radio Button (8x and 16x)</label>
|
||||
<description>Loxone's radio button controls (Radio).</description>
|
||||
<state pattern="%d" min="0" max="16" step="1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dimmerTypeId">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Loxone Dimmer</label>
|
||||
<description>Loxone's dimmer control</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colorPickerTypeId">
|
||||
<item-type>Color</item-type>
|
||||
<label>Loxone ColorPicker</label>
|
||||
<description>Loxone's Color Picker V2 control</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="roDateTimeTypeId">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Date Time</label>
|
||||
<description>Date and time of an event</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="iRoomV2ActiveModeTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Active Mode</label>
|
||||
<description>Loxone Intelligent Room Controller V2 Active Mode.</description>
|
||||
<state pattern="%d" min="0" max="3" step="1" readOnly="true">
|
||||
<options>
|
||||
<option value="0">Economy</option>
|
||||
<option value="1">Comfort temperature</option>
|
||||
<option value="2">Building protection</option>
|
||||
<option value="3">Manual</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="iRoomV2OperatingModeTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Operating Mode</label>
|
||||
<description>Loxone Intelligent Room Controller V2 Operating Mode.</description>
|
||||
<state pattern="%d" min="0" max="5" step="1">
|
||||
<options>
|
||||
<option value="0">Automatic, heating and cooling allowed</option>
|
||||
<option value="1">Automatic, only heating allowed</option>
|
||||
<option value="2">Automatic, only cooling allowed</option>
|
||||
<option value="3">Manual, heating and cooling allowed</option>
|
||||
<option value="4">Manual, only heating allowed</option>
|
||||
<option value="5">Manual, only cooling allowed</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="iRoomV2PrepareStateTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Prepare State</label>
|
||||
<description>Loxone Intelligent Room Controller V2 Prepare State.</description>
|
||||
<state pattern="%d" min="-1" max="1" step="1" readOnly="true">
|
||||
<options>
|
||||
<option value="-1">Cooling down</option>
|
||||
<option value="0">No action</option>
|
||||
<option value="1">Heating up</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="iRoomV2ComfortToleranceTypeId">
|
||||
<item-type>Number</item-type>
|
||||
<label>Comfort Tolerance Temperature</label>
|
||||
<description>Loxone Intelligent Room Controller V2 Comfort Tolerance.</description>
|
||||
<state min="0.5" max="3"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlAlarm} - version for alarm without presence sensors
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlAlarmNoPresenceTest extends LxControlTest {
|
||||
static final String ARM_DELAYED_CHANNEL = " / Arm Delayed";
|
||||
static final String NEXT_LEVEL_CHANNEL = " / Next Level";
|
||||
static final String NEXT_LEVEL_DELAY_CHANNEL = " / Next Level Delay";
|
||||
static final String NEXT_LEVEL_DELAY_TOTAL_CHANNEL = " / Next Level Delay Total";
|
||||
static final String LEVEL_CHANNEL = " / Level";
|
||||
static final String START_TIME_CHANNEL = " / Start Time";
|
||||
static final String ARMED_DELAY_CHANNEL = " / Armed Delay";
|
||||
static final String ARMED_TOTAL_DELAY_CHANNEL = " / Armed Total Delay";
|
||||
static final String SENSORS_CHANNEL = " / Sensors";
|
||||
static final String QUIT_CHANNEL = " / Acknowledge";
|
||||
|
||||
private static final String numberChannels[] = { NEXT_LEVEL_CHANNEL, NEXT_LEVEL_DELAY_CHANNEL,
|
||||
NEXT_LEVEL_DELAY_TOTAL_CHANNEL, LEVEL_CHANNEL, ARMED_DELAY_CHANNEL, ARMED_TOTAL_DELAY_CHANNEL };
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("233d5db0-0333-5865-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Burglar Alarm No Presence");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlAlarm.class, 2, 1, 11, 12, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
// read-write channels
|
||||
testChannel("Switch");
|
||||
testChannel("Switch", ARM_DELAYED_CHANNEL);
|
||||
testChannel("Switch", QUIT_CHANNEL, null, null, null, null, true, null);
|
||||
// read-only channels
|
||||
testChannel("Number", NEXT_LEVEL_CHANNEL);
|
||||
testChannel("Number", NEXT_LEVEL_DELAY_CHANNEL);
|
||||
testChannel("Number", NEXT_LEVEL_DELAY_TOTAL_CHANNEL);
|
||||
testChannel("Number", LEVEL_CHANNEL);
|
||||
testChannel("Number", ARMED_DELAY_CHANNEL);
|
||||
testChannel("Number", ARMED_TOTAL_DELAY_CHANNEL);
|
||||
testChannel("String", SENSORS_CHANNEL);
|
||||
testChannel("DateTime", START_TIME_CHANNEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandsDefaultChannel() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
changeLoxoneState("disabledmove", 0.0);
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("on");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("off");
|
||||
changeLoxoneState("disabledmove", 1.0);
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("on");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("off");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandsArmWithDelayChannel() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
changeLoxoneState("disabledmove", 0.0);
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.ON);
|
||||
testAction("delayedon");
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.OFF);
|
||||
testAction("off");
|
||||
changeLoxoneState("disabledmove", 1.0);
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.ON);
|
||||
testAction("delayedon");
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.OFF);
|
||||
testAction("off");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandsQuitChannel() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
executeCommand(QUIT_CHANNEL, OnOffType.ON);
|
||||
testAction("quit");
|
||||
executeCommand(QUIT_CHANNEL, OnOffType.OFF);
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumberChannels() {
|
||||
testNumberChannel(NEXT_LEVEL_CHANNEL, "nextlevel");
|
||||
testNumberChannel(NEXT_LEVEL_DELAY_CHANNEL, "nextleveldelay");
|
||||
testNumberChannel(NEXT_LEVEL_DELAY_TOTAL_CHANNEL, "nextleveldelaytotal");
|
||||
testNumberChannel(LEVEL_CHANNEL, "level");
|
||||
testNumberChannel(ARMED_DELAY_CHANNEL, "armeddelay");
|
||||
testNumberChannel(ARMED_TOTAL_DELAY_CHANNEL, "armeddelaytotal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartedTimeChannel() {
|
||||
changeLoxoneState("starttime", "2019-11-18 14:54:21");
|
||||
LocalDateTime ldt = LocalDateTime.of(2019, 11, 18, 14, 54, 21);
|
||||
ZonedDateTime dt = ldt.atZone(ZoneId.systemDefault());
|
||||
testChannelState(START_TIME_CHANNEL, new DateTimeType(dt));
|
||||
|
||||
changeLoxoneState("starttime", "something else");
|
||||
testChannelState(START_TIME_CHANNEL, null);
|
||||
|
||||
changeLoxoneState("starttime", "1981-01-02 03:04:05");
|
||||
ldt = LocalDateTime.of(1981, 1, 2, 3, 4, 5);
|
||||
dt = ldt.atZone(ZoneId.systemDefault());
|
||||
testChannelState(START_TIME_CHANNEL, new DateTimeType(dt));
|
||||
|
||||
changeLoxoneState("starttime", "1981-13-02 03:04:05");
|
||||
testChannelState(START_TIME_CHANNEL, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSensorsChannel() {
|
||||
testChannelState(SENSORS_CHANNEL, null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
changeLoxoneState("sensors", "test sensors channel string " + i);
|
||||
testChannelState(SENSORS_CHANNEL, new StringType("test sensors channel string " + i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLevelAndAcknowledge() {
|
||||
changeLoxoneState("level", 0.0);
|
||||
testChannel("Switch", QUIT_CHANNEL, null, null, null, null, true, null);
|
||||
for (Double i = 1.0; i <= 6.0; i++) {
|
||||
changeLoxoneState("level", i);
|
||||
testChannel("Switch", QUIT_CHANNEL, null, null, null, null, false, null);
|
||||
changeLoxoneState("level", 0.0);
|
||||
testChannel("Switch", QUIT_CHANNEL, null, null, null, null, true, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void testNumberChannel(String channel, String state) {
|
||||
Map<String, State> states = new HashMap<>();
|
||||
for (String s : numberChannels) {
|
||||
states.put(s, getChannelState(s));
|
||||
}
|
||||
for (Double i = -100.0; i <= 100.0; i += 2.341) {
|
||||
changeLoxoneState(state, i);
|
||||
testChannelState(channel, new DecimalType(i));
|
||||
states.entrySet().stream().filter(v -> !v.getKey().equals(channel)).forEach(v -> {
|
||||
String key = v.getKey();
|
||||
assertEquals(states.get(key), getChannelState(key));
|
||||
});
|
||||
}
|
||||
changeLoxoneState(state, Double.NaN);
|
||||
testChannelState(channel, UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlAlarm} - version with motion sensors
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlAlarmWithPresenceTest extends LxControlAlarmNoPresenceTest {
|
||||
private static final String MOTION_SENSORS_CHANNEL = " / Motion Sensors";
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("133d5db0-0333-5865-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Burglar Alarm With Presence");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlAlarm.class, 2, 1, 12, 13, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testChannels() {
|
||||
super.testChannels();
|
||||
testChannel("Switch", MOTION_SENSORS_CHANNEL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testCommandsDefaultChannel() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
changeLoxoneState("disabledmove", 0.0);
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("on/1");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("off");
|
||||
changeLoxoneState("disabledmove", 1.0);
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("on/0");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("off");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testCommandsArmWithDelayChannel() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
changeLoxoneState("disabledmove", 0.0);
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.ON);
|
||||
testAction("delayedon/1");
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.OFF);
|
||||
testAction("off");
|
||||
changeLoxoneState("disabledmove", 1.0);
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.ON);
|
||||
testAction("delayedon/0");
|
||||
executeCommand(ARM_DELAYED_CHANNEL, OnOffType.OFF);
|
||||
testAction("off");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandsMotionSensors() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
executeCommand(MOTION_SENSORS_CHANNEL, OnOffType.ON);
|
||||
testAction("dismv/0");
|
||||
executeCommand(MOTION_SENSORS_CHANNEL, OnOffType.OFF);
|
||||
testAction("dismv/1");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneMotionSensorsChanges() {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
changeLoxoneState("disabledmove", 1.0);
|
||||
testChannelState(MOTION_SENSORS_CHANNEL, OnOffType.OFF);
|
||||
changeLoxoneState("disabledmove", 0.0);
|
||||
testChannelState(MOTION_SENSORS_CHANNEL, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlDimmer}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlDimmerTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("131b19cd-03c0-640f-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Dimmer Control");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlDimmer.class, 1, 0, 1, 1, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Dimmer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxonePositionMinMaxChanges() {
|
||||
// filling in missing state values
|
||||
testChannelState(null);
|
||||
changeLoxoneState("step", 1.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("position", 50.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("min", 0.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("max", 100.0);
|
||||
testChannelState(new PercentType(50));
|
||||
|
||||
// potential division by zero
|
||||
changeLoxoneState("min", 55.0);
|
||||
changeLoxoneState("max", 55.0);
|
||||
testChannelState(null);
|
||||
|
||||
changeLoxoneState("min", 200.0);
|
||||
changeLoxoneState("max", 400.0);
|
||||
// out of range
|
||||
changeLoxoneState("position", 199.9);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("position", 400.1);
|
||||
testChannelState(null);
|
||||
// scaling within range
|
||||
changeLoxoneState("position", 200.0);
|
||||
testChannelState(PercentType.ZERO);
|
||||
changeLoxoneState("position", 400.0);
|
||||
testChannelState(PercentType.HUNDRED);
|
||||
changeLoxoneState("position", 300.0);
|
||||
testChannelState(new PercentType(50));
|
||||
// special value meaning switched off
|
||||
changeLoxoneState("position", 0.0);
|
||||
testChannelState(PercentType.ZERO);
|
||||
|
||||
// reversed range boundaries
|
||||
changeLoxoneState("min", 50.0);
|
||||
changeLoxoneState("max", 20.0);
|
||||
// here dimmer still turned off
|
||||
testChannelState(PercentType.ZERO);
|
||||
// here within wrong range
|
||||
changeLoxoneState("position", 30.0);
|
||||
testChannelState(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnOffPercentCommands() {
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("On");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("Off");
|
||||
|
||||
changeLoxoneState("min", 1000.0);
|
||||
changeLoxoneState("max", 3000.0);
|
||||
changeLoxoneState("step", 100.0);
|
||||
executeCommand(PercentType.HUNDRED);
|
||||
testAction("3000.0");
|
||||
executeCommand(new PercentType(50));
|
||||
testAction("2000.0");
|
||||
executeCommand(new PercentType(1));
|
||||
testAction("1020.0");
|
||||
executeCommand(PercentType.ZERO);
|
||||
testAction("0.0");
|
||||
|
||||
executeCommand(StopMoveType.MOVE);
|
||||
testAction(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncreaseDecreaseCommands() {
|
||||
changeLoxoneState("min", 123.0);
|
||||
changeLoxoneState("max", 456.0);
|
||||
changeLoxoneState("step", 23.0);
|
||||
changeLoxoneState("position", 400.0);
|
||||
testChannelState(new PercentType(83));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("423.0");
|
||||
changeLoxoneState("position", 423.0);
|
||||
testChannelState(new PercentType(90));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("446.0");
|
||||
changeLoxoneState("position", 446.0);
|
||||
testChannelState(new PercentType(96));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("456.0"); // trim to max
|
||||
changeLoxoneState("position", 456.0);
|
||||
testChannelState(PercentType.HUNDRED);
|
||||
changeLoxoneState("step", 100.0);
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("356.0");
|
||||
changeLoxoneState("position", 356.0);
|
||||
testChannelState(new PercentType(69));
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("256.0");
|
||||
changeLoxoneState("position", 256.0);
|
||||
testChannelState(new PercentType(39));
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("156.0");
|
||||
changeLoxoneState("position", 156.0);
|
||||
testChannelState(new PercentType(9));
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("123.0"); // trim to min
|
||||
changeLoxoneState("position", 123.0);
|
||||
testChannelState(PercentType.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Test class for {@link LxControlIRoomControllerV2}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlIRoomControllerV2Test extends LxControlTest {
|
||||
private static final String ACTIVE_MODE_CHANNEL = "/ Active Mode";
|
||||
private static final String OPERATING_MODE_CHANNEL = "/ Operating Mode";
|
||||
private static final String PREPARE_STATE_CHANNEL = "/ Prepare State";
|
||||
private static final String OPEN_WINDOW_CHANNEL = "/ Open Window";
|
||||
private static final String TEMP_ACTUAL_CHANNEL = "/ Current Temperature";
|
||||
private static final String TEMP_TARGET_CHANNEL = "/ Target Temperature";
|
||||
private static final String COMFORT_TEMPERATURE_CHANNEL = "/ Comfort Temperature";
|
||||
private static final String COMFORT_TEMPERATURE_OFFSET_CHANNEL = "/ Comfort Temperature Offset";
|
||||
private static final String COMFORT_TOLERANCE_CHANNEL = "/ Comfort Tolerance";
|
||||
private static final String ABSENT_MIN_OFFSET_CHANNEL = "/ Absent Min Offset";
|
||||
private static final String ABSENT_MAX_OFFSET_CHANNEL = "/ Absent Max Offset";
|
||||
private static final String FROST_PROTECT_TEMPERATURE_CHANNEL = "/ Frost Protect Temperature";
|
||||
private static final String HEAT_PROTECT_TEMPERATURE_CHANNEL = "/ Heat Protect Temperature";
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("14328f8a-21c9-7c0d-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Intelligent Room Controller");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlIRoomControllerV2.class, 1, 0, 13, 13, 17);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
Set<String> tempTags = new HashSet<>();
|
||||
tempTags.add("CurrentTemperature");
|
||||
testChannel("Number", ACTIVE_MODE_CHANNEL);
|
||||
testChannel("Number", OPERATING_MODE_CHANNEL);
|
||||
testChannel("Number", PREPARE_STATE_CHANNEL);
|
||||
testChannel("Switch", OPEN_WINDOW_CHANNEL);
|
||||
testChannel("Number", TEMP_ACTUAL_CHANNEL, null, null, null, "%.1f°", true, null, tempTags);
|
||||
testChannel("Number", TEMP_TARGET_CHANNEL, null, null, null, "%.1f°", false, null, tempTags);
|
||||
testChannel("Number", COMFORT_TEMPERATURE_CHANNEL, null, null, null, "%.1f°", false, null, tempTags);
|
||||
testChannel("Number", COMFORT_TEMPERATURE_OFFSET_CHANNEL, null, null, null, "%.1f°", false, null, tempTags);
|
||||
testChannel("Number", COMFORT_TOLERANCE_CHANNEL, null, null, null, "%.1f°", false, null, tempTags);
|
||||
testChannel("Number", ABSENT_MIN_OFFSET_CHANNEL, null, null, null, "%.1f°", false, null, tempTags);
|
||||
testChannel("Number", ABSENT_MAX_OFFSET_CHANNEL, null, null, null, "%.1f°", false, null, tempTags);
|
||||
testChannel("Number", FROST_PROTECT_TEMPERATURE_CHANNEL, null, null, null, "%.1f°", true, null, tempTags);
|
||||
testChannel("Number", HEAT_PROTECT_TEMPERATURE_CHANNEL, null, null, null, "%.1f°", true, null, tempTags);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModeStateChanges() {
|
||||
for (int i = 0; i <= 3; i++) {
|
||||
changeLoxoneState("activemode", Double.valueOf(i));
|
||||
testChannelState(ACTIVE_MODE_CHANNEL, new DecimalType(i));
|
||||
}
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
changeLoxoneState("operatingmode", Double.valueOf(i));
|
||||
testChannelState(OPERATING_MODE_CHANNEL, new DecimalType(i));
|
||||
}
|
||||
for (int i = -1; i <= 1; i++) {
|
||||
changeLoxoneState("preparestate", Double.valueOf(i));
|
||||
testChannelState(PREPARE_STATE_CHANNEL, new DecimalType(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowStateChanges() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
changeLoxoneState("openwindow", 0.0);
|
||||
testChannelState(OPEN_WINDOW_CHANNEL, OnOffType.OFF);
|
||||
changeLoxoneState("openwindow", 1.0);
|
||||
testChannelState(OPEN_WINDOW_CHANNEL, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTemperatureStateChanges() {
|
||||
for (Double i = -50.0; i < 50.0; i += 0.3) {
|
||||
changeLoxoneState("tempactual", i);
|
||||
changeLoxoneState("temptarget", i + 0.01);
|
||||
changeLoxoneState("comforttemperature", i + 0.02);
|
||||
changeLoxoneState("comforttemperatureoffset", i + 0.03);
|
||||
changeLoxoneState("comforttolerance", i + 0.04);
|
||||
changeLoxoneState("absentminoffset", i + 0.05);
|
||||
changeLoxoneState("absentmaxoffset", i + 0.06);
|
||||
changeLoxoneState("frostprotecttemperature", i + 0.07);
|
||||
changeLoxoneState("heatprotecttemperature", i + 0.08);
|
||||
testChannelState(TEMP_ACTUAL_CHANNEL, new DecimalType(i));
|
||||
testChannelState(TEMP_TARGET_CHANNEL, new DecimalType(i + 0.01));
|
||||
testChannelState(COMFORT_TEMPERATURE_CHANNEL, new DecimalType(i + 0.02));
|
||||
testChannelState(COMFORT_TEMPERATURE_OFFSET_CHANNEL, new DecimalType(i + 0.03));
|
||||
testChannelState(COMFORT_TOLERANCE_CHANNEL, new DecimalType(i + 0.04));
|
||||
testChannelState(ABSENT_MIN_OFFSET_CHANNEL, new DecimalType(i + 0.05));
|
||||
testChannelState(ABSENT_MAX_OFFSET_CHANNEL, new DecimalType(i + 0.06));
|
||||
testChannelState(FROST_PROTECT_TEMPERATURE_CHANNEL, new DecimalType(i + 0.07));
|
||||
testChannelState(HEAT_PROTECT_TEMPERATURE_CHANNEL, new DecimalType(i + 0.08));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModeCommands() {
|
||||
testAction(null);
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
executeCommand(OPERATING_MODE_CHANNEL, new DecimalType(i));
|
||||
testAction("setOperatingMode/" + i);
|
||||
}
|
||||
testAction(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTemperatureCommands() {
|
||||
testAction(null);
|
||||
for (Double t = -50.0; t < 50.0; t += 0.03) {
|
||||
DecimalType a = new DecimalType(t);
|
||||
executeCommand(TEMP_TARGET_CHANNEL, a);
|
||||
testAction("setManualTemperature/" + t.toString());
|
||||
executeCommand(COMFORT_TEMPERATURE_CHANNEL, a);
|
||||
testAction("setComfortTemperature/" + t.toString());
|
||||
executeCommand(COMFORT_TEMPERATURE_OFFSET_CHANNEL, a);
|
||||
testAction("setComfortModeTemp/" + t.toString());
|
||||
executeCommand(COMFORT_TOLERANCE_CHANNEL, a);
|
||||
testAction("setComfortTolerance/" + t.toString());
|
||||
executeCommand(ABSENT_MAX_OFFSET_CHANNEL, a);
|
||||
testAction("setAbsentMaxTemperature/" + t.toString());
|
||||
executeCommand(ABSENT_MIN_OFFSET_CHANNEL, a);
|
||||
testAction("setAbsentMinTemperature/" + t.toString());
|
||||
}
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlInfoOnlyAnalog} - check tags for temperature category
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlInfoOnlyAnalogTempTagTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("0fec5dc3-003e-8800-ffff555fb0c34b9e", "0fe3a451-0283-2afa-ffff403fb0c34b9e",
|
||||
"0fb99a98-02df-46f1-ffff403fb0c34b9e", "Info Only Analog Temperature");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlInfoOnlyAnalog.class, 2, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Number", null, null, null, null, "%.1f", true, null, Collections.singleton("CurrentTemperature"));
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlInfoOnlyAnalog}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlInfoOnlyAnalogTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("0fec5dc3-003e-8800-ffff403fb0c34b9e", "0fe3a451-0283-2afa-ffff403fb0c34b9e",
|
||||
"0fe665f4-0161-4773-ffff403fb0c34b9e", "Info Only Analog");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlInfoOnlyAnalog.class, 2, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Number", null, null, null, null, "%.2f", true, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
for (Double i = -1000.0; i < 1000.0; i += 33.7324323) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(new DecimalType(i));
|
||||
}
|
||||
changeLoxoneState("value", 0.0);
|
||||
testChannelState(DecimalType.ZERO);
|
||||
changeLoxoneState("value", Double.NaN);
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
changeLoxoneState("value", Double.MAX_VALUE);
|
||||
testChannelState(new DecimalType(Double.MAX_VALUE));
|
||||
changeLoxoneState("value", Double.POSITIVE_INFINITY);
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
changeLoxoneState("value", Double.MIN_VALUE);
|
||||
testChannelState(new DecimalType(Double.MIN_VALUE));
|
||||
changeLoxoneState("value", Double.NEGATIVE_INFINITY);
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlInfoOnlyDigital}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlInfoOnlyDigitalTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("101b50f7-0306-98fb-ffff403fb0c34b9e", "0e368d32-014f-4604-ffff403fb0c34b9e",
|
||||
"101b563d-0302-78bd-ffff403fb0c34b9e", "Info Only Digital");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlInfoOnlyDigital.class, 1, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Switch");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
for (Double i = 2.0; i < 100.0; i++) {
|
||||
changeLoxoneState("active", 0.0);
|
||||
testChannelState(OnOffType.OFF);
|
||||
changeLoxoneState("active", 1.0);
|
||||
testChannelState(OnOffType.ON);
|
||||
changeLoxoneState("active", 1.0 / i);
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlJalousie}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlJalousieTest extends LxControlTest {
|
||||
|
||||
private static final String ROLLERSHUTTER_CHANNEL = null;
|
||||
private static final String SHADE_CHANNEL = " / Shade";
|
||||
private static final String AUTO_SHADE_CHANNEL = " / Auto Shade";
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("0e367c09-0161-e2c1-ffff403fb0c34b9e", "0e368d32-014f-4604-ffff403fb0c34b9e",
|
||||
"0b734138-033e-02d8-ffff403fb0c34b9e", "Window Blinds");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlJalousie.class, 1, 0, 3, 3, 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Rollershutter", Collections.singleton("Blinds"));
|
||||
Set<String> tags = Collections.singleton("Switchable");
|
||||
testChannel("Switch", SHADE_CHANNEL, tags);
|
||||
testChannel("Switch", AUTO_SHADE_CHANNEL, tags);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxonePositionAutoShadeStates() {
|
||||
boolean a = false;
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, null);
|
||||
testChannelState(SHADE_CHANNEL, OnOffType.OFF);
|
||||
testChannelState(AUTO_SHADE_CHANNEL, null);
|
||||
for (int i = 0; i <= 100; i++) {
|
||||
changeLoxoneState("position", i / 100.0);
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, new PercentType(i));
|
||||
testChannelState(SHADE_CHANNEL, OnOffType.OFF);
|
||||
changeLoxoneState("autoactive", a ? 1.0 : 0.0);
|
||||
testChannelState(AUTO_SHADE_CHANNEL, a ? OnOffType.ON : OnOffType.OFF);
|
||||
a = !a;
|
||||
}
|
||||
changeLoxoneState("position", 100.1);
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, null);
|
||||
changeLoxoneState("position", -0.1);
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
executeCommand(SHADE_CHANNEL, OnOffType.ON);
|
||||
testAction("shade");
|
||||
executeCommand(SHADE_CHANNEL, OnOffType.OFF);
|
||||
testAction(null);
|
||||
executeCommand(SHADE_CHANNEL, DecimalType.ZERO);
|
||||
testAction(null);
|
||||
executeCommand(AUTO_SHADE_CHANNEL, OnOffType.ON);
|
||||
testAction("auto");
|
||||
executeCommand(AUTO_SHADE_CHANNEL, OnOffType.OFF);
|
||||
testAction("NoAuto");
|
||||
executeCommand(AUTO_SHADE_CHANNEL, DecimalType.ZERO);
|
||||
testAction(null);
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, UpDownType.UP);
|
||||
testAction("FullUp");
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, UpDownType.DOWN);
|
||||
testAction("FullDown");
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, StopMoveType.STOP);
|
||||
testAction("Stop");
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, StopMoveType.MOVE);
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMovingToPosition() {
|
||||
changeLoxoneState("position", 0.1);
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, new PercentType(10));
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, new PercentType(73));
|
||||
testAction("FullDown");
|
||||
changeLoxoneState("up", 0.0);
|
||||
changeLoxoneState("down", 1.0);
|
||||
for (int i = 10; i <= 72; i++) {
|
||||
changeLoxoneState("position", i / 100.0);
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, new PercentType(i));
|
||||
}
|
||||
changeLoxoneState("position", 0.73);
|
||||
testAction("Stop");
|
||||
changeLoxoneState("position", 0.74);
|
||||
testAction(null);
|
||||
changeLoxoneState("up", 0.0);
|
||||
changeLoxoneState("down", 0.0);
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, new PercentType(10));
|
||||
testAction("FullUp");
|
||||
changeLoxoneState("up", 1.0);
|
||||
changeLoxoneState("down", 0.0);
|
||||
for (int i = 74; i >= 11; i--) {
|
||||
changeLoxoneState("position", i / 100.0);
|
||||
testChannelState(ROLLERSHUTTER_CHANNEL, new PercentType(i));
|
||||
}
|
||||
changeLoxoneState("position", 0.10);
|
||||
testAction("Stop");
|
||||
changeLoxoneState("position", 0.09);
|
||||
testAction(null);
|
||||
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, new PercentType(50));
|
||||
testAction("FullDown");
|
||||
changeLoxoneState("up", 0.0);
|
||||
changeLoxoneState("down", 1.0);
|
||||
changeLoxoneState("position", 0.80);
|
||||
testAction("Stop");
|
||||
changeLoxoneState("position", 0.50);
|
||||
testAction(null);
|
||||
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, new PercentType(90));
|
||||
testAction("FullDown");
|
||||
changeLoxoneState("up", 0.0);
|
||||
changeLoxoneState("down", 0.0);
|
||||
changeLoxoneState("position", 0.95);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 0.85);
|
||||
testAction(null);
|
||||
|
||||
changeLoxoneState("down", 1.0);
|
||||
changeLoxoneState("position", 0.85);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 0.95);
|
||||
testAction("Stop");
|
||||
changeLoxoneState("position", 0.85);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 0.95);
|
||||
testAction(null);
|
||||
changeLoxoneState("down", 0.0);
|
||||
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, new PercentType(30));
|
||||
testAction("FullUp");
|
||||
changeLoxoneState("up", 1.0);
|
||||
changeLoxoneState("down", 0.0);
|
||||
changeLoxoneState("position", 0.40);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 0.20);
|
||||
testAction("Stop");
|
||||
changeLoxoneState("position", 0.40);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 0.20);
|
||||
testAction(null);
|
||||
changeLoxoneState("up", 0.0);
|
||||
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, PercentType.HUNDRED);
|
||||
testAction("FullDown");
|
||||
changeLoxoneState("up", 0.0);
|
||||
changeLoxoneState("down", 1.0);
|
||||
changeLoxoneState("position", 0.80);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 1.00);
|
||||
testAction(null);
|
||||
changeLoxoneState("down", 0.0);
|
||||
|
||||
executeCommand(ROLLERSHUTTER_CHANNEL, PercentType.ZERO);
|
||||
testAction("FullUp");
|
||||
changeLoxoneState("up", 1.0);
|
||||
changeLoxoneState("down", 0.0);
|
||||
changeLoxoneState("position", 0.20);
|
||||
testAction(null);
|
||||
changeLoxoneState("position", 0.00);
|
||||
testAction(null);
|
||||
changeLoxoneState("up", 0.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlLeftRightAnalog}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlLeftRightAnalogTest extends LxControlUpDownAnalogTest {
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
min = 1072.123;
|
||||
max = 1123.458;
|
||||
step = 23.987;
|
||||
format = "value: %.2f";
|
||||
setupControl("131b1a96-02b9-f6e9-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Left Right Analog Input");
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlLeftRightDigital}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlLeftRightDigitalTest extends LxControlUpDownDigitalTest {
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
upChannel = " / Left";
|
||||
downChannel = " / Right";
|
||||
setupControl("0fd08ca6-01a6-d72a-efff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0b734138-033e-02d4-ffff403fb0c34b9e", "Second Floor Scene");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlLeftRightDigital.class, 1, 0, 2, 2, 0);
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlSwitch}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlLightControllerV2Test extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("1076668f-0101-7076-ffff403fb0c34b9e", "0b734138-03ac-03f0-ffff403fb0c34b9e",
|
||||
"0b734138-033e-02d4-ffff403fb0c34b9e", "Lighting controller");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlLightControllerV2.class, 1, 5, 1, 6, 4);
|
||||
testSubControl(LxControlSwitch.class, "Overall Switch");
|
||||
testSubControl(LxControlSwitch.class, "Hall Study Lights");
|
||||
testSubControl(LxControlSwitch.class, "Roof Lights");
|
||||
testSubControl(LxControlSwitch.class, "Hall Left Lights");
|
||||
testSubControl(LxControlSwitch.class, "Hall Right Lights");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Number", Collections.singleton("Scene"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
executeCommand(UpDownType.UP);
|
||||
testAction("plus");
|
||||
executeCommand(UpDownType.DOWN);
|
||||
testAction("minus");
|
||||
}
|
||||
executeCommand(new DecimalType(1));
|
||||
testAction(null);
|
||||
|
||||
loadMoodList1();
|
||||
|
||||
executeCommand(new DecimalType(0));
|
||||
testAction(null);
|
||||
executeCommand(new DecimalType(1));
|
||||
testAction(null);
|
||||
executeCommand(new DecimalType(2));
|
||||
testAction("changeTo/2");
|
||||
executeCommand(new DecimalType(3));
|
||||
testAction("changeTo/3");
|
||||
executeCommand(new DecimalType(4));
|
||||
testAction("changeTo/4");
|
||||
executeCommand(new DecimalType(5));
|
||||
testAction("changeTo/5");
|
||||
executeCommand(new DecimalType(6));
|
||||
testAction(null);
|
||||
executeCommand(new DecimalType(777));
|
||||
testAction("changeTo/777");
|
||||
executeCommand(new DecimalType(778));
|
||||
testAction("changeTo/778");
|
||||
executeCommand(new DecimalType(779));
|
||||
testAction(null);
|
||||
clearMoodList();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoodListChanges() {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
loadMoodList1();
|
||||
loadMoodList2();
|
||||
}
|
||||
clearMoodList();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActiveMoodChanges() {
|
||||
loadMoodList2();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
changeLoxoneState("activemoods", "[4]");
|
||||
testActiveMoods(779, 4);
|
||||
changeLoxoneState("activemoods", "[4,6]");
|
||||
testActiveMoods(779, 4, 6);
|
||||
changeLoxoneState("activemoods", "[4,6,7,8,5]");
|
||||
testActiveMoods(779, 4, 6, 7, 8, 5);
|
||||
changeLoxoneState("activemoods", "[4,6,7,8,5,777,778]");
|
||||
testActiveMoods(779, 4, 6, 7, 8, 5, 777, 778);
|
||||
changeLoxoneState("activemoods", "[6,8,777]");
|
||||
testActiveMoods(779, 6, 8, 777);
|
||||
changeLoxoneState("activemoods", "[779]");
|
||||
testActiveMoods(779, 779);
|
||||
changeLoxoneState("activemoods", "[1,2,3,500,900]");
|
||||
testActiveMoods(779);
|
||||
changeLoxoneState("activemoods", "[1,2,3,6,500,900]");
|
||||
testActiveMoods(779, 6);
|
||||
changeLoxoneState("activemoods", "[5]");
|
||||
testActiveMoods(779, 5);
|
||||
changeLoxoneState("activemoods", "[778]");
|
||||
testActiveMoods(779, 778);
|
||||
}
|
||||
clearMoodList();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoodAddRemove() {
|
||||
loadMoodList1();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
handler.extraControls.values().forEach(c -> {
|
||||
LxControlMood m = (LxControlMood) c;
|
||||
if (!m.getId().equals(778)) {
|
||||
executeCommand(m, OnOffType.ON);
|
||||
testAction("addMood/" + m.getId());
|
||||
executeCommand(m, OnOffType.OFF);
|
||||
testAction("removeMood/" + m.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
clearMoodList();
|
||||
}
|
||||
|
||||
private void testActiveMoods(Integer offId, Integer... moods) {
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
for (Integer id : moods) {
|
||||
if (!offId.equals(id)) {
|
||||
LxControlMood m = getMood(id);
|
||||
testChannelState(m, OnOffType.ON);
|
||||
}
|
||||
ids.add(id);
|
||||
}
|
||||
handler.extraControls.values().stream()
|
||||
.filter(c -> !ids.contains(((LxControlMood) c).getId()) && !((LxControlMood) c).getId().equals(offId))
|
||||
.forEach(c -> testChannelState(c, OnOffType.OFF));
|
||||
|
||||
if (ids.size() == 1) {
|
||||
testChannelState(new DecimalType(ids.get(0)));
|
||||
} else {
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMoodList1() {
|
||||
String list = loadMoodList("MoodList1.json");
|
||||
changeLoxoneState("moodlist", list);
|
||||
List<StateOption> options = new ArrayList<>();
|
||||
options.add(new StateOption("2", "Side Lights"));
|
||||
options.add(new StateOption("3", "Play Lights"));
|
||||
options.add(new StateOption("4", "Study Only"));
|
||||
options.add(new StateOption("5", "Low Lights"));
|
||||
options.add(new StateOption("777", "Bright"));
|
||||
options.add(new StateOption("778", "Off"));
|
||||
testMoodList(options, 778);
|
||||
}
|
||||
|
||||
private void loadMoodList2() {
|
||||
String list = loadMoodList("MoodList2.json");
|
||||
changeLoxoneState("moodlist", list);
|
||||
List<StateOption> options = new ArrayList<>();
|
||||
options.add(new StateOption("4", "Study Only Changed Name")); // changed name
|
||||
options.add(new StateOption("5", "Low Lights")); // same as in list 1
|
||||
options.add(new StateOption("6", "Play Lights")); // changed id
|
||||
options.add(new StateOption("7", "New Mood 1"));
|
||||
options.add(new StateOption("8", "New Mood 2"));
|
||||
options.add(new StateOption("777", "Bright"));
|
||||
options.add(new StateOption("778", "Off"));
|
||||
options.add(new StateOption("779", "New Off"));
|
||||
testMoodList(options, 779);
|
||||
}
|
||||
|
||||
private LxControlMood getMood(Integer id) {
|
||||
LxControl ctrl = handler.extraControls.get(new LxUuid("1076668f-0101-7076-ffff403fb0c34b9e-M" + id));
|
||||
assertNotNull(ctrl);
|
||||
assertThat(ctrl, is(instanceOf(LxControlMood.class)));
|
||||
return (LxControlMood) ctrl;
|
||||
}
|
||||
|
||||
private void clearMoodList() {
|
||||
changeLoxoneState("moodlist", "[]");
|
||||
List<StateOption> options = new ArrayList<>();
|
||||
testMoodList(options, 0);
|
||||
}
|
||||
|
||||
private void testMoodList(List<StateOption> options, Integer offId) {
|
||||
assertEquals(options.size(), handler.extraControls.size());
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Integer min = null;
|
||||
Integer max = null;
|
||||
for (StateOption o : options) {
|
||||
testMood(o.getLabel(), o.getValue(), o.getValue().equals(offId.toString()));
|
||||
Integer id = Integer.parseInt(o.getValue());
|
||||
assertNotNull(id);
|
||||
if (min == null || id < min) {
|
||||
min = id;
|
||||
}
|
||||
if (max == null || id > max) {
|
||||
max = id;
|
||||
}
|
||||
}
|
||||
assertNotNull(min);
|
||||
assertNotNull(max);
|
||||
testChannel("Number", null, new BigDecimal(min), new BigDecimal(max), BigDecimal.ONE, null, false, options,
|
||||
Collections.singleton("Scene"));
|
||||
}
|
||||
|
||||
private void testMood(String name, String id, boolean isStatic) {
|
||||
LxControlMood mood = getMood(Integer.parseInt(id));
|
||||
assertEquals(new LxUuid("0b734138-03ac-03f0-ffff403fb0c34b9e"), mood.getRoom().getUuid());
|
||||
assertEquals(new LxUuid("0b734138-033e-02d4-ffff403fb0c34b9e"), mood.getCategory().getUuid());
|
||||
assertEquals(name, mood.getName());
|
||||
assertEquals(id, mood.getId().toString());
|
||||
if (isStatic) {
|
||||
assertEquals(0, mood.getChannels().size());
|
||||
} else {
|
||||
assertEquals(1, mood.getChannels().size());
|
||||
testChannel(mood, "Switch", Collections.singleton("Lighting"));
|
||||
}
|
||||
}
|
||||
|
||||
private String loadMoodList(String name) {
|
||||
InputStream stream = LxControlLightControllerV2Test.class.getResourceAsStream(name);
|
||||
assertNotNull(stream);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
|
||||
assertNotNull(reader);
|
||||
String msg = reader.lines().collect(Collectors.joining(System.lineSeparator()));
|
||||
assertNotNull(msg);
|
||||
// mood list comes as a single line JSON from Loxone Miniserver
|
||||
msg = msg.replaceAll("[\\t+\\n+]", "");
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlMeter}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlMeterTest extends LxControlTest {
|
||||
private static final String ACTUAL_VALUE_CHANNEL = " / Current";
|
||||
private static final String TOTAL_VALUE_CHANNEL = " / Total";
|
||||
private static final String RESET_CHANNEL = " / Reset";
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("13b3ea27-00fc-6f1b-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Energy Meter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlMeter.class, 1, 0, 3, 3, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Number", ACTUAL_VALUE_CHANNEL, null, null, null, "%.3fkW", true, null);
|
||||
testChannel("Number", TOTAL_VALUE_CHANNEL, null, null, null, "%.1fkWh", true, null);
|
||||
testChannel("Switch", RESET_CHANNEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
for (Double i = 0.0; i < 50.0; i += 0.25) {
|
||||
changeLoxoneState("actual", i);
|
||||
changeLoxoneState("total", i * 2.0);
|
||||
testChannelState(ACTUAL_VALUE_CHANNEL, new DecimalType(i));
|
||||
testChannelState(TOTAL_VALUE_CHANNEL, new DecimalType(i * 2.0));
|
||||
testChannelState(RESET_CHANNEL, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
testAction(null);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
executeCommand(RESET_CHANNEL, OnOffType.ON);
|
||||
testAction("reset");
|
||||
testChannelState(RESET_CHANNEL, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlPushbutton}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlPushbuttonTest extends LxControlSwitchTest {
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("0e3684cc-026e-28e0-ffff403fb0c34b9e", "0b734138-038c-035e-ffff403fb0c34b9e",
|
||||
"0b734138-033e-02d8-ffff403fb0c34b9e", "Kitchen All Blinds Up");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlPushbutton.class, 1, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Switch", Collections.singleton("Switchable"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testCommands() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("Pulse");
|
||||
executeCommand(DecimalType.ZERO);
|
||||
testAction(null);
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("Off");
|
||||
executeCommand(StringType.EMPTY);
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlRadio} - variant with no 'all off' selection
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlRadioTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("4255054f-0355-af47-ffff403fb0c34b9e", "11d68cf4-0080-7697-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Sprinkler 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlRadio.class, 2, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
List<StateOption> opts = new ArrayList<>();
|
||||
for (Integer i = 1; i <= 6; i++) {
|
||||
opts.add(new StateOption(i.toString(), "Sprinkler " + i.toString()));
|
||||
}
|
||||
testChannel("Number", null, BigDecimal.ZERO, new BigDecimal(16), BigDecimal.ONE, null, false, opts);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneCommonStateChanges() {
|
||||
testChannelState(null);
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
changeLoxoneState("activeoutput", i * 1.0);
|
||||
testChannelState(new DecimalType(i));
|
||||
}
|
||||
changeLoxoneState("activeoutput", 0.5);
|
||||
testChannelState(null);
|
||||
|
||||
changeLoxoneState("activeoutput", 7.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("activeoutput", 17.0);
|
||||
testChannelState(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneZeroIndexChanges() {
|
||||
changeLoxoneState("activeoutput", 0.0);
|
||||
testChannelState(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommonCommands() {
|
||||
for (Integer i = 1; i <= 6; i++) {
|
||||
executeCommand(new DecimalType(i));
|
||||
testAction(i.toString());
|
||||
}
|
||||
executeCommand(new DecimalType(7));
|
||||
testAction(null);
|
||||
executeCommand(new DecimalType(17));
|
||||
testAction(null);
|
||||
|
||||
executeCommand(PercentType.HUNDRED);
|
||||
testAction(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZeroIndexCommands() {
|
||||
executeCommand(DecimalType.ZERO);
|
||||
testAction(null);
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlRadio} - variant with 'all off' selection
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlRadioTest2 extends LxControlRadioTest {
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("1255054f-0355-af47-ffff403fb0c34b9e", "11d68cf4-0080-7697-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Sprinkler 2");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testChannels() {
|
||||
List<StateOption> opts = new ArrayList<>();
|
||||
for (Integer i = 1; i <= 6; i++) {
|
||||
opts.add(new StateOption(i.toString(), "Sprinkler " + i.toString()));
|
||||
}
|
||||
opts.add(new StateOption("0", "All Off"));
|
||||
testChannel("Number", null, BigDecimal.ZERO, new BigDecimal(16), BigDecimal.ONE, null, false, opts);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testLoxoneZeroIndexChanges() {
|
||||
changeLoxoneState("activeoutput", 0.0);
|
||||
testChannelState(new DecimalType(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testZeroIndexCommands() {
|
||||
executeCommand(DecimalType.ZERO);
|
||||
testAction("reset");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("reset");
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlSlider} - this is actually the same control as up down analog
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlSliderTest extends LxControlUpDownAnalogTest {
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
min = 120.0;
|
||||
max = 450.0;
|
||||
step = 3.333;
|
||||
format = "%.1f";
|
||||
setupControl("131fb314-0370-c93c-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Slider Virtual Input");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlSlider.class, 1, 0, 1, 1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlSwitch}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlSwitchTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("0f2f6b5d-0349-83b1-ffff403fb0c34b9e", "0b734138-038c-0382-ffff403fb0c34b9e",
|
||||
"0b734138-033e-02d4-ffff403fb0c34b9e", "Switch Button");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlSwitch.class, 1, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Switch", Collections.singleton("Lighting"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
for (Double i = 2.0; i < 100.0; i++) {
|
||||
changeLoxoneState("active", 0.0);
|
||||
testChannelState(OnOffType.OFF);
|
||||
changeLoxoneState("active", 1.0);
|
||||
testChannelState(OnOffType.ON);
|
||||
changeLoxoneState("active", 1.0 / i);
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("On");
|
||||
executeCommand(DecimalType.ZERO);
|
||||
testAction(null);
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("Off");
|
||||
executeCommand(StringType.EMPTY);
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.openhab.binding.loxone.internal.types.LxCategory;
|
||||
import org.openhab.binding.loxone.internal.types.LxContainer;
|
||||
import org.openhab.binding.loxone.internal.types.LxState;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.type.ChannelKind;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
/**
|
||||
* Common test framework class for all (@link LxControl} objects
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
class LxControlTest {
|
||||
LxServerHandlerDummy handler;
|
||||
LxUuid controlUuid;
|
||||
LxUuid roomUuid;
|
||||
LxUuid categoryUuid;
|
||||
String controlName;
|
||||
|
||||
void setupControl(String controlUuid, String roomUuid, String categoryUuid, String controlName) {
|
||||
this.controlUuid = new LxUuid(controlUuid);
|
||||
this.roomUuid = new LxUuid(roomUuid);
|
||||
this.categoryUuid = new LxUuid(categoryUuid);
|
||||
this.controlName = controlName;
|
||||
handler = new LxServerHandlerDummy();
|
||||
handler.loadConfiguration();
|
||||
}
|
||||
|
||||
<T> void testControlCreation(Class<T> testClass, int numberOfControls, int numberOfSubcontrols,
|
||||
int numberOfChannels, int numberOfChannelsWithSubs, int numberOfStates) {
|
||||
assertEquals(numberOfControls, numberOfControls(testClass));
|
||||
LxControl c = getControl(controlUuid);
|
||||
assertNotNull(c);
|
||||
Map<LxUuid, LxControl> subC = c.getSubControls();
|
||||
assertNotNull(subC);
|
||||
assertEquals(numberOfSubcontrols, subC.size());
|
||||
assertEquals(controlUuid, c.getUuid());
|
||||
assertEquals(controlName, c.getName());
|
||||
assertEquals(controlName, c.getLabel());
|
||||
LxContainer room = c.getRoom();
|
||||
assertNotNull(room);
|
||||
assertEquals(roomUuid, room.getUuid());
|
||||
LxCategory cat = c.getCategory();
|
||||
assertNotNull(cat);
|
||||
assertEquals(categoryUuid, cat.getUuid());
|
||||
assertEquals(numberOfChannels, c.getChannels().size());
|
||||
assertEquals(numberOfChannelsWithSubs, c.getChannelsWithSubcontrols().size());
|
||||
assertEquals(numberOfStates, c.getStates().size());
|
||||
}
|
||||
|
||||
void testChannel(LxControl ctrl, String itemType, String namePostFix, BigDecimal min, BigDecimal max,
|
||||
BigDecimal step, String format, Boolean readOnly, List<StateOption> options, Set<String> tags) {
|
||||
assertNotNull(ctrl);
|
||||
Channel c = getChannel(getExpectedName(ctrl.getLabel(), ctrl.getRoom().getName(), namePostFix), ctrl);
|
||||
assertNotNull(c);
|
||||
assertNotNull(c.getUID());
|
||||
assertNotNull(c.getDescription());
|
||||
assertEquals(itemType, c.getAcceptedItemType());
|
||||
assertEquals(ChannelKind.STATE, c.getKind());
|
||||
StateDescription d = handler.stateDescriptions.get(c.getUID());
|
||||
if (readOnly != null || min != null || max != null || step != null || format != null || options != null) {
|
||||
assertNotNull(d);
|
||||
assertEquals(min, d.getMinimum());
|
||||
assertEquals(max, d.getMaximum());
|
||||
assertEquals(step, d.getStep());
|
||||
assertEquals(format, d.getPattern());
|
||||
assertEquals(readOnly, d.isReadOnly());
|
||||
List<StateOption> opts = d.getOptions();
|
||||
if (options == null) {
|
||||
assertTrue(opts.isEmpty());
|
||||
} else {
|
||||
assertNotNull(opts);
|
||||
assertEquals(options.size(), opts.size());
|
||||
options.forEach(o -> {
|
||||
String label = o.getLabel();
|
||||
long num = opts.stream().filter(
|
||||
f -> label != null && label.equals(f.getLabel()) && o.getValue().equals(f.getValue()))
|
||||
.collect(Collectors.counting());
|
||||
assertEquals(1, num);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
assertNull(d);
|
||||
}
|
||||
if (tags != null) {
|
||||
assertThat(c.getDefaultTags(), hasItems(tags.toArray(new String[0])));
|
||||
} else {
|
||||
assertThat(c.getDefaultTags(), empty());
|
||||
}
|
||||
}
|
||||
|
||||
void testChannel(String itemType, String namePostFix, BigDecimal min, BigDecimal max, BigDecimal step,
|
||||
String format, Boolean readOnly, List<StateOption> options, Set<String> tags) {
|
||||
LxControl ctrl = getControl(controlUuid);
|
||||
testChannel(ctrl, itemType, namePostFix, min, max, step, format, readOnly, options, tags);
|
||||
}
|
||||
|
||||
void testChannel(String itemType) {
|
||||
testChannel(itemType, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
void testChannel(String itemType, Set<String> tags) {
|
||||
testChannel(itemType, null, null, null, null, null, null, null, tags);
|
||||
}
|
||||
|
||||
void testChannel(LxControl ctrl, String itemType) {
|
||||
testChannel(ctrl, itemType, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
void testChannel(LxControl ctrl, String itemType, Set<String> tags) {
|
||||
testChannel(ctrl, itemType, null, null, null, null, null, null, null, tags);
|
||||
}
|
||||
|
||||
void testChannel(String itemType, String namePostFix) {
|
||||
testChannel(itemType, namePostFix, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
void testChannel(String itemType, String namePostFix, Set<String> tags) {
|
||||
testChannel(itemType, namePostFix, null, null, null, null, null, null, tags);
|
||||
}
|
||||
|
||||
void testChannel(String itemType, String namePostFix, BigDecimal min, BigDecimal max, BigDecimal step,
|
||||
String format, Boolean readOnly, List<StateOption> options) {
|
||||
testChannel(itemType, namePostFix, min, max, step, format, readOnly, options, null);
|
||||
}
|
||||
|
||||
State getChannelState(LxControl ctrl, String namePostFix) {
|
||||
assertNotNull(ctrl);
|
||||
Channel c = getChannel(getExpectedName(ctrl.getLabel(), ctrl.getRoom().getName(), namePostFix), ctrl);
|
||||
assertNotNull(c);
|
||||
return ctrl.getChannelState(c.getUID());
|
||||
}
|
||||
|
||||
State getChannelState(String namePostFix) {
|
||||
LxControl ctrl = getControl(controlUuid);
|
||||
return getChannelState(ctrl, namePostFix);
|
||||
}
|
||||
|
||||
void testChannelState(LxControl ctrl, String namePostFix, State expectedValue) {
|
||||
State current = getChannelState(ctrl, namePostFix);
|
||||
if (expectedValue != null) {
|
||||
assertNotNull(current);
|
||||
}
|
||||
assertEquals(expectedValue, current);
|
||||
}
|
||||
|
||||
void testChannelState(String namePostFix, State expectedValue) {
|
||||
LxControl ctrl = getControl(controlUuid);
|
||||
testChannelState(ctrl, namePostFix, expectedValue);
|
||||
}
|
||||
|
||||
void testChannelState(State expectedValue) {
|
||||
testChannelState((String) null, expectedValue);
|
||||
}
|
||||
|
||||
void testChannelState(LxControl ctrl, State expectedValue) {
|
||||
testChannelState(ctrl, null, expectedValue);
|
||||
}
|
||||
|
||||
void changeLoxoneState(String stateName, Object value) {
|
||||
LxControl ctrl = getControl(controlUuid);
|
||||
assertNotNull(ctrl);
|
||||
LxState state = ctrl.getStates().get(stateName);
|
||||
assertNotNull(state);
|
||||
state.setStateValue(value);
|
||||
}
|
||||
|
||||
void executeCommand(LxControl ctrl, String namePostFix, Command command) {
|
||||
assertNotNull(ctrl);
|
||||
Channel c = getChannel(getExpectedName(ctrl.getLabel(), ctrl.getRoom().getName(), namePostFix), ctrl);
|
||||
assertNotNull(c);
|
||||
try {
|
||||
ctrl.handleCommand(c.getUID(), command);
|
||||
} catch (IOException e) {
|
||||
fail("This exception should never happen in test environment.");
|
||||
}
|
||||
}
|
||||
|
||||
void executeCommand(String namePostFix, Command command) {
|
||||
LxControl ctrl = getControl(controlUuid);
|
||||
executeCommand(ctrl, namePostFix, command);
|
||||
}
|
||||
|
||||
void executeCommand(LxControl ctrl, Command command) {
|
||||
executeCommand(ctrl, null, command);
|
||||
}
|
||||
|
||||
void executeCommand(Command command) {
|
||||
executeCommand((String) null, command);
|
||||
}
|
||||
|
||||
void testAction(String expectedAction, int numberOfActions) {
|
||||
assertEquals(numberOfActions, handler.actionQueue.size());
|
||||
if (numberOfActions > 0) {
|
||||
String action = handler.actionQueue.poll();
|
||||
assertNotNull(action);
|
||||
assertEquals(controlUuid + "/" + expectedAction, action);
|
||||
}
|
||||
}
|
||||
|
||||
void testAction(String expectedAction) {
|
||||
if (expectedAction == null) {
|
||||
testAction(null, 0);
|
||||
} else {
|
||||
testAction(expectedAction, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSubControl(Type type, String name) {
|
||||
LxControl ctrl = getControl(controlUuid);
|
||||
assertNotNull(ctrl);
|
||||
long n = ctrl.getSubControls().values().stream().filter(c -> name.equals(c.getName()))
|
||||
.collect(Collectors.counting());
|
||||
assertEquals(1L, n);
|
||||
}
|
||||
|
||||
private Channel getChannel(String name, LxControl c) {
|
||||
List<Channel> channels = c.getChannels();
|
||||
List<Channel> filtered = channels.stream().filter(a -> name.equals(a.getLabel())).collect(Collectors.toList());
|
||||
assertEquals(1, filtered.size());
|
||||
return filtered.get(0);
|
||||
}
|
||||
|
||||
private <T> long numberOfControls(Class<T> c) {
|
||||
Collection<LxControl> v = handler.controls.values();
|
||||
return v.stream().filter(o -> c.equals(o.getClass())).collect(Collectors.counting());
|
||||
}
|
||||
|
||||
private LxControl getControl(LxUuid uuid) {
|
||||
return handler.controls.get(uuid);
|
||||
}
|
||||
|
||||
private String getExpectedName(String controlName, String roomName, String postFix) {
|
||||
return roomName + " / " + controlName + (postFix != null ? postFix : "");
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlTextState}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlTextStateTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("106bed36-016d-6dd8-ffffffe6109fb656", "0b734138-038c-0386-ffff403fb0c34b9e",
|
||||
"0fe665f4-0161-4773-ffff403fb0c34b9e", "Gate");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlTextState.class, 1, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("String", null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
String s = new String();
|
||||
for (char c = ' '; c <= '~'; c++) {
|
||||
changeLoxoneState("textandicon", s);
|
||||
testChannelState(new StringType(s));
|
||||
s = s + c;
|
||||
}
|
||||
s = s + "\n\tabc\ndef\n";
|
||||
changeLoxoneState("textandicon", s);
|
||||
testChannelState(new StringType(s));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlTimedSwitch}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlTimedSwitchTest extends LxControlTest {
|
||||
private static final String DELAY_CHANNEL = " / Deactivation Delay";
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("1326771c-030e-3a7c-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Stairwell Light Switch");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlTimedSwitch.class, 1, 0, 2, 2, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Switch", Collections.singleton("Switchable"));
|
||||
testChannel("Number", DELAY_CHANNEL, new BigDecimal(-1), null, null, null, true, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
testChannelState(null);
|
||||
testChannelState(DELAY_CHANNEL, null);
|
||||
changeLoxoneState("deactivationdelaytotal", 100.0);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
changeLoxoneState("deactivationdelay", 0.0);
|
||||
testChannelState(OnOffType.OFF);
|
||||
testChannelState(DELAY_CHANNEL, DecimalType.ZERO);
|
||||
changeLoxoneState("deactivationdelay", -1.0);
|
||||
testChannelState(OnOffType.ON);
|
||||
testChannelState(DELAY_CHANNEL, DecimalType.ZERO);
|
||||
}
|
||||
for (Double i = 100.0; i >= 1.0; i--) {
|
||||
changeLoxoneState("deactivationdelay", i);
|
||||
testChannelState(OnOffType.ON);
|
||||
testChannelState(DELAY_CHANNEL, new DecimalType(i));
|
||||
}
|
||||
changeLoxoneState("deactivationdelay", 0.0);
|
||||
testChannelState(OnOffType.OFF);
|
||||
testChannelState(DELAY_CHANNEL, DecimalType.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("Pulse");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("Off");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlTracker}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlTrackerTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("132aa43b-01d4-56ea-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Tracker Control");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlTracker.class, 1, 0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("String", null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
String s = new String();
|
||||
for (int j = 0; j < i; j++) {
|
||||
for (char c = 'a'; c <= 'a' + j; c++) {
|
||||
s = s + c;
|
||||
}
|
||||
if (j != i - 1) {
|
||||
s = s + '|';
|
||||
}
|
||||
}
|
||||
changeLoxoneState("entries", s);
|
||||
testChannelState(new StringType(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlUpDownAnalog}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlUpDownAnalogTest extends LxControlTest {
|
||||
Double min;
|
||||
Double max;
|
||||
Double step;
|
||||
String format;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
min = 50.0;
|
||||
max = 150.0;
|
||||
step = 10.0;
|
||||
format = "%.1f";
|
||||
setupControl("131b1a96-02b9-f6e9-eeff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Up Down Analog Input");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlUpDownAnalog.class, 1, 0, 1, 1, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Number", null, new BigDecimal(min), new BigDecimal(max), new BigDecimal(step), format, false,
|
||||
null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneStateChanges() {
|
||||
testChannelState(null);
|
||||
for (int j = 0; j < 2; j++) {
|
||||
changeLoxoneState("error", 0.0);
|
||||
for (Double i = min - 50.0; i < min; i += 0.5) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(null);
|
||||
}
|
||||
for (Double i = min; i <= max; i += 0.5) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(new DecimalType(i));
|
||||
}
|
||||
for (Double i = max + 0.5; i < max + 50.0; i += 0.5) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(null);
|
||||
}
|
||||
changeLoxoneState("error", 1.0);
|
||||
for (Double i = min - 50.0; i < max + 50.0; i += 0.5) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
for (Double i = min - 50.0; i < min; i += 0.5) {
|
||||
executeCommand(new DecimalType(i));
|
||||
testAction(null);
|
||||
}
|
||||
for (Double i = min; i <= max; i += 0.5) {
|
||||
executeCommand(new DecimalType(i));
|
||||
testAction(i.toString());
|
||||
}
|
||||
for (Double i = max + 0.5; i < max + 50.0; i += 0.5) {
|
||||
executeCommand(new DecimalType(i));
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlUpDownDigital}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlUpDownDigitalTest extends LxControlTest {
|
||||
String upChannel;
|
||||
String downChannel;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
upChannel = " / Up";
|
||||
downChannel = " / Down";
|
||||
setupControl("0fd08ca6-01a6-d72a-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0b734138-033e-02d4-ffff403fb0c34b9e", "First Floor Scene");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlUpDownDigital.class, 1, 0, 2, 2, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Switch", upChannel);
|
||||
testChannel("Switch", downChannel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommands() {
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
|
||||
executeCommand(upChannel, OnOffType.ON);
|
||||
testAction("UpOn");
|
||||
testChannelState(upChannel, OnOffType.ON);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
executeCommand(upChannel, OnOffType.OFF);
|
||||
testAction("UpOff");
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
|
||||
executeCommand(downChannel, OnOffType.ON);
|
||||
testAction("DownOn");
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.ON);
|
||||
executeCommand(downChannel, OnOffType.OFF);
|
||||
testAction("DownOff");
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
|
||||
executeCommand(upChannel, OnOffType.ON);
|
||||
testAction("UpOn");
|
||||
testChannelState(upChannel, OnOffType.ON);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
executeCommand(downChannel, OnOffType.ON);
|
||||
testAction("DownOn");
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.ON);
|
||||
|
||||
executeCommand(upChannel, OnOffType.ON);
|
||||
testAction("UpOn");
|
||||
testChannelState(upChannel, OnOffType.ON);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
|
||||
executeCommand(upChannel, OnOffType.OFF);
|
||||
testAction("UpOff");
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
|
||||
executeCommand(upChannel, OnOffType.OFF);
|
||||
testAction(null);
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
|
||||
executeCommand(downChannel, OnOffType.OFF);
|
||||
testAction(null);
|
||||
testChannelState(upChannel, OnOffType.OFF);
|
||||
testChannelState(downChannel, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlValueSelector} - version which allows for increasing only
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlValueSelectorIncrTest extends LxControlValueSelectorTest {
|
||||
@Override
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("132a7b7e-0022-3aac-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Selection Switch Increase Only");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testIncreaseDecreaseCommands() {
|
||||
changeLoxoneState("min", 123.0);
|
||||
changeLoxoneState("max", 456.0);
|
||||
changeLoxoneState("step", 23.0);
|
||||
changeLoxoneState("value", 400.0);
|
||||
testChannelState(new PercentType(83));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("423.0");
|
||||
changeLoxoneState("value", 423.0);
|
||||
testChannelState(new PercentType(90));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("446.0");
|
||||
changeLoxoneState("value", 446.0);
|
||||
testChannelState(new PercentType(96));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("456.0"); // trim to max
|
||||
changeLoxoneState("value", 456.0);
|
||||
testChannelState(PercentType.HUNDRED);
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction(null);
|
||||
changeLoxoneState("step", 100.0);
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction(null);
|
||||
changeLoxoneState("value", 123.0);
|
||||
testChannelState(PercentType.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlValueSelector}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlValueSelectorTest extends LxControlTest {
|
||||
private static final String NUMBER_CHANNEL = " / Number";
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("432a7b7e-0022-3aac-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Selection Switch");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlValueSelector.class, 2, 0, 2, 2, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("Dimmer");
|
||||
testChannel("Number", NUMBER_CHANNEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneValueMinMaxChanges() {
|
||||
// filling in missing state values
|
||||
testChannelState(null);
|
||||
changeLoxoneState("step", 1.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("value", 50.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("min", 0.0);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("max", 100.0);
|
||||
testChannelState(new PercentType(50));
|
||||
testChannel("Dimmer", null, BigDecimal.ZERO, new BigDecimal(100), BigDecimal.ONE, "%.0f", false, null);
|
||||
testChannel("Number", NUMBER_CHANNEL, BigDecimal.ZERO, new BigDecimal(100), BigDecimal.ONE, "%.0f", false,
|
||||
null);
|
||||
|
||||
// potential division by zero
|
||||
changeLoxoneState("min", 55.0);
|
||||
changeLoxoneState("max", 55.0);
|
||||
testChannelState(null);
|
||||
|
||||
changeLoxoneState("min", 200.0);
|
||||
changeLoxoneState("max", 400.0);
|
||||
testChannel("Dimmer", null, new BigDecimal(200), new BigDecimal(400), BigDecimal.ONE, "%.0f", false, null);
|
||||
testChannel("Number", NUMBER_CHANNEL, new BigDecimal(200), new BigDecimal(400), BigDecimal.ONE, "%.0f", false,
|
||||
null);
|
||||
|
||||
// out of range
|
||||
changeLoxoneState("value", 199.9);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("value", 400.1);
|
||||
testChannelState(null);
|
||||
changeLoxoneState("value", 0.0);
|
||||
testChannelState(null);
|
||||
// scaling within range
|
||||
changeLoxoneState("value", 200.0);
|
||||
testChannelState(PercentType.ZERO);
|
||||
changeLoxoneState("value", 400.0);
|
||||
testChannelState(PercentType.HUNDRED);
|
||||
changeLoxoneState("value", 300.0);
|
||||
testChannelState(new PercentType(50));
|
||||
// reversed range boundaries
|
||||
changeLoxoneState("min", 50.0);
|
||||
changeLoxoneState("max", 20.0);
|
||||
changeLoxoneState("value", 30.0);
|
||||
testChannelState(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnOffPercentCommands() {
|
||||
changeLoxoneState("min", 1000.0);
|
||||
changeLoxoneState("max", 3000.0);
|
||||
changeLoxoneState("step", 100.0);
|
||||
|
||||
executeCommand(OnOffType.ON);
|
||||
testAction("3000.0");
|
||||
executeCommand(OnOffType.OFF);
|
||||
testAction("1000.0");
|
||||
|
||||
executeCommand(PercentType.HUNDRED);
|
||||
testAction("3000.0");
|
||||
executeCommand(new PercentType(50));
|
||||
testAction("2000.0");
|
||||
executeCommand(new PercentType(1));
|
||||
testAction("1020.0");
|
||||
executeCommand(PercentType.ZERO);
|
||||
testAction("1000.0");
|
||||
|
||||
executeCommand(StopMoveType.MOVE);
|
||||
testAction(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncreaseDecreaseCommands() {
|
||||
changeLoxoneState("min", 123.0);
|
||||
changeLoxoneState("max", 456.0);
|
||||
changeLoxoneState("step", 23.0);
|
||||
changeLoxoneState("value", 400.0);
|
||||
testChannelState(new PercentType(83));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("423.0");
|
||||
changeLoxoneState("value", 423.0);
|
||||
testChannelState(new PercentType(90));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("446.0");
|
||||
changeLoxoneState("value", 446.0);
|
||||
testChannelState(new PercentType(96));
|
||||
executeCommand(IncreaseDecreaseType.INCREASE);
|
||||
testAction("456.0"); // trim to max
|
||||
changeLoxoneState("value", 456.0);
|
||||
testChannelState(PercentType.HUNDRED);
|
||||
changeLoxoneState("step", 100.0);
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("356.0");
|
||||
changeLoxoneState("value", 356.0);
|
||||
testChannelState(new PercentType(69));
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("256.0");
|
||||
changeLoxoneState("value", 256.0);
|
||||
testChannelState(new PercentType(39));
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("156.0");
|
||||
changeLoxoneState("value", 156.0);
|
||||
testChannelState(new PercentType(9));
|
||||
executeCommand(IncreaseDecreaseType.DECREASE);
|
||||
testAction("123.0"); // trim to min
|
||||
changeLoxoneState("value", 123.0);
|
||||
testChannelState(PercentType.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumberCommands() {
|
||||
changeLoxoneState("min", 100.0);
|
||||
changeLoxoneState("max", 300.0);
|
||||
changeLoxoneState("step", 10.0);
|
||||
for (Double i = 0.0; i < 100.0; i += 0.35) {
|
||||
executeCommand(NUMBER_CHANNEL, new DecimalType(i));
|
||||
testAction(null);
|
||||
}
|
||||
for (Double i = 100.0; i <= 300.0; i += 0.47) {
|
||||
executeCommand(NUMBER_CHANNEL, new DecimalType(i));
|
||||
testAction(i.toString());
|
||||
}
|
||||
for (Double i = 300.01; i < 400.0; i += 0.59) {
|
||||
executeCommand(NUMBER_CHANNEL, new DecimalType(i));
|
||||
testAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoxoneNumberChanges() {
|
||||
testChannelState(null);
|
||||
changeLoxoneState("min", 100.0);
|
||||
changeLoxoneState("max", 300.0);
|
||||
changeLoxoneState("step", 10.0);
|
||||
for (Double i = 0.0; i < 100.0; i += 0.35) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(NUMBER_CHANNEL, null);
|
||||
}
|
||||
for (Double i = 100.0; i <= 300.0; i += 0.47) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(NUMBER_CHANNEL, new DecimalType(i));
|
||||
}
|
||||
for (Double i = 300.01; i < 400.0; i += 0.59) {
|
||||
changeLoxoneState("value", i);
|
||||
testChannelState(NUMBER_CHANNEL, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.loxone.internal.controls;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
|
||||
/**
|
||||
* Test class for (@link LxControlWebPage}
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxControlWebPageTest extends LxControlTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
setupControl("132d2791-00f8-d532-ffff403fb0c34b9e", "0b734138-037d-034e-ffff403fb0c34b9e",
|
||||
"0fe650c2-0004-d446-ffff504f9410790f", "Webpage 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControlCreation() {
|
||||
testControlCreation(LxControlWebPage.class, 1, 0, 2, 2, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
testChannel("String", " / URL", null, null, null, null, true, null);
|
||||
testChannel("String", " / URL HD", null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannelStates() {
|
||||
testChannelState(" / URL", new StringType("http://low.res"));
|
||||
testChannelState(" / URL HD", new StringType("http://hi.res"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.loxone.internal.controls;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.openhab.binding.loxone.internal.LxBindingConfiguration;
|
||||
import org.openhab.binding.loxone.internal.LxServerHandlerApi;
|
||||
import org.openhab.binding.loxone.internal.types.LxConfig;
|
||||
import org.openhab.binding.loxone.internal.types.LxUuid;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* Dummy implementation of thing handler and its API towards controls.
|
||||
*
|
||||
* @author Pawel Pieczul - initial contribution
|
||||
*
|
||||
*/
|
||||
public class LxServerHandlerDummy implements LxServerHandlerApi {
|
||||
|
||||
Gson gson;
|
||||
LxConfig config;
|
||||
LxBindingConfiguration bindingConfig = new LxBindingConfiguration();
|
||||
|
||||
Queue<String> actionQueue = new LinkedList<>();
|
||||
|
||||
Map<LxUuid, LxControl> controls;
|
||||
Map<LxUuid, LxControl> extraControls = new HashMap<>();
|
||||
Map<ChannelUID, StateDescription> stateDescriptions = new HashMap<>();
|
||||
|
||||
public LxServerHandlerDummy() {
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
builder.registerTypeAdapter(LxUuid.class, LxUuid.DESERIALIZER);
|
||||
builder.registerTypeAdapter(LxControl.class, LxControl.DESERIALIZER);
|
||||
gson = builder.create();
|
||||
}
|
||||
|
||||
void loadConfiguration() {
|
||||
InputStream stream = LxServerHandlerDummy.class.getResourceAsStream("LoxAPP3.json");
|
||||
assertNotNull(stream);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
|
||||
assertNotNull(reader);
|
||||
String msg = reader.lines().collect(Collectors.joining(System.lineSeparator()));
|
||||
assertNotNull(msg);
|
||||
|
||||
stateDescriptions.clear();
|
||||
|
||||
LxConfig config = gson.fromJson(msg, LxConfig.class);
|
||||
config.finalize(this);
|
||||
controls = config.controls;
|
||||
assertNotNull(controls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendAction(LxUuid id, String operation) throws IOException {
|
||||
actionQueue.add(id + "/" + operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addControl(LxControl control) {
|
||||
extraControls.put(control.getUuid(), control);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeControl(LxControl control) {
|
||||
LxControl ctrl = extraControls.remove(control.getUuid());
|
||||
assertNotNull(ctrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChannelState(ChannelUID channelId, State state) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChannelStateDescription(ChannelUID channelId, StateDescription description) {
|
||||
assertNotNull(channelId);
|
||||
assertNotNull(description);
|
||||
stateDescriptions.put(channelId, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSetting(String name) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSettings(Map<String, String> properties) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public Gson getGson() {
|
||||
return gson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThingUID getThingId() {
|
||||
return new ThingUID("loxone:miniserver:12345678");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
[
|
||||
{
|
||||
"name": "Low Lights",
|
||||
"id": 5,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Side Lights",
|
||||
"id": 2,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Play Lights",
|
||||
"id": 3,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Study Only",
|
||||
"id": 4,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Bright",
|
||||
"id": 777,
|
||||
"static": false,
|
||||
"used": 1
|
||||
},
|
||||
{
|
||||
"name": "Off",
|
||||
"id": 778,
|
||||
"static": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
[
|
||||
{
|
||||
"name": "Low Lights",
|
||||
"id": 5,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Play Lights",
|
||||
"id": 6,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Study Only Changed Name",
|
||||
"id": 4,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "New Mood 1",
|
||||
"id": 7,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "New Mood 2",
|
||||
"id": 8,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "Bright",
|
||||
"id": 777,
|
||||
"static": false,
|
||||
"used": 1
|
||||
},
|
||||
{
|
||||
"name": "Off",
|
||||
"id": 778,
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"name": "New Off",
|
||||
"id": 779,
|
||||
"static": true
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user