added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.mqtt.generic/.classpath
Normal file
32
bundles/org.openhab.binding.mqtt.generic/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.mqtt.generic/.project
Normal file
23
bundles/org.openhab.binding.mqtt.generic/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.mqtt.generic</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>
|
||||
44
bundles/org.openhab.binding.mqtt.generic/NOTICE
Normal file
44
bundles/org.openhab.binding.mqtt.generic/NOTICE
Normal file
@@ -0,0 +1,44 @@
|
||||
This content is produced and maintained by the Eclipse SmartHome project.
|
||||
|
||||
* Project home: https://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/.
|
||||
|
||||
The MQTT Homie convention is made available under the MIT license, attached at the end of this file.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/eclipse/smarthome
|
||||
|
||||
== Copyright Holders
|
||||
|
||||
See the NOTICE file distributed with the source code at
|
||||
https://github.com/eclipse/smarthome/blob/master/NOTICE
|
||||
for detailed information regarding copyright ownership.
|
||||
|
||||
== MIT license ==
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Marvin Roger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
289
bundles/org.openhab.binding.mqtt.generic/README.md
Normal file
289
bundles/org.openhab.binding.mqtt.generic/README.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# MQTT Things and Channels Binding
|
||||
|
||||
MQTT is one of the most commonly used protocols in IoT (Internet of Things) projects. It stands for Message Queuing Telemetry Transport.
|
||||
|
||||
It is designed as a lightweight messaging protocol that uses publish/subscribe operations to exchange data between clients and the server.
|
||||
|
||||

|
||||
|
||||
MQTT servers are called brokers and the clients are simply the connected devices.
|
||||
|
||||
* When a device (a client) wants to send data to the broker, we call this operation a “publish”.
|
||||
* When a device (a client) wants to receive data from the broker, we call this operation a “subscribe”.
|
||||
|
||||
|
||||

