added migrated 2.x add-ons

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

View File

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

View File

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

View 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.

View 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 Architecture](doc/mqtt.jpg)
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”.
![Publish and Subscribe](doc/subpub.png)
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
* &#64;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
*
* &#64;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();
}

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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 {
* * &#64;MandatoryField
* String name;
* }
* </pre>
*
* @author David Graeff - Initial contribution
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD })
public @interface MandatoryField {
boolean value() default true;
}

View File

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

View File

@@ -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>
* &#64;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 "$";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
binding.mqttgeneric.name = Allgemeines MQTT Binding
binding.mqttgeneric.description = Verknüpfung von MQTT Topics mit Things

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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" }
```