|
||||
|
||||
openHAB itself is not an MQTT Broker and needs to connect to one as a regular client.
|
||||
Therefore you must have configured a *Broker Thing* first via the **MQTT Broker Binding**!
|
||||
|
||||
## MQTT Topics
|
||||
|
||||
If a client subscribes to a broker, it is certainly not interested in all published messages.
|
||||
Instead it subscribes to specific **topics**. A topic can look like this: "mydevice/temperature".
|
||||
|
||||
Example:
|
||||
|
||||
Let's assume there is an MQTT capable light bulb.
|
||||
|
||||
It has a unique id amongst all light bulbs, say "device123". The manufacturer decided to accept new
|
||||
brightness values on "device123/brightness/set". In openHAB we call that a **command topic**.
|
||||
|
||||
And now assume that we have a mobile phone (or openHAB itself) and we register with the MQTT broker,
|
||||
and want to retrieve the current brightness value. The manufacturer specified that this value can
|
||||
be found on "device123/brightness". In openHAB we call that a **state topic**.
|
||||
|
||||
This pattern is very common, that you have a command and a state topic. A sensor would only have a state topic,
|
||||
naturally.
|
||||
|
||||
Because every manufacturer can device on his own on which topic his devices publish, this
|
||||
binding can unfortunately not provide any auto-discovery means.
|
||||
|
||||
If you use an open source IoT device, the chances are high,
|
||||
that it has the MQTT convention Homie or HomeAssistant implemented. Those conventions specify the topic
|
||||
topology and allow auto discovery. Please have a look at the specific openHAB bindings.
|
||||
|
||||
## Supported Things
|
||||
|
||||
Because of the very generic structure of MQTT, this binding allows you to add an arbitrary number
|
||||
of so called "Generic MQTT Things" to organize yourself.
|
||||
|
||||
On each of those things you can add an arbitrary number of channels.
|
||||
|
||||
Remember that you need a configured broker Thing first!
|
||||
|
||||
You can add the following channels:
|
||||
|
||||
#### Supported Channels
|
||||
|
||||
* **string**: This channel can show the received text on the given topic and can send text to a given topic.
|
||||
* **number**: This channel can show the received number on the given topic and can send a number to a given topic. It can have a min, max and step values.
|
||||
* **dimmer**: This channel handles numeric values as percentages. It can have min, max and step values.
|
||||
* **contact**: This channel represents an open/close state of a given topic.
|
||||
* **switch**: This channel represents an on/off state of a given topic and can send an on/off value to a given topic.
|
||||
* **colorRGB**: This channel handles color values in RGB format. (Deprecated)
|
||||
* **colorHSB**: This channel handles color values in HSB format. (Deprecated)
|
||||
* **color**: This channel handles color values in HSB, RGB or xyY (x,y,brightness) formats.
|
||||
* **location**: This channel handles a location.
|
||||
* **image**: This channel handles binary images in common java supported formats (bmp,jpg,png).
|
||||
* **datetime**: This channel handles date/time values.
|
||||
* **rollershutter**: This channel is for rollershutters.
|
||||
|
||||
## Channel Configuration
|
||||
|
||||
* __stateTopic__: The MQTT topic that represents the state of the thing. This can be empty, the thing channel will be a state-less trigger then. You can use a wildcard topic like "sensors/+/event" to retrieve state from multiple MQTT topics.
|
||||
* __transformationPattern__: An optional transformation pattern like [JSONPath](http://goessner.net/articles/JsonPath/index.html#e2) that is applied to all incoming MQTT values.
|
||||
* __transformationPatternOut__: An optional transformation pattern like [JSONPath](http://goessner.net/articles/JsonPath/index.html#e2) that is applied before publishing a value to MQTT.
|
||||
* __commandTopic__: The MQTT topic that commands are send to. This can be empty, the thing channel will be read-only then. Transformations are not applied for sending data.
|
||||
* __formatBeforePublish__: Format a value before it is published to the MQTT broker. The default is to just pass the channel/item state. If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s". Currently only "%s" is supported.
|
||||
* __postCommand__: If `true`, the received MQTT value will not only update the state of linked items, but command it.
|
||||
The default is `false`.
|
||||
You usually need this to be `true` if your item is also linked to another channel, say a KNX actor, and you want a received MQTT payload to command that KNX actor.
|
||||
* __retained__: The value will be published to the command topic as retained message. A retained value stays on the broker and can even be seen by MQTT clients that are subscribing at a later point in time.
|
||||
* __qos__: QoS of this channel. Overrides the connection QoS (defined in broker connection).
|
||||
* __trigger__: If `true`, the state topic will not update a state, but trigger a channel instead.
|
||||
|
||||
### Channel Type "string"
|
||||
|
||||
* __allowedStates__: An optional comma separated list of allowed states. Example: "ONE,TWO,THREE"
|
||||
|
||||
You can connect this channel to a String item.
|
||||
|
||||
### Channel Type "number"
|
||||
|
||||
* __min__: An optional minimum value.
|
||||
* __max__: An optional maximum value.
|
||||
* __step__: For decrease, increase commands the step needs to be known
|
||||
* __unit__: Unit of measurement (optional). For supported units see [OpenHAB: List of Units](https://www.openhab.org/docs/concepts/units-of-measurement.html#list-of-units). Examples: "°C", "°F"
|
||||
|
||||
A decimal value (like 0.2) is send to the MQTT topic if the number has a fractional part.
|
||||
If you always require an integer, please use the formatter.
|
||||
|
||||
You can connect this channel to a Number item.
|
||||
|
||||
### Channel Type "dimmer"
|
||||
|
||||
* __on__: An optional string (like "ON"/"Open") that is recognized as minimum.
|
||||
* __off__: An optional string (like "OFF"/"Close") that is recognized as maximum.
|
||||
* __min__: A required minimum value.
|
||||
* __max__: A required maximum value.
|
||||
* __step__: For decrease, increase commands the step needs to be known
|
||||
|
||||
The value is internally stored as a percentage for a value between **min** and **max**.
|
||||
|
||||
The channel will publish a value between `min` and `max`.
|
||||
|
||||
You can connect this channel to a Rollershutter or Dimmer item.
|
||||
|
||||
### Channel Type "contact", "switch"
|
||||
|
||||
* __on__: An optional number (like 1, 10) or a string (like "ON"/"Open") that is recognized as on/open state.
|
||||
* __off__: An optional number (like 0, -10) or a string (like "OFF"/"Close") that is recognized as off/closed state.
|
||||
|
||||
The contact channel by default recognizes `"OPEN"` and `"CLOSED"`. You can connect this channel to a Contact item.
|
||||
The switch channel by default recognizes `"ON"` and `"OFF"`. You can connect this channel to a Switch item.
|
||||
|
||||
If **on** and **off** are not configured it publishes the strings mentioned before respectively.
|
||||
|
||||
You can connect this channel to a Contact or Switch item.
|
||||
|
||||
### Channel Type "color"
|
||||
|
||||
* __color_mode__: A required string that defines the color representation: "hsb", "rgb" or "xyY" (x,y,brightness).
|
||||
* __on__: An optional string (like "BRIGHT") that is recognized as on state. (ON will always be recognized.)
|
||||
* __off__: An optional string (like "DARK") that is recognized as off state. (OFF will always be recognized.)
|
||||
* __onBrightness__: If you connect this channel to a Switch item and turn it on,
|
||||
|
||||
color and saturation are preserved from the last state, but
|
||||
the brightness will be set to this configured initial brightness (default: 10%).
|
||||
|
||||
You can connect this channel to a Color, Dimmer and Switch item.
|
||||
|
||||
This channel will publish the color as comma separated list to the MQTT broker,
|
||||
e.g. "112,54,123" for the RGB color mode (0-255 per component), "360,100,100" for the HSB color mode (0-359 for hue and 0-100 for saturation and brightness),
|
||||
and "0.640074,0.329970,100" for the xyY color mode (0-1 for x and y, and 0-100 for brightness).
|
||||
|
||||
The channel expects values on the corresponding MQTT topic to be in this format as well.
|
||||
|
||||
### Channel Type "colorRGB", "colorHSB" (Deprecated)
|
||||
|
||||
* __on__: An optional string (like "BRIGHT") that is recognized as on state. (ON will always be recognized.)
|
||||
* __off__: An optional string (like "DARK") that is recognized as off state. (OFF will always be recognized.)
|
||||
* __onBrightness__: If you connect this channel to a Switch item and turn it on,
|
||||
|
||||
color and saturation are preserved from the last state, but
|
||||
the brightness will be set to this configured initial brightness (default: 10%).
|
||||
|
||||
You can connect this channel to a Color, Dimmer and Switch item.
|
||||
|
||||
This channel will publish the color as comma separated list to the MQTT broker,
|
||||
e.g. "112,54,123" for an RGB channel (0-255 per component) and "360,100,100" for a HSB channel (0-359 for hue and 0-100 for saturation and brightness).
|
||||
|
||||
The channel expects values on the corresponding MQTT topic to be in this format as well.
|
||||
|
||||
### Channel Type "location"
|
||||
|
||||
You can connect this channel to a Location item.
|
||||
|
||||
The channel will publish the location as comma separated list to the MQTT broker,
|
||||
e.g. "112,54,123" for latitude, longitude, altitude. The altitude is optional.
|
||||
|
||||
The channel expects values on the corresponding MQTT topic to be in this format as well.
|
||||
|
||||
### Channel Type "image"
|
||||
|
||||
You can connect this channel to an Image item. This is a read-only channel.
|
||||
|
||||
The channel expects values on the corresponding MQTT topic to contain the binary
|
||||
data of a bmp, jpg, png or any other format that the installed java runtime supports.
|
||||
|
||||
### Channel Type "datetime"
|
||||
|
||||
You can connect this channel to a DateTime item.
|
||||
|
||||
The channel will publish the date/time in the format "yyyy-MM-dd'T'HH:mm"
|
||||
for example 2018-01-01T12:14:00. If you require another format, please use the formatter.
|
||||
|
||||
The channel expects values on the corresponding MQTT topic to be in this format as well.
|
||||
|
||||
### Channel Type "rollershutter"
|
||||
|
||||
* __on__: An optional string (like "Open") that is recognized as `UP` state.
|
||||
* __off__: An optional string (like "Close") that is recognized as `DOWN` state.
|
||||
* __stop__: An optional string (like "Stop") that is recognized as `STOP` state.
|
||||
|
||||
Internally `UP` is converted to 0%, `DOWN` to 100%.
|
||||
If strings are defined for these values, they are used for sending commands to the broker, too.
|
||||
|
||||
You can connect this channel to a Rollershutter or Dimmer item.
|
||||
|
||||
|
||||
## Rule Actions
|
||||
|
||||
This binding includes a rule action, which allows one to publish MQTT messages from within rules.
|
||||
There is a separate instance for each MQTT broker (i.e. bridge), which can be retrieved through
|
||||
|
||||
```
|
||||
val mqttActions = getActions("mqtt","mqtt:systemBroker:embedded-mqtt-broker")
|
||||
```
|
||||
|
||||
where the first parameter always has to be `mqtt` and the second (`mqtt:systemBroker:embedded-mqtt-broker`) is the Thing UID of the broker that should be used.
|
||||
Once this action instance is retrieved, you can invoke the `publishMQTT(String topic, String value, Boolean retained)` method on it:
|
||||
|
||||
```
|
||||
mqttActions.publishMQTT("mytopic","myvalue", true)
|
||||
```
|
||||
|
||||
The retained argument is optional and if not supplied defaults to `false`.
|
||||
|
||||
## Limitations
|
||||
|
||||
* The HomeAssistant Fan Components only support ON/OFF.
|
||||
* The HomeAssistant Cover Components only support OPEN/CLOSE/STOP.
|
||||
* The HomeAssistant Light Component does not support XY color changes.
|
||||
* The HomeAssistant Climate Components is not yet supported.
|
||||
|
||||
## Incoming Value Transformation
|
||||
|
||||
All mentioned channels allow an optional transformation for incoming MQTT topic values.
|
||||
|
||||
This is required if your received value is wrapped in a JSON or XML response.
|
||||
|
||||
Here are a few examples to unwrap a value from a complex response:
|
||||
|
||||
| Received value | Tr. Service | Transformation |
|
||||
|---------------------------------------------------------------------|-------------|-------------------------------------------|
|
||||
| `{device: {status: { temperature: 23.2 }}}` | JSONPATH | `JSONPATH:$.device.status.temperature` |
|
||||
| `<device><status><temperature>23.2</temperature></status></device>` | XPath | `XPath:/device/status/temperature/text()` |
|
||||
| `THEVALUE:23.2°C` | REGEX | `REGEX::(.*?)°` |
|
||||
|
||||
Transformations can be chained by separating them with the mathematical intersection character "∩".
|
||||
Please note that the incoming value will be discarded if one transformation fails (e.g. REGEX did not match).
|
||||
|
||||
## Outgoing Value Transformation
|
||||
|
||||
All mentioned channels allow an optional transformation for outgoing values.
|
||||
Please prefer formatting as described in the next section whenever possible.
|
||||
Please note that value will be discarded and not sent if one transformation fails (e.g. REGEX did not match).
|
||||
|
||||
## Format before Publish
|
||||
|
||||
This feature is quite powerful in transforming an item state before it is published to the MQTT broker.
|
||||
It has the syntax: `%[flags][width]conversion`.
|
||||
Find the full documentation on the [Java](https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html) web page.
|
||||
|
||||
The default is "%s" which means: Output the item state as string.
|
||||
|
||||
Here are a few examples:
|
||||
|
||||
* All uppercase: "%S". Just use the upper case letter for the conversion argument.
|
||||
* Apply a prefix: "myprefix%s"
|
||||
* Apply a suffix: "%s suffix"
|
||||
* Number precision: ".4f" for a 4 digit precision. Use the "+" flag to always add a sign: "+.4f".
|
||||
* Decimal to Hexadecimal/Octal/Scientific: For example "60" with "%x", "%o", "%e" becomes "74", "3C", "60".
|
||||
* Date/Time: To reference the item state multiple times, use "%1$". Use the "tX" conversion where "X" can be any of [h,H,m,M,I,k,l,S,p,B,b,A,a,y,Y,d,e].
|
||||
- For an output of *May 23, 1995* use "%1$**tb** %1$**te**,%1$**tY**".
|
||||
- For an output of *23.05.1995* use "%1$**td**.%1$**tm**.%1$**tY**".
|
||||
- For an output of *23:15* use "%1$**tH**:%1$**tM**".
|
||||
|
||||
Default pattern applied for each type:
|
||||
| Type | Parameter | Pattern | Comment |
|
||||
| ---------------- | --------------------------------- | ------------------- | ------- |
|
||||
| __string__ | String | "%s" |
|
||||
| __number__ | BigDecimal | "%f" | The default will remove trailing zeros after the decimal point.
|
||||
| __dimmer__ | BigDecimal | "%f" | The default will remove trailing zeros after the decimal point.
|
||||
| __contact__ | String | -- | No pattern supported. Always **on** and **off** strings.
|
||||
| __switch__ | String | -- | No pattern supported. Always **on** and **off** strings.
|
||||
| __colorRGB__ | BigDecimal, BigDecimal, BigDecimal| "%1$d,%2$d,%3$d" | Parameters are **red**, **green** and **blue** components.
|
||||
| __colorHSB__ | BigDecimal, BigDecimal, BigDecimal| "%1$d,%2$d,%3$d" | Parameters are **hue**, **saturation** and **brightness** components.
|
||||
| __location__ | BigDecimal, BigDecimal | "%2$f,%3$f,%1$f" | Parameters are **altitude**, **latitude** and **longitude**, altitude is only in default pattern, if value is not '0'.
|
||||
| __image__ | -- | -- | No publishing supported.
|
||||
| __datetime__ | ZonedDateTime | "%1$tY-%1$tm-%1$tdT%1$tH:%1$tM:%1$tS.%1$tN" | Trailing zeros of the nanoseconds are removed.
|
||||
| __rollershutter__| String | "%s" | No pattern supported. Always **up**, **down**, **stop** string or integer percent value.
|
||||
|
||||
Any outgoing value transformation will **always** result in a __string__ value.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* If you get the error "No MQTT client": Please update your installation.
|
||||
* If you use the Mosquitto broker: Please be aware that there is a relatively low setting for retained messages. At some point messages will just not being delivered anymore: Change the setting.
|
||||
BIN
bundles/org.openhab.binding.mqtt.generic/doc/mqtt.jpg
Normal file
BIN
bundles/org.openhab.binding.mqtt.generic/doc/mqtt.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
bundles/org.openhab.binding.mqtt.generic/doc/subpub.png
Normal file
BIN
bundles/org.openhab.binding.mqtt.generic/doc/subpub.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
25
bundles/org.openhab.binding.mqtt.generic/pom.xml
Normal file
25
bundles/org.openhab.binding.mqtt.generic/pom.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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.mqtt.generic</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: MQTT Things and Channels</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.mqtt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.mqtt.generic-${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-mqtt-generic" description="MQTT Binding Generic" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-mqtt</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}</bundle>
|
||||
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
</features>
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
import org.openhab.binding.mqtt.generic.values.Value;
|
||||
import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelGroupUID;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.util.UIDUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base class for MQTT thing handlers. If you are going to implement an MQTT convention, you probably
|
||||
* want to inherit from here.
|
||||
*
|
||||
* <p>
|
||||
* This base class will make sure you get a working {@link MqttBrokerConnection}, you will be informed
|
||||
* when to start your subscriptions ({@link #start(MqttBrokerConnection)}) and when to free your resources
|
||||
* because of a lost connection ({@link AbstractMQTTThingHandler#stop()}).
|
||||
*
|
||||
* <p>
|
||||
* If you inherit from this base class, you must use {@link ChannelState} to (a) keep a cached channel value,
|
||||
* (b) to link a MQTT topic value to a channel value ("MQTT state topic") and (c) to have a secondary MQTT topic
|
||||
* where any changes to the {@link ChannelState} are send to ("MQTT command topic").
|
||||
*
|
||||
* <p>
|
||||
* You are expected to keep your channel data structure organized in a way, to resolve a {@link ChannelUID} to
|
||||
* the corresponding {@link ChannelState} in {@link #getChannelState(ChannelUID)}.
|
||||
*
|
||||
* <p>
|
||||
* To inform the framework of changed values, received via MQTT, a {@link ChannelState} calls a listener callback.
|
||||
* While setting up your {@link ChannelState} you would set the callback to your thing handler,
|
||||
* because this base class implements {@link ChannelStateUpdateListener}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractMQTTThingHandler extends BaseThingHandler
|
||||
implements ChannelStateUpdateListener, AvailabilityTracker {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractMQTTThingHandler.class);
|
||||
// Timeout for the entire tree parsing and subscription
|
||||
private final int subscribeTimeout;
|
||||
|
||||
protected @Nullable MqttBrokerConnection connection;
|
||||
|
||||
private AtomicBoolean messageReceived = new AtomicBoolean(false);
|
||||
private Map<String, @Nullable ChannelState> availabilityStates = new ConcurrentHashMap<>();
|
||||
|
||||
public AbstractMQTTThingHandler(Thing thing, int subscribeTimeout) {
|
||||
super(thing);
|
||||
this.subscribeTimeout = subscribeTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the channel state for the given channelUID.
|
||||
*
|
||||
* @param channelUID The channelUID
|
||||
* @return A channel state. May be null.
|
||||
*/
|
||||
public abstract @Nullable ChannelState getChannelState(ChannelUID channelUID);
|
||||
|
||||
/**
|
||||
* Start the topic discovery and subscribe to all channel state topics on all {@link ChannelState}s.
|
||||
* Put the thing ONLINE on success otherwise complete the returned future exceptionally.
|
||||
*
|
||||
* @param connection A started broker connection
|
||||
* @return A future that completes normal on success and exceptionally on any errors.
|
||||
*/
|
||||
protected abstract CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection);
|
||||
|
||||
/**
|
||||
* Called when the MQTT connection disappeared.
|
||||
* You should clean up all resources that depend on a working connection.
|
||||
*/
|
||||
protected void stop() {
|
||||
clearAllAvailabilityTopics();
|
||||
resetMessageReceived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final @Nullable ChannelState data = getChannelState(channelUID);
|
||||
|
||||
if (data == null) {
|
||||
logger.warn("Channel {} not supported!", channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
State state = data.getCache().getChannelState();
|
||||
if (state instanceof UnDefType) {
|
||||
logger.debug("Channel {} received REFRESH but no value cached, ignoring", channelUID);
|
||||
} else {
|
||||
updateState(channelUID, state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.isReadOnly()) {
|
||||
logger.trace("Channel {} is a read-only channel, ignoring command {}", channelUID, command);
|
||||
return;
|
||||
}
|
||||
|
||||
final CompletableFuture<Boolean> future = data.publishValue(command);
|
||||
future.handle((v, ex) -> {
|
||||
if (ex != null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getLocalizedMessage());
|
||||
logger.debug("Failed publishing value {} to topic {}: {}", command, data.getCommandTopic(),
|
||||
ex.getMessage());
|
||||
} else {
|
||||
logger.debug("Successfully published value {} to topic {}", command, data.getCommandTopic());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
stop();
|
||||
connection = null;
|
||||
return;
|
||||
}
|
||||
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
AbstractBrokerHandler h = getBridgeHandler();
|
||||
if (h == null) {
|
||||
resetMessageReceived();
|
||||
logger.warn("Bridge handler not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
final MqttBrokerConnection connection;
|
||||
try {
|
||||
connection = h.getConnectionAsync().get(500, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException ignored) {
|
||||
resetMessageReceived();
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
|
||||
"Bridge handler has no valid broker connection!");
|
||||
return;
|
||||
}
|
||||
this.connection = connection;
|
||||
|
||||
// Start up (subscribe to MQTT topics). Limit with a timeout and catch exceptions.
|
||||
// We do not set the thing to ONLINE here in the AbstractBase, that is the responsibility of a derived
|
||||
// class.
|
||||
try {
|
||||
Collection<CompletableFuture<@Nullable Void>> futures = availabilityStates.values().stream().map(s -> {
|
||||
if (s != null) {
|
||||
return s.start(connection, scheduler, 0);
|
||||
}
|
||||
return CompletableFuture.allOf();
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
futures.add(start(connection));
|
||||
|
||||
futures.stream().collect(FutureCollector.allOf()).exceptionally(e -> {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
return null;
|
||||
}).get(subscribeTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException ignored) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Did not receive all required topics");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bride handler. The bridge is from the "MQTT" bundle.
|
||||
*/
|
||||
public @Nullable AbstractBrokerHandler getBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
return null;
|
||||
}
|
||||
return (AbstractBrokerHandler) bridge.getHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bridge status.
|
||||
*/
|
||||
public ThingStatusInfo getBridgeStatus() {
|
||||
Bridge b = getBridge();
|
||||
if (b != null) {
|
||||
return b.getStatusInfo();
|
||||
} else {
|
||||
return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
bridgeStatusChanged(getBridgeStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
stop();
|
||||
super.handleRemoval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stop();
|
||||
try {
|
||||
unsubscribeAll().get(500, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("unsubscription on disposal failed for {}: ", thing.getUID(), e);
|
||||
}
|
||||
connection = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* this method must unsubscribe all topics used by this thing handler
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract CompletableFuture<Void> unsubscribeAll();
|
||||
|
||||
@Override
|
||||
public void updateChannelState(ChannelUID channelUID, State value) {
|
||||
if (messageReceived.compareAndSet(false, true)) {
|
||||
calculateThingStatus();
|
||||
}
|
||||
super.updateState(channelUID, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void triggerChannel(ChannelUID channelUID, String event) {
|
||||
if (messageReceived.compareAndSet(false, true)) {
|
||||
calculateThingStatus();
|
||||
}
|
||||
super.triggerChannel(channelUID, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postChannelCommand(ChannelUID channelUID, Command command) {
|
||||
postCommand(channelUID, command);
|
||||
}
|
||||
|
||||
public @Nullable MqttBrokerConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for tests only to inject a broker connection.
|
||||
*
|
||||
* @param connection MQTT Broker connection
|
||||
*/
|
||||
public void setConnection(MqttBrokerConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAvailabilityTopic(String availability_topic, String payload_available,
|
||||
String payload_not_available) {
|
||||
availabilityStates.computeIfAbsent(availability_topic, topic -> {
|
||||
Value value = new OnOffValue(payload_available, payload_not_available);
|
||||
ChannelGroupUID groupUID = new ChannelGroupUID(getThing().getUID(), "availablility");
|
||||
ChannelUID channelUID = new ChannelUID(groupUID, UIDUtils.encode(topic));
|
||||
ChannelState state = new ChannelState(ChannelConfigBuilder.create().withStateTopic(topic).build(),
|
||||
channelUID, value, new ChannelStateUpdateListener() {
|
||||
@Override
|
||||
public void updateChannelState(ChannelUID channelUID, State value) {
|
||||
calculateThingStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void triggerChannel(ChannelUID channelUID, String eventPayload) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postChannelCommand(ChannelUID channelUID, Command value) {
|
||||
}
|
||||
});
|
||||
MqttBrokerConnection connection = getConnection();
|
||||
if (connection != null) {
|
||||
state.start(connection, scheduler, 0);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAvailabilityTopic(@NonNull String availability_topic) {
|
||||
availabilityStates.computeIfPresent(availability_topic, (topic, state) -> {
|
||||
if (connection != null && state != null) {
|
||||
state.stop();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllAvailabilityTopics() {
|
||||
Set<String> topics = new HashSet<>(availabilityStates.keySet());
|
||||
topics.forEach(this::removeAvailabilityTopic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetMessageReceived() {
|
||||
if (messageReceived.compareAndSet(true, false)) {
|
||||
calculateThingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
protected void calculateThingStatus() {
|
||||
final boolean availabilityTopicsSeen;
|
||||
|
||||
if (availabilityStates.isEmpty()) {
|
||||
availabilityTopicsSeen = true;
|
||||
} else {
|
||||
availabilityTopicsSeen = availabilityStates.values().stream().allMatch(
|
||||
c -> c != null && OnOffType.ON.equals(c.getCache().getChannelState().as(OnOffType.class)));
|
||||
}
|
||||
updateThingStatus(messageReceived.get(), availabilityTopicsSeen);
|
||||
}
|
||||
|
||||
protected abstract void updateThingStatus(boolean messageReceived, boolean availabilityTopicsSeen);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Interface to keep track of the availability of device using an availability topic or messages received
|
||||
*
|
||||
* @author Jochen Klein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AvailabilityTracker {
|
||||
|
||||
/**
|
||||
* Adds an availability topic to determine the availability of a device.
|
||||
* <p>
|
||||
* Availability topics are usually set by the device as LWT.
|
||||
*
|
||||
* @param availability_topic
|
||||
* @param payload_available
|
||||
* @param payload_not_available
|
||||
*/
|
||||
public void addAvailabilityTopic(String availability_topic, String payload_available, String payload_not_available);
|
||||
|
||||
public void removeAvailabilityTopic(String availability_topic);
|
||||
|
||||
public void clearAllAvailabilityTopics();
|
||||
|
||||
/**
|
||||
* resets the indicator, if messages have been received.
|
||||
* <p>
|
||||
* This is used to time out the availability of the device after some time without receiving a message.
|
||||
*/
|
||||
public void resetMessageReceived();
|
||||
}
|
||||
@@ -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.mqtt.generic;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A user can add custom channels to an MQTT Thing.
|
||||
* <p>
|
||||
* This class contains the channel configuration.
|
||||
* <p>
|
||||
* You may want to extend this for channel configurations of MQTT extensions.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelConfig {
|
||||
/** This is either a state topic or a trigger topic, depending on {@link #trigger}. */
|
||||
public String stateTopic = "";
|
||||
public String commandTopic = "";
|
||||
|
||||
/**
|
||||
* If true, the channel state is not updated on a new message.
|
||||
* Instead a postCommand() call is performed.
|
||||
*/
|
||||
public boolean postCommand = false;
|
||||
public @Nullable Integer qos;
|
||||
public boolean retained = false;
|
||||
/** If true, the state topic will not update a state, but trigger a channel instead. */
|
||||
public boolean trigger = false;
|
||||
public String unit = "";
|
||||
|
||||
public String transformationPattern = "";
|
||||
public String transformationPatternOut = "";
|
||||
public String formatBeforePublish = "%s";
|
||||
public String allowedStates = "";
|
||||
|
||||
public @Nullable BigDecimal min;
|
||||
public @Nullable BigDecimal max;
|
||||
public @Nullable BigDecimal step;
|
||||
public @Nullable String on;
|
||||
public @Nullable String off;
|
||||
public @Nullable String stop;
|
||||
|
||||
public int onBrightness = 10;
|
||||
public String colorMode = "";
|
||||
}
|
||||
@@ -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.mqtt.generic;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link ChannelConfig} is required for the {@link ChannelState} object.
|
||||
* For easily creating a configuration, use this builder.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelConfigBuilder {
|
||||
private final ChannelConfig config = new ChannelConfig();
|
||||
|
||||
private ChannelConfigBuilder() {
|
||||
}
|
||||
|
||||
public static ChannelConfigBuilder create() {
|
||||
return new ChannelConfigBuilder();
|
||||
}
|
||||
|
||||
public static ChannelConfigBuilder create(@Nullable String stateTopic, @Nullable String commandTopic) {
|
||||
return new ChannelConfigBuilder().withStateTopic(stateTopic).withCommandTopic(commandTopic);
|
||||
}
|
||||
|
||||
public ChannelConfig build() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public ChannelConfigBuilder withFormatter(String formatter) {
|
||||
config.formatBeforePublish = formatter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelConfigBuilder withStateTopic(@Nullable String topic) {
|
||||
if (topic != null) {
|
||||
config.stateTopic = topic;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelConfigBuilder withCommandTopic(@Nullable String topic) {
|
||||
if (topic != null) {
|
||||
config.commandTopic = topic;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelConfigBuilder withRetain(boolean retain) {
|
||||
config.retained = retain;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelConfigBuilder withQos(@Nullable Integer qos) {
|
||||
config.qos = qos;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelConfigBuilder makeTrigger(boolean trigger) {
|
||||
config.trigger = trigger;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.binding.mqtt.generic.values.Value;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This object consists of an {@link Value}, which is updated on the respective MQTT topic change.
|
||||
* Updates to the value are propagated via the {@link ChannelStateUpdateListener}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelState implements MqttMessageSubscriber {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChannelState.class);
|
||||
|
||||
// Immutable channel configuration
|
||||
protected final boolean readOnly;
|
||||
protected final ChannelUID channelUID;
|
||||
protected final ChannelConfig config;
|
||||
|
||||
/** Channel value **/
|
||||
protected final Value cachedValue;
|
||||
|
||||
// Runtime variables
|
||||
private @Nullable MqttBrokerConnection connection;
|
||||
protected final List<ChannelStateTransformation> transformationsIn = new ArrayList<>();
|
||||
protected final List<ChannelStateTransformation> transformationsOut = new ArrayList<>();
|
||||
private @Nullable ChannelStateUpdateListener channelStateUpdateListener;
|
||||
protected boolean hasSubscribed = false;
|
||||
private @Nullable ScheduledFuture<?> scheduledFuture;
|
||||
private CompletableFuture<@Nullable Void> future = CompletableFuture.completedFuture(null);
|
||||
private final Object futureLock = new Object();
|
||||
|
||||
/**
|
||||
* Creates a new channel state.
|
||||
*
|
||||
* @param config The channel configuration
|
||||
* @param channelUID The channelUID is used for the {@link ChannelStateUpdateListener} to notify about value changes
|
||||
* @param cachedValue MQTT only notifies us once about a value, during the subscribe. The channel state therefore
|
||||
* needs a cache for the current value.
|
||||
* @param channelStateUpdateListener A channel state update listener
|
||||
*/
|
||||
public ChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
|
||||
@Nullable ChannelStateUpdateListener channelStateUpdateListener) {
|
||||
this.config = config;
|
||||
this.channelStateUpdateListener = channelStateUpdateListener;
|
||||
this.channelUID = channelUID;
|
||||
this.cachedValue = cachedValue;
|
||||
this.readOnly = StringUtils.isBlank(config.commandTopic);
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return this.readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a transformation that is applied for each received MQTT topic value.
|
||||
* The transformations are executed in order.
|
||||
*
|
||||
* @param transformation A transformation
|
||||
*/
|
||||
public void addTransformation(ChannelStateTransformation transformation) {
|
||||
transformationsIn.add(transformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a transformation that is applied for each value to be published.
|
||||
* The transformations are executed in order.
|
||||
*
|
||||
* @param transformation A transformation
|
||||
*/
|
||||
public void addTransformationOut(ChannelStateTransformation transformation) {
|
||||
transformationsOut.add(transformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear transformations
|
||||
*/
|
||||
public void clearTransformations() {
|
||||
transformationsIn.clear();
|
||||
transformationsOut.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached value state object of this message subscriber.
|
||||
* <p>
|
||||
* MQTT only notifies us once about a value, during the subscribe.
|
||||
* The channel state therefore needs a cache for the current value.
|
||||
* If MQTT has not yet published a value, the cache might still be in UNDEF state.
|
||||
* </p>
|
||||
*/
|
||||
public Value getCache() {
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the channelUID
|
||||
*/
|
||||
public ChannelUID channelUID() {
|
||||
return channelUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incoming message from the MqttBrokerConnection
|
||||
*
|
||||
* @param topic The topic. Is the same as the field stateTopic.
|
||||
* @param payload The byte payload. Must be UTF8 encoded text or binary data.
|
||||
*/
|
||||
@Override
|
||||
public void processMessage(String topic, byte[] payload) {
|
||||
final ChannelStateUpdateListener channelStateUpdateListener = this.channelStateUpdateListener;
|
||||
if (channelStateUpdateListener == null) {
|
||||
logger.warn("MQTT message received for topic {}, but MessageSubscriber object hasn't been started!", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedValue.isBinary()) {
|
||||
cachedValue.update(payload);
|
||||
channelStateUpdateListener.updateChannelState(channelUID, cachedValue.getChannelState());
|
||||
receivedOrTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
// String value: Apply transformations
|
||||
String strValue = new String(payload, StandardCharsets.UTF_8);
|
||||
for (ChannelStateTransformation t : transformationsIn) {
|
||||
String transformedValue = t.processValue(strValue);
|
||||
if (transformedValue != null) {
|
||||
strValue = transformedValue;
|
||||
} else {
|
||||
logger.debug("Transformation '{}' returned null on '{}', discarding message", strValue, t.serviceName);
|
||||
receivedOrTimeout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Is trigger?: Special handling
|
||||
if (config.trigger) {
|
||||
channelStateUpdateListener.triggerChannel(channelUID, strValue);
|
||||
receivedOrTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
Command command = TypeParser.parseCommand(cachedValue.getSupportedCommandTypes(), strValue);
|
||||
if (command == null) {
|
||||
logger.warn("Incoming payload '{}' not supported by type '{}'", strValue,
|
||||
cachedValue.getClass().getSimpleName());
|
||||
receivedOrTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
Command postOnlyCommand = cachedValue.isPostOnly(command);
|
||||
if (postOnlyCommand != null) {
|
||||
channelStateUpdateListener.postChannelCommand(channelUID, postOnlyCommand);
|
||||
receivedOrTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
// Map the string to an ESH command, update the cached value and post the command to the framework
|
||||
try {
|
||||
cachedValue.update(command);
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
logger.warn("Command '{}' not supported by type '{}': {}", strValue, cachedValue.getClass().getSimpleName(),
|
||||
e.getMessage());
|
||||
receivedOrTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.postCommand) {
|
||||
channelStateUpdateListener.postChannelCommand(channelUID, (Command) cachedValue.getChannelState());
|
||||
} else {
|
||||
channelStateUpdateListener.updateChannelState(channelUID, cachedValue.getChannelState());
|
||||
}
|
||||
receivedOrTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state topic. Might be an empty string if this is a stateless channel (TRIGGER kind channel).
|
||||
*/
|
||||
public String getStateTopic() {
|
||||
return config.stateTopic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the command topic. Might be an empty string, if this is a read-only channel.
|
||||
*/
|
||||
public String getCommandTopic() {
|
||||
return config.commandTopic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channelType ID which also happens to be an item-type
|
||||
*/
|
||||
public String getItemType() {
|
||||
return cachedValue.getItemType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a stateful channel.
|
||||
*/
|
||||
public boolean isStateful() {
|
||||
return config.retained;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the subscription to the state topic and resets the channelStateUpdateListener.
|
||||
*
|
||||
* @return A future that completes with true if unsubscribing from the state topic succeeded.
|
||||
* It completes with false if no connection is established and completes exceptionally otherwise.
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> stop() {
|
||||
final MqttBrokerConnection connection = this.connection;
|
||||
if (connection != null && StringUtils.isNotBlank(config.stateTopic)) {
|
||||
return connection.unsubscribe(config.stateTopic, this).thenRun(this::internalStop);
|
||||
} else {
|
||||
internalStop();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void internalStop() {
|
||||
logger.debug("Unsubscribed channel {} form topic: {}", this.channelUID, config.stateTopic);
|
||||
this.connection = null;
|
||||
this.channelStateUpdateListener = null;
|
||||
hasSubscribed = false;
|
||||
cachedValue.resetState();
|
||||
}
|
||||
|
||||
private void receivedOrTimeout() {
|
||||
final ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
|
||||
if (scheduledFuture != null) { // Cancel timeout
|
||||
scheduledFuture.cancel(false);
|
||||
this.scheduledFuture = null;
|
||||
}
|
||||
future.complete(null);
|
||||
}
|
||||
|
||||
private @Nullable Void subscribeFail(Throwable e) {
|
||||
final ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
|
||||
if (scheduledFuture != null) { // Cancel timeout
|
||||
scheduledFuture.cancel(false);
|
||||
this.scheduledFuture = null;
|
||||
}
|
||||
future.completeExceptionally(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the state topic on the given connection and informs about updates on the given listener.
|
||||
*
|
||||
* @param connection A broker connection
|
||||
* @param scheduler A scheduler to realize the timeout
|
||||
* @param timeout A timeout in milliseconds. Can be 0 to disable the timeout and let the future return earlier.
|
||||
* @return A future that completes with true if the subscribing worked, with false if the stateTopic is not set
|
||||
* and exceptionally otherwise.
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
|
||||
int timeout) {
|
||||
synchronized (futureLock) {
|
||||
// if the connection is still the same, the subscription is still present, otherwise we need to renew
|
||||
if (hasSubscribed || !future.isDone() && connection.equals(this.connection)) {
|
||||
return future;
|
||||
}
|
||||
hasSubscribed = false;
|
||||
|
||||
this.connection = connection;
|
||||
|
||||
if (StringUtils.isBlank(config.stateTopic)) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
this.future = new CompletableFuture<>();
|
||||
}
|
||||
connection.subscribe(config.stateTopic, this).thenRun(() -> {
|
||||
hasSubscribed = true;
|
||||
logger.debug("Subscribed channel {} to topic: {}", this.channelUID, config.stateTopic);
|
||||
if (timeout > 0 && !future.isDone()) {
|
||||
this.scheduledFuture = scheduler.schedule(this::receivedOrTimeout, timeout, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
receivedOrTimeout();
|
||||
}
|
||||
}).exceptionally(this::subscribeFail);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this channel has subscribed to its MQTT topics.
|
||||
* You need to call {@link #start(MqttBrokerConnection, ScheduledExecutorService, int)} and
|
||||
* have a stateTopic set, to subscribe this channel.
|
||||
*/
|
||||
public boolean hasSubscribed() {
|
||||
return this.hasSubscribed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a value on MQTT. A command topic needs to be set in the configuration.
|
||||
*
|
||||
* @param command The command to send
|
||||
* @return A future that completes with true if the publishing worked and false if it is a readonly topic
|
||||
* and exceptionally otherwise.
|
||||
*/
|
||||
public CompletableFuture<Boolean> publishValue(Command command) {
|
||||
cachedValue.update(command);
|
||||
|
||||
Value mqttCommandValue = cachedValue;
|
||||
|
||||
final MqttBrokerConnection connection = this.connection;
|
||||
|
||||
if (connection == null) {
|
||||
CompletableFuture<Boolean> f = new CompletableFuture<>();
|
||||
f.completeExceptionally(new IllegalStateException(
|
||||
"The connection object has not been set. start() should have been called!"));
|
||||
return f;
|
||||
}
|
||||
|
||||
if (readOnly) {
|
||||
logger.debug(
|
||||
"You have tried to publish {} to the mqtt topic '{}' that was marked read-only. You can't 'set' anything on a sensor state topic for example.",
|
||||
mqttCommandValue, config.commandTopic);
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
// Outgoing transformations
|
||||
for (ChannelStateTransformation t : transformationsOut) {
|
||||
String commandString = mqttCommandValue.getMQTTpublishValue(null);
|
||||
String transformedValue = t.processValue(commandString);
|
||||
if (transformedValue != null) {
|
||||
Value textValue = new TextValue();
|
||||
textValue.update(new StringType(transformedValue));
|
||||
mqttCommandValue = textValue;
|
||||
} else {
|
||||
logger.debug("Transformation '{}' returned null on '{}', discarding message", mqttCommandValue,
|
||||
t.serviceName);
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
}
|
||||
|
||||
String commandString;
|
||||
|
||||
// Formatter: Applied before the channel state value is published to the MQTT broker.
|
||||
if (config.formatBeforePublish.length() > 0) {
|
||||
try {
|
||||
commandString = mqttCommandValue.getMQTTpublishValue(config.formatBeforePublish);
|
||||
} catch (IllegalFormatException e) {
|
||||
logger.debug("Format pattern incorrect for {}", channelUID, e);
|
||||
commandString = mqttCommandValue.getMQTTpublishValue(null);
|
||||
}
|
||||
} else {
|
||||
commandString = mqttCommandValue.getMQTTpublishValue(null);
|
||||
}
|
||||
|
||||
int qos = (config.qos != null) ? config.qos : connection.getQos();
|
||||
|
||||
return connection.publish(config.commandTopic, commandString.getBytes(), qos, config.retained);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The channelStateUpdateListener
|
||||
*/
|
||||
public @Nullable ChannelStateUpdateListener getChannelStateUpdateListener() {
|
||||
return channelStateUpdateListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channelStateUpdateListener The channelStateUpdateListener to set
|
||||
*/
|
||||
public void setChannelStateUpdateListener(ChannelStateUpdateListener channelStateUpdateListener) {
|
||||
this.channelStateUpdateListener = channelStateUpdateListener;
|
||||
}
|
||||
|
||||
public @Nullable MqttBrokerConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for tests only to inject a broker connection. Use
|
||||
* {@link #start(MqttBrokerConnection, ScheduledExecutorService, int)} instead.
|
||||
*
|
||||
* @param connection MQTT Broker connection
|
||||
*/
|
||||
public void setConnection(MqttBrokerConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.transform.TransformationException;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A transformation for a {@link ChannelState}. It is applied for each received value on an MQTT topic.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelStateTransformation {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChannelStateTransformation.class);
|
||||
private final TransformationServiceProvider provider;
|
||||
private WeakReference<@Nullable TransformationService> transformationService = new WeakReference<>(null);
|
||||
final String pattern;
|
||||
final String serviceName;
|
||||
|
||||
/**
|
||||
* Creates a new channel state transformer.
|
||||
*
|
||||
* @param pattern A transformation pattern, starting with the transformation service
|
||||
* name,followed by a colon and the transformation itself. An Example:
|
||||
* JSONPATH:$.device.status.temperature for a json {device: {status: {
|
||||
* temperature: 23.2 }}}.
|
||||
* @param provider The transformation service provider
|
||||
*/
|
||||
public ChannelStateTransformation(String pattern, TransformationServiceProvider provider) {
|
||||
this.provider = provider;
|
||||
int index = pattern.indexOf(':');
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"The transformation pattern must consist of the type and the pattern separated by a colon");
|
||||
}
|
||||
String type = pattern.substring(0, index).toUpperCase();
|
||||
this.pattern = pattern.substring(index + 1);
|
||||
this.serviceName = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new channel state transformer.
|
||||
*
|
||||
* @param serviceName A transformation service name.
|
||||
* @param pattern A transformation. An Example:
|
||||
* $.device.status.temperature for a json {device: {status: {
|
||||
* temperature: 23.2 }}} (for type <code>JSONPATH</code>).
|
||||
* @param provider The transformation service provider
|
||||
*/
|
||||
public ChannelStateTransformation(String serviceName, String pattern, TransformationServiceProvider provider) {
|
||||
this.serviceName = serviceName;
|
||||
this.pattern = pattern;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called by the {@link ChannelState} for each incoming MQTT value.
|
||||
*
|
||||
* @param value The incoming value
|
||||
* @return The transformed value
|
||||
*/
|
||||
protected @Nullable String processValue(String value) {
|
||||
TransformationService transformationService = this.transformationService.get();
|
||||
if (transformationService == null) {
|
||||
transformationService = provider.getTransformationService(serviceName);
|
||||
if (transformationService == null) {
|
||||
logger.warn("Transformation service {} for pattern {} not found!", serviceName, pattern);
|
||||
return value;
|
||||
}
|
||||
this.transformationService = new WeakReference<>(transformationService);
|
||||
}
|
||||
String returnValue = null;
|
||||
try {
|
||||
returnValue = transformationService.transform(pattern, value);
|
||||
} catch (TransformationException e) {
|
||||
logger.warn("Executing the {}-transformation failed: {}", serviceName, e.getMessage());
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ChannelStateUpdateListener {
|
||||
/**
|
||||
* A new value got published on a configured MQTT topic associated with the given channel uid.
|
||||
*
|
||||
* @param channelUID The channel uid
|
||||
* @param value The new value. Doesn't necessarily need to be different than the value before.
|
||||
*/
|
||||
void updateChannelState(ChannelUID channelUID, State value);
|
||||
|
||||
/**
|
||||
* A new value got published on a configured MQTT topic associated with the given channel uid.
|
||||
* The channel is configured to post the new state as command.
|
||||
*
|
||||
* @param channelUID The channel uid
|
||||
* @param value The new value. Doesn't necessarily need to be different than the value before.
|
||||
*/
|
||||
void postChannelCommand(ChannelUID channelUID, Command value);
|
||||
|
||||
/**
|
||||
* A new value got published on a configured MQTT topic associated with the given channel uid.
|
||||
* The channel is of kind Trigger.
|
||||
*
|
||||
* @param channelUID The channel uid
|
||||
* @param value The new value. Doesn't necessarily need to be different than the value before.
|
||||
*/
|
||||
void triggerChannel(ChannelUID channelUID, String eventPayload);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
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.binding.mqtt.generic.internal.MqttThingHandlerFactory;
|
||||
import org.openhab.binding.mqtt.generic.internal.handler.GenericMQTTThingHandler;
|
||||
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;
|
||||
|
||||
/**
|
||||
* If the user configures a generic channel and defines for example minimum/maximum/readonly,
|
||||
* we need to dynamically override the xml default state.
|
||||
* This service is started on-demand only, as soon as {@link MqttThingHandlerFactory} requires it.
|
||||
*
|
||||
* It is filled with new state descriptions within the {@link GenericMQTTThingHandler}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, MqttChannelStateDescriptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class MqttChannelStateDescriptionProvider implements DynamicStateDescriptionProvider {
|
||||
|
||||
private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
|
||||
private final Logger logger = LoggerFactory.getLogger(MqttChannelStateDescriptionProvider.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
|
||||
*/
|
||||
public void setDescription(ChannelUID channelUID, StateDescription description) {
|
||||
logger.debug("Adding state description for channel {}", channelUID);
|
||||
descriptions.put(channelUID, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered state descriptions
|
||||
*/
|
||||
public void removeAllDescriptions() {
|
||||
logger.debug("Removing all state descriptions");
|
||||
descriptions.clear();
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given channel state description.
|
||||
*
|
||||
* @param channel The channel
|
||||
*/
|
||||
public void remove(ChannelUID channel) {
|
||||
descriptions.remove(channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.internal.MqttThingHandlerFactory;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.ThingTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelGroupType;
|
||||
import org.openhab.core.thing.type.ChannelGroupTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelGroupTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.thing.type.ThingType;
|
||||
import org.openhab.core.thing.type.ThingTypeBuilder;
|
||||
import org.openhab.core.thing.type.ThingTypeRegistry;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* An MQTT Extension might want to provide additional, dynamic channel types, based on auto-discovery.
|
||||
* <p>
|
||||
* Just retrieve the `MqttChannelTypeProvider` OSGi service and add (and remove) your channel types.
|
||||
* <p>
|
||||
* This provider is started on-demand only, as soon as {@link MqttThingHandlerFactory} or an extension requires it.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = false, service = { ThingTypeProvider.class, ChannelTypeProvider.class,
|
||||
ChannelGroupTypeProvider.class, MqttChannelTypeProvider.class })
|
||||
public class MqttChannelTypeProvider implements ThingTypeProvider, ChannelGroupTypeProvider, ChannelTypeProvider {
|
||||
private final ThingTypeRegistry typeRegistry;
|
||||
|
||||
private final Map<ChannelTypeUID, ChannelType> types = new HashMap<>();
|
||||
private final Map<ChannelGroupTypeUID, ChannelGroupType> groups = new HashMap<>();
|
||||
private final Map<ThingTypeUID, ThingType> things = new HashMap<>();
|
||||
|
||||
@Activate
|
||||
public MqttChannelTypeProvider(@Reference ThingTypeRegistry typeRegistry) {
|
||||
super();
|
||||
this.typeRegistry = typeRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
|
||||
return types.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
|
||||
return types.get(channelTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelGroupType getChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID,
|
||||
@Nullable Locale locale) {
|
||||
return groups.get(channelGroupTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ChannelGroupType> getChannelGroupTypes(@Nullable Locale locale) {
|
||||
return groups.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ThingType> getThingTypes(@Nullable Locale locale) {
|
||||
return things.values();
|
||||
}
|
||||
|
||||
public Set<ThingTypeUID> getThingTypeUIDs() {
|
||||
return things.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingType getThingType(ThingTypeUID thingTypeUID, @Nullable Locale locale) {
|
||||
return things.get(thingTypeUID);
|
||||
}
|
||||
|
||||
public void removeChannelType(ChannelTypeUID uid) {
|
||||
types.remove(uid);
|
||||
}
|
||||
|
||||
public void removeChannelGroupType(ChannelGroupTypeUID uid) {
|
||||
groups.remove(uid);
|
||||
}
|
||||
|
||||
public void setChannelGroupType(ChannelGroupTypeUID uid, ChannelGroupType type) {
|
||||
groups.put(uid, type);
|
||||
}
|
||||
|
||||
public void setChannelType(ChannelTypeUID uid, ChannelType type) {
|
||||
types.put(uid, type);
|
||||
}
|
||||
|
||||
public void removeThingType(ThingTypeUID uid) {
|
||||
things.remove(uid);
|
||||
}
|
||||
|
||||
public void setThingType(ThingTypeUID uid, ThingType type) {
|
||||
things.put(uid, type);
|
||||
}
|
||||
|
||||
public void setThingTypeIfAbsent(ThingTypeUID uid, ThingType type) {
|
||||
things.putIfAbsent(uid, type);
|
||||
}
|
||||
|
||||
public ThingTypeBuilder derive(ThingTypeUID newTypeId, ThingTypeUID baseTypeId) {
|
||||
ThingType baseType = typeRegistry.getThingType(baseTypeId);
|
||||
|
||||
ThingTypeBuilder result = ThingTypeBuilder.instance(newTypeId, baseType.getLabel())
|
||||
.withChannelGroupDefinitions(baseType.getChannelGroupDefinitions())
|
||||
.withChannelDefinitions(baseType.getChannelDefinitions())
|
||||
.withExtensibleChannelTypeIds(baseType.getExtensibleChannelTypeIds())
|
||||
.withSupportedBridgeTypeUIDs(baseType.getSupportedBridgeTypeUIDs())
|
||||
.withProperties(baseType.getProperties()).isListed(false);
|
||||
|
||||
String representationProperty = baseType.getRepresentationProperty();
|
||||
if (representationProperty != null) {
|
||||
result = result.withRepresentationProperty(representationProperty);
|
||||
}
|
||||
URI configDescriptionURI = baseType.getConfigDescriptionURI();
|
||||
if (configDescriptionURI != null) {
|
||||
result = result.withConfigDescriptionURI(configDescriptionURI);
|
||||
}
|
||||
String category = baseType.getCategory();
|
||||
if (category != null) {
|
||||
result = result.withCategory(category);
|
||||
}
|
||||
String description = baseType.getDescription();
|
||||
if (description != null) {
|
||||
result = result.withDescription(description);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
|
||||
/**
|
||||
* Provide a transformation service which can be used during MQTT topic transformation.
|
||||
*
|
||||
* @author Simon Kaufmann - initial contribution and API
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface TransformationServiceProvider {
|
||||
/**
|
||||
* Provide a {@link TransformationService} matching the given type.
|
||||
*
|
||||
* @param type the type of the requested {@link TransformationService}.
|
||||
* @return a {@link TransformationService} matching the given type.
|
||||
*/
|
||||
@Nullable
|
||||
TransformationService getTransformationService(String type);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link MqttBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MqttBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "mqtt";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID GENERIC_MQTT_THING = new ThingTypeUID(BINDING_ID, "topic");
|
||||
|
||||
// Generic thing channel types
|
||||
public static final String COLOR_RGB = "colorRGB";
|
||||
public static final String COLOR_HSB = "colorHSB";
|
||||
public static final String COLOR = "color";
|
||||
public static final String CONTACT = "contact";
|
||||
public static final String DIMMER = "dimmer";
|
||||
public static final String NUMBER = "number";
|
||||
public static final String STRING = "string";
|
||||
public static final String SWITCH = "switch";
|
||||
public static final String IMAGE = "image";
|
||||
public static final String LOCATION = "location";
|
||||
public static final String DATETIME = "datetime";
|
||||
public static final String ROLLERSHUTTER = "rollershutter";
|
||||
public static final String TRIGGER = "trigger";
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.internal;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
||||
import org.openhab.binding.mqtt.generic.internal.handler.GenericMQTTThingHandler;
|
||||
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.openhab.core.transform.TransformationHelper;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
import org.osgi.service.component.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link MqttThingHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class)
|
||||
@NonNullByDefault
|
||||
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider {
|
||||
private @NonNullByDefault({}) MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
|
||||
.of(MqttBindingConstants.GENERIC_MQTT_THING).collect(Collectors.toSet());
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Activate
|
||||
@Override
|
||||
protected void activate(ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
@Override
|
||||
protected void deactivate(ComponentContext componentContext) {
|
||||
super.deactivate(componentContext);
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setStateDescriptionProvider(MqttChannelStateDescriptionProvider stateDescription) {
|
||||
this.stateDescriptionProvider = stateDescription;
|
||||
}
|
||||
|
||||
protected void unsetStateDescriptionProvider(MqttChannelStateDescriptionProvider stateDescription) {
|
||||
this.stateDescriptionProvider = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(MqttBindingConstants.GENERIC_MQTT_THING)) {
|
||||
return new GenericMQTTThingHandler(thing, stateDescriptionProvider, this, 1500);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable TransformationService getTransformationService(String type) {
|
||||
return TransformationHelper.getTransformationService(bundleContext, type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.internal.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
|
||||
import org.openhab.binding.mqtt.generic.ChannelConfig;
|
||||
import org.openhab.binding.mqtt.generic.ChannelState;
|
||||
import org.openhab.binding.mqtt.generic.ChannelStateTransformation;
|
||||
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
||||
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
||||
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
|
||||
import org.openhab.binding.mqtt.generic.values.Value;
|
||||
import org.openhab.binding.mqtt.generic.values.ValueFactory;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
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.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This handler manages manual created Things with manually added channels to link to MQTT topics.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericMQTTThingHandler extends AbstractMQTTThingHandler implements ChannelStateUpdateListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(GenericMQTTThingHandler.class);
|
||||
final Map<ChannelUID, ChannelState> channelStateByChannelUID = new HashMap<>();
|
||||
protected final MqttChannelStateDescriptionProvider stateDescProvider;
|
||||
protected final TransformationServiceProvider transformationServiceProvider;
|
||||
|
||||
/**
|
||||
* Creates a new Thing handler for generic MQTT channels.
|
||||
*
|
||||
* @param thing The thing of this handler
|
||||
* @param stateDescProvider A channel state provider
|
||||
* @param transformationServiceProvider The transformation service provider
|
||||
* @param subscribeTimeout The subscribe timeout
|
||||
*/
|
||||
public GenericMQTTThingHandler(Thing thing, MqttChannelStateDescriptionProvider stateDescProvider,
|
||||
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout) {
|
||||
super(thing, subscribeTimeout);
|
||||
this.stateDescProvider = stateDescProvider;
|
||||
this.transformationServiceProvider = transformationServiceProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
|
||||
return channelStateByChannelUID.get(channelUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe on all channel static topics on all {@link ChannelState}s.
|
||||
* If subscribing on all channels worked, the thing is put ONLINE, else OFFLINE.
|
||||
*
|
||||
* @param connection A started broker connection
|
||||
*/
|
||||
@Override
|
||||
protected CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection) {
|
||||
return channelStateByChannelUID.values().stream().map(c -> c.start(connection, scheduler, 0))
|
||||
.collect(FutureCollector.allOf()).thenRun(this::calculateThingStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stop() {
|
||||
channelStateByChannelUID.values().forEach(c -> c.getCache().resetState());
|
||||
super.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// Remove all state descriptions of this handler
|
||||
channelStateByChannelUID.forEach((uid, state) -> stateDescProvider.remove(uid));
|
||||
super.dispose();
|
||||
// there is a design flaw, we can't clean up our stuff because it is needed by the super-class on disposal for
|
||||
// unsubscribing
|
||||
channelStateByChannelUID.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> unsubscribeAll() {
|
||||
return CompletableFuture.allOf(
|
||||
channelStateByChannelUID.values().stream().map(ChannelState::stop).toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* For every Thing channel there exists a corresponding {@link ChannelState}. It consists of the MQTT state
|
||||
* and MQTT command topic, the ChannelUID and a value state.
|
||||
*
|
||||
* @param channelConfig The channel configuration that contains MQTT state and command topic and multiple other
|
||||
* configurations.
|
||||
* @param channelUID The channel UID
|
||||
* @param valueState The channel value state
|
||||
* @return
|
||||
*/
|
||||
protected ChannelState createChannelState(ChannelConfig channelConfig, ChannelUID channelUID, Value valueState) {
|
||||
ChannelState state = new ChannelState(channelConfig, channelUID, valueState, this);
|
||||
String[] transformations;
|
||||
|
||||
// Incoming value transformations
|
||||
transformations = channelConfig.transformationPattern.split("∩");
|
||||
Stream.of(transformations).filter(StringUtils::isNotBlank)
|
||||
.map(t -> new ChannelStateTransformation(t, transformationServiceProvider))
|
||||
.forEach(t -> state.addTransformation(t));
|
||||
|
||||
// Outgoing value transformations
|
||||
transformations = channelConfig.transformationPatternOut.split("∩");
|
||||
Stream.of(transformations).filter(StringUtils::isNotBlank)
|
||||
.map(t -> new ChannelStateTransformation(t, transformationServiceProvider))
|
||||
.forEach(t -> state.addTransformationOut(t));
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
GenericThingConfiguration config = getConfigAs(GenericThingConfiguration.class);
|
||||
|
||||
String availabilityTopic = config.availabilityTopic;
|
||||
|
||||
if (availabilityTopic != null) {
|
||||
addAvailabilityTopic(availabilityTopic, config.payloadAvailable, config.payloadNotAvailable);
|
||||
} else {
|
||||
clearAllAvailabilityTopics();
|
||||
}
|
||||
|
||||
List<ChannelUID> configErrors = new ArrayList<>();
|
||||
for (Channel channel : thing.getChannels()) {
|
||||
final ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
|
||||
if (channelTypeUID == null) {
|
||||
logger.warn("Channel {} has no type", channel.getLabel());
|
||||
continue;
|
||||
}
|
||||
final ChannelConfig channelConfig = channel.getConfiguration().as(ChannelConfig.class);
|
||||
try {
|
||||
Value value = ValueFactory.createValueState(channelConfig, channelTypeUID.getId());
|
||||
ChannelState channelState = createChannelState(channelConfig, channel.getUID(), value);
|
||||
channelStateByChannelUID.put(channel.getUID(), channelState);
|
||||
StateDescription description = value
|
||||
.createStateDescription(StringUtils.isBlank(channelConfig.commandTopic)).build()
|
||||
.toStateDescription();
|
||||
if (description != null) {
|
||||
stateDescProvider.setDescription(channel.getUID(), description);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Channel configuration error", e);
|
||||
configErrors.add(channel.getUID());
|
||||
}
|
||||
}
|
||||
|
||||
// If some channels could not start up, put the entire thing offline and display the channels
|
||||
// in question to the user.
|
||||
if (!configErrors.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Remove and recreate: "
|
||||
+ configErrors.stream().map(ChannelUID::getAsString).collect(Collectors.joining(",")));
|
||||
return;
|
||||
}
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateThingStatus(boolean messageReceived, boolean availibilityTopicsSeen) {
|
||||
if (messageReceived || availibilityTopicsSeen) {
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* The {@link GenericMQTTThingHandler} manages Things that are responsible for MQTT components.
|
||||
* This class contains the necessary configuration for such a Thing handler.
|
||||
*
|
||||
* @author Jochen Klein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericThingConfiguration {
|
||||
/**
|
||||
* topic for the availability channel
|
||||
*/
|
||||
public @Nullable String availabilityTopic;
|
||||
|
||||
/**
|
||||
* payload for the availability topic when the device is available.
|
||||
*/
|
||||
public String payloadAvailable = OnOffType.ON.toString();
|
||||
|
||||
/**
|
||||
* payload for the availability topic when the device is *not* available.
|
||||
*/
|
||||
public String payloadNotAvailable = OnOffType.OFF.toString();
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.mapping;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* MQTT does not directly support key-value configuration maps. We do need those for device discovery and thing
|
||||
* configuration though.<br>
|
||||
* Different conventions came up with different solutions, and one is to have "attribute classes".
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* An attribute class is a java class that extends {@link AbstractMqttAttributeClass} and
|
||||
* contains annotated fields where each field corresponds to a MQTT topic.
|
||||
* To automatically subscribe to all fields, a call to
|
||||
* {@link #subscribeAndReceive(MqttBrokerConnection, ScheduledExecutorService, String, AttributeChanged, int)} is
|
||||
* required.
|
||||
* Unsubscribe with a call to {@link #unsubscribe(AbstractMqttAttributeClass)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The Homie 3.x convention uses attribute classes for Devices, Nodes and Properties configuration.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The given object, called bean in this context, can consist of all basic java types boolean, int, double, long,
|
||||
* String, respective object wrappers like Integer, Double, Long, the BigDecimal type and Enum types. Enums need to be
|
||||
* declared within the bean class though. Arrays like String[] are supported as well, but require an annotation because
|
||||
* the separator needs to be known.
|
||||
* </p>
|
||||
* A topic prefix can be defined for the entire class or for a single field. A field annotation overwrites a class
|
||||
* annotation.
|
||||
*
|
||||
* An example:
|
||||
*
|
||||
* <pre>
|
||||
* @TopicPrefix("$")
|
||||
* class MyAttributes extends AbstractMqttAttributeClass {
|
||||
* public String testString;
|
||||
* public @MapToField(splitCharacter=",") String[] multipleStrings;
|
||||
*
|
||||
* public int anInt = 2;
|
||||
*
|
||||
* public enum AnEnum {
|
||||
* Value1,
|
||||
* Value2
|
||||
* };
|
||||
* public AnEnum anEnum = AnEnum.Value1;
|
||||
*
|
||||
* public BigDecimal aDecimalValue
|
||||
*
|
||||
* @Override
|
||||
* public Object getFieldsOf() {
|
||||
* return this;
|
||||
* }
|
||||
* };
|
||||
* </pre>
|
||||
*
|
||||
* You would use this class in this way:
|
||||
*
|
||||
* <pre>
|
||||
* MyAttributes bean = new MyAttributes();
|
||||
* bean.subscribe(connection, new ScheduledExecutorService(), "mqtt/topic/bean", null, 500)
|
||||
* .thenRun(() -> System.out.println("subscribed"));
|
||||
* </pre>
|
||||
*
|
||||
* The above attribute class would end up with subscriptions to "mqtt/topic/bean/$testString",
|
||||
* "mqtt/topic/bean/$multipleStrings", "mqtt/topic/bean/$anInt" and so on. It is assumed that all MQTT messages are
|
||||
* UTF-8 strings.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractMqttAttributeClass implements SubscribeFieldToMQTTtopic.FieldChanged {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractMqttAttributeClass.class);
|
||||
protected transient List<SubscribeFieldToMQTTtopic> subscriptions = new ArrayList<>();
|
||||
public transient WeakReference<@Nullable MqttBrokerConnection> connection = new WeakReference<>(null);
|
||||
protected transient WeakReference<@Nullable ScheduledExecutorService> scheduler = new WeakReference<>(null);
|
||||
private final String prefix;
|
||||
private transient String basetopic = "";
|
||||
protected transient AttributeChanged attributeChangedListener = (b, c, d, e, f) -> {
|
||||
};
|
||||
private transient boolean complete = false;
|
||||
|
||||
/**
|
||||
* Implement this interface to be notified of an updated field.
|
||||
*/
|
||||
public interface AttributeChanged {
|
||||
/**
|
||||
* An attribute has changed
|
||||
*
|
||||
* @param name The name of the field
|
||||
* @param value The new value
|
||||
* @param connection The broker connection
|
||||
* @param scheduler The scheduler that was used for timeouts
|
||||
* @param allMandatoryFieldsReceived True if now all mandatory fields have values
|
||||
*/
|
||||
void attributeChanged(String name, Object value, MqttBrokerConnection connection,
|
||||
ScheduledExecutorService scheduler, boolean allMandatoryFieldsReceived);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
protected AbstractMqttAttributeClass() {
|
||||
TopicPrefix topicUsesPrefix = getFieldsOf().getClass().getAnnotation(TopicPrefix.class);
|
||||
prefix = (topicUsesPrefix != null) ? topicUsesPrefix.value() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all topics of the managed object.
|
||||
*
|
||||
* @param connection A broker connection to remove the subscriptions from.
|
||||
* @param objectWithFields A bean class
|
||||
* @return Returns a future that completes as soon as all unsubscriptions have been performed.
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> unsubscribe() {
|
||||
final MqttBrokerConnection connection = this.connection.get();
|
||||
if (connection == null) {
|
||||
subscriptions.clear();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
final CompletableFuture<?>[] futures = subscriptions.stream().map(m -> connection.unsubscribe(m.topic, m))
|
||||
.toArray(CompletableFuture[]::new);
|
||||
subscriptions.clear();
|
||||
return CompletableFuture.allOf(futures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to all subtopics on a MQTT broker connection base topic that match field names of s java object.
|
||||
* The fields will be kept in sync with their respective topics. Optionally, you can register update-observers for
|
||||
* specific fields.
|
||||
*
|
||||
* @param connection A MQTT broker connection.
|
||||
* @param scheduler A scheduler for timeouts.
|
||||
* @param basetopic The base topic. Given a base topic of "base/topic", a field "test" would be registered as
|
||||
* "base/topic/test".
|
||||
* @param attributeChangedListener Field change listener
|
||||
* @param timeout Timeout per subscription in milliseconds. The returned future completes after this time
|
||||
* even if no
|
||||
* message has been received for a single MQTT topic.
|
||||
* @return Returns a future that completes as soon as values for all subscriptions have been received or have timed
|
||||
* out.
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> subscribeAndReceive(MqttBrokerConnection connection,
|
||||
ScheduledExecutorService scheduler, String basetopic, @Nullable AttributeChanged attributeChangedListener,
|
||||
int timeout) {
|
||||
// We first need to unsubscribe old subscriptions if any
|
||||
final CompletableFuture<@Nullable Void> startFuture;
|
||||
if (!subscriptions.isEmpty()) {
|
||||
startFuture = unsubscribe();
|
||||
} else {
|
||||
startFuture = CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
this.connection = new WeakReference<>(connection);
|
||||
this.scheduler = new WeakReference<>(scheduler);
|
||||
this.basetopic = basetopic;
|
||||
if (attributeChangedListener != null) {
|
||||
this.attributeChangedListener = attributeChangedListener;
|
||||
} else {
|
||||
this.attributeChangedListener = (b, c, d, e, f) -> {
|
||||
};
|
||||
}
|
||||
|
||||
subscriptions = getAllFields(getFieldsOf().getClass()).stream().filter(AbstractMqttAttributeClass::filterField)
|
||||
.map(this::mapFieldToSubscriber).collect(Collectors.toList());
|
||||
|
||||
final CompletableFuture<?>[] futures = subscriptions.stream()
|
||||
.map(m -> m.subscribeAndReceive(connection, timeout)).toArray(CompletableFuture[]::new);
|
||||
return CompletableFuture.allOf(startFuture, CompletableFuture.allOf(futures));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return fields of the given class as well as all super classes.
|
||||
*
|
||||
* @param clazz The class
|
||||
* @return A list of Field objects
|
||||
*/
|
||||
protected static List<Field> getAllFields(Class<?> clazz) {
|
||||
List<Field> fields = new ArrayList<>();
|
||||
|
||||
Class<?> currentClass = clazz;
|
||||
while (currentClass != null) {
|
||||
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given field is not final and not transient
|
||||
*/
|
||||
protected static boolean filterField(Field field) {
|
||||
return !Modifier.isFinal(field.getModifiers()) && !Modifier.isTransient(field.getModifiers())
|
||||
&& !Modifier.isStatic(field.getModifiers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the given field to a newly created {@link SubscribeFieldToMQTTtopic}.
|
||||
* Requires the scheduler of this class to be set.
|
||||
*
|
||||
* @param field A field
|
||||
* @return A newly created {@link SubscribeFieldToMQTTtopic}.
|
||||
*/
|
||||
protected SubscribeFieldToMQTTtopic mapFieldToSubscriber(Field field) {
|
||||
final ScheduledExecutorService scheduler = this.scheduler.get();
|
||||
if (scheduler == null) {
|
||||
throw new IllegalStateException("No scheduler set!");
|
||||
}
|
||||
|
||||
MandatoryField mandatoryField = field.getAnnotation(MandatoryField.class);
|
||||
@SuppressWarnings("null")
|
||||
boolean mandatory = mandatoryField != null;
|
||||
|
||||
TopicPrefix topicUsesPrefix = field.getAnnotation(TopicPrefix.class);
|
||||
@SuppressWarnings("null")
|
||||
String localPrefix = (topicUsesPrefix != null) ? topicUsesPrefix.value() : prefix;
|
||||
|
||||
final String topic = basetopic + "/" + localPrefix + field.getName();
|
||||
|
||||
return createSubscriber(scheduler, field, topic, mandatory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a field subscriber for the given field on the given object
|
||||
*
|
||||
* @param scheduler A scheduler for the timeout functionality
|
||||
* @param field The field
|
||||
* @param topic The full topic to subscribe to
|
||||
* @param mandatory True of this field is a mandatory one. A timeout will cause a future to complete exceptionally.
|
||||
* @return Returns a MQTT message subscriber for a single class field
|
||||
*/
|
||||
public SubscribeFieldToMQTTtopic createSubscriber(ScheduledExecutorService scheduler, Field field, String topic,
|
||||
boolean mandatory) {
|
||||
return new SubscribeFieldToMQTTtopic(scheduler, field, this, topic, mandatory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this attribute class has received a value for each mandatory field.
|
||||
* In contrast to the parameter "allMandatoryFieldsReceived" of
|
||||
* {@link AttributeChanged#attributeChanged(String, Object, MqttBrokerConnection, ScheduledExecutorService, boolean)}
|
||||
* this flag will only be updated after that call.
|
||||
*
|
||||
* <p>
|
||||
* You can use this behaviour to compare if a changed field was the last one to complete
|
||||
* this attribute class. E.g.:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* void attributeChanged(..., boolean allMandatoryFieldsReceived) {
|
||||
* if (allMandatoryFieldsReceived && !attributes.isComplete()) {
|
||||
* // The attribute class is now complete but wasn't before...
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
return complete;
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the observed MQTT topics got a new value. Apply this to the given field now
|
||||
* and propagate the changed value event.
|
||||
*/
|
||||
@Override
|
||||
public void fieldChanged(Field field, Object value) {
|
||||
// This object holds only a weak reference to connection and scheduler.
|
||||
// Attribute classes should perform an unsubscribe when a connection is lost.
|
||||
// We fail the future exceptionally here if that didn't happen so that everyone knows.
|
||||
final MqttBrokerConnection connection = this.connection.get();
|
||||
final ScheduledExecutorService scheduler = this.scheduler.get();
|
||||
if (connection == null || scheduler == null) {
|
||||
logger.warn("No connection or scheduler set!");
|
||||
return;
|
||||
}
|
||||
// Set field. It is not a reason to fail the future exceptionally if a field could not be set.
|
||||
// But at least issue a warning to the log.
|
||||
try {
|
||||
field.set(getFieldsOf(), value);
|
||||
final boolean newComplete = !subscriptions.stream().anyMatch(s -> s.isMandatory() && !s.hasReceivedValue());
|
||||
attributeChangedListener.attributeChanged(field.getName(), value, connection, scheduler, newComplete);
|
||||
complete = newComplete;
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
logger.warn("Could not assign value {} to field {}", value, field, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this method in your field class and return "this".
|
||||
*/
|
||||
public abstract Object getFieldsOf();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.mapping;
|
||||
|
||||
/**
|
||||
* Color modes supported by the binding.
|
||||
*
|
||||
* @author Aitor Iturrioz - Initial contribution
|
||||
*/
|
||||
public enum ColorMode {
|
||||
HSB,
|
||||
RGB,
|
||||
XYY
|
||||
}
|
||||
@@ -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.mqtt.generic.mapping;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* If the MQTT topic value needs to be transformed first before assigned to a field,
|
||||
* annotate that field with this annotation.
|
||||
*
|
||||
* <p>
|
||||
* Example: Two MQTT topics are "my-example/testname" with value "abc" and "my-example/values" with value "abc,def". The
|
||||
* corresponding attribute class looks like this:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* class MyExample extends AbstractMqttAttributeClass {
|
||||
* enum Testnames {
|
||||
* abc_
|
||||
* };
|
||||
*
|
||||
* @MapToField(suffix = "_")
|
||||
* Testnames testname;
|
||||
*
|
||||
* @MapToField(splitCharacter = ",")
|
||||
* String[] values;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(FIELD)
|
||||
public @interface MQTTvalueTransform {
|
||||
String suffix() default "";
|
||||
|
||||
String prefix() default "";
|
||||
|
||||
String splitCharacter() default "";
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.mapping;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotate an attribute class field to mark it as required. If a required topic value cannot be received
|
||||
* within a given timeframe, the entire attribute classes
|
||||
* {@link AbstractMqttAttributeClass#subscribeAndReceive(org.openhab.core.io.transport.mqtt.MqttBrokerConnection, java.util.concurrent.ScheduledExecutorService, String, org.openhab.binding.mqtt.generic.internal.mapping.AbstractMqttAttributeClass.AttributeChanged, int)}
|
||||
* call will fail.
|
||||
*
|
||||
* <p>
|
||||
* Example: The MQTT topic is "my-example/name". The corresponding attribute class looks like this:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
|
||||
* class MyExample extends AbstractMqttAttributeClass {
|
||||
* * @MandatoryField
|
||||
* String name;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ FIELD })
|
||||
public @interface MandatoryField {
|
||||
boolean value() default true;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.mapping;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.io.transport.mqtt.MqttException;
|
||||
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Use this class to subscribe to a given MQTT topic via a {@link MqttMessageSubscriber}
|
||||
* and convert received values to the type of the given field and notify the user of the changed value.
|
||||
*
|
||||
* Used by {@link AbstractMqttAttributeClass}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SubscribeFieldToMQTTtopic implements MqttMessageSubscriber {
|
||||
private final Logger logger = LoggerFactory.getLogger(SubscribeFieldToMQTTtopic.class);
|
||||
protected CompletableFuture<@Nullable Void> future = new CompletableFuture<>();
|
||||
public final Field field;
|
||||
public final FieldChanged changeConsumer;
|
||||
public final String topic;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private @Nullable ScheduledFuture<?> scheduledFuture;
|
||||
private final boolean mandatory;
|
||||
private boolean receivedValue = false;
|
||||
|
||||
/**
|
||||
* Implement this interface to be notified of an updated field.
|
||||
*/
|
||||
public interface FieldChanged {
|
||||
void fieldChanged(Field field, Object value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link SubscribeFieldToMQTTtopic}.
|
||||
*
|
||||
* @param scheduler A scheduler to realize subscription timeouts.
|
||||
* @param field The destination field.
|
||||
* @param fieldChangeListener A listener for field changes. This is only called if the received value
|
||||
* could successfully be converted to the field type.
|
||||
* @param topic The MQTT topic.
|
||||
* @param mandatory True of this field is a mandatory one. A timeout will cause a future to complete exceptionally.
|
||||
*/
|
||||
public SubscribeFieldToMQTTtopic(ScheduledExecutorService scheduler, Field field, FieldChanged fieldChangeListener,
|
||||
String topic, boolean mandatory) {
|
||||
this.scheduler = scheduler;
|
||||
this.field = field;
|
||||
this.changeConsumer = fieldChangeListener;
|
||||
this.topic = topic;
|
||||
this.mandatory = mandatory;
|
||||
}
|
||||
|
||||
static Object numberConvert(Object value, Class<?> type) throws IllegalArgumentException, NumberFormatException {
|
||||
Object result = value;
|
||||
// Handle the conversion case of BigDecimal to Float,Double,Long,Integer and the respective
|
||||
// primitive types
|
||||
String typeName = type.getSimpleName();
|
||||
if (value instanceof BigDecimal && !type.equals(BigDecimal.class)) {
|
||||
BigDecimal bdValue = (BigDecimal) value;
|
||||
if (type.equals(Float.class) || typeName.equals("float")) {
|
||||
result = bdValue.floatValue();
|
||||
} else if (type.equals(Double.class) || typeName.equals("double")) {
|
||||
result = bdValue.doubleValue();
|
||||
} else if (type.equals(Long.class) || typeName.equals("long")) {
|
||||
result = bdValue.longValue();
|
||||
} else if (type.equals(Integer.class) || typeName.equals("int")) {
|
||||
result = bdValue.intValue();
|
||||
}
|
||||
} else
|
||||
// Handle the conversion case of String to Float,Double,Long,Integer,BigDecimal and the respective
|
||||
// primitive types
|
||||
if (value instanceof String && !type.equals(String.class)) {
|
||||
String bdValue = (String) value;
|
||||
if (type.equals(Float.class) || typeName.equals("float")) {
|
||||
result = Float.valueOf(bdValue);
|
||||
} else if (type.equals(Double.class) || typeName.equals("double")) {
|
||||
result = Double.valueOf(bdValue);
|
||||
} else if (type.equals(Long.class) || typeName.equals("long")) {
|
||||
result = Long.valueOf(bdValue);
|
||||
} else if (type.equals(BigDecimal.class)) {
|
||||
result = new BigDecimal(bdValue);
|
||||
} else if (type.equals(Integer.class) || typeName.equals("int")) {
|
||||
result = Integer.valueOf(bdValue);
|
||||
} else if (type.equals(Boolean.class) || typeName.equals("boolean")) {
|
||||
result = Boolean.valueOf(bdValue);
|
||||
} else if (type.isEnum()) {
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
final Class<? extends Enum> enumType = (Class<? extends Enum>) type;
|
||||
@SuppressWarnings("unchecked")
|
||||
Enum<?> enumValue = Enum.valueOf(enumType, value.toString());
|
||||
result = enumValue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback by the {@link MqttBrokerConnection} if a matching topic received a new value.
|
||||
* Because routing is already done by aforementioned class, the topic parameter is not checked again.
|
||||
*
|
||||
* @param topic The MQTT topic. Not used.
|
||||
* @param payload The MQTT payload.
|
||||
*/
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@Override
|
||||
public void processMessage(String topic, byte[] payload) {
|
||||
final ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
|
||||
if (scheduledFuture != null) { // Cancel timeout
|
||||
scheduledFuture.cancel(false);
|
||||
this.scheduledFuture = null;
|
||||
}
|
||||
|
||||
if (payload.length == 0) {
|
||||
logger.debug("NULL payload on topic: {}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
String valueStr = new String(payload, StandardCharsets.UTF_8);
|
||||
|
||||
// Check if there is a manipulation annotation attached to the field
|
||||
final MQTTvalueTransform transform = field.getAnnotation(MQTTvalueTransform.class);
|
||||
Object value;
|
||||
if (transform != null) {
|
||||
// Add a prefix/suffix to the value
|
||||
valueStr = transform.prefix() + valueStr + transform.suffix();
|
||||
// Split the value if the field is an array. Convert numbers/enums if necessary.
|
||||
value = field.getType().isArray() ? valueStr.split(transform.splitCharacter())
|
||||
: numberConvert(valueStr, field.getType());
|
||||
} else if (field.getType().isArray()) {
|
||||
throw new IllegalArgumentException("No split character defined!");
|
||||
} else {
|
||||
// Convert numbers/enums if necessary
|
||||
value = numberConvert(valueStr, field.getType());
|
||||
}
|
||||
receivedValue = true;
|
||||
changeConsumer.fieldChanged(field, value);
|
||||
future.complete(null);
|
||||
}
|
||||
|
||||
void timeoutReached() {
|
||||
if (mandatory) {
|
||||
future.completeExceptionally(new Exception("Did not receive mandatory topic value: " + topic));
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the MQTT topic. A {@link SubscribeFieldToMQTTtopic} cannot be stopped.
|
||||
* You need to manually unsubscribe from the {@link #topic} before disposing.
|
||||
*
|
||||
* @param connection An MQTT connection.
|
||||
* @param timeout Timeout in milliseconds. The returned future completes after this time even if no message has
|
||||
* been received for the MQTT topic.
|
||||
* @return Returns a future that completes if either a value is received for the topic or a timeout happens.
|
||||
* @throws MqttException If an MQTT IO exception happens this exception is thrown.
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> subscribeAndReceive(MqttBrokerConnection connection, int timeout) {
|
||||
connection.subscribe(topic, this).exceptionally(e -> {
|
||||
logger.debug("Failed to subscribe to topic {}", topic, e);
|
||||
final ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
|
||||
if (scheduledFuture != null) { // Cancel timeout
|
||||
scheduledFuture.cancel(false);
|
||||
this.scheduledFuture = null;
|
||||
}
|
||||
future.complete(null);
|
||||
return false;
|
||||
}).thenRun(() -> {
|
||||
if (!future.isDone()) {
|
||||
this.scheduledFuture = scheduler.schedule(this::timeoutReached, timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the corresponding field has received a value at least once.
|
||||
*/
|
||||
public boolean hasReceivedValue() {
|
||||
return receivedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the corresponding field is mandatory.
|
||||
*/
|
||||
public boolean isMandatory() {
|
||||
return mandatory;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.mapping;
|
||||
|
||||
import static java.lang.annotation.ElementType.*;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotate an attribute class or class fields if the MQTT topic differs compared to the field name by a prefix.
|
||||
* The default prefix if annotated without an argument is "$".
|
||||
*
|
||||
* <p>
|
||||
* Example: The MQTT topic is "my-example/$testname". The corresponding attribute class looks like this:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* @TopicPrefix
|
||||
* class MyExample extends AbstractMqttAttributeClass {
|
||||
* String testname;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ TYPE, FIELD })
|
||||
public @interface TopicPrefix {
|
||||
String value() default "$";
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.tools;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* In some MQTT conventions there are topics dedicated to list further subtopics.
|
||||
* We need to watch those topics and maintain observable lists for those children.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This class consists of a mapping ID->subtopic. Apply an array of subtopics
|
||||
* via the {@link #apply(String[], Function, Function, Consumer)} method.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Restore children from configuration by using {@link #put(String, Object)}.
|
||||
* </p>
|
||||
*
|
||||
* For example in homie 3.x these topics are meant to be watched:
|
||||
*
|
||||
* <pre>
|
||||
* * homie/mydevice/$nodes
|
||||
* * homie/mydevice/mynode/$properties
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* An example value of "homie/mydevice/$nodes" could be "lamp1,lamp2,switch", which means there are
|
||||
* "homie/mydevice/lamp1","homie/mydevice/lamp2" and "homie/mydevice/switch" existing and this map
|
||||
* would contain 3 entries [lamp1->Node, lamp2->Node, switch->Node].
|
||||
* </p>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*
|
||||
* @param <T> Any object
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChildMap<T> {
|
||||
protected Map<String, T> map = new TreeMap<>();
|
||||
|
||||
public Stream<T> stream() {
|
||||
return map.values().stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the map in way that it matches the entries of the given childIDs.
|
||||
*
|
||||
* @param future A future that completes as soon as all children have their added-action performed.
|
||||
* @param childIDs The list of IDs that should be in the map. Everything else currently in the map will be removed.
|
||||
* @param addedAction A function where the newly added child is given as an argument to perform any actions on it.
|
||||
* A future is expected as a return value that completes as soon as said action is performed.
|
||||
* @param supplyNewChild A function where the ID of a new child is given and the created child is
|
||||
* expected as a
|
||||
* result.
|
||||
* @param removedCallback A callback, that is called whenever a child got removed by the
|
||||
* {@link #apply(CompletableFuture, String[], Function)} method.
|
||||
* @return Complete successfully if all "addedAction" complete successfully, otherwise complete exceptionally.
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> apply(String[] childIDs,
|
||||
final Function<T, CompletableFuture<Void>> addedAction, final Function<String, T> supplyNewChild,
|
||||
final Consumer<T> removedCallback) {
|
||||
Set<String> arrayValues = Stream.of(childIDs).collect(Collectors.toSet());
|
||||
|
||||
// Add all entries to the map, that are not in there yet.
|
||||
final Map<String, T> newSubnodes = arrayValues.stream().filter(entry -> !this.map.containsKey(entry))
|
||||
.collect(Collectors.toMap(k -> k, supplyNewChild));
|
||||
this.map.putAll(newSubnodes);
|
||||
|
||||
// Remove any entries that are not listed in the 'childIDs'.
|
||||
this.map.entrySet().removeIf(entry -> {
|
||||
if (!arrayValues.contains(entry.getKey())) {
|
||||
removedCallback.accept(entry.getValue());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Apply the 'addedAction' function for all new entries.
|
||||
return CompletableFuture
|
||||
.allOf(newSubnodes.values().stream().map(addedAction).toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size of this map.
|
||||
*/
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item with the given id
|
||||
*
|
||||
* @param key The id
|
||||
* @return The item
|
||||
*/
|
||||
public T get(@Nullable String key) {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the map
|
||||
*/
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method only to restore a child from configuration.
|
||||
*
|
||||
* @param key The ID
|
||||
* @param value The subnode object
|
||||
*/
|
||||
public void put(String key, T value) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.tools;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Collects objects over time until a specified delay passed by.
|
||||
* Then call the user back with a list of accumulated objects and start over again.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*
|
||||
* @param <T> Any object
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DelayedBatchProcessing<T> implements Consumer<T> {
|
||||
private final int delay;
|
||||
private final Consumer<List<T>> consumer;
|
||||
private final List<T> queue = Collections.synchronizedList(new ArrayList<>());
|
||||
private final ScheduledExecutorService executor;
|
||||
protected final AtomicReference<@Nullable ScheduledFuture<?>> futureRef = new AtomicReference<>();
|
||||
|
||||
/**
|
||||
* Creates a {@link DelayedBatchProcessing}.
|
||||
*
|
||||
* @param delay A delay in milliseconds
|
||||
* @param consumer A consumer of the list of collected objects
|
||||
* @param executor A scheduled executor service
|
||||
*/
|
||||
public DelayedBatchProcessing(int delay, Consumer<List<T>> consumer, ScheduledExecutorService executor) {
|
||||
this.delay = delay;
|
||||
this.consumer = consumer;
|
||||
this.executor = executor;
|
||||
if (delay <= 0) {
|
||||
throw new IllegalArgumentException("Delay need to be greater than 0!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new object to the batch process list. Every time a new object is received,
|
||||
* the delay timer is rescheduled.
|
||||
*
|
||||
* @param t An object
|
||||
*/
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
queue.add(t);
|
||||
cancel(futureRef.getAndSet(executor.schedule(this::run, delay, TimeUnit.MILLISECONDS)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the so far accumulated objects, but do not deliver them to the target consumer anymore.
|
||||
*
|
||||
* @return A list of accumulated objects
|
||||
*/
|
||||
public List<T> join() {
|
||||
cancel(futureRef.getAndSet(null));
|
||||
List<T> lqueue = new ArrayList<>();
|
||||
synchronized (queue) {
|
||||
lqueue.addAll(queue);
|
||||
queue.clear();
|
||||
}
|
||||
return lqueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if there is a delayed processing going on.
|
||||
*/
|
||||
public boolean isArmed() {
|
||||
ScheduledFuture<?> scheduledFuture = this.futureRef.get();
|
||||
return scheduledFuture != null && !scheduledFuture.isDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver queued items now to the target consumer.
|
||||
*/
|
||||
public void forceProcessNow() {
|
||||
cancel(futureRef.getAndSet(null));
|
||||
run();
|
||||
}
|
||||
|
||||
private void run() {
|
||||
List<T> lqueue = new ArrayList<>();
|
||||
synchronized (queue) {
|
||||
lqueue.addAll(queue);
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
if (!lqueue.isEmpty()) {
|
||||
consumer.accept(lqueue);
|
||||
}
|
||||
}
|
||||
|
||||
private static void cancel(@Nullable ScheduledFuture<?> future) {
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
|
||||
/**
|
||||
* JsonReader delegate.
|
||||
*
|
||||
* This class allows to overwrite parts of the {@link JsonReader} functionality
|
||||
*
|
||||
* @author Jochen Klein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonReaderDelegate extends JsonReader {
|
||||
|
||||
/**
|
||||
* Retrieve the 'original' {@link JsonReader} after removing all {@link JsonReaderDelegate}s
|
||||
*
|
||||
* @param in the current {@link JsonReader}
|
||||
* @return the original {@link JsonReader} after removing all {@link JsonReaderDelegate}s
|
||||
*/
|
||||
public static JsonReader getDelegate(final JsonReader in) {
|
||||
JsonReader current = in;
|
||||
while (current instanceof JsonReaderDelegate) {
|
||||
current = ((JsonReaderDelegate) current).delegate;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private final JsonReader delegate;
|
||||
|
||||
public JsonReaderDelegate(JsonReader delegate) {
|
||||
/* super class demands a Reader. This will never be used as all requests are forwarded to the delegate */
|
||||
super(new StringReader(""));
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginArray() throws IOException {
|
||||
delegate.beginArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginObject() throws IOException {
|
||||
delegate.beginObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endArray() throws IOException {
|
||||
delegate.endArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endObject() throws IOException {
|
||||
delegate.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return delegate.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return delegate.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() throws IOException {
|
||||
return delegate.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean nextBoolean() throws IOException {
|
||||
return delegate.nextBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double nextDouble() throws IOException {
|
||||
return delegate.nextDouble();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextInt() throws IOException {
|
||||
return delegate.nextInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextLong() throws IOException {
|
||||
return delegate.nextLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextName() throws IOException {
|
||||
return delegate.nextName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextNull() throws IOException {
|
||||
delegate.nextNull();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextString() throws IOException {
|
||||
return delegate.nextString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonToken peek() throws IOException {
|
||||
return delegate.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipValue() throws IOException {
|
||||
delegate.skipValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.tools;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
|
||||
|
||||
/**
|
||||
* Waits for a topic value to appear on a MQTT topic. One-time usable only per instance.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WaitForTopicValue {
|
||||
private final CompletableFuture<String> composeFuture;
|
||||
|
||||
/**
|
||||
* Creates an a instance.
|
||||
*
|
||||
* @param connection A broker connection.
|
||||
* @param topic The topic
|
||||
*/
|
||||
public WaitForTopicValue(MqttBrokerConnection connection, String topic) {
|
||||
final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
final MqttMessageSubscriber mqttMessageSubscriber = (t, payload) -> {
|
||||
future.complete(new String(payload, StandardCharsets.UTF_8));
|
||||
};
|
||||
future.whenComplete((r, e) -> {
|
||||
connection.unsubscribe(topic, mqttMessageSubscriber);
|
||||
});
|
||||
|
||||
composeFuture = connection.subscribe(topic, mqttMessageSubscriber).thenCompose(b -> future);
|
||||
}
|
||||
|
||||
/**
|
||||
* Free any resources
|
||||
*/
|
||||
public void stop() {
|
||||
composeFuture.completeExceptionally(new Exception("Stopped"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the value to appear on the MQTT broker.
|
||||
*
|
||||
* @param timeoutInMS Maximum time in milliseconds to wait for the value.
|
||||
* @return Return the value or null if timed out.
|
||||
*/
|
||||
public @Nullable String waitForTopicValue(int timeoutInMS) {
|
||||
try {
|
||||
return composeFuture.get(timeoutInMS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void timeout() {
|
||||
if (!composeFuture.isDone()) {
|
||||
composeFuture.completeExceptionally(new TimeoutException());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a future that completes successfully with a topic value or fails exceptionally with a timeout exception.
|
||||
*
|
||||
* @param scheduler A scheduler for the timeout
|
||||
* @param timeoutInMS The timeout in milliseconds
|
||||
* @return The future
|
||||
*/
|
||||
public CompletableFuture<String> waitForTopicValueAsync(ScheduledExecutorService scheduler, int timeoutInMS) {
|
||||
scheduler.schedule(this::timeout, timeoutInMS, TimeUnit.MILLISECONDS);
|
||||
return composeFuture;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.utils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
/**
|
||||
* Collector to combine a stream of CompletableFutures.
|
||||
*
|
||||
* @author Jochen Klein - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class FutureCollector {
|
||||
|
||||
public static <X> Collector<CompletableFuture<X>, Set<CompletableFuture<X>>, CompletableFuture<Void>> allOf() {
|
||||
return Collector.<CompletableFuture<X>, Set<CompletableFuture<X>>, CompletableFuture<Void>> of(
|
||||
(Supplier<Set<CompletableFuture<X>>>) HashSet::new, Set::add, (left, right) -> {
|
||||
left.addAll(right);
|
||||
return left;
|
||||
}, a -> {
|
||||
return CompletableFuture.allOf(a.toArray(new CompletableFuture[a.size()]));
|
||||
}, Collector.Characteristics.UNORDERED);
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.ws.rs.NotSupportedException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Implements a color value.
|
||||
*
|
||||
* <p>
|
||||
* Accepts user updates from a HSBType, OnOffType and StringType.
|
||||
* </p>
|
||||
* Accepts MQTT state updates as OnOffType and a
|
||||
* StringType with comma separated HSB ("h,s,b"), RGB ("r,g,b"), CIE xyY ("x,y,Y") and on, off strings.
|
||||
* On, Off strings can be customized.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
* @author Aitor Iturrioz - Add CIE xyY colors support
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ColorValue extends Value {
|
||||
private final Logger logger = LoggerFactory.getLogger(ColorValue.class);
|
||||
|
||||
private final ColorMode colorMode;
|
||||
private final String onValue;
|
||||
private final String offValue;
|
||||
private final int onBrightness;
|
||||
|
||||
/**
|
||||
* Creates a non initialized color value.
|
||||
*
|
||||
* @param colorMode The color mode: HSB, RGB or XYY.
|
||||
* @param onValue The ON value string. This will be compared to MQTT messages.
|
||||
* @param offValue The OFF value string. This will be compared to MQTT messages.
|
||||
* @param onBrightness When receiving a ON command, the brightness percentage is set to this value
|
||||
*/
|
||||
public ColorValue(ColorMode colorMode, @Nullable String onValue, @Nullable String offValue, int onBrightness) {
|
||||
super(CoreItemFactory.COLOR,
|
||||
Stream.of(OnOffType.class, PercentType.class, StringType.class).collect(Collectors.toList()));
|
||||
|
||||
if (onBrightness > 100) {
|
||||
throw new IllegalArgumentException("Brightness parameter must be <= 100");
|
||||
}
|
||||
|
||||
this.colorMode = colorMode;
|
||||
this.onValue = onValue == null ? "ON" : onValue;
|
||||
this.offValue = offValue == null ? "OFF" : offValue;
|
||||
this.onBrightness = onBrightness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the color state.
|
||||
*/
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
HSBType oldvalue = (state == UnDefType.UNDEF) ? new HSBType() : (HSBType) state;
|
||||
if (command instanceof HSBType) {
|
||||
state = (HSBType) command;
|
||||
} else if (command instanceof OnOffType) {
|
||||
OnOffType boolValue = ((OnOffType) command);
|
||||
PercentType minOn = new PercentType(Math.max(oldvalue.getBrightness().intValue(), onBrightness));
|
||||
state = new HSBType(oldvalue.getHue(), oldvalue.getSaturation(),
|
||||
boolValue == OnOffType.ON ? minOn : new PercentType(0));
|
||||
} else if (command instanceof PercentType) {
|
||||
state = new HSBType(oldvalue.getHue(), oldvalue.getSaturation(), (PercentType) command);
|
||||
} else {
|
||||
final String updatedValue = command.toString();
|
||||
if (onValue.equals(updatedValue)) {
|
||||
PercentType minOn = new PercentType(Math.max(oldvalue.getBrightness().intValue(), onBrightness));
|
||||
state = new HSBType(oldvalue.getHue(), oldvalue.getSaturation(), minOn);
|
||||
} else if (offValue.equals(updatedValue)) {
|
||||
state = new HSBType(oldvalue.getHue(), oldvalue.getSaturation(), new PercentType(0));
|
||||
} else {
|
||||
String[] split = updatedValue.split(",");
|
||||
if (split.length != 3) {
|
||||
throw new IllegalArgumentException(updatedValue + " is not a valid string syntax");
|
||||
}
|
||||
switch (this.colorMode) {
|
||||
case HSB:
|
||||
state = new HSBType(updatedValue);
|
||||
break;
|
||||
case RGB:
|
||||
state = HSBType.fromRGB(Integer.parseInt(split[0]), Integer.parseInt(split[1]),
|
||||
Integer.parseInt(split[2]));
|
||||
break;
|
||||
case XYY:
|
||||
HSBType temp_state = HSBType.fromXY(Float.parseFloat(split[0]), Float.parseFloat(split[1]));
|
||||
state = new HSBType(temp_state.getHue(), temp_state.getSaturation(), new PercentType(split[2]));
|
||||
break;
|
||||
default:
|
||||
logger.warn("Non supported color mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal factor = new BigDecimal(2.5);
|
||||
|
||||
/**
|
||||
* Converts the color state to a string.
|
||||
*
|
||||
* @return Returns the color value depending on the color mode: as a HSB/HSV string (hue,saturation,brightness ->
|
||||
* "60,100,100"), as an RGB string (red,green,blue -> "255,255,0") or as a xyY string
|
||||
* ("0.419321,0.505255,100.00").
|
||||
*/
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
if (state == UnDefType.UNDEF) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String formatPattern = pattern;
|
||||
if (formatPattern == null || "%s".equals(formatPattern)) {
|
||||
if (this.colorMode == ColorMode.XYY) {
|
||||
formatPattern = "%1$f,%2$f,%3$.2f";
|
||||
} else {
|
||||
formatPattern = "%1$d,%2$d,%3$d";
|
||||
}
|
||||
}
|
||||
|
||||
HSBType hsb_state = (HSBType) state;
|
||||
|
||||
switch (this.colorMode) {
|
||||
case HSB:
|
||||
return String.format(formatPattern, hsb_state.getHue().intValue(), hsb_state.getSaturation().intValue(),
|
||||
hsb_state.getBrightness().intValue());
|
||||
case RGB:
|
||||
PercentType[] rgb = hsb_state.toRGB();
|
||||
return String.format(formatPattern, rgb[0].toBigDecimal().multiply(factor).intValue(),
|
||||
rgb[1].toBigDecimal().multiply(factor).intValue(),
|
||||
rgb[2].toBigDecimal().multiply(factor).intValue());
|
||||
case XYY:
|
||||
PercentType[] xyY = hsb_state.toXY();
|
||||
return String.format(Locale.ROOT, formatPattern, xyY[0].floatValue() / 100.0f,
|
||||
xyY[1].floatValue() / 100.0f, hsb_state.getBrightness().floatValue());
|
||||
default:
|
||||
throw new NotSupportedException(String.format("Non supported color mode: {}", this.colorMode));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Implements a datetime value.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DateTimeValue extends Value {
|
||||
public DateTimeValue() {
|
||||
super(CoreItemFactory.DATETIME, Stream.of(DateTimeType.class, StringType.class).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
if (command instanceof DateTimeType) {
|
||||
state = ((DateTimeType) command);
|
||||
} else {
|
||||
state = DateTimeType.valueOf(command.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
if (state == UnDefType.UNDEF) {
|
||||
return "";
|
||||
}
|
||||
String formatPattern = pattern;
|
||||
if (formatPattern == null || "%s".contentEquals(formatPattern)) {
|
||||
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(((DateTimeType) state).getZonedDateTime());
|
||||
}
|
||||
return String.format(formatPattern, ((DateTimeType) state).getZonedDateTime());
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Implements an image value.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ImageValue extends Value {
|
||||
public ImageValue() {
|
||||
super(CoreItemFactory.IMAGE, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
throw new IllegalArgumentException("Binary type. Command not allowed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBinary() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Implements a location value.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LocationValue extends Value {
|
||||
public LocationValue() {
|
||||
super(CoreItemFactory.LOCATION, Stream.of(PointType.class, StringType.class).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getMQTTpublishValue(@Nullable String pattern) {
|
||||
String formatPattern = pattern;
|
||||
PointType point = ((PointType) state);
|
||||
|
||||
if (formatPattern == null || "%s".equals(formatPattern)) {
|
||||
if (point.getAltitude().toBigDecimal().equals(BigDecimal.ZERO)) {
|
||||
formatPattern = "%2$f,%3$f";
|
||||
} else {
|
||||
formatPattern = "%2$f,%3$f,%1$f";
|
||||
}
|
||||
}
|
||||
return String.format(Locale.ROOT, formatPattern, point.getAltitude().toBigDecimal(),
|
||||
point.getLatitude().toBigDecimal(), point.getLongitude().toBigDecimal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
if (command instanceof PointType) {
|
||||
state = ((PointType) command);
|
||||
} else {
|
||||
state = PointType.valueOf(command.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.values;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import tec.uom.se.AbstractUnit;
|
||||
|
||||
/**
|
||||
* Implements a number value.
|
||||
*
|
||||
* If min / max limits are set, values below / above are (almost) silently ignored.
|
||||
*
|
||||
* <p>
|
||||
* Accepts user updates and MQTT state updates from a DecimalType, IncreaseDecreaseType and UpDownType.
|
||||
* </p>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NumberValue extends Value {
|
||||
private final Logger logger = LoggerFactory.getLogger(NumberValue.class);
|
||||
private final @Nullable BigDecimal min;
|
||||
private final @Nullable BigDecimal max;
|
||||
private final BigDecimal step;
|
||||
private final String unit;
|
||||
|
||||
public NumberValue(@Nullable BigDecimal min, @Nullable BigDecimal max, @Nullable BigDecimal step,
|
||||
@Nullable String unit) {
|
||||
super(CoreItemFactory.NUMBER, Stream.of(QuantityType.class, IncreaseDecreaseType.class, UpDownType.class)
|
||||
.collect(Collectors.toList()));
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.step = step == null ? BigDecimal.ONE : step;
|
||||
this.unit = unit == null ? "" : unit;
|
||||
}
|
||||
|
||||
protected boolean checkConditions(BigDecimal newValue, DecimalType oldvalue) {
|
||||
if (min != null && newValue.compareTo(min) == -1) {
|
||||
logger.trace("Number not accepted as it is below the configured minimum");
|
||||
return false;
|
||||
}
|
||||
if (max != null && newValue.compareTo(max) == 1) {
|
||||
logger.trace("Number not accepted as it is above the configured maximum");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
if (state == UnDefType.UNDEF) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String formatPattern = pattern;
|
||||
if (formatPattern == null) {
|
||||
formatPattern = "%s";
|
||||
}
|
||||
|
||||
return state.format(formatPattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
DecimalType oldvalue = (state == UnDefType.UNDEF) ? new DecimalType() : (DecimalType) state;
|
||||
BigDecimal newValue = null;
|
||||
if (command instanceof DecimalType) {
|
||||
if (!checkConditions(((DecimalType) command).toBigDecimal(), oldvalue)) {
|
||||
return;
|
||||
}
|
||||
state = (DecimalType) command;
|
||||
} else if (command instanceof IncreaseDecreaseType || command instanceof UpDownType) {
|
||||
if (command == IncreaseDecreaseType.INCREASE || command == UpDownType.UP) {
|
||||
newValue = oldvalue.toBigDecimal().add(step);
|
||||
} else {
|
||||
newValue = oldvalue.toBigDecimal().subtract(step);
|
||||
}
|
||||
if (!checkConditions(newValue, oldvalue)) {
|
||||
return;
|
||||
}
|
||||
state = new DecimalType(newValue);
|
||||
} else if (command instanceof QuantityType<?>) {
|
||||
QuantityType<?> qType = (QuantityType<?>) command;
|
||||
|
||||
if (qType.getUnit().isCompatible(AbstractUnit.ONE)) {
|
||||
newValue = qType.toBigDecimal();
|
||||
} else {
|
||||
qType = qType.toUnit(unit);
|
||||
if (qType != null) {
|
||||
newValue = qType.toBigDecimal();
|
||||
}
|
||||
}
|
||||
if (newValue != null) {
|
||||
if (!checkConditions(newValue, oldvalue)) {
|
||||
return;
|
||||
}
|
||||
state = new DecimalType(newValue);
|
||||
}
|
||||
} else {
|
||||
newValue = new BigDecimal(command.toString());
|
||||
if (!checkConditions(newValue, oldvalue)) {
|
||||
return;
|
||||
}
|
||||
state = new DecimalType(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
|
||||
StateDescriptionFragmentBuilder builder = super.createStateDescription(readOnly);
|
||||
BigDecimal max = this.max;
|
||||
if (max != null) {
|
||||
builder = builder.withMaximum(max);
|
||||
}
|
||||
BigDecimal min = this.min;
|
||||
if (min != null) {
|
||||
builder = builder.withMinimum(min);
|
||||
}
|
||||
builder = builder.withStep(step);
|
||||
if (this.unit.length() > 0) {
|
||||
builder = builder.withPattern("%s " + this.unit.replace("%", "%%"));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.values;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.CommandDescriptionBuilder;
|
||||
import org.openhab.core.types.CommandOption;
|
||||
|
||||
/**
|
||||
* Implements an on/off boolean value.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OnOffValue extends Value {
|
||||
private final String onState;
|
||||
private final String offState;
|
||||
private final String onCommand;
|
||||
private final String offCommand;
|
||||
|
||||
/**
|
||||
* Creates a switch On/Off type, that accepts "ON", "1" for on and "OFF","0" for off.
|
||||
*/
|
||||
public OnOffValue() {
|
||||
this(OnOffType.ON.name(), OnOffType.OFF.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SWITCH On/Off value.
|
||||
*
|
||||
* values send in messages will be the same as those expected in incomming messages
|
||||
*
|
||||
* @param onValue The ON value string. This will be compared to MQTT messages.
|
||||
* @param offValue The OFF value string. This will be compared to MQTT messages.
|
||||
*/
|
||||
public OnOffValue(@Nullable String onValue, @Nullable String offValue) {
|
||||
this(onValue, offValue, onValue, offValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SWITCH On/Off value.
|
||||
*
|
||||
* @param onState The ON value string. This will be compared to MQTT messages.
|
||||
* @param offState The OFF value string. This will be compared to MQTT messages.
|
||||
* @param onCommand The ON value string. This will be send in MQTT messages.
|
||||
* @param offCommand The OFF value string. This will be send in MQTT messages.
|
||||
*/
|
||||
public OnOffValue(@Nullable String onState, @Nullable String offState, @Nullable String onCommand,
|
||||
@Nullable String offCommand) {
|
||||
super(CoreItemFactory.SWITCH, Stream.of(OnOffType.class, StringType.class).collect(Collectors.toList()));
|
||||
this.onState = onState == null ? OnOffType.ON.name() : onState;
|
||||
this.offState = offState == null ? OnOffType.OFF.name() : offState;
|
||||
this.onCommand = onCommand == null ? OnOffType.ON.name() : onCommand;
|
||||
this.offCommand = offCommand == null ? OnOffType.OFF.name() : offCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
if (command instanceof OnOffType) {
|
||||
state = (OnOffType) command;
|
||||
} else {
|
||||
final String updatedValue = command.toString();
|
||||
if (onState.equals(updatedValue)) {
|
||||
state = OnOffType.ON;
|
||||
} else if (offState.equals(updatedValue)) {
|
||||
state = OnOffType.OFF;
|
||||
} else {
|
||||
state = OnOffType.valueOf(updatedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
String formatPattern = pattern;
|
||||
if (formatPattern == null) {
|
||||
formatPattern = "%s";
|
||||
}
|
||||
|
||||
return String.format(formatPattern, state == OnOffType.ON ? onCommand : offCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandDescriptionBuilder createCommandDescription() {
|
||||
CommandDescriptionBuilder builder = super.createCommandDescription();
|
||||
builder = builder.withCommandOption(new CommandOption(onCommand, onCommand));
|
||||
builder = builder.withCommandOption(new CommandOption(offCommand, offCommand));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Implements an open/close boolean value.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OpenCloseValue extends Value {
|
||||
private final String openString;
|
||||
private final String closeString;
|
||||
|
||||
/**
|
||||
* Creates a contact Open/Close type.
|
||||
*/
|
||||
public OpenCloseValue() {
|
||||
super(CoreItemFactory.CONTACT, Stream.of(OpenClosedType.class, StringType.class).collect(Collectors.toList()));
|
||||
this.openString = OpenClosedType.OPEN.name();
|
||||
this.closeString = OpenClosedType.CLOSED.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new contact Open/Close value.
|
||||
*
|
||||
* @param openValue The ON value string. This will be compared to MQTT messages.
|
||||
* @param closeValue The OFF value string. This will be compared to MQTT messages.
|
||||
*/
|
||||
public OpenCloseValue(@Nullable String openValue, @Nullable String closeValue) {
|
||||
super(CoreItemFactory.CONTACT, Stream.of(OpenClosedType.class, StringType.class).collect(Collectors.toList()));
|
||||
this.openString = openValue == null ? OpenClosedType.OPEN.name() : openValue;
|
||||
this.closeString = closeValue == null ? OpenClosedType.CLOSED.name() : closeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
if (command instanceof OpenClosedType) {
|
||||
state = (OpenClosedType) command;
|
||||
} else {
|
||||
final String updatedValue = command.toString();
|
||||
if (openString.equals(updatedValue)) {
|
||||
state = OpenClosedType.OPEN;
|
||||
} else if (closeString.equals(updatedValue)) {
|
||||
state = OpenClosedType.CLOSED;
|
||||
} else {
|
||||
state = OpenClosedType.valueOf(updatedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
String formatPattern = pattern;
|
||||
if (formatPattern == null) {
|
||||
formatPattern = "%s";
|
||||
}
|
||||
|
||||
return String.format(formatPattern, state == OpenClosedType.OPEN ? openString : closeString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.values;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
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.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import tec.uom.se.unit.Units;
|
||||
|
||||
/**
|
||||
* Implements a percentage value. Minimum and maximum are definable.
|
||||
*
|
||||
* <p>
|
||||
* Accepts user updates from a DecimalType, IncreaseDecreaseType and UpDownType.
|
||||
* If this is a percent value, PercentType
|
||||
* </p>
|
||||
* Accepts MQTT state updates as DecimalType, IncreaseDecreaseType and UpDownType
|
||||
* StringType with comma separated HSB ("h,s,b"), RGB ("r,g,b") and on, off strings.
|
||||
* On, Off strings can be customized.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PercentageValue extends Value {
|
||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||
private final BigDecimal min;
|
||||
private final BigDecimal max;
|
||||
private final BigDecimal span;
|
||||
private final BigDecimal step;
|
||||
private final BigDecimal stepPercent;
|
||||
private final @Nullable String onValue;
|
||||
private final @Nullable String offValue;
|
||||
|
||||
public PercentageValue(@Nullable BigDecimal min, @Nullable BigDecimal max, @Nullable BigDecimal step,
|
||||
@Nullable String onValue, @Nullable String offValue) {
|
||||
super(CoreItemFactory.DIMMER, Stream.of(DecimalType.class, QuantityType.class, IncreaseDecreaseType.class,
|
||||
OnOffType.class, UpDownType.class, StringType.class).collect(Collectors.toList()));
|
||||
this.onValue = onValue;
|
||||
this.offValue = offValue;
|
||||
this.min = min == null ? BigDecimal.ZERO : min;
|
||||
this.max = max == null ? HUNDRED : max;
|
||||
if (this.min.compareTo(this.max) >= 0) {
|
||||
throw new IllegalArgumentException("Min need to be smaller than max!");
|
||||
}
|
||||
this.span = this.max.subtract(this.min);
|
||||
this.step = step == null ? BigDecimal.ONE : step;
|
||||
this.stepPercent = this.step.multiply(HUNDRED).divide(this.span, MathContext.DECIMAL128);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
PercentType oldvalue = (state == UnDefType.UNDEF) ? new PercentType() : (PercentType) state;
|
||||
// Nothing do to -> We have received a percentage
|
||||
if (command instanceof PercentType) {
|
||||
state = (PercentType) command;
|
||||
} else //
|
||||
// A decimal type need to be converted according to the current min/max values
|
||||
if (command instanceof DecimalType) {
|
||||
BigDecimal v = ((DecimalType) command).toBigDecimal();
|
||||
v = v.subtract(min).multiply(HUNDRED).divide(max.subtract(min), MathContext.DECIMAL128);
|
||||
state = new PercentType(v);
|
||||
} else //
|
||||
// A quantity type need to be converted according to the current min/max values
|
||||
if (command instanceof QuantityType) {
|
||||
QuantityType<?> qty = ((QuantityType<?>) command).toUnit(Units.PERCENT);
|
||||
if (qty != null) {
|
||||
BigDecimal v = qty.toBigDecimal();
|
||||
v = v.subtract(min).multiply(HUNDRED).divide(max.subtract(min), MathContext.DECIMAL128);
|
||||
state = new PercentType(v);
|
||||
}
|
||||
} else //
|
||||
// Increase or decrease by "step"
|
||||
if (command instanceof IncreaseDecreaseType) {
|
||||
if (((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE) {
|
||||
final BigDecimal v = oldvalue.toBigDecimal().add(stepPercent);
|
||||
state = v.compareTo(HUNDRED) <= 0 ? new PercentType(v) : PercentType.HUNDRED;
|
||||
} else {
|
||||
final BigDecimal v = oldvalue.toBigDecimal().subtract(stepPercent);
|
||||
state = v.compareTo(BigDecimal.ZERO) >= 0 ? new PercentType(v) : PercentType.ZERO;
|
||||
}
|
||||
} else //
|
||||
// On/Off equals 100 or 0 percent
|
||||
if (command instanceof OnOffType) {
|
||||
state = ((OnOffType) command) == OnOffType.ON ? PercentType.HUNDRED : PercentType.ZERO;
|
||||
} else//
|
||||
// Increase or decrease by "step"
|
||||
if (command instanceof UpDownType) {
|
||||
if (((UpDownType) command) == UpDownType.UP) {
|
||||
final BigDecimal v = oldvalue.toBigDecimal().add(stepPercent);
|
||||
state = v.compareTo(HUNDRED) <= 0 ? new PercentType(v) : PercentType.HUNDRED;
|
||||
} else {
|
||||
final BigDecimal v = oldvalue.toBigDecimal().subtract(stepPercent);
|
||||
state = v.compareTo(BigDecimal.ZERO) >= 0 ? new PercentType(v) : PercentType.ZERO;
|
||||
}
|
||||
} else //
|
||||
// Check against custom on/off values
|
||||
if (command instanceof StringType) {
|
||||
if (onValue != null && command.toString().equals(onValue)) {
|
||||
state = new PercentType(max);
|
||||
} else if (offValue != null && command.toString().equals(offValue)) {
|
||||
state = new PercentType(min);
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown String!");
|
||||
}
|
||||
} else {
|
||||
// We are desperate -> Try to parse the command as number value
|
||||
state = PercentType.valueOf(command.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
if (state == UnDefType.UNDEF) {
|
||||
return "";
|
||||
}
|
||||
// Formula: From percentage to custom min/max: value*span/100+min
|
||||
// Calculation need to happen with big decimals to either return a straight integer or a decimal depending on
|
||||
// the value.
|
||||
BigDecimal value = ((PercentType) state).toBigDecimal().multiply(span).divide(HUNDRED, MathContext.DECIMAL128)
|
||||
.add(min).stripTrailingZeros();
|
||||
|
||||
String formatPattern = pattern;
|
||||
if (formatPattern == null) {
|
||||
formatPattern = "%s";
|
||||
}
|
||||
|
||||
return new DecimalType(value).format(formatPattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
|
||||
return super.createStateDescription(readOnly).withMaximum(max).withMinimum(min).withStep(step)
|
||||
.withPattern("%s %%");
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Implements an rollershutter value.
|
||||
* <p>
|
||||
* The stop, up and down strings have multiple purposes.
|
||||
* For one if those strings are received via MQTT they are recognised as corresponding commands
|
||||
* and also posted as Commands to the framework.
|
||||
* And if a user commands an Item->Channel to perform Stop the corresponding string is send. For Up,Down
|
||||
* the percentage 0 and 100 is send.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RollershutterValue extends Value {
|
||||
private final @Nullable String upString;
|
||||
private final @Nullable String downString;
|
||||
private final String stopString;
|
||||
private boolean nextIsStop = false; // If set: getMQTTpublishValue will return the stop string
|
||||
|
||||
/**
|
||||
* Creates a new rollershutter value.
|
||||
*
|
||||
* @param upString The UP value string. This will be compared to MQTT messages.
|
||||
* @param downString The DOWN value string. This will be compared to MQTT messages.
|
||||
* @param stopString The STOP value string. This will be compared to MQTT messages.
|
||||
*/
|
||||
public RollershutterValue(@Nullable String upString, @Nullable String downString, @Nullable String stopString) {
|
||||
super(CoreItemFactory.ROLLERSHUTTER,
|
||||
Stream.of(UpDownType.class, StopMoveType.class, PercentType.class, StringType.class)
|
||||
.collect(Collectors.toList()));
|
||||
this.upString = upString;
|
||||
this.downString = downString;
|
||||
this.stopString = stopString == null ? StopMoveType.STOP.name() : stopString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
nextIsStop = false;
|
||||
if (command instanceof StopMoveType) {
|
||||
nextIsStop = (((StopMoveType) command) == StopMoveType.STOP);
|
||||
return;
|
||||
} else if (command instanceof UpDownType) {
|
||||
state = ((UpDownType) command) == UpDownType.UP ? PercentType.ZERO : PercentType.HUNDRED;
|
||||
return;
|
||||
} else if (command instanceof PercentType) {
|
||||
state = (PercentType) command;
|
||||
return;
|
||||
} else if (command instanceof StringType) {
|
||||
final String updatedValue = command.toString();
|
||||
if (updatedValue.equals(upString)) {
|
||||
state = PercentType.ZERO;
|
||||
return;
|
||||
} else if (updatedValue.equals(downString)) {
|
||||
state = PercentType.HUNDRED;
|
||||
return;
|
||||
} else if (updatedValue.equals(stopString)) {
|
||||
nextIsStop = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Cannot call update() with " + command.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* The stop command will not update the internal state and is posted to the framework.
|
||||
* <p>
|
||||
* The Up/Down commands (100%/0%) are not updating the state directly and are also
|
||||
* posted as percent value to the framework. It is up to the user if the posted values
|
||||
* are applied to the item state immediately (autoupdate=true) or not.
|
||||
*/
|
||||
@Override
|
||||
public @Nullable Command isPostOnly(Command command) {
|
||||
if (command instanceof UpDownType) {
|
||||
return command;
|
||||
} else if (command instanceof StopMoveType) {
|
||||
return command;
|
||||
} else if (command instanceof StringType) {
|
||||
final String updatedValue = command.toString();
|
||||
if (updatedValue.equals(upString)) {
|
||||
return UpDownType.UP.as(PercentType.class);
|
||||
} else if (updatedValue.equals(downString)) {
|
||||
return UpDownType.DOWN.as(PercentType.class);
|
||||
} else if (updatedValue.equals(stopString)) {
|
||||
return StopMoveType.STOP;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
final String upString = this.upString;
|
||||
final String downString = this.downString;
|
||||
if (this.nextIsStop) {
|
||||
this.nextIsStop = false;
|
||||
return stopString;
|
||||
} else if (state instanceof PercentType) {
|
||||
if (state.equals(PercentType.HUNDRED) && downString != null) {
|
||||
return downString;
|
||||
} else if (state.equals(PercentType.ZERO) && upString != null) {
|
||||
return upString;
|
||||
} else {
|
||||
return String.valueOf(((PercentType) state).intValue());
|
||||
}
|
||||
} else {
|
||||
return "UNDEF";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.CommandDescriptionBuilder;
|
||||
import org.openhab.core.types.CommandOption;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
/**
|
||||
* Implements a text/string value. Allows to restrict the incoming value to a set of states.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TextValue extends Value {
|
||||
private final @Nullable Set<String> states;
|
||||
|
||||
/**
|
||||
* Create a string value with a limited number of allowed states.
|
||||
*
|
||||
* @param states Allowed states. Empty states are filtered out. If the resulting set is empty, all string values
|
||||
* will be allowed.
|
||||
*/
|
||||
public TextValue(String[] states) {
|
||||
super(CoreItemFactory.STRING, Collections.singletonList(StringType.class));
|
||||
Set<String> s = Stream.of(states).filter(e -> StringUtils.isNotBlank(e)).collect(Collectors.toSet());
|
||||
if (!s.isEmpty()) {
|
||||
this.states = s;
|
||||
} else {
|
||||
this.states = null;
|
||||
}
|
||||
}
|
||||
|
||||
public TextValue() {
|
||||
super(CoreItemFactory.STRING, Collections.singletonList(StringType.class));
|
||||
this.states = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Command command) throws IllegalArgumentException {
|
||||
final Set<String> states = this.states;
|
||||
String valueStr = command.toString();
|
||||
if (states != null && !states.contains(valueStr)) {
|
||||
throw new IllegalArgumentException("Value " + valueStr + " not within range");
|
||||
}
|
||||
state = new StringType(valueStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return valid states. Can be null.
|
||||
*/
|
||||
public @Nullable Set<String> getStates() {
|
||||
return states;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
|
||||
StateDescriptionFragmentBuilder builder = super.createStateDescription(readOnly);
|
||||
final Set<String> states = this.states;
|
||||
if (states != null) {
|
||||
for (String state : states) {
|
||||
builder = builder.withOption(new StateOption(state, state));
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandDescriptionBuilder createCommandDescription() {
|
||||
CommandDescriptionBuilder builder = super.createCommandDescription();
|
||||
final Set<String> commands = this.states;
|
||||
if (states != null) {
|
||||
for (String command : commands) {
|
||||
builder = builder.withCommandOption(new CommandOption(command, command));
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -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.mqtt.generic.values;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLConnection;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.CommandDescriptionBuilder;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* MQTT topics are not inherently typed.
|
||||
*
|
||||
* <p>
|
||||
* With this class users are able to map MQTT topic values to framework types,
|
||||
* for example for numbers {@link NumberValue}, boolean values {@link OnOffValue}, percentage values
|
||||
* {@link PercentageValue}, string values {@link TextValue} and more.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This class and the encapsulated (cached) state are necessary, because MQTT can't be queried,
|
||||
* but we still need to be able to respond to framework requests for a value.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* {@link #getCache()} is used to retrieve a topic state and a call to {@link #update(Command)} sets the value.
|
||||
* </p>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class Value {
|
||||
protected State state = UnDefType.UNDEF;
|
||||
protected final List<Class<? extends Command>> commandTypes;
|
||||
private final String itemType;
|
||||
|
||||
protected Value(String itemType, List<Class<? extends Command>> commandTypes) {
|
||||
this.itemType = itemType;
|
||||
this.commandTypes = commandTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of supported command types. The order of the list is important.
|
||||
* <p>
|
||||
* The framework will try to parse an incoming string into one of those command types,
|
||||
* starting with the first and continue until it succeeds.
|
||||
* </p>
|
||||
* <p>
|
||||
* Your {@link #update(Command)} method must accept all command types of this list.
|
||||
* You may accept more command types. This allows you to restrict the parsing of incoming
|
||||
* MQTT values to the listed types, but handle more user commands.
|
||||
* </p>
|
||||
* A prominent example is the {@link NumberValue}, which does not return {@link PercentType},
|
||||
* because that would interfere with {@link DecimalType} while parsing the MQTT value.
|
||||
* It does however accept a {@link PercentType} for {@link #update(Command)}, because a
|
||||
* linked Item could send that type of command.
|
||||
*/
|
||||
public final List<Class<? extends Command>> getSupportedCommandTypes() {
|
||||
return commandTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item-type (one of {@link CoreItemFactory}).
|
||||
*/
|
||||
public final String getItemType() {
|
||||
return itemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value state.
|
||||
*/
|
||||
public final State getChannelState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getMQTTpublishValue(@Nullable String pattern) {
|
||||
if (pattern == null) {
|
||||
return state.format("%s");
|
||||
}
|
||||
return state.format(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a binary type.
|
||||
*/
|
||||
public boolean isBinary() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the MQTT connection is not yet initialised or no values have
|
||||
* been received yet, the default value is {@link UnDefType#UNDEF}. To restore to the
|
||||
* default value after a connection got lost etc, this method will be called.
|
||||
*/
|
||||
public final void resetState() {
|
||||
state = UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal value state with the given command.
|
||||
*
|
||||
* @param command The command to update the internal value.
|
||||
* @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
|
||||
*/
|
||||
public abstract void update(Command command) throws IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Returns the given command if it cannot be handled by {@link #update(Command)}
|
||||
* or {@link #update(byte[])} and need to be posted straight to the framework instead.
|
||||
* Returns null otherwise.
|
||||
*
|
||||
* @param command The command to decide about
|
||||
*/
|
||||
public @Nullable Command isPostOnly(Command command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal value state with the given binary payload.
|
||||
*
|
||||
* @param data The binary payload to update the internal value.
|
||||
* @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
|
||||
*/
|
||||
public void update(byte data[]) throws IllegalArgumentException {
|
||||
String mimeType = null;
|
||||
|
||||
// URLConnection.guessContentTypeFromStream(input) is not sufficient to detect all JPEG files
|
||||
if (data.length >= 2 && data[0] == (byte) 0xFF && data[1] == (byte) 0xD8 && data[data.length - 2] == (byte) 0xFF
|
||||
&& data[data.length - 1] == (byte) 0xD9) {
|
||||
mimeType = "image/jpeg";
|
||||
} else {
|
||||
try (final ByteArrayInputStream input = new ByteArrayInputStream(data)) {
|
||||
try {
|
||||
mimeType = URLConnection.guessContentTypeFromStream(input);
|
||||
} catch (final IOException ignored) {
|
||||
}
|
||||
} catch (final IOException ignored) {
|
||||
}
|
||||
}
|
||||
state = new RawType(data, mimeType == null ? RawType.DEFAULT_MIME_TYPE : mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state description fragment builder for this value state.
|
||||
*
|
||||
* @param readOnly True if this is a read-only value.
|
||||
* @return A state description fragment builder
|
||||
*/
|
||||
public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
|
||||
return StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).withPattern("%s");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the command description builder for this value state.
|
||||
*
|
||||
* @return A command description builder
|
||||
*/
|
||||
public CommandDescriptionBuilder createCommandDescription() {
|
||||
return CommandDescriptionBuilder.create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.values;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.mqtt.generic.ChannelConfig;
|
||||
import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants;
|
||||
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
|
||||
|
||||
/**
|
||||
* A factory t
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ValueFactory {
|
||||
/**
|
||||
* Creates a new channel state value.
|
||||
*
|
||||
* @param config The channel configuration
|
||||
* @param channelTypeID The channel type, for instance TEXT_CHANNEL.
|
||||
*/
|
||||
public static Value createValueState(ChannelConfig config, String channelTypeID) throws IllegalArgumentException {
|
||||
Value value;
|
||||
switch (channelTypeID) {
|
||||
case MqttBindingConstants.STRING:
|
||||
value = StringUtils.isBlank(config.allowedStates) ? new TextValue()
|
||||
: new TextValue(config.allowedStates.split(","));
|
||||
break;
|
||||
case MqttBindingConstants.DATETIME:
|
||||
value = new DateTimeValue();
|
||||
break;
|
||||
case MqttBindingConstants.IMAGE:
|
||||
value = new ImageValue();
|
||||
break;
|
||||
case MqttBindingConstants.LOCATION:
|
||||
value = new LocationValue();
|
||||
break;
|
||||
case MqttBindingConstants.NUMBER:
|
||||
value = new NumberValue(config.min, config.max, config.step, config.unit);
|
||||
break;
|
||||
case MqttBindingConstants.DIMMER:
|
||||
value = new PercentageValue(config.min, config.max, config.step, config.on, config.off);
|
||||
break;
|
||||
case MqttBindingConstants.COLOR_HSB:
|
||||
value = new ColorValue(ColorMode.HSB, config.on, config.off, config.onBrightness);
|
||||
break;
|
||||
case MqttBindingConstants.COLOR_RGB:
|
||||
value = new ColorValue(ColorMode.RGB, config.on, config.off, config.onBrightness);
|
||||
break;
|
||||
case MqttBindingConstants.COLOR:
|
||||
value = new ColorValue(ColorMode.valueOf(config.colorMode), config.on, config.off, config.onBrightness);
|
||||
break;
|
||||
case MqttBindingConstants.SWITCH:
|
||||
value = new OnOffValue(config.on, config.off);
|
||||
break;
|
||||
case MqttBindingConstants.CONTACT:
|
||||
value = new OpenCloseValue(config.on, config.off);
|
||||
break;
|
||||
case MqttBindingConstants.ROLLERSHUTTER:
|
||||
value = new RollershutterValue(config.on, config.off, config.stop);
|
||||
break;
|
||||
case MqttBindingConstants.TRIGGER:
|
||||
config.trigger = true;
|
||||
value = new TextValue();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("ChannelTypeUID not recognised: " + channelTypeID);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:color_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter a value before it is published to MQTT or before a
|
||||
received value is assigned to an item.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="colorMode" type="text">
|
||||
<label>Color Mode</label>
|
||||
<description>Defines the color representation format of the payload. "HSB" by default.</description>
|
||||
<options>
|
||||
<option value="HSB">HSB (Hue, Saturation, Brightness)</option>
|
||||
<option value="RGB">RGB (Red, Green, Blue)</option>
|
||||
<option value="XYY">CIE xyY (x, y, Brightness)</option>
|
||||
</options>
|
||||
<default>HSB</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="stateTopic" type="text">
|
||||
<label>MQTT State Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the state. This can be left empty, the
|
||||
channel will be state-less command-only channel.</description>
|
||||
</parameter>
|
||||
<parameter name="commandTopic" type="text">
|
||||
<label>MQTT Command Topic</label>
|
||||
<description>An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
|
||||
a json {device: {status: { temperature: 23.2 }}}.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="transformationPatternOut" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Transformation</label>
|
||||
<description><![CDATA[
|
||||
Applies a transformation before publishing a MQTT topic value.
|
||||
|
||||
Transformations are specialised in extracting a value, but some transformations like
|
||||
the MAP one could be useful.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="formatBeforePublish" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Format</label>
|
||||
<description><![CDATA[
|
||||
Format a value before it is published to the MQTT broker.
|
||||
The default is to just pass the channel/item state.
|
||||
|
||||
If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s".
|
||||
If you want to adjust the precision of a number to for example 4 digits, you would use "%.4f".
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
<default>%s</default>
|
||||
</parameter>
|
||||
<parameter name="qos" type="integer" min="0" max="2" required="false">
|
||||
<label>QoS</label>
|
||||
<description>MQTT QoS of this channel (0, 1, 2). Default is QoS of the broker connection.</description>
|
||||
<options>
|
||||
<option value="0">At most once (best effort delivery "fire and forget")</option>
|
||||
<option value="1">At least once (guaranteed that a message will be delivered at least once)</option>
|
||||
<option value="2">Exactly once (guarantees that each message is received only once by the counterpart)</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="retained" type="boolean">
|
||||
<label>Retained</label>
|
||||
<description>The value will be published to the command topic as retained message. A retained value stays on the
|
||||
broker and can even be seen by MQTT clients that are subscribing at a later point in time.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="postCommand" type="boolean">
|
||||
<label>Is Command</label>
|
||||
<description>If the received MQTT value should not only update the state of linked items, but command them, enable
|
||||
this option.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="on" type="text">
|
||||
<label>On/Open Value</label>
|
||||
<description>A number (like 1, 10) or a string (like "enabled") that is recognised as on/open state. You can use this
|
||||
parameter for a second keyword, next to ON (OPEN respectively on a Contact).</description>
|
||||
<default>1</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="off" type="text">
|
||||
<label>Off/Closed Value</label>
|
||||
<description>A number (like 0, -10) or a string (like "disabled") that is recognised as off/closed state. You can use
|
||||
this parameter for a second keyword, next to OFF (CLOSED respectively on a Contact).</description>
|
||||
<default>0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="onBrightness" type="integer" min="1" max="100">
|
||||
<label>Initial Brightness</label>
|
||||
<description>If you connect this channel to a Switch item and turn it on,
|
||||
color and saturation are preserved from the
|
||||
last state, but
|
||||
the brightness will be set to this configured initial brightness percentage.</description>
|
||||
<default>10</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:dimmer_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter a value before it is published to MQTT or before a
|
||||
received value is assigned to an item.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="stateTopic" type="text">
|
||||
<label>MQTT State Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the state. This can be left empty, the
|
||||
channel will be state-less command-only channel.</description>
|
||||
</parameter>
|
||||
<parameter name="commandTopic" type="text">
|
||||
<label>MQTT Command Topic</label>
|
||||
<description>An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
|
||||
a json {device: {status: { temperature: 23.2 }}}.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="transformationPatternOut" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Transformation</label>
|
||||
<description><![CDATA[
|
||||
Applies a transformation before publishing a MQTT topic value.
|
||||
|
||||
Transformations are specialised in extracting a value, but some transformations like
|
||||
the MAP one could be useful.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="formatBeforePublish" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Format</label>
|
||||
<description><![CDATA[
|
||||
Format a value before it is published to the MQTT broker.
|
||||
The default is to just pass the channel/item state.
|
||||
|
||||
If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s".
|
||||
If you want to adjust the precision of a number to for example 4 digits, you would use "%.4f".
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
<default>%s</default>
|
||||
</parameter>
|
||||
<parameter name="qos" type="integer" min="0" max="2" required="false">
|
||||
<label>QoS</label>
|
||||
<description>MQTT QoS of this channel (0, 1, 2). Default is QoS of the broker connection.</description>
|
||||
<options>
|
||||
<option value="0">At most once (best effort delivery "fire and forget")</option>
|
||||
<option value="1">At least once (guaranteed that a message will be delivered at least once)</option>
|
||||
<option value="2">Exactly once (guarantees that each message is received only once by the counterpart)</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="retained" type="boolean">
|
||||
<label>Retained</label>
|
||||
<description>The value will be published to the command topic as retained message. A retained value stays on the
|
||||
broker and can even be seen by MQTT clients that are subscribing at a later point in time.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="postCommand" type="boolean">
|
||||
<label>Is Command</label>
|
||||
<description>If the received MQTT value should not only update the state of linked items, but command them, enable
|
||||
this option.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="min" type="decimal">
|
||||
<label>Absolute Minimum</label>
|
||||
<description>This configuration represents the minimum of the allowed range. For a percentage channel that equals
|
||||
zero percent.</description>
|
||||
</parameter>
|
||||
<parameter name="max" type="decimal">
|
||||
<label>Absolute Maximum</label>
|
||||
<description>This configuration represents the maximum of the allowed range. For a percentage channel that equals
|
||||
one-hundred percent.</description>
|
||||
</parameter>
|
||||
<parameter name="step" type="decimal">
|
||||
<label>Delta Value</label>
|
||||
<description>A number/dimmer channel can receive INCREASE/DECREASE commands and computes the target number by adding
|
||||
or subtracting this delta value.</description>
|
||||
<default>1.0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="on" type="text">
|
||||
<label>Custom On/Open Value</label>
|
||||
<description>A number (like 1, 10) or a string (like "enabled") that is additionally recognised as on/open state. You
|
||||
can use this parameter for a second keyword, next to ON (OPEN respectively on a Contact).</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="off" type="text">
|
||||
<label>Custom Off/Closed Value</label>
|
||||
<description>A number (like 0, -10) or a string (like "disabled") that is additionally recognised as off/closed
|
||||
state. You can use this parameter for a second keyword, next to OFF (CLOSED respectively on a Contact).</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:number_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter a value before it is published to MQTT or before a
|
||||
received value is assigned to an item.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="stateTopic" type="text">
|
||||
<label>MQTT State Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the state. This can be left empty, the
|
||||
channel will be state-less command-only channel.</description>
|
||||
</parameter>
|
||||
<parameter name="commandTopic" type="text">
|
||||
<label>MQTT Command Topic</label>
|
||||
<description>An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
|
||||
a json {device: {status: { temperature: 23.2 }}}.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="transformationPatternOut" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Transformation</label>
|
||||
<description><![CDATA[
|
||||
Applies a transformation before publishing a MQTT topic value.
|
||||
|
||||
Transformations are specialised in extracting a value, but some transformations like
|
||||
the MAP one could be useful.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="formatBeforePublish" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Format</label>
|
||||
<description><![CDATA[
|
||||
Format a value before it is published to the MQTT broker.
|
||||
The default is to just pass the channel/item state.
|
||||
|
||||
If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s".
|
||||
If you want to adjust the precision of a number to for example 4 digits, you would use "%.4f".
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
<default>%s</default>
|
||||
</parameter>
|
||||
<parameter name="qos" type="integer" min="0" max="2" required="false">
|
||||
<label>QoS</label>
|
||||
<description>MQTT QoS of this channel (0, 1, 2). Default is QoS of the broker connection.</description>
|
||||
<options>
|
||||
<option value="0">At most once (best effort delivery "fire and forget")</option>
|
||||
<option value="1">At least once (guaranteed that a message will be delivered at least once)</option>
|
||||
<option value="2">Exactly once (guarantees that each message is received only once by the counterpart)</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="retained" type="boolean">
|
||||
<label>Retained</label>
|
||||
<description>The value will be published to the command topic as retained message. A retained value stays on the
|
||||
broker and can even be seen by MQTT clients that are subscribing at a later point in time.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="postCommand" type="boolean">
|
||||
<label>Is Command</label>
|
||||
<description>If the received MQTT value should not only update the state of linked items, but command them, enable
|
||||
this option.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="min" type="decimal">
|
||||
<label>Absolute Minimum</label>
|
||||
<description>This configuration represents the minimum of the allowed range. For a percentage channel that equals
|
||||
zero percent.</description>
|
||||
</parameter>
|
||||
<parameter name="max" type="decimal">
|
||||
<label>Absolute Maximum</label>
|
||||
<description>This configuration represents the maximum of the allowed range. For a percentage channel that equals
|
||||
one-hundred percent.</description>
|
||||
</parameter>
|
||||
<parameter name="step" type="decimal">
|
||||
<label>Delta Value</label>
|
||||
<description>A number/dimmer channel can receive INCREASE/DECREASE commands and computes the target number by adding
|
||||
or subtracting this delta value.</description>
|
||||
<default>1.0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="unit" type="text">
|
||||
<label>Unit Of Measurement</label>
|
||||
<description>Unit of measurement (optional). The unit is used for representing the value in the GUI as well as for
|
||||
converting incoming values (like from '°F' to '°C'). Examples: "°C", "°F"</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:rollershutter_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter a value before it is published to MQTT or before a
|
||||
received value is assigned to an item.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="stateTopic" type="text">
|
||||
<label>MQTT State Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the state. This can be left empty, the
|
||||
channel will be state-less command-only channel.</description>
|
||||
</parameter>
|
||||
<parameter name="commandTopic" type="text">
|
||||
<label>MQTT Command Topic</label>
|
||||
<description>An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
|
||||
a json {device: {status: { temperature: 23.2 }}}.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="transformationPatternOut" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Transformation</label>
|
||||
<description><![CDATA[
|
||||
Applies a transformation before publishing a MQTT topic value.
|
||||
|
||||
Transformations are specialised in extracting a value, but some transformations like
|
||||
the MAP one could be useful.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="formatBeforePublish" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Format</label>
|
||||
<description><![CDATA[
|
||||
Format a value before it is published to the MQTT broker.
|
||||
The default is to just pass the channel/item state.
|
||||
|
||||
If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s".
|
||||
If you want to adjust the precision of a number to for example 4 digits, you would use "%.4f".
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
<default>%s</default>
|
||||
</parameter>
|
||||
<parameter name="qos" type="integer" min="0" max="2" required="false">
|
||||
<label>QoS</label>
|
||||
<description>MQTT QoS of this channel (0, 1, 2). Default is QoS of the broker connection.</description>
|
||||
<options>
|
||||
<option value="0">At most once (best effort delivery "fire and forget")</option>
|
||||
<option value="1">At least once (guaranteed that a message will be delivered at least once)</option>
|
||||
<option value="2">Exactly once (guarantees that each message is received only once by the counterpart)</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="retained" type="boolean">
|
||||
<label>Retained</label>
|
||||
<description>The value will be published to the command topic as retained message. A retained value stays on the
|
||||
broker and can even be seen by MQTT clients that are subscribing at a later point in time.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="postCommand" type="boolean">
|
||||
<label>Is Command</label>
|
||||
<description>If the received MQTT value should not only update the state of linked items, but command them, enable
|
||||
this option.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="on" type="text">
|
||||
<label>Up Value</label>
|
||||
<description>A string (like "OPEN") that is recognised as UP state. You can use this parameter for a second keyword,
|
||||
next to UP.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="off" type="text">
|
||||
<label>Down Value</label>
|
||||
<description>A string (like "CLOSE") that is recognised as DOWN state. You can use this parameter for a second
|
||||
keyword, next to DOWN.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="stop" type="text">
|
||||
<label>Stop Value</label>
|
||||
<description>A string (like "STOP") that is recognised as stop state. Will set the rollershutter state to undefined,
|
||||
because the current position is unknown at that point.</description>
|
||||
<default>STOP</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:string_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter a value before it is published to MQTT or before a
|
||||
received value is assigned to an item.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="stateTopic" type="text">
|
||||
<label>MQTT State Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the state. This can be left empty, the
|
||||
channel will be state-less command-only channel.</description>
|
||||
</parameter>
|
||||
<parameter name="commandTopic" type="text">
|
||||
<label>MQTT Command Topic</label>
|
||||
<description>An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
|
||||
a json {device: {status: { temperature: 23.2 }}}.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="transformationPatternOut" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Transformation</label>
|
||||
<description><![CDATA[
|
||||
Applies a transformation before publishing a MQTT topic value.
|
||||
|
||||
Transformations are specialised in extracting a value, but some transformations like
|
||||
the MAP one could be useful.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="formatBeforePublish" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Format</label>
|
||||
<description><![CDATA[
|
||||
Format a value before it is published to the MQTT broker.
|
||||
The default is to just pass the channel/item state.
|
||||
|
||||
If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s".
|
||||
If you want to adjust the precision of a number to for example 4 digits, you would use "%.4f".
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
<default>%s</default>
|
||||
</parameter>
|
||||
<parameter name="qos" type="integer" min="0" max="2" required="false">
|
||||
<label>QoS</label>
|
||||
<description>MQTT QoS of this channel (0, 1, 2). Default is QoS of the broker connection.</description>
|
||||
<options>
|
||||
<option value="0">At most once (best effort delivery "fire and forget")</option>
|
||||
<option value="1">At least once (guaranteed that a message will be delivered at least once)</option>
|
||||
<option value="2">Exactly once (guarantees that each message is received only once by the counterpart)</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="retained" type="boolean">
|
||||
<label>Retained</label>
|
||||
<description>The value will be published to the command topic as retained message. A retained value stays on the
|
||||
broker and can even be seen by MQTT clients that are subscribing at a later point in time.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="postCommand" type="boolean">
|
||||
<label>Is Command</label>
|
||||
<description>If the received MQTT value should not only update the state of linked items, but command them, enable
|
||||
this option.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="allowedStates" type="text">
|
||||
<label>Allowed States</label>
|
||||
<description>If your MQTT topic is limited to a set of one or more specific commands or specific states, define those
|
||||
states here. Separate multiple states with commas. An example for a light bulb state set: ON,DIMMED,OFF</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:switch_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter a value before it is published to MQTT or before a
|
||||
received value is assigned to an item.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="stateTopic" type="text">
|
||||
<label>MQTT State Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the state. This can be left empty, the
|
||||
channel will be state-less command-only channel.</description>
|
||||
</parameter>
|
||||
<parameter name="commandTopic" type="text">
|
||||
<label>MQTT Command Topic</label>
|
||||
<description>An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
|
||||
a json {device: {status: { temperature: 23.2 }}}.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="transformationPatternOut" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Transformation</label>
|
||||
<description><![CDATA[
|
||||
Applies a transformation before publishing a MQTT topic value.
|
||||
|
||||
Transformations are specialised in extracting a value, but some transformations like
|
||||
the MAP one could be useful.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="formatBeforePublish" type="text" groupName="transformations">
|
||||
<label>Outgoing Value Format</label>
|
||||
<description><![CDATA[
|
||||
Format a value before it is published to the MQTT broker.
|
||||
The default is to just pass the channel/item state.
|
||||
|
||||
If you want to apply a prefix, say "MYCOLOR,", you would use "MYCOLOR,%s".
|
||||
If you want to adjust the precision of a number to for example 4 digits, you would use "%.4f".
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
<default>%s</default>
|
||||
</parameter>
|
||||
<parameter name="qos" type="integer" min="0" max="2" required="false">
|
||||
<label>QoS</label>
|
||||
<description>MQTT QoS of this channel (0, 1, 2). Default is QoS of the broker connection.</description>
|
||||
<options>
|
||||
<option value="0">At most once (best effort delivery "fire and forget")</option>
|
||||
<option value="1">At least once (guaranteed that a message will be delivered at least once)</option>
|
||||
<option value="2">Exactly once (guarantees that each message is received only once by the counterpart)</option>
|
||||
</options>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="retained" type="boolean">
|
||||
<label>Retained</label>
|
||||
<description>The value will be published to the command topic as retained message. A retained value stays on the
|
||||
broker and can even be seen by MQTT clients that are subscribing at a later point in time.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="postCommand" type="boolean">
|
||||
<label>Is Command</label>
|
||||
<description>If the received MQTT value should not only update the state of linked items, but command them, enable
|
||||
this option.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="on" type="text">
|
||||
<label>Custom On/Open Value</label>
|
||||
<description>A number (like 1, 10) or a string (like "enabled") that is additionally recognised as on/open state. You
|
||||
can use this parameter for a second keyword, next to ON (OPEN respectively on a Contact).</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="off" type="text">
|
||||
<label>Custom Off/Closed Value</label>
|
||||
<description>A number (like 0, -10) or a string (like "disabled") that is additionally recognised as off/closed
|
||||
state. You can use this parameter for a second keyword, next to OFF (CLOSED respectively on a Contact).</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:mqtt:trigger_channel">
|
||||
<parameter-group name="transformations">
|
||||
<label>Transform Values</label>
|
||||
<description>These configuration parameters allow you to alter before a received value is used in the trigger.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="stateTopic" type="text" required="true">
|
||||
<label>MQTT Trigger Topic</label>
|
||||
<description>An MQTT topic that this thing will subscribe to, to receive the trigger</description>
|
||||
</parameter>
|
||||
<parameter name="transformationPattern" type="text" groupName="transformations">
|
||||
<label>Incoming Value Transformations</label>
|
||||
<description><![CDATA[
|
||||
Applies transformations to an incoming MQTT topic value.
|
||||
This can be used to map the events sent by the device to common values for all devices using,
|
||||
e.g. the MAP transformation.
|
||||
|
||||
You can chain transformations by separating them with the intersection character ∩.
|
||||
]]></description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,2 @@
|
||||
binding.mqttgeneric.name = Allgemeines MQTT Binding
|
||||
binding.mqttgeneric.description = Verknüpfung von MQTT Topics mit Things
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="mqtt"
|
||||
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">
|
||||
|
||||
<channel-type id="string">
|
||||
<item-type>String</item-type>
|
||||
<label>Text Value</label>
|
||||
<config-description-ref uri="thing-type:mqtt:string_channel"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="datetime">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Date/Time Value</label>
|
||||
<description>Current date and/or time</description>
|
||||
<config-description-ref uri="thing-type:mqtt:string_channel"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="image">
|
||||
<item-type>Image</item-type>
|
||||
<label>Image</label>
|
||||
<description>An image to display. Send a binary bmp, jpg, png or any other supported format to this channel.</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description-ref uri="thing-type:mqtt:string_channel"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="location">
|
||||
<item-type>Location</item-type>
|
||||
<label>Location</label>
|
||||
<description>GPS coordinates as Latitude,Longitude,Altitude</description>
|
||||
<config-description-ref uri="thing-type:mqtt:string_channel"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="number">
|
||||
<item-type>Number</item-type>
|
||||
<label>Number Value</label>
|
||||
<config-description-ref uri="thing-type:mqtt:number_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dimmer">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Percentage Value</label>
|
||||
<config-description-ref uri="thing-type:mqtt:dimmer_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="switch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>On/Off Switch</label>
|
||||
<config-description-ref uri="thing-type:mqtt:switch_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="contact">
|
||||
<item-type>Contact</item-type>
|
||||
<label>Open/Close Contact</label>
|
||||
<config-description-ref uri="thing-type:mqtt:switch_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rollershutter">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Rollershutter</label>
|
||||
<config-description-ref uri="thing-type:mqtt:rollershutter_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colorRGB">
|
||||
<item-type>Color</item-type>
|
||||
<label>Color Value (Red,Green,Blue)</label>
|
||||
<description></description>
|
||||
<config-description-ref uri="thing-type:mqtt:color_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colorHSB">
|
||||
<item-type>Color</item-type>
|
||||
<label>Color Value (Hue,Saturation,Brightness)</label>
|
||||
<description></description>
|
||||
<config-description-ref uri="thing-type:mqtt:color_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="color">
|
||||
<item-type>Color</item-type>
|
||||
<label>Color Value (HSB, RGB or CIE xyY)</label>
|
||||
<description></description>
|
||||
<config-description-ref uri="thing-type:mqtt:color_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="trigger">
|
||||
<item-type>Trigger</item-type>
|
||||
<label>Trigger</label>
|
||||
<description></description>
|
||||
<config-description-ref uri="thing-type:mqtt:trigger_channel"></config-description-ref>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="mqtt"
|
||||
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="topic"
|
||||
extensible="string,number,dimmer,switch,contact,colorRGB,colorHSB,color,datetime,image,location,rollershutter,trigger">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="broker"/>
|
||||
<bridge-type-ref id="systemBroker"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Generic MQTT Thing</label>
|
||||
<description>You need a configured Broker first. Dynamically add channels of various types to this Thing. Link
|
||||
different MQTT topics to each channel.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="availabilityTopic" type="text">
|
||||
<label>Availability Topic</label>
|
||||
<description>Topic of the LWT of the device</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="payloadAvailable" type="text">
|
||||
<label>Payload available</label>
|
||||
<description>Payload of the 'Availability Topic', when the device is available. Default: 'ON'</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="payloadNotAvailable" type="text">
|
||||
<label>Payload not availabe</label>
|
||||
<description>Payload of the 'Availability Topic', when the device is *not* available. Default: 'OFF'</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelStateHelper {
|
||||
public static void setConnection(ChannelState cs, MqttBrokerConnection connection) {
|
||||
cs.setConnection(connection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
|
||||
import org.openhab.binding.mqtt.generic.values.ColorValue;
|
||||
import org.openhab.binding.mqtt.generic.values.DateTimeValue;
|
||||
import org.openhab.binding.mqtt.generic.values.ImageValue;
|
||||
import org.openhab.binding.mqtt.generic.values.LocationValue;
|
||||
import org.openhab.binding.mqtt.generic.values.NumberValue;
|
||||
import org.openhab.binding.mqtt.generic.values.PercentageValue;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
|
||||
/**
|
||||
* Tests the {@link ChannelState} class.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class ChannelStateTests {
|
||||
@Mock
|
||||
private MqttBrokerConnection connection;
|
||||
|
||||
@Mock
|
||||
private ChannelStateUpdateListener channelStateUpdateListener;
|
||||
|
||||
@Mock
|
||||
private ChannelUID channelUID;
|
||||
|
||||
@Spy
|
||||
private TextValue textValue;
|
||||
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
private ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
initMocks(this);
|
||||
CompletableFuture<Void> voidFutureComplete = new CompletableFuture<>();
|
||||
voidFutureComplete.complete(null);
|
||||
doReturn(voidFutureComplete).when(connection).unsubscribeAll();
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(),
|
||||
anyBoolean());
|
||||
|
||||
scheduler = new ScheduledThreadPoolExecutor(1);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noInteractionTimeoutTest() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, textValue, channelStateUpdateListener));
|
||||
c.start(connection, scheduler, 50).get(100, TimeUnit.MILLISECONDS);
|
||||
verify(connection).subscribe(eq("state"), eq(c));
|
||||
c.stop().get();
|
||||
verify(connection).unsubscribe(eq("state"), eq(c));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishFormatTest() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, textValue, channelStateUpdateListener));
|
||||
|
||||
c.start(connection, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
|
||||
verify(connection).subscribe(eq("state"), eq(c));
|
||||
|
||||
c.publishValue(new StringType("UPDATE")).get();
|
||||
verify(connection).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE".getBytes())), anyInt(),
|
||||
eq(false));
|
||||
|
||||
c.config.formatBeforePublish = "prefix%s";
|
||||
c.publishValue(new StringType("UPDATE")).get();
|
||||
verify(connection).publish(eq("command"), argThat(p -> Arrays.equals(p, "prefixUPDATE".getBytes())), anyInt(),
|
||||
eq(false));
|
||||
|
||||
c.config.formatBeforePublish = "%1$s-%1$s";
|
||||
c.publishValue(new StringType("UPDATE")).get();
|
||||
verify(connection).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE-UPDATE".getBytes())), anyInt(),
|
||||
eq(false));
|
||||
|
||||
c.config.formatBeforePublish = "%s";
|
||||
c.config.retained = true;
|
||||
c.publishValue(new StringType("UPDATE")).get();
|
||||
verify(connection).publish(eq("command"), any(), anyInt(), eq(true));
|
||||
|
||||
c.stop().get();
|
||||
verify(connection).unsubscribe(eq("state"), eq(c));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveWildcardTest() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
ChannelState c = spy(new ChannelState(ChannelConfigBuilder.create("state/+/topic", "command").build(),
|
||||
channelUID, textValue, channelStateUpdateListener));
|
||||
|
||||
CompletableFuture<@Nullable Void> future = c.start(connection, scheduler, 100);
|
||||
c.processMessage("state/bla/topic", "A TEST".getBytes());
|
||||
future.get(300, TimeUnit.MILLISECONDS);
|
||||
|
||||
assertThat(textValue.getChannelState().toString(), is("A TEST"));
|
||||
verify(channelStateUpdateListener).updateChannelState(eq(channelUID), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveStringTest() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, textValue, channelStateUpdateListener));
|
||||
|
||||
CompletableFuture<@Nullable Void> future = c.start(connection, scheduler, 100);
|
||||
c.processMessage("state", "A TEST".getBytes());
|
||||
future.get(300, TimeUnit.MILLISECONDS);
|
||||
|
||||
assertThat(textValue.getChannelState().toString(), is("A TEST"));
|
||||
verify(channelStateUpdateListener).updateChannelState(eq(channelUID), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveDecimalTest() {
|
||||
NumberValue value = new NumberValue(null, null, new BigDecimal(10), null);
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "15".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("15"));
|
||||
|
||||
c.processMessage("state", "INCREASE".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("25"));
|
||||
|
||||
c.processMessage("state", "DECREASE".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("15"));
|
||||
|
||||
verify(channelStateUpdateListener, times(3)).updateChannelState(eq(channelUID), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveDecimalFractionalTest() {
|
||||
NumberValue value = new NumberValue(null, null, new BigDecimal(10.5), null);
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "5.5".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("5.5"));
|
||||
|
||||
c.processMessage("state", "INCREASE".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("16.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receivePercentageTest() {
|
||||
PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
|
||||
null);
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "-100".getBytes()); // 0%
|
||||
assertThat(value.getChannelState().toString(), is("0"));
|
||||
|
||||
c.processMessage("state", "100".getBytes()); // 100%
|
||||
assertThat(value.getChannelState().toString(), is("100"));
|
||||
|
||||
c.processMessage("state", "0".getBytes()); // 50%
|
||||
assertThat(value.getChannelState().toString(), is("50"));
|
||||
|
||||
c.processMessage("state", "INCREASE".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("55"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("10"));
|
||||
assertThat(value.getMQTTpublishValue("%03.0f"), is("010"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveRGBColorTest() {
|
||||
ColorValue value = new ColorValue(ColorMode.RGB, "FON", "FOFF", 10);
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "ON".getBytes()); // Normal on state
|
||||
assertThat(value.getChannelState().toString(), is("0,0,10"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("25,25,25"));
|
||||
|
||||
c.processMessage("state", "FOFF".getBytes()); // Custom off state
|
||||
assertThat(value.getChannelState().toString(), is("0,0,0"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0,0,0"));
|
||||
|
||||
c.processMessage("state", "10".getBytes()); // Brightness only
|
||||
assertThat(value.getChannelState().toString(), is("0,0,10"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("25,25,25"));
|
||||
|
||||
HSBType t = HSBType.fromRGB(12, 18, 231);
|
||||
|
||||
c.processMessage("state", "12,18,231".getBytes());
|
||||
assertThat(value.getChannelState(), is(t)); // HSB
|
||||
// rgb -> hsv -> rgb is quite lossy
|
||||
assertThat(value.getMQTTpublishValue(null), is("13,20,225"));
|
||||
assertThat(value.getMQTTpublishValue("%3$d,%2$d,%1$d"), is("225,20,13"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveHSBColorTest() {
|
||||
ColorValue value = new ColorValue(ColorMode.HSB, "FON", "FOFF", 10);
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "ON".getBytes()); // Normal on state
|
||||
assertThat(value.getChannelState().toString(), is("0,0,10"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0,0,10"));
|
||||
|
||||
c.processMessage("state", "FOFF".getBytes()); // Custom off state
|
||||
assertThat(value.getChannelState().toString(), is("0,0,0"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0,0,0"));
|
||||
|
||||
c.processMessage("state", "10".getBytes()); // Brightness only
|
||||
assertThat(value.getChannelState().toString(), is("0,0,10"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0,0,10"));
|
||||
|
||||
c.processMessage("state", "12,18,100".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("12,18,100"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("12,18,100"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveXYYColorTest() {
|
||||
ColorValue value = new ColorValue(ColorMode.XYY, "FON", "FOFF", 10);
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "ON".getBytes()); // Normal on state
|
||||
assertThat(value.getChannelState().toString(), is("0,0,10"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0.312716,0.329002,10.00"));
|
||||
|
||||
c.processMessage("state", "FOFF".getBytes()); // Custom off state
|
||||
assertThat(value.getChannelState().toString(), is("0,0,0"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0.312716,0.329002,0.00"));
|
||||
|
||||
c.processMessage("state", "10".getBytes()); // Brightness only
|
||||
assertThat(value.getChannelState().toString(), is("0,0,10"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("0.312716,0.329002,10.00"));
|
||||
|
||||
HSBType t = HSBType.fromXY(0.3f, 0.6f);
|
||||
|
||||
c.processMessage("state", "0.3,0.6,100".getBytes());
|
||||
assertThat(value.getChannelState(), is(t)); // HSB
|
||||
assertThat(value.getMQTTpublishValue(null), is("0.300000,0.600000,100.00"));
|
||||
assertThat(value.getMQTTpublishValue("%3$.1f,%2$.4f,%1$.4f"), is("100.0,0.6000,0.3000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveLocationTest() {
|
||||
LocationValue value = new LocationValue();
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
c.processMessage("state", "46.833974, 7.108433".getBytes());
|
||||
assertThat(value.getChannelState().toString(), is("46.833974,7.108433"));
|
||||
assertThat(value.getMQTTpublishValue(null), is("46.833974,7.108433"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveDateTimeTest() {
|
||||
DateTimeValue value = new DateTimeValue();
|
||||
ChannelState subject = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
subject.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
ZonedDateTime zd = ZonedDateTime.now();
|
||||
String datetime = zd.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
|
||||
subject.processMessage("state", datetime.getBytes());
|
||||
|
||||
String channelState = value.getChannelState().toString();
|
||||
assertTrue("Expected '" + channelState + "' to start with '" + datetime + "'",
|
||||
channelState.startsWith(datetime));
|
||||
assertThat(value.getMQTTpublishValue(null), is(datetime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveImageTest() {
|
||||
ImageValue value = new ImageValue();
|
||||
ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
|
||||
c.start(connection, mock(ScheduledExecutorService.class), 100);
|
||||
|
||||
byte[] payload = new byte[] { (byte) 0xFF, (byte) 0xD8, 0x01, 0x02, (byte) 0xFF, (byte) 0xD9 };
|
||||
c.processMessage("state", payload);
|
||||
assertThat(value.getChannelState(), is(instanceOf(RawType.class)));
|
||||
assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static org.openhab.binding.mqtt.generic.internal.handler.ThingChannelConstants.*;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.openhab.binding.mqtt.generic.internal.handler.GenericMQTTThingHandler;
|
||||
import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.io.transport.mqtt.MqttException;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
|
||||
/**
|
||||
* Tests cases for {@link ThingHandler} to test the json transformation.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class ChannelStateTransformationTests {
|
||||
|
||||
@Mock
|
||||
private TransformationService jsonPathService;
|
||||
|
||||
@Mock
|
||||
private TransformationServiceProvider transformationServiceProvider;
|
||||
|
||||
@Mock
|
||||
private ThingHandlerCallback callback;
|
||||
|
||||
@Mock
|
||||
private Thing thing;
|
||||
|
||||
@Mock
|
||||
private AbstractBrokerHandler bridgeHandler;
|
||||
|
||||
@Mock
|
||||
private MqttBrokerConnection connection;
|
||||
|
||||
private GenericMQTTThingHandler thingHandler;
|
||||
|
||||
@Before
|
||||
public void setUp() throws ConfigurationException, MqttException {
|
||||
initMocks(this);
|
||||
|
||||
ThingStatusInfo thingStatus = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
|
||||
|
||||
// Mock the thing: We need the thingUID and the bridgeUID
|
||||
when(thing.getUID()).thenReturn(testGenericThing);
|
||||
when(thing.getChannels()).thenReturn(thingChannelListWithJson);
|
||||
when(thing.getStatusInfo()).thenReturn(thingStatus);
|
||||
when(thing.getConfiguration()).thenReturn(new Configuration());
|
||||
|
||||
// Return the mocked connection object if the bridge handler is asked for it
|
||||
when(bridgeHandler.getConnectionAsync()).thenReturn(CompletableFuture.completedFuture(connection));
|
||||
|
||||
CompletableFuture<Void> voidFutureComplete = new CompletableFuture<>();
|
||||
voidFutureComplete.complete(null);
|
||||
doReturn(voidFutureComplete).when(connection).unsubscribeAll();
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
|
||||
|
||||
thingHandler = spy(new GenericMQTTThingHandler(thing, mock(MqttChannelStateDescriptionProvider.class),
|
||||
transformationServiceProvider, 1500));
|
||||
when(transformationServiceProvider.getTransformationService(anyString())).thenReturn(jsonPathService);
|
||||
|
||||
thingHandler.setCallback(callback);
|
||||
// Return the bridge handler if the thing handler asks for it
|
||||
doReturn(bridgeHandler).when(thingHandler).getBridgeHandler();
|
||||
|
||||
// We are by default online
|
||||
doReturn(thingStatus).when(thingHandler).getBridgeStatus();
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void initialize() throws MqttException {
|
||||
when(thing.getChannels()).thenReturn(thingChannelListWithJson);
|
||||
|
||||
thingHandler.initialize();
|
||||
ChannelState channelConfig = thingHandler.getChannelState(textChannelUID);
|
||||
assertThat(channelConfig.transformationsIn.get(0).pattern, is(jsonPathPattern));
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void processMessageWithJSONPath() throws Exception {
|
||||
when(jsonPathService.transform(jsonPathPattern, jsonPathJSON)).thenReturn("23.2");
|
||||
|
||||
thingHandler.initialize();
|
||||
ChannelState channelConfig = thingHandler.getChannelState(textChannelUID);
|
||||
channelConfig.setChannelStateUpdateListener(thingHandler);
|
||||
|
||||
ChannelStateTransformation transformation = channelConfig.transformationsIn.get(0);
|
||||
|
||||
byte payload[] = jsonPathJSON.getBytes();
|
||||
assertThat(transformation.pattern, is(jsonPathPattern));
|
||||
// Test process message
|
||||
channelConfig.processMessage(channelConfig.getStateTopic(), payload);
|
||||
|
||||
verify(callback).stateUpdated(eq(textChannelUID), argThat(arg -> "23.2".equals(arg.toString())));
|
||||
assertThat(channelConfig.getCache().getChannelState().toString(), is("23.2"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ThingHandlerHelper {
|
||||
public static void setConnection(AbstractMQTTThingHandler h, MqttBrokerConnection connection) {
|
||||
h.connection = connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.internal.handler;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.openhab.binding.mqtt.generic.internal.handler.ThingChannelConstants.*;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.openhab.binding.mqtt.generic.ChannelConfig;
|
||||
import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
|
||||
import org.openhab.binding.mqtt.generic.ChannelState;
|
||||
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||
import org.openhab.binding.mqtt.generic.ThingHandlerHelper;
|
||||
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.binding.mqtt.generic.values.ValueFactory;
|
||||
import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
/**
|
||||
* Tests cases for {@link GenericMQTTThingHandler}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class GenericThingHandlerTests {
|
||||
@Mock
|
||||
private ThingHandlerCallback callback;
|
||||
|
||||
@Mock
|
||||
private Thing thing;
|
||||
|
||||
@Mock
|
||||
private AbstractBrokerHandler bridgeHandler;
|
||||
|
||||
@Mock
|
||||
private MqttBrokerConnection connection;
|
||||
|
||||
private GenericMQTTThingHandler thingHandler;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
ThingStatusInfo thingStatus = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
// Mock the thing: We need the thingUID and the bridgeUID
|
||||
when(thing.getUID()).thenReturn(testGenericThing);
|
||||
when(thing.getChannels()).thenReturn(thingChannelList);
|
||||
when(thing.getStatusInfo()).thenReturn(thingStatus);
|
||||
when(thing.getConfiguration()).thenReturn(new Configuration());
|
||||
|
||||
// Return the mocked connection object if the bridge handler is asked for it
|
||||
when(bridgeHandler.getConnectionAsync()).thenReturn(CompletableFuture.completedFuture(connection));
|
||||
|
||||
CompletableFuture<Void> voidFutureComplete = new CompletableFuture<>();
|
||||
voidFutureComplete.complete(null);
|
||||
doReturn(voidFutureComplete).when(connection).unsubscribeAll();
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(),
|
||||
anyBoolean());
|
||||
|
||||
thingHandler = spy(new GenericMQTTThingHandler(thing, mock(MqttChannelStateDescriptionProvider.class),
|
||||
mock(TransformationServiceProvider.class), 1500));
|
||||
thingHandler.setCallback(callback);
|
||||
|
||||
// Return the bridge handler if the thing handler asks for it
|
||||
doReturn(bridgeHandler).when(thingHandler).getBridgeHandler();
|
||||
|
||||
// The broker connection bridge is by default online
|
||||
doReturn(thingStatus).when(thingHandler).getBridgeStatus();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void initializeWithUnknownThingUID() {
|
||||
ChannelConfig config = textConfiguration().as(ChannelConfig.class);
|
||||
thingHandler.createChannelState(config, new ChannelUID(testGenericThing, "test"),
|
||||
ValueFactory.createValueState(config, unknownChannel.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialize() {
|
||||
thingHandler.initialize();
|
||||
verify(thingHandler).bridgeStatusChanged(any());
|
||||
verify(thingHandler).start(any());
|
||||
assertThat(thingHandler.getConnection(), is(connection));
|
||||
|
||||
ChannelState channelConfig = thingHandler.channelStateByChannelUID.get(textChannelUID);
|
||||
assertThat(channelConfig.getStateTopic(), is("test/state"));
|
||||
assertThat(channelConfig.getCommandTopic(), is("test/command"));
|
||||
|
||||
verify(connection).subscribe(eq(channelConfig.getStateTopic()), eq(channelConfig));
|
||||
|
||||
verify(callback).statusUpdated(eq(thing), argThat((arg) -> arg.getStatus().equals(ThingStatus.ONLINE)
|
||||
&& arg.getStatusDetail().equals(ThingStatusDetail.NONE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleCommandRefresh() {
|
||||
TextValue value = spy(new TextValue());
|
||||
value.update(new StringType("DEMOVALUE"));
|
||||
|
||||
ChannelState channelConfig = mock(ChannelState.class);
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(channelConfig).start(any(), any(), anyInt());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(channelConfig).stop();
|
||||
doReturn(value).when(channelConfig).getCache();
|
||||
doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
|
||||
thingHandler.initialize();
|
||||
|
||||
ThingHandlerHelper.setConnection(thingHandler, connection);
|
||||
|
||||
thingHandler.handleCommand(textChannelUID, RefreshType.REFRESH);
|
||||
verify(callback).stateUpdated(eq(textChannelUID), argThat(arg -> "DEMOVALUE".equals(arg.toString())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleCommandUpdateString() {
|
||||
TextValue value = spy(new TextValue());
|
||||
ChannelState channelConfig = spy(
|
||||
new ChannelState(ChannelConfigBuilder.create("stateTopic", "commandTopic").build(), textChannelUID,
|
||||
value, thingHandler));
|
||||
doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
|
||||
thingHandler.initialize();
|
||||
ThingHandlerHelper.setConnection(thingHandler, connection);
|
||||
|
||||
StringType updateValue = new StringType("UPDATE");
|
||||
thingHandler.handleCommand(textChannelUID, updateValue);
|
||||
verify(value).update(eq(updateValue));
|
||||
assertThat(channelConfig.getCache().getChannelState().toString(), is("UPDATE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleCommandUpdateBoolean() {
|
||||
OnOffValue value = spy(new OnOffValue("ON", "OFF"));
|
||||
ChannelState channelConfig = spy(
|
||||
new ChannelState(ChannelConfigBuilder.create("stateTopic", "commandTopic").build(), textChannelUID,
|
||||
value, thingHandler));
|
||||
doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
|
||||
thingHandler.initialize();
|
||||
ThingHandlerHelper.setConnection(thingHandler, connection);
|
||||
|
||||
StringType updateValue = new StringType("ON");
|
||||
thingHandler.handleCommand(textChannelUID, updateValue);
|
||||
|
||||
verify(value).update(eq(updateValue));
|
||||
assertThat(channelConfig.getCache().getChannelState(), is(OnOffType.ON));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processMessage() {
|
||||
TextValue textValue = new TextValue();
|
||||
ChannelState channelConfig = spy(
|
||||
new ChannelState(ChannelConfigBuilder.create("test/state", "test/state/set").build(), textChannelUID,
|
||||
textValue, thingHandler));
|
||||
doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
|
||||
thingHandler.initialize();
|
||||
byte payload[] = "UPDATE".getBytes();
|
||||
// Test process message
|
||||
channelConfig.processMessage("test/state", payload);
|
||||
|
||||
verify(callback, atLeastOnce()).statusUpdated(eq(thing),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
|
||||
verify(callback).stateUpdated(eq(textChannelUID), argThat(arg -> "UPDATE".equals(arg.toString())));
|
||||
assertThat(textValue.getChannelState().toString(), is("UPDATE"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.internal.handler;
|
||||
|
||||
import static org.openhab.binding.mqtt.generic.internal.MqttBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* Static test definitions, like thing, bridge and channel definitions
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ThingChannelConstants {
|
||||
// Common ThingUID and ChannelUIDs
|
||||
public static final ThingUID testGenericThing = new ThingUID(GENERIC_MQTT_THING, "genericthing");
|
||||
|
||||
public static final ChannelTypeUID textChannel = new ChannelTypeUID(BINDING_ID, MqttBindingConstants.STRING);
|
||||
public static final ChannelTypeUID textWithJsonChannel = new ChannelTypeUID(BINDING_ID,
|
||||
MqttBindingConstants.STRING);
|
||||
public static final ChannelTypeUID onoffChannel = new ChannelTypeUID(BINDING_ID, MqttBindingConstants.SWITCH);
|
||||
public static final ChannelTypeUID numberChannel = new ChannelTypeUID(BINDING_ID, MqttBindingConstants.NUMBER);
|
||||
public static final ChannelTypeUID percentageChannel = new ChannelTypeUID(BINDING_ID, MqttBindingConstants.DIMMER);
|
||||
public static final ChannelTypeUID unknownChannel = new ChannelTypeUID(BINDING_ID, "unknown");
|
||||
|
||||
public static final ChannelUID textChannelUID = new ChannelUID(testGenericThing, "mytext");
|
||||
|
||||
public static final String jsonPathJSON = "{ \"device\": { \"status\": { \"temperature\": 23.2 }}}";
|
||||
public static final String jsonPathPattern = "$.device.status.temperature";
|
||||
|
||||
public static final List<Channel> thingChannelList = new ArrayList<>();
|
||||
public static final List<Channel> thingChannelListWithJson = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Create a channel with exact the parameters we need for the tests
|
||||
*
|
||||
* @param id Channel ID
|
||||
* @param acceptedType Accept type
|
||||
* @param config The configuration
|
||||
* @param channelTypeUID ChannelTypeUID provided by the static definitions
|
||||
* @return
|
||||
*/
|
||||
public static Channel cb(String id, String acceptedType, Configuration config, ChannelTypeUID channelTypeUID) {
|
||||
return ChannelBuilder.create(new ChannelUID(testGenericThing, id), acceptedType).withConfiguration(config)
|
||||
.withType(channelTypeUID).build();
|
||||
}
|
||||
|
||||
static {
|
||||
thingChannelList.add(cb("mytext", "TextItemType", textConfiguration(), textChannel));
|
||||
thingChannelList.add(cb("onoff", "OnOffType", onoffConfiguration(), onoffChannel));
|
||||
thingChannelList.add(cb("num", "NumberType", numberConfiguration(), numberChannel));
|
||||
thingChannelList.add(cb("percent", "NumberType", percentageConfiguration(), percentageChannel));
|
||||
|
||||
thingChannelListWithJson.add(cb("mytext", "TextItemType", textConfigurationWithJson(), textWithJsonChannel));
|
||||
thingChannelListWithJson.add(cb("onoff", "OnOffType", onoffConfiguration(), onoffChannel));
|
||||
thingChannelListWithJson.add(cb("num", "NumberType", numberConfiguration(), numberChannel));
|
||||
thingChannelListWithJson.add(cb("percent", "NumberType", percentageConfiguration(), percentageChannel));
|
||||
}
|
||||
|
||||
static Configuration textConfiguration() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stateTopic", "test/state");
|
||||
data.put("commandTopic", "test/command");
|
||||
return new Configuration(data);
|
||||
}
|
||||
|
||||
static Configuration textConfigurationWithJson() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stateTopic", "test/state");
|
||||
data.put("commandTopic", "test/command");
|
||||
data.put("transformationPattern", "JSONPATH:" + jsonPathPattern);
|
||||
return new Configuration(data);
|
||||
}
|
||||
|
||||
private static Configuration numberConfiguration() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stateTopic", "test/state");
|
||||
data.put("commandTopic", "test/command");
|
||||
data.put("min", BigDecimal.valueOf(1));
|
||||
data.put("max", BigDecimal.valueOf(99));
|
||||
data.put("step", BigDecimal.valueOf(2));
|
||||
data.put("isDecimal", true);
|
||||
return new Configuration(data);
|
||||
}
|
||||
|
||||
private static Configuration percentageConfiguration() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stateTopic", "test/state");
|
||||
data.put("commandTopic", "test/command");
|
||||
data.put("on", "ON");
|
||||
data.put("off", "OFF");
|
||||
return new Configuration(data);
|
||||
}
|
||||
|
||||
private static Configuration onoffConfiguration() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stateTopic", "test/state");
|
||||
data.put("commandTopic", "test/command");
|
||||
data.put("on", "ON");
|
||||
data.put("off", "OFF");
|
||||
data.put("inverse", true);
|
||||
return new Configuration(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.mapping;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass.AttributeChanged;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
|
||||
/**
|
||||
* Tests cases for {@link org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass}.
|
||||
*
|
||||
* <p>
|
||||
* How it works:
|
||||
*
|
||||
* <ol>
|
||||
* <li>A DTO (data transfer object) is defined, here it is {@link Attributes}, which extends
|
||||
* {@link org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass}.
|
||||
* <li>The createSubscriber method is mocked so that no real MQTTConnection interaction happens.
|
||||
* <li>The subscribeAndReceive method is called.
|
||||
* </ol>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class MqttTopicClassMapperTests {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ FIELD })
|
||||
private @interface TestValue {
|
||||
String value() default "";
|
||||
}
|
||||
|
||||
@TopicPrefix
|
||||
public static class Attributes extends AbstractMqttAttributeClass {
|
||||
public transient String ignoreTransient = "";
|
||||
public final String ignoreFinal = "";
|
||||
|
||||
public @TestValue("string") String aString;
|
||||
public @TestValue("false") Boolean aBoolean;
|
||||
public @TestValue("10") Long aLong;
|
||||
public @TestValue("10") Integer aInteger;
|
||||
public @TestValue("10") BigDecimal aDecimal;
|
||||
|
||||
public @TestValue("10") @TopicPrefix("a") int Int = 24;
|
||||
public @TestValue("false") boolean aBool = true;
|
||||
public @TestValue("abc,def") @MQTTvalueTransform(splitCharacter = ",") String[] properties;
|
||||
|
||||
public enum ReadyState {
|
||||
unknown,
|
||||
init,
|
||||
ready,
|
||||
}
|
||||
|
||||
public @TestValue("init") ReadyState state = ReadyState.unknown;
|
||||
|
||||
public enum DataTypeEnum {
|
||||
unknown,
|
||||
integer_,
|
||||
float_,
|
||||
}
|
||||
|
||||
public @TestValue("integer") @MQTTvalueTransform(suffix = "_") DataTypeEnum datatype = DataTypeEnum.unknown;
|
||||
|
||||
@Override
|
||||
public @NonNull Object getFieldsOf() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Mock
|
||||
MqttBrokerConnection connection;
|
||||
|
||||
@Mock
|
||||
ScheduledExecutorService executor;
|
||||
|
||||
@Mock
|
||||
AttributeChanged fieldChangedObserver;
|
||||
|
||||
@Spy
|
||||
Object countInjectedFields = new Object();
|
||||
int injectedFields = 0;
|
||||
|
||||
// A completed future is returned for a subscribe call to the attributes
|
||||
final CompletableFuture<Boolean> future = CompletableFuture.completedFuture(true);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
|
||||
injectedFields = (int) Stream.of(countInjectedFields.getClass().getDeclaredFields())
|
||||
.filter(AbstractMqttAttributeClass::filterField).count();
|
||||
}
|
||||
|
||||
public Object createSubscriberAnswer(InvocationOnMock invocation) {
|
||||
final AbstractMqttAttributeClass attributes = (AbstractMqttAttributeClass) invocation.getMock();
|
||||
final ScheduledExecutorService scheduler = (ScheduledExecutorService) invocation.getArguments()[0];
|
||||
final Field field = (Field) invocation.getArguments()[1];
|
||||
final String topic = (String) invocation.getArguments()[2];
|
||||
final boolean mandatory = (boolean) invocation.getArguments()[3];
|
||||
final SubscribeFieldToMQTTtopic s = spy(
|
||||
new SubscribeFieldToMQTTtopic(scheduler, field, attributes, topic, mandatory));
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(s).subscribeAndReceive(any(), anyInt());
|
||||
return s;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscribeToCorrectFields() {
|
||||
Attributes attributes = spy(new Attributes());
|
||||
|
||||
doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
|
||||
anyBoolean());
|
||||
|
||||
// Subscribe now to all fields
|
||||
CompletableFuture<Void> future = attributes.subscribeAndReceive(connection, executor, "homie/device123", null,
|
||||
10);
|
||||
assertThat(future.isDone(), is(true));
|
||||
assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
|
||||
}
|
||||
|
||||
// TODO timeout
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@Test
|
||||
public void subscribeAndReceive() throws IllegalArgumentException, IllegalAccessException {
|
||||
final Attributes attributes = spy(new Attributes());
|
||||
|
||||
doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
|
||||
anyBoolean());
|
||||
|
||||
verify(connection, times(0)).subscribe(anyString(), any());
|
||||
|
||||
// Subscribe now to all fields
|
||||
CompletableFuture<Void> future = attributes.subscribeAndReceive(connection, executor, "homie/device123",
|
||||
fieldChangedObserver, 10);
|
||||
assertThat(future.isDone(), is(true));
|
||||
|
||||
// We expect 10 subscriptions now
|
||||
assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
|
||||
|
||||
int loopCounter = 0;
|
||||
|
||||
// Assign each field the value of the test annotation via the processMessage method
|
||||
for (SubscribeFieldToMQTTtopic f : attributes.subscriptions) {
|
||||
@Nullable
|
||||
TestValue annotation = f.field.getAnnotation(TestValue.class);
|
||||
// A non-annotated field means a Mockito injected field.
|
||||
// Ignore that and complete the corresponding future.
|
||||
if (annotation == null) {
|
||||
f.future.complete(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
verify(f).subscribeAndReceive(any(), anyInt());
|
||||
|
||||
// Simulate a received MQTT value and use the annotation data as input.
|
||||
f.processMessage(f.topic, annotation.value().getBytes());
|
||||
verify(fieldChangedObserver, times(++loopCounter)).attributeChanged(any(), any(), any(), any(),
|
||||
anyBoolean());
|
||||
|
||||
// Check each value if the assignment worked
|
||||
if (!f.field.getType().isArray()) {
|
||||
assertNotNull(f.field.getName() + " is null", f.field.get(attributes));
|
||||
// Consider if a mapToField was used that would manipulate the received value
|
||||
MQTTvalueTransform mapToField = f.field.getAnnotation(MQTTvalueTransform.class);
|
||||
String prefix = mapToField != null ? mapToField.prefix() : "";
|
||||
String suffix = mapToField != null ? mapToField.suffix() : "";
|
||||
assertThat(f.field.get(attributes).toString(), is(prefix + annotation.value() + suffix));
|
||||
} else {
|
||||
assertThat(Stream.of((String[]) f.field.get(attributes)).reduce((v, i) -> v + "," + i).orElse(""),
|
||||
is(annotation.value()));
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(future.isDone(), is(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.mapping;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.openhab.binding.mqtt.generic.mapping.SubscribeFieldToMQTTtopic.FieldChanged;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
|
||||
/**
|
||||
* Tests cases for {@link org.openhab.binding.mqtt.generic.mapping.SubscribeFieldToMQTTtopic}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class SubscribeFieldToMQTTtopicTests {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ FIELD })
|
||||
private @interface TestValue {
|
||||
String value() default "";
|
||||
}
|
||||
|
||||
@TopicPrefix
|
||||
public static class Attributes extends AbstractMqttAttributeClass {
|
||||
@SuppressWarnings("unused")
|
||||
public transient String ignoreTransient = "";
|
||||
@SuppressWarnings("unused")
|
||||
public final String ignoreFinal = "";
|
||||
|
||||
public @TestValue("string") String aString;
|
||||
public @TestValue("false") Boolean aBoolean;
|
||||
public @TestValue("10") Long aLong;
|
||||
public @TestValue("10") Integer aInteger;
|
||||
public @TestValue("10") BigDecimal aDecimal;
|
||||
|
||||
public @TestValue("10") @TopicPrefix("a") int Int = 24;
|
||||
public @TestValue("false") boolean aBool = true;
|
||||
public @TestValue("abc,def") @MQTTvalueTransform(splitCharacter = ",") String[] properties;
|
||||
|
||||
public enum ReadyState {
|
||||
unknown,
|
||||
init,
|
||||
ready,
|
||||
}
|
||||
|
||||
public @TestValue("init") ReadyState state = ReadyState.unknown;
|
||||
|
||||
public enum DataTypeEnum {
|
||||
unknown,
|
||||
integer_,
|
||||
float_,
|
||||
}
|
||||
|
||||
public @TestValue("integer") @MQTTvalueTransform(suffix = "_") DataTypeEnum datatype = DataTypeEnum.unknown;
|
||||
|
||||
@Override
|
||||
public @NonNull Object getFieldsOf() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Attributes attributes = new Attributes();
|
||||
|
||||
@Mock
|
||||
MqttBrokerConnection connection;
|
||||
|
||||
@Mock
|
||||
SubscribeFieldToMQTTtopic fieldSubscriber;
|
||||
|
||||
@Mock
|
||||
FieldChanged fieldChanged;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
|
||||
}
|
||||
|
||||
@Test(expected = TimeoutException.class)
|
||||
public void TimeoutIfNoMessageReceive()
|
||||
throws InterruptedException, NoSuchFieldException, ExecutionException, TimeoutException {
|
||||
final Field field = Attributes.class.getField("Int");
|
||||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
SubscribeFieldToMQTTtopic subscriber = new SubscribeFieldToMQTTtopic(scheduler, field, fieldChanged,
|
||||
"homie/device123", false);
|
||||
subscriber.subscribeAndReceive(connection, 1000).get(50, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Test(expected = ExecutionException.class)
|
||||
public void MandatoryMissing()
|
||||
throws InterruptedException, NoSuchFieldException, ExecutionException, TimeoutException {
|
||||
final Field field = Attributes.class.getField("Int");
|
||||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
SubscribeFieldToMQTTtopic subscriber = new SubscribeFieldToMQTTtopic(scheduler, field, fieldChanged,
|
||||
"homie/device123", true);
|
||||
subscriber.subscribeAndReceive(connection, 50).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void MessageReceive()
|
||||
throws InterruptedException, NoSuchFieldException, ExecutionException, TimeoutException {
|
||||
final FieldChanged changed = (field, value) -> {
|
||||
try {
|
||||
field.set(attributes.getFieldsOf(), value);
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
};
|
||||
final Field field = Attributes.class.getField("Int");
|
||||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
SubscribeFieldToMQTTtopic subscriber = new SubscribeFieldToMQTTtopic(scheduler, field, changed,
|
||||
"homie/device123", false);
|
||||
CompletableFuture<@Nullable Void> future = subscriber.subscribeAndReceive(connection, 1000);
|
||||
|
||||
// Simulate a received MQTT message
|
||||
subscriber.processMessage("ignored", "10".getBytes());
|
||||
// No timeout should happen
|
||||
future.get(50, TimeUnit.MILLISECONDS);
|
||||
assertThat(attributes.Int, is(10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.generic.values;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
|
||||
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.OpenClosedType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
|
||||
/**
|
||||
* Test cases for the value classes. They should throw exceptions if the wrong command type is used
|
||||
* for an update. The percent value class should raise an exception if the value is out of range.
|
||||
*
|
||||
* The on/off value class should accept a multitude of values including the custom defined ones.
|
||||
*
|
||||
* The string value class states are tested.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class ValueTests {
|
||||
Command p(Value v, String str) {
|
||||
return TypeParser.parseCommand(v.getSupportedCommandTypes(), str);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void illegalTextStateUpdate() {
|
||||
TextValue v = new TextValue("one,two".split(","));
|
||||
v.update(p(v, "three"));
|
||||
}
|
||||
|
||||
public void textStateUpdate() {
|
||||
TextValue v = new TextValue("one,two".split(","));
|
||||
v.update(p(v, "one"));
|
||||
}
|
||||
|
||||
public void colorUpdate() {
|
||||
ColorValue v = new ColorValue(ColorMode.RGB, "fancyON", "fancyOFF", 77);
|
||||
v.update(p(v, "255, 255, 255"));
|
||||
|
||||
v.update(p(v, "OFF"));
|
||||
assertThat(((HSBType) v.getChannelState()).getBrightness().intValue(), is(0));
|
||||
v.update(p(v, "ON"));
|
||||
assertThat(((HSBType) v.getChannelState()).getBrightness().intValue(), is(77));
|
||||
|
||||
v.update(p(v, "0"));
|
||||
assertThat(((HSBType) v.getChannelState()).getBrightness().intValue(), is(0));
|
||||
v.update(p(v, "1"));
|
||||
assertThat(((HSBType) v.getChannelState()).getBrightness().intValue(), is(1));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void illegalColorUpdate() {
|
||||
ColorValue v = new ColorValue(ColorMode.RGB, null, null, 10);
|
||||
v.update(p(v, "255,255,abc"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void illegalNumberCommand() {
|
||||
NumberValue v = new NumberValue(null, null, null, null);
|
||||
v.update(OnOffType.OFF);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void illegalPercentCommand() {
|
||||
PercentageValue v = new PercentageValue(null, null, null, null, null);
|
||||
v.update(new StringType("demo"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void illegalOnOffCommand() {
|
||||
OnOffValue v = new OnOffValue(null, null);
|
||||
v.update(new DecimalType(101.0));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void illegalPercentUpdate() {
|
||||
PercentageValue v = new PercentageValue(null, null, null, null, null);
|
||||
v.update(new DecimalType(101.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onoffUpdate() {
|
||||
OnOffValue v = new OnOffValue("fancyON", "fancyOff");
|
||||
// Test with command
|
||||
v.update(OnOffType.OFF);
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(OnOffType.OFF));
|
||||
v.update(OnOffType.ON);
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(OnOffType.ON));
|
||||
|
||||
// Test with string, representing the command
|
||||
v.update(new StringType("OFF"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(OnOffType.OFF));
|
||||
v.update(new StringType("ON"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(OnOffType.ON));
|
||||
|
||||
// Test with custom string, setup in the constructor
|
||||
v.update(new StringType("fancyOff"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getMQTTpublishValue("=%s"), is("=fancyOff"));
|
||||
assertThat(v.getChannelState(), is(OnOffType.OFF));
|
||||
v.update(new StringType("fancyON"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getMQTTpublishValue("=%s"), is("=fancyON"));
|
||||
assertThat(v.getChannelState(), is(OnOffType.ON));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openCloseUpdate() {
|
||||
OpenCloseValue v = new OpenCloseValue("fancyON", "fancyOff");
|
||||
// Test with command
|
||||
v.update(OpenClosedType.CLOSED);
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(OpenClosedType.CLOSED));
|
||||
v.update(OpenClosedType.OPEN);
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(OpenClosedType.OPEN));
|
||||
|
||||
// Test with string, representing the command
|
||||
v.update(new StringType("CLOSED"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(OpenClosedType.CLOSED));
|
||||
v.update(new StringType("OPEN"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(OpenClosedType.OPEN));
|
||||
|
||||
// Test with custom string, setup in the constructor
|
||||
v.update(new StringType("fancyOff"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(OpenClosedType.CLOSED));
|
||||
v.update(new StringType("fancyON"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(OpenClosedType.OPEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollershutterUpdateWithStrings() {
|
||||
RollershutterValue v = new RollershutterValue("fancyON", "fancyOff", "fancyStop");
|
||||
// Test with command
|
||||
v.update(UpDownType.UP);
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(PercentType.ZERO));
|
||||
v.update(UpDownType.DOWN);
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(PercentType.HUNDRED));
|
||||
|
||||
// Test with custom string
|
||||
v.update(new StringType("fancyON"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyON"));
|
||||
assertThat(v.getChannelState(), is(PercentType.ZERO));
|
||||
v.update(new StringType("fancyOff"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("fancyOff"));
|
||||
assertThat(v.getChannelState(), is(PercentType.HUNDRED));
|
||||
v.update(new PercentType(27));
|
||||
assertThat(v.getMQTTpublishValue(null), is("27"));
|
||||
assertThat(v.getChannelState(), is(new PercentType(27)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollershutterUpdateWithOutStrings() {
|
||||
RollershutterValue v = new RollershutterValue(null, null, "fancyStop");
|
||||
// Test with command
|
||||
v.update(UpDownType.UP);
|
||||
assertThat(v.getMQTTpublishValue(null), is("0"));
|
||||
assertThat(v.getChannelState(), is(PercentType.ZERO));
|
||||
v.update(UpDownType.DOWN);
|
||||
assertThat(v.getMQTTpublishValue(null), is("100"));
|
||||
assertThat(v.getChannelState(), is(PercentType.HUNDRED));
|
||||
|
||||
// Test with custom string
|
||||
v.update(PercentType.ZERO);
|
||||
assertThat(v.getMQTTpublishValue(null), is("0"));
|
||||
assertThat(v.getChannelState(), is(PercentType.ZERO));
|
||||
v.update(PercentType.HUNDRED);
|
||||
assertThat(v.getMQTTpublishValue(null), is("100"));
|
||||
assertThat(v.getChannelState(), is(PercentType.HUNDRED));
|
||||
v.update(new PercentType(27));
|
||||
assertThat(v.getMQTTpublishValue(null), is("27"));
|
||||
assertThat(v.getChannelState(), is(new PercentType(27)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percentCalc() {
|
||||
PercentageValue v = new PercentageValue(new BigDecimal(10.0), new BigDecimal(110.0), new BigDecimal(1.0), null,
|
||||
null);
|
||||
v.update(new DecimalType("110.0"));
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(100)));
|
||||
assertThat(v.getMQTTpublishValue(null), is("110"));
|
||||
v.update(new DecimalType(10.0));
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(0)));
|
||||
assertThat(v.getMQTTpublishValue(null), is("10"));
|
||||
|
||||
v.update(OnOffType.ON);
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(100)));
|
||||
v.update(OnOffType.OFF);
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percentMQTTValue() {
|
||||
PercentageValue v = new PercentageValue(null, null, null, null, null);
|
||||
v.update(new DecimalType("10.10000"));
|
||||
assertThat(v.getMQTTpublishValue(null), is("10.1"));
|
||||
for (int i = 0; i <= 100; i++) {
|
||||
v.update(new DecimalType(i));
|
||||
assertThat(v.getMQTTpublishValue(null), is("" + i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percentCustomOnOff() {
|
||||
PercentageValue v = new PercentageValue(new BigDecimal("0.0"), new BigDecimal("100.0"), new BigDecimal("1.0"),
|
||||
"on", "off");
|
||||
v.update(new StringType("on"));
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(100)));
|
||||
v.update(new StringType("off"));
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decimalCalc() {
|
||||
PercentageValue v = new PercentageValue(new BigDecimal("0.1"), new BigDecimal("1.0"), new BigDecimal("0.1"),
|
||||
null, null);
|
||||
v.update(new DecimalType(1.0));
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(100)));
|
||||
v.update(new DecimalType(0.1));
|
||||
assertThat((PercentType) v.getChannelState(), is(new PercentType(0)));
|
||||
v.update(new DecimalType(0.2));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 11.11f, 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void increaseDecreaseCalc() {
|
||||
PercentageValue v = new PercentageValue(new BigDecimal("1.0"), new BigDecimal("11.0"), new BigDecimal("0.5"),
|
||||
null, null);
|
||||
|
||||
// Normal operation.
|
||||
v.update(new DecimalType("6.0"));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 50.0f, 0.01f);
|
||||
v.update(IncreaseDecreaseType.INCREASE);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 55.0f, 0.01f);
|
||||
v.update(IncreaseDecreaseType.DECREASE);
|
||||
v.update(IncreaseDecreaseType.DECREASE);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 45.0f, 0.01f);
|
||||
|
||||
// Lower limit.
|
||||
v.update(new DecimalType("1.1"));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 1.0f, 0.01f);
|
||||
v.update(IncreaseDecreaseType.DECREASE);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 0.0f, 0.01f);
|
||||
|
||||
// Upper limit.
|
||||
v.update(new DecimalType("10.8"));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 98.0f, 0.01f);
|
||||
v.update(IncreaseDecreaseType.INCREASE);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 100.0f, 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upDownCalc() {
|
||||
PercentageValue v = new PercentageValue(new BigDecimal("1.0"), new BigDecimal("11.0"), new BigDecimal("0.5"),
|
||||
null, null);
|
||||
|
||||
// Normal operation.
|
||||
v.update(new DecimalType("6.0"));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 50.0f, 0.01f);
|
||||
v.update(UpDownType.UP);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 55.0f, 0.01f);
|
||||
v.update(UpDownType.DOWN);
|
||||
v.update(UpDownType.DOWN);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 45.0f, 0.01f);
|
||||
|
||||
// Lower limit.
|
||||
v.update(new DecimalType("1.1"));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 1.0f, 0.01f);
|
||||
v.update(UpDownType.DOWN);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 0.0f, 0.01f);
|
||||
|
||||
// Upper limit.
|
||||
v.update(new DecimalType("10.8"));
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 98.0f, 0.01f);
|
||||
v.update(UpDownType.UP);
|
||||
assertEquals(((PercentType) v.getChannelState()).floatValue(), 100.0f, 0.01f);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void percentCalcInvalid() {
|
||||
PercentageValue v = new PercentageValue(new BigDecimal(10.0), new BigDecimal(110.0), new BigDecimal(1.0), null,
|
||||
null);
|
||||
v.update(new DecimalType(9.0));
|
||||
}
|
||||
}
|
||||
190
bundles/org.openhab.binding.mqtt.generic/xtend_examples.md
Normal file
190
bundles/org.openhab.binding.mqtt.generic/xtend_examples.md
Normal file
@@ -0,0 +1,190 @@
|
||||
|
||||
## Examples
|
||||
|
||||
Have a look at the following textual examples.
|
||||
|
||||
### A broker Thing with a Generic MQTT Thing and a few channels
|
||||
|
||||
demo1.things:
|
||||
|
||||
```xtend
|
||||
Bridge mqtt:broker:myUnsecureBroker [ host="192.168.0.42", secure=false ]
|
||||
{
|
||||
Thing topic mything {
|
||||
Channels:
|
||||
Type switch : lamp "Kitchen Lamp" [ stateTopic="lamp/enabled", commandTopic="lamp/enabled/set" ]
|
||||
Type switch : fancylamp "Fancy Lamp" [ stateTopic="fancy/lamp/state", commandTopic="fancy/lamp/command", on="i-am-on", off="i-am-off" ]
|
||||
Type string : alarmpanel "Alarm system" [ stateTopic="alarm/panel/state", commandTopic="alarm/panel/set", allowedStates="ARMED_HOME,ARMED_AWAY,UNARMED" ]
|
||||
Type color : lampcolor "Kitchen Lamp color" [ stateTopic="lamp/color", commandTopic="lamp/color/set", rgb=true ]
|
||||
Type dimmer : blind "Blind" [ stateTopic="blind/state", commandTopic="blind/set", min=0, max=5, step=1 ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
demo2.things:
|
||||
|
||||
```xtend
|
||||
Bridge mqtt:broker:WorkBroker "Work Broker" [ host="localhost", port="1883", secure=false, username="openhabian", password="ohmqtt", clientID="WORKOPENHAB24" ]
|
||||
|
||||
Thing mqtt:topic:WorkBroker:WorkSonoff "Work Sonoff" (mqtt:broker:WorkBroker) @ "Home" {
|
||||
Channels:
|
||||
Type switch : WorkLight "Work Light" [ stateTopic="stat/worklight/POWER", commandTopic="cmnd/worklight/POWER" ]
|
||||
Type switch : WorkLightTele "Work Tele" [ stateTopic="tele/worklight/STATE", transformationPattern="JSONPATH:$.POWER" ]
|
||||
}
|
||||
```
|
||||
tasmota.things: Example of a Tasmota Device with Availablity-Topic state and standard Online/Offline message-payload
|
||||
```xtend
|
||||
Bridge mqtt:broker:mybroker [ host="192.168.0.42", secure=false ]
|
||||
{
|
||||
Thing mqtt:topic:SP111 "SP111" [availabilityTopic="tele/tasmota/LWT", payloadAvailable="Online", payloadNotAvailable="Offline"]{
|
||||
Channels:
|
||||
Type switch : power "Power" [ stateTopic="tele/tasmota/STATE", commandTopic="cmnd/tasmota/POWER", transformationPattern="JSONPATH:$.POWER", on="ON", off="OFF" ]
|
||||
Type number : powerload "Power load" [ stateTopic="tele/tasmota/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Power"]
|
||||
Type number : voltage "Line voltage" [ stateTopic="tele/tasmota/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Voltage"]
|
||||
Type number : current "Line current" [ stateTopic="tele/tasmota/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Current"]
|
||||
Type number : total "Total energy today" [ stateTopic="tele/tasmota/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Today"]
|
||||
Type number : totalyest "Total energy yesterday" [ stateTopic="tele/tasmota/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Yesterday"]
|
||||
Type number : rssi "WiFi Signal Strength" [ stateTopic="tele/tasmota/STATE", transformationPattern="JSONPATH:$.Wifi.RSSI"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using .things and .items files for configuration, items and channels follow the format of:
|
||||
|
||||
```xtend
|
||||
<ITEM-TYPE> <ITEM-NAME> "<FRIENDLY-NAME>" { channel="mqtt:topic:<BROKER-NAME>:<THING-NAME>:<CHANNEL-NAME>" }
|
||||
```
|
||||
|
||||
demo1.items:
|
||||
|
||||
```xtend
|
||||
Switch Kitchen_Light "Kitchen Light" { channel="mqtt:topic:myUnsecureBroker:mything:lamp" }
|
||||
Rollershutter shutter "Blind" { channel="mqtt:topic:myUnsecureBroker:mything:blind" }
|
||||
```
|
||||
|
||||
demo2.items:
|
||||
|
||||
```xtend
|
||||
Switch SW_WorkLight "Work Light Switch" { channel="mqtt:topic:WorkBroker:WorkSonoff:WorkLight", channel="mqtt:topic:WorkBroker:WorkSonoff:WorkLightTele" }
|
||||
```
|
||||
|
||||
### Publish an MQTT value on startup
|
||||
|
||||
An example "demo.rules" rule to publish to `system/started` with the value `true` on every start:
|
||||
|
||||
```xtend
|
||||
rule "Send startup message"
|
||||
when
|
||||
System started
|
||||
then
|
||||
val actions = getActions("mqtt","mqtt:broker:myUnsecureBroker")
|
||||
actions.publishMQTT("system/started","true")
|
||||
end
|
||||
```
|
||||
|
||||
### Synchronize two instances
|
||||
|
||||
To synchronize item items from a SOURCE openHAB instance to a DESTINATION instance, do the following:
|
||||
|
||||
Define a broker and a trigger channel for your DESTINATION openHAB installation (`thing` file):
|
||||
|
||||
```xtend
|
||||
Bridge mqtt:broker:myUnsecureBroker [ host="192.168.0.42", secure=false ]
|
||||
{
|
||||
Channels:
|
||||
Type publishTrigger : myTriggerChannel "Receive everything" [ stateTopic="allItems/#", separator="#" ]
|
||||
}
|
||||
```
|
||||
|
||||
The trigger channel will trigger for each received message on the MQTT topic "allItems/".
|
||||
Now push those changes to your items in a `rules` file:
|
||||
|
||||
```xtend
|
||||
rule "Receive all"
|
||||
when
|
||||
Channel "mqtt:broker:myUnsecureBroker:myTriggerChannel" triggered
|
||||
then
|
||||
//The receivedEvent String contains unneeded elements like the mqtt topic, we only need everything after the "/" as this is were item name and state are
|
||||
val parts1 = receivedEvent.toString.split("/").get(1)
|
||||
val parts2 = parts1.split("#")
|
||||
sendCommand(parts2.get(0), parts2.get(1))
|
||||
end
|
||||
```
|
||||
|
||||
On your SOURCE openHAB installation, you need to define a group `myGroupOfItems` and add all items
|
||||
to it that you want to synchronize. Then add this rule to a `rule` file:
|
||||
|
||||
```xtend
|
||||
rule "Publish all"
|
||||
when
|
||||
Member of myGroupOfItems changed
|
||||
then
|
||||
val actions = getActions("mqtt","mqtt:broker:myUnsecureBroker")
|
||||
actions.publishMQTT("allItems/"+triggeringItem.name,triggeringItem.state.toString)
|
||||
end
|
||||
```
|
||||
|
||||
## Converting an MQTT1 installation
|
||||
|
||||
The conversion is straight forward, but need to be done for each item.
|
||||
You do not need to convert everything in one go. MQTT1 and MQTT2 can coexist.
|
||||
|
||||
> For mqtt1 make sure you have enabled the Legacy 1.x repository and installed "mqtt1".
|
||||
|
||||
### 1 Command / 1 State topic
|
||||
|
||||
Assume you have this item:
|
||||
|
||||
```xtend
|
||||
Switch ExampleItem "Heatpump Power" { mqtt=">[mosquitto:heatpump/set:command:*:DEFAULT)],<[mosquitto:heatpump:JSONPATH($.power)]" }
|
||||
```
|
||||
|
||||
This converts to an entry in your *.things file with a **Broker Thing** and a **Generic MQTT Thing** that uses the bridge:
|
||||
|
||||
```xtend
|
||||
Bridge mqtt:broker:myUnsecureBroker [ host="192.168.0.42", secure=false ]
|
||||
{
|
||||
Thing topic mything "My Thing" {
|
||||
Channels:
|
||||
Type switch : heatpumpChannel "Heatpump Power" [ stateTopic="heatpump", commandTopic="heatpump/set", transformationPattern="JSONPATH:$.power" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add as many channels as you have items and add the *stateTopic* and *commandTopic* accordingly.
|
||||
|
||||
Your items change to:
|
||||
|
||||
```xtend
|
||||
Switch ExampleItem "Heatpump Power" { channel="mqtt:topic:myUnsecureBroker:mything:heatpumpChannel" }
|
||||
```
|
||||
|
||||
|
||||
### 1 Command / 2 State topics
|
||||
|
||||
If you receive updates from two different topics, you need to create multiple channels now, 1 for each MQTT receive topic.
|
||||
|
||||
```xtend
|
||||
Switch ExampleItem "Heatpump Power" { mqtt=">[mosquitto:heatpump/set:command:*:DEFAULT)],<[mosquitto:heatpump/state1:state:*:DEFAULT,<[mosquitto:heatpump/state2:state:*:DEFAULT" }
|
||||
```
|
||||
|
||||
This converts to:
|
||||
|
||||
```xtend
|
||||
Bridge mqtt:broker:myUnsecureBroker [ host="192.168.0.42", secure=false ]
|
||||
{
|
||||
Thing topic mything "My Thing" {
|
||||
Channels:
|
||||
Type switch : heatpumpChannel "Heatpump Power" [ stateTopic="heatpump/state1", commandTopic="heatpump/set" ]
|
||||
Type switch : heatpumpChannel2 "Heatpump Power" [ stateTopic="heatpump/state2" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Link both channels to one item. That item will publish to "heatpump/set" on a change and
|
||||
receive values from "heatpump/state1" and "heatpump/state2".
|
||||
|
||||
```xtend
|
||||
Switch ExampleItem "Heatpump Power" { channel="mqtt:topic:myUnsecureBroker:mything:heatpumpChannel",
|
||||
channel="mqtt:topic:myUnsecureBroker:mything:heatpumpChannel2" }
|
||||
```
|
||||
Reference in New Issue
Block a user