[mqtt.generic] Support chaining transformations without an intersection symbol (#17290)

* [mqtt.generic] Support chaining transformations without an intersection symbol

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng 2024-09-01 20:54:37 +10:00 committed by GitHub
parent 03863feab4
commit 9c5689df90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 299 additions and 532 deletions

View File

@ -58,7 +58,8 @@ The following optional parameters can be set for the Thing:
- **availabilityTopic**: The MQTT topic that represents the availability of the thing. This can be the thing's LWT topic. - **availabilityTopic**: The MQTT topic that represents the availability of the thing. This can be the thing's LWT topic.
- **payloadAvailable**: Payload of the `Availability Topic`, when the device is available. Default: `ON`. - **payloadAvailable**: Payload of the `Availability Topic`, when the device is available. Default: `ON`.
- **payloadNotAvailable**: Payload of the `Availability Topic`, when the device is _not_ available. Default: `OFF`. - **payloadNotAvailable**: Payload of the `Availability Topic`, when the device is _not_ available. Default: `OFF`.
- **transformationPattern**: An optional transformation pattern like [JSONPath](https://goessner.net/articles/JsonPath/index.html#e2) that is applied to the incoming availability payload. Transformations can be chained by separating them with the mathematical intersection character "∩". The result of the transformations is then checked against `payloadAvailable` and `payloadNotAvailable`. - **transformationPattern**: An optional transformation pattern like [JSONPath](https://goessner.net/articles/JsonPath/index.html#e2) that is applied to the incoming availability payload.
The result of the transformations is then checked against `payloadAvailable` and `payloadNotAvailable`.
## Supported Channels ## Supported Channels
@ -266,7 +267,16 @@ If the availability status is available, it can be configured to set the Thing s
```java ```java
Thing mqtt:topic:bedroom1-switch (mqtt:broker:myInsecureBroker) [ availabilityTopic="tele/bedroom1-switch/LWT", payloadAvailable="Online", payloadNotAvailable="Offline" ] { Thing mqtt:topic:bedroom1-switch (mqtt:broker:myInsecureBroker) [ availabilityTopic="tele/bedroom1-switch/LWT", payloadAvailable="Online", payloadNotAvailable="Offline" ] {
Channels: Channels:
Type switch : power [ stateTopic="stat/bedroom1-switch/RESULT", transformationPattern="REGEX:(.*POWER.*)∩JSONPATH:$.POWER", commandTopic="cmnd/bedroom1-switch/POWER" ] Type switch : power [ stateTopic="stat/bedroom1-switch/RESULT", transformationPattern="REGEX((.*POWER.*))∩JSONPATH($.POWER)", commandTopic="cmnd/bedroom1-switch/POWER" ]
}
```
The transformation pattern can be chained using the intersection character "∩" as above, or by listing them separately:
```java
Thing mqtt:topic:bedroom1-switch (mqtt:broker:myInsecureBroker) [ availabilityTopic="tele/bedroom1-switch/LWT", payloadAvailable="Online", payloadNotAvailable="Offline" ] {
Channels:
Type switch : power [ stateTopic="stat/bedroom1-switch/RESULT", transformationPattern="REGEX((.*POWER.*))","JSONPATH($.POWER)", commandTopic="cmnd/bedroom1-switch/POWER" ]
} }
``` ```
@ -277,6 +287,19 @@ Thing mqtt:topic:bedroom1-switch (mqtt:broker:myInsecureBroker) [ availabilityTo
- The HomeAssistant Light Component does not support XY color changes. - The HomeAssistant Light Component does not support XY color changes.
- The HomeAssistant Climate Components is not yet supported. - The HomeAssistant Climate Components is not yet supported.
## Value Transformations
[Transformations](/docs/configuration/transformations.html) can be applied to:
- Incoming availability payload
- Incoming value
- Outgoing value
Transformations can be chained in the UI by listing each transformation on a separate line, or by separating them with the mathematical intersection character "∩".
Transformations are defined using this syntax: `TYPE(FUNCTION)`, e.g.: `JSONPATH($.path)`.
The syntax: `TYPE:FUNCTION` is still supported, e.g.: `JSONPATH:$.path`.
Please note that the values will be discarded if one of the transformations failed (e.g. REGEX did not match) or returned `null`.
## Incoming Value Transformation ## Incoming Value Transformation
All mentioned channels allow an optional transformation for incoming MQTT topic values. All mentioned channels allow an optional transformation for incoming MQTT topic values.
@ -285,20 +308,22 @@ 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: Here are a few examples to unwrap a value from a complex response:
| Received value | Tr. Service | Transformation | | Received value | Tr. Service | Transformation |
|---------------------------------------------------------------------|-------------|-------------------------------------------| | ------------------------------------------------------------------- | ---------------- | ------------------------------------------ |
| `{device: {status: { temperature: 23.2 }}}` | JSONPATH | `JSONPATH:$.device.status.temperature` | | `{device: {status: { temperature: 23.2 }}}` | JSONPATH | `JSONPATH($.device.status.temperature)` |
| `<device><status><temperature>23.2</temperature></status></device>` | XPath | `XPath:/device/status/temperature/text()` | | `<device><status><temperature>23.2</temperature></status></device>` | XPath | `XPath(/device/status/temperature/text())` |
| `THEVALUE:23.2°C` | REGEX | `REGEX::(.*?)°` | | `THEVALUE:23.2°C` | REGEX | `REGEX(:(.*?)°)` |
| `abc` | JS (UI defined) | `JS(config:js:35edb3735a)` |
Transformations can be chained by separating them with the mathematical intersection character "∩". | `abc` | JS (file based) | `JS(to_uppercase.js)` |
Please note that the incoming value will be discarded if one transformation fails (e.g. REGEX did not match). | `abc` | JS (inline) | `JS(\| input.toUpperCase() )` |
| `true` | MAP (UI defined) | `MAP(config:map:54facda0f7)` |
| `true` | MAP (file based) | `MAP(status.map)` |
| `true` | MAP (inline) | `MAP(\|true=ON;false=OFF)` |
## Outgoing Value Transformation ## Outgoing Value Transformation
All mentioned channels allow an optional transformation for outgoing values. All mentioned channels allow an optional transformation for outgoing values.
Please prefer formatting as described in the next section whenever possible. 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 ## Format before Publish
@ -321,19 +346,19 @@ Here are a few examples:
- For an output of _23:15_ use "%1$**tH**:%1$**tM**". - For an output of _23:15_ use "%1$**tH**:%1$**tM**".
Default pattern applied for each type: Default pattern applied for each type:
| Type | Parameter | Pattern | Comment | | Type | Parameter | Pattern | Comment |
| ---------------- | --------------------------------- | ------------------- | ------- | | ----------------- | ---------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **string** | String | "%s" | | **string** | String | "%s" | |
| **number** | BigDecimal | "%f" | The default will remove trailing zeros after the decimal point. | **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. | **dimmer** | BigDecimal | "%f" | The default will remove trailing zeros after the decimal point. |
| **contact** | String | -- | No pattern supported. Always **on** and **off** strings. | **contact** | String | -- | No pattern supported. Always **on** and **off** strings. |
| **switch** | 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. | **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. | **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'. | **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. | **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. | **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. | **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. Any outgoing value transformation will **always** result in a **string** value.

View File

@ -13,6 +13,7 @@
package org.openhab.binding.mqtt.generic; package org.openhab.binding.mqtt.generic;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -301,18 +302,19 @@ public abstract class AbstractMQTTThingHandler extends BaseThingHandler
@Override @Override
public void addAvailabilityTopic(String availability_topic, String payload_available, public void addAvailabilityTopic(String availability_topic, String payload_available,
String payload_not_available) { String payload_not_available) {
addAvailabilityTopic(availability_topic, payload_available, payload_not_available, null, null); addAvailabilityTopic(availability_topic, payload_available, payload_not_available, List.of());
} }
@Override @Override
public void addAvailabilityTopic(String availability_topic, String payload_available, String payload_not_available, public void addAvailabilityTopic(String availability_topic, String payload_available, String payload_not_available,
@Nullable String transformation_pattern, List<String> transformation_pattern) {
@Nullable TransformationServiceProvider transformationServiceProvider) {
availabilityStates.computeIfAbsent(availability_topic, topic -> { availabilityStates.computeIfAbsent(availability_topic, topic -> {
Value value = new OnOffValue(payload_available, payload_not_available); Value value = new OnOffValue(payload_available, payload_not_available);
ChannelGroupUID groupUID = new ChannelGroupUID(getThing().getUID(), "availability"); ChannelGroupUID groupUID = new ChannelGroupUID(getThing().getUID(), "availability");
ChannelUID channelUID = new ChannelUID(groupUID, UIDUtils.encode(topic)); ChannelUID channelUID = new ChannelUID(groupUID, UIDUtils.encode(topic));
ChannelState state = new ChannelState(ChannelConfigBuilder.create().withStateTopic(topic).build(), ChannelState state = new ChannelState(
ChannelConfigBuilder.create().withStateTopic(topic)
.withTransformationPattern(transformation_pattern).build(),
channelUID, value, new ChannelStateUpdateListener() { channelUID, value, new ChannelStateUpdateListener() {
@Override @Override
public void updateChannelState(ChannelUID channelUID, State value) { public void updateChannelState(ChannelUID channelUID, State value) {
@ -328,9 +330,6 @@ public abstract class AbstractMQTTThingHandler extends BaseThingHandler
public void postChannelCommand(ChannelUID channelUID, Command value) { public void postChannelCommand(ChannelUID channelUID, Command value) {
} }
}); });
if (transformation_pattern != null && transformationServiceProvider != null) {
state.addTransformation(transformation_pattern, transformationServiceProvider);
}
MqttBrokerConnection connection = getConnection(); MqttBrokerConnection connection = getConnection();
if (connection != null) { if (connection != null) {
state.start(connection, scheduler, 0); state.start(connection, scheduler, 0);

View File

@ -12,8 +12,9 @@
*/ */
package org.openhab.binding.mqtt.generic; package org.openhab.binding.mqtt.generic;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* Interface to keep track of the availability of device using an availability topic or messages received * Interface to keep track of the availability of device using an availability topic or messages received
@ -72,12 +73,9 @@ public interface AvailabilityTracker {
* @param payload_not_available The value for the topic to indicate the device is offline. * @param payload_not_available The value for the topic to indicate the device is offline.
* @param transformation_pattern A transformation pattern to process the value before comparing to * @param transformation_pattern A transformation pattern to process the value before comparing to
* payload_available/payload_not_available. * payload_available/payload_not_available.
* @param transformationServiceProvider The service provider to obtain the transformation service (required only if
* transformation_pattern is not null).
*/ */
void addAvailabilityTopic(String availability_topic, String payload_available, String payload_not_available, void addAvailabilityTopic(String availability_topic, String payload_available, String payload_not_available,
@Nullable String transformation_pattern, List<String> transformation_pattern);
@Nullable TransformationServiceProvider transformationServiceProvider);
void removeAvailabilityTopic(String availability_topic); void removeAvailabilityTopic(String availability_topic);

View File

@ -13,6 +13,7 @@
package org.openhab.binding.mqtt.generic; package org.openhab.binding.mqtt.generic;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -47,8 +48,8 @@ public class ChannelConfig {
public boolean trigger = false; public boolean trigger = false;
public String unit = ""; public String unit = "";
public String transformationPattern = ""; public List<String> transformationPattern = List.of();
public String transformationPatternOut = ""; public List<String> transformationPatternOut = List.of();
public String formatBeforePublish = "%s"; public String formatBeforePublish = "%s";
public String allowedStates = ""; public String allowedStates = "";

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.mqtt.generic; package org.openhab.binding.mqtt.generic;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -80,4 +82,14 @@ public class ChannelConfigBuilder {
config.trigger = trigger; config.trigger = trigger;
return this; return this;
} }
public ChannelConfigBuilder withTransformationPattern(List<String> pattern) {
config.transformationPattern = pattern;
return this;
}
public ChannelConfigBuilder withTransformationPatternOut(List<String> pattern) {
config.transformationPatternOut = pattern;
return this;
}
} }

View File

@ -13,14 +13,12 @@
package org.openhab.binding.mqtt.generic; package org.openhab.binding.mqtt.generic;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.IllegalFormatException; import java.util.IllegalFormatException;
import java.util.List; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -33,6 +31,7 @@ import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StopMoveType; import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.Type; import org.openhab.core.types.Type;
@ -60,8 +59,8 @@ public class ChannelState implements MqttMessageSubscriber {
// Runtime variables // Runtime variables
private @Nullable MqttBrokerConnection connection; private @Nullable MqttBrokerConnection connection;
protected final List<ChannelStateTransformation> transformationsIn = new ArrayList<>(); protected final ChannelTransformation incomingTransformation;
protected final List<ChannelStateTransformation> transformationsOut = new ArrayList<>(); protected final ChannelTransformation outgoingTransformation;
private @Nullable ChannelStateUpdateListener channelStateUpdateListener; private @Nullable ChannelStateUpdateListener channelStateUpdateListener;
protected boolean hasSubscribed = false; protected boolean hasSubscribed = false;
private @Nullable ScheduledFuture<?> scheduledFuture; private @Nullable ScheduledFuture<?> scheduledFuture;
@ -84,56 +83,14 @@ public class ChannelState implements MqttMessageSubscriber {
this.channelUID = channelUID; this.channelUID = channelUID;
this.cachedValue = cachedValue; this.cachedValue = cachedValue;
this.readOnly = config.commandTopic.isBlank(); this.readOnly = config.commandTopic.isBlank();
this.incomingTransformation = new ChannelTransformation(config.transformationPattern);
this.outgoingTransformation = new ChannelTransformation(config.transformationPatternOut);
} }
public boolean isReadOnly() { public boolean isReadOnly() {
return this.readOnly; 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);
}
public void addTransformation(String transformation, TransformationServiceProvider transformationServiceProvider) {
parseTransformation(transformation, transformationServiceProvider).forEach(t -> addTransformation(t));
}
/**
* 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);
}
public void addTransformationOut(String transformation,
TransformationServiceProvider transformationServiceProvider) {
parseTransformation(transformation, transformationServiceProvider).forEach(t -> addTransformationOut(t));
}
public static Stream<ChannelStateTransformation> parseTransformation(String transformation,
TransformationServiceProvider transformationServiceProvider) {
String[] transformations = transformation.split("");
return Stream.of(transformations).filter(t -> !t.isBlank())
.map(t -> new ChannelStateTransformation(t, transformationServiceProvider));
}
/**
* Clear transformations
*/
public void clearTransformations() {
transformationsIn.clear();
transformationsOut.clear();
}
/** /**
* Returns the cached value state object of this message subscriber. * Returns the cached value state object of this message subscriber.
* <p> * <p>
@ -176,15 +133,15 @@ public class ChannelState implements MqttMessageSubscriber {
// String value: Apply transformations // String value: Apply transformations
String strValue = new String(payload, StandardCharsets.UTF_8); String strValue = new String(payload, StandardCharsets.UTF_8);
for (ChannelStateTransformation t : transformationsIn) { if (incomingTransformation.isPresent()) {
String transformedValue = t.processValue(strValue); Optional<String> transformedValue = incomingTransformation.apply(strValue);
if (transformedValue != null) { if (transformedValue.isEmpty()) {
strValue = transformedValue; logger.debug("Transformation '{}' returned null on '{}', discarding message", strValue,
} else { incomingTransformation);
logger.debug("Transformation '{}' returned null on '{}', discarding message", strValue, t.serviceName);
receivedOrTimeout(); receivedOrTimeout();
return; return;
} }
strValue = transformedValue.get();
} }
// Is trigger?: Special handling // Is trigger?: Special handling
@ -380,7 +337,7 @@ public class ChannelState implements MqttMessageSubscriber {
} }
// Outgoing transformations // Outgoing transformations
for (ChannelStateTransformation t : transformationsOut) { if (outgoingTransformation.isPresent()) {
Command cValue = mqttCommandValue; Command cValue = mqttCommandValue;
// Only pass numeric value for QuantityType. // Only pass numeric value for QuantityType.
if (mqttCommandValue instanceof QuantityType<?> qtCommandValue) { if (mqttCommandValue instanceof QuantityType<?> qtCommandValue) {
@ -388,15 +345,15 @@ public class ChannelState implements MqttMessageSubscriber {
} }
String commandString = mqttFormatter.getMQTTpublishValue(cValue, "%s"); String commandString = mqttFormatter.getMQTTpublishValue(cValue, "%s");
String transformedValue = t.processValue(commandString); Optional<String> transformedValue = outgoingTransformation.apply(commandString);
if (transformedValue != null) { if (transformedValue.isEmpty()) {
mqttFormatter = new TextValue(); logger.debug("Transformation '{}' returned null on '{}', discarding message", outgoingTransformation,
mqttCommandValue = new StringType(transformedValue); commandString);
} else {
logger.debug("Transformation '{}' returned null on '{}', discarding message", mqttCommandValue,
t.serviceName);
return CompletableFuture.completedFuture(false); return CompletableFuture.completedFuture(false);
} }
mqttFormatter = new TextValue();
mqttCommandValue = new StringType(transformedValue.get());
} }
String commandString; String commandString;

View File

@ -1,98 +0,0 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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: {}. Pattern: '{}'. Value: '{}'", serviceName,
e.getMessage(), pattern, value);
}
return returnValue;
}
}

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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

@ -19,15 +19,12 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; 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.binding.mqtt.generic.internal.handler.GenericMQTTThingHandler;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; 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.ComponentContext;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
@ -42,7 +39,7 @@ import org.osgi.service.component.annotations.Reference;
*/ */
@Component(service = ThingHandlerFactory.class) @Component(service = ThingHandlerFactory.class)
@NonNullByDefault @NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider { public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private @NonNullByDefault({}) MqttChannelStateDescriptionProvider stateDescriptionProvider; private @NonNullByDefault({}) MqttChannelStateDescriptionProvider stateDescriptionProvider;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.GENERIC_MQTT_THING).collect(Collectors.toSet()); .of(MqttBindingConstants.GENERIC_MQTT_THING).collect(Collectors.toSet());
@ -78,13 +75,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(MqttBindingConstants.GENERIC_MQTT_THING)) { if (thingTypeUID.equals(MqttBindingConstants.GENERIC_MQTT_THING)) {
return new GenericMQTTThingHandler(thing, stateDescriptionProvider, this, 1500); return new GenericMQTTThingHandler(thing, stateDescriptionProvider, 1500);
} }
return null; return null;
} }
@Override
public @Nullable TransformationService getTransformationService(String type) {
return TransformationHelper.getTransformationService(bundleContext, type);
}
} }

View File

@ -29,7 +29,6 @@ import org.openhab.binding.mqtt.generic.ChannelConfig;
import org.openhab.binding.mqtt.generic.ChannelState; import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.generic.utils.FutureCollector; import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
@ -59,21 +58,18 @@ public class GenericMQTTThingHandler extends AbstractMQTTThingHandler implements
private final Logger logger = LoggerFactory.getLogger(GenericMQTTThingHandler.class); private final Logger logger = LoggerFactory.getLogger(GenericMQTTThingHandler.class);
final Map<ChannelUID, ChannelState> channelStateByChannelUID = new HashMap<>(); final Map<ChannelUID, ChannelState> channelStateByChannelUID = new HashMap<>();
protected final MqttChannelStateDescriptionProvider stateDescProvider; protected final MqttChannelStateDescriptionProvider stateDescProvider;
protected final TransformationServiceProvider transformationServiceProvider;
/** /**
* Creates a new Thing handler for generic MQTT channels. * Creates a new Thing handler for generic MQTT channels.
* *
* @param thing The thing of this handler * @param thing The thing of this handler
* @param stateDescProvider A channel state provider * @param stateDescProvider A channel state provider
* @param transformationServiceProvider The transformation service provider
* @param subscribeTimeout The subscribe timeout * @param subscribeTimeout The subscribe timeout
*/ */
public GenericMQTTThingHandler(Thing thing, MqttChannelStateDescriptionProvider stateDescProvider, public GenericMQTTThingHandler(Thing thing, MqttChannelStateDescriptionProvider stateDescProvider,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout) { int subscribeTimeout) {
super(thing, subscribeTimeout); super(thing, subscribeTimeout);
this.stateDescProvider = stateDescProvider; this.stateDescProvider = stateDescProvider;
this.transformationServiceProvider = transformationServiceProvider;
} }
@Override @Override
@ -129,14 +125,7 @@ public class GenericMQTTThingHandler extends AbstractMQTTThingHandler implements
* @return * @return
*/ */
protected ChannelState createChannelState(ChannelConfig channelConfig, ChannelUID channelUID, Value valueState) { protected ChannelState createChannelState(ChannelConfig channelConfig, ChannelUID channelUID, Value valueState) {
ChannelState state = new ChannelState(channelConfig, channelUID, valueState, this); return new ChannelState(channelConfig, channelUID, valueState, this);
// Incoming value transformations
state.addTransformation(channelConfig.transformationPattern, transformationServiceProvider);
// Outgoing value transformations
state.addTransformationOut(channelConfig.transformationPatternOut, transformationServiceProvider);
return state;
} }
@Override @Override
@ -230,7 +219,7 @@ public class GenericMQTTThingHandler extends AbstractMQTTThingHandler implements
if (availabilityTopic != null) { if (availabilityTopic != null) {
addAvailabilityTopic(availabilityTopic, config.payloadAvailable, config.payloadNotAvailable, addAvailabilityTopic(availabilityTopic, config.payloadAvailable, config.payloadNotAvailable,
config.transformationPattern, transformationServiceProvider); config.transformationPattern);
} else { } else {
clearAllAvailabilityTopics(); clearAllAvailabilityTopics();
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.mqtt.generic.internal.handler; package org.openhab.binding.mqtt.generic.internal.handler;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
@ -42,5 +44,5 @@ public class GenericThingConfiguration {
/** /**
* transformation pattern for the availability payload * transformation pattern for the availability payload
*/ */
public @Nullable String transformationPattern; public List<String> transformationPattern = List.of();
} }

View File

@ -32,18 +32,19 @@
<label>MQTT Command Topic</label> <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> <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>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. Applies transformations to an incoming MQTT topic value.
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
a json {device: {status: { temperature: 23.2 }}}. a json {device: {status: { temperature: 23.2 }}}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPatternOut" type="text" groupName="transformations"> <parameter name="transformationPatternOut" type="text" groupName="transformations" multiple="true">
<label>Outgoing Value Transformation</label> <label>Outgoing Value Transformation</label>
<description><![CDATA[ <description><![CDATA[
Applies a transformation before publishing a MQTT topic value. Applies a transformation before publishing a MQTT topic value.

View File

@ -21,18 +21,19 @@
<label>MQTT Command Topic</label> <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> <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>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. Applies transformations to an incoming MQTT topic value.
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
a json {device: {status: { temperature: 23.2 }}}. a json {device: {status: { temperature: 23.2 }}}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPatternOut" type="text" groupName="transformations"> <parameter name="transformationPatternOut" type="text" groupName="transformations" multiple="true">
<label>Outgoing Value Transformation</label> <label>Outgoing Value Transformation</label>
<description><![CDATA[ <description><![CDATA[
Applies a transformation before publishing a MQTT topic value. Applies a transformation before publishing a MQTT topic value.

View File

@ -21,18 +21,19 @@
<label>MQTT Command Topic</label> <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> <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>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. Applies transformations to an incoming MQTT topic value.
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
a json {device: {status: { temperature: 23.2 }}}. a json {device: {status: { temperature: 23.2 }}}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPatternOut" type="text" groupName="transformations"> <parameter name="transformationPatternOut" type="text" groupName="transformations" multiple="true">
<label>Outgoing Value Transformation</label> <label>Outgoing Value Transformation</label>
<description><![CDATA[ <description><![CDATA[
Applies a transformation before publishing a MQTT topic value. Applies a transformation before publishing a MQTT topic value.

View File

@ -27,18 +27,19 @@
<description>An MQTT topic that this thing will send a STOP command to. If not set, it will send STOP commands to the <description>An MQTT topic that this thing will send a STOP command to. If not set, it will send STOP commands to the
main commandTopic.</description> main commandTopic.</description>
</parameter> </parameter>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. Applies transformations to an incoming MQTT topic value.
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
a json {device: {status: { temperature: 23.2 }}}. a json {device: {status: { temperature: 23.2 }}}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPatternOut" type="text" groupName="transformations"> <parameter name="transformationPatternOut" type="text" groupName="transformations" multiple="true">
<label>Outgoing Value Transformation</label> <label>Outgoing Value Transformation</label>
<description><![CDATA[ <description><![CDATA[
Applies a transformation before publishing a MQTT topic value. Applies a transformation before publishing a MQTT topic value.

View File

@ -21,18 +21,19 @@
<label>MQTT Command Topic</label> <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> <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>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. Applies transformations to an incoming MQTT topic value.
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
a json {device: {status: { temperature: 23.2 }}}. a json {device: {status: { temperature: 23.2 }}}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPatternOut" type="text" groupName="transformations"> <parameter name="transformationPatternOut" type="text" groupName="transformations" multiple="true">
<label>Outgoing Value Transformation</label> <label>Outgoing Value Transformation</label>
<description><![CDATA[ <description><![CDATA[
Applies a transformation before publishing a MQTT topic value. Applies a transformation before publishing a MQTT topic value.

View File

@ -21,18 +21,19 @@
<label>MQTT Command Topic</label> <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> <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>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. Applies transformations to an incoming MQTT topic value.
A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for
a json {device: {status: { temperature: 23.2 }}}. a json {device: {status: { temperature: 23.2 }}}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPatternOut" type="text" groupName="transformations"> <parameter name="transformationPatternOut" type="text" groupName="transformations" multiple="true">
<label>Outgoing Value Transformation</label> <label>Outgoing Value Transformation</label>
<description><![CDATA[ <description><![CDATA[
Applies a transformation before publishing a MQTT topic value. Applies a transformation before publishing a MQTT topic value.

View File

@ -15,14 +15,15 @@
<label>MQTT Trigger Topic</label> <label>MQTT Trigger Topic</label>
<description>An MQTT topic that this thing will subscribe to, to receive the trigger</description> <description>An MQTT topic that this thing will subscribe to, to receive the trigger</description>
</parameter> </parameter>
<parameter name="transformationPattern" type="text" groupName="transformations"> <parameter name="transformationPattern" type="text" groupName="transformations" multiple="true">
<label>Incoming Value Transformations</label> <label>Incoming Value Transformations</label>
<description><![CDATA[ <description><![CDATA[
Applies transformations to an incoming MQTT topic value. 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, This can be used to map the events sent by the device to common values for all devices using,
e.g. the MAP transformation. e.g. the MAP transformation.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]></description> ]]></description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>

View File

@ -12,7 +12,7 @@ thing-type.config.mqtt.topic.payloadAvailable.description = Payload of the 'Avai
thing-type.config.mqtt.topic.payloadNotAvailable.label = Device Unavailable Payload thing-type.config.mqtt.topic.payloadNotAvailable.label = Device Unavailable Payload
thing-type.config.mqtt.topic.payloadNotAvailable.description = Payload of the 'Availability Topic', when the device is *not* available. Default: 'OFF' thing-type.config.mqtt.topic.payloadNotAvailable.description = Payload of the 'Availability Topic', when the device is *not* available. Default: 'OFF'
thing-type.config.mqtt.topic.transformationPattern.label = Availability Payload Transformations thing-type.config.mqtt.topic.transformationPattern.label = Availability Payload Transformations
thing-type.config.mqtt.topic.transformationPattern.description = Applies transformations to the incoming availability payload. A transformation example for a received JSON would be "JSONPATH:$.status" for a json {status: "Online"}. You can chain transformations by separating them with the intersection character ∩. thing-type.config.mqtt.topic.transformationPattern.description = Applies transformations to the incoming availability payload. A transformation example for a received JSON would be "JSONPATH:$.status" for a json {status: "Online"}. You can chain transformations by listing each transformation on a separate line, or by separating them with the intersection character ∩.
# channel types # channel types
@ -64,7 +64,7 @@ thing-type.config.mqtt.color_channel.retained.description = The value will be pu
thing-type.config.mqtt.color_channel.stateTopic.label = MQTT State Topic thing-type.config.mqtt.color_channel.stateTopic.label = MQTT State Topic
thing-type.config.mqtt.color_channel.stateTopic.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. thing-type.config.mqtt.color_channel.stateTopic.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.
thing-type.config.mqtt.color_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.color_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.color_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.color_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.
thing-type.config.mqtt.color_channel.transformationPatternOut.label = Outgoing Value Transformation thing-type.config.mqtt.color_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.color_channel.transformationPatternOut.description = 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. thing-type.config.mqtt.color_channel.transformationPatternOut.description = 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.
thing-type.config.mqtt.dimmer_channel.commandTopic.label = MQTT Command Topic thing-type.config.mqtt.dimmer_channel.commandTopic.label = MQTT Command Topic
@ -95,7 +95,7 @@ thing-type.config.mqtt.dimmer_channel.stateTopic.description = An MQTT topic tha
thing-type.config.mqtt.dimmer_channel.step.label = Delta Value thing-type.config.mqtt.dimmer_channel.step.label = Delta Value
thing-type.config.mqtt.dimmer_channel.step.description = A number/dimmer channel can receive INCREASE/DECREASE commands and computes the target number by adding or subtracting this delta value. thing-type.config.mqtt.dimmer_channel.step.description = A number/dimmer channel can receive INCREASE/DECREASE commands and computes the target number by adding or subtracting this delta value.
thing-type.config.mqtt.dimmer_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.dimmer_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.dimmer_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.dimmer_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.
thing-type.config.mqtt.dimmer_channel.transformationPatternOut.label = Outgoing Value Transformation thing-type.config.mqtt.dimmer_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.dimmer_channel.transformationPatternOut.description = 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. thing-type.config.mqtt.dimmer_channel.transformationPatternOut.description = 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.
thing-type.config.mqtt.number_channel.commandTopic.label = MQTT Command Topic thing-type.config.mqtt.number_channel.commandTopic.label = MQTT Command Topic
@ -122,7 +122,7 @@ thing-type.config.mqtt.number_channel.stateTopic.description = An MQTT topic tha
thing-type.config.mqtt.number_channel.step.label = Delta Value thing-type.config.mqtt.number_channel.step.label = Delta Value
thing-type.config.mqtt.number_channel.step.description = A number/dimmer channel can receive INCREASE/DECREASE commands and computes the target number by adding or subtracting this delta value. thing-type.config.mqtt.number_channel.step.description = A number/dimmer channel can receive INCREASE/DECREASE commands and computes the target number by adding or subtracting this delta value.
thing-type.config.mqtt.number_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.number_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.number_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.number_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.
thing-type.config.mqtt.number_channel.transformationPatternOut.label = Outgoing Value Transformation thing-type.config.mqtt.number_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.number_channel.transformationPatternOut.description = 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. thing-type.config.mqtt.number_channel.transformationPatternOut.description = 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.
thing-type.config.mqtt.number_channel.unit.label = Unit Of Measurement thing-type.config.mqtt.number_channel.unit.label = Unit Of Measurement
@ -161,7 +161,7 @@ thing-type.config.mqtt.rollershutter_channel.stopCommandTopic.description = An M
thing-type.config.mqtt.rollershutter_channel.transformExtentsToString.label = Transform Commands at Extents to String thing-type.config.mqtt.rollershutter_channel.transformExtentsToString.label = Transform Commands at Extents to String
thing-type.config.mqtt.rollershutter_channel.transformExtentsToString.description = If a command is 0 or 100, send that as UP or DOWN commands instead. Useful if your device doesn't support going to a specific position - only opening or closing, but you have front ends (say HomeKit) or rules that will only send percentage commands instead of UP/DOWN. thing-type.config.mqtt.rollershutter_channel.transformExtentsToString.description = If a command is 0 or 100, send that as UP or DOWN commands instead. Useful if your device doesn't support going to a specific position - only opening or closing, but you have front ends (say HomeKit) or rules that will only send percentage commands instead of UP/DOWN.
thing-type.config.mqtt.rollershutter_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.rollershutter_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.rollershutter_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.rollershutter_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.
thing-type.config.mqtt.rollershutter_channel.transformationPatternOut.label = Outgoing Value Transformation thing-type.config.mqtt.rollershutter_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.rollershutter_channel.transformationPatternOut.description = 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. thing-type.config.mqtt.rollershutter_channel.transformationPatternOut.description = 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.
thing-type.config.mqtt.string_channel.allowedStates.label = Allowed States thing-type.config.mqtt.string_channel.allowedStates.label = Allowed States
@ -186,7 +186,7 @@ thing-type.config.mqtt.string_channel.retained.description = The value will be p
thing-type.config.mqtt.string_channel.stateTopic.label = MQTT State Topic thing-type.config.mqtt.string_channel.stateTopic.label = MQTT State Topic
thing-type.config.mqtt.string_channel.stateTopic.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. thing-type.config.mqtt.string_channel.stateTopic.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.
thing-type.config.mqtt.string_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.string_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.string_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.string_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.
thing-type.config.mqtt.string_channel.transformationPatternOut.label = Outgoing Value Transformation thing-type.config.mqtt.string_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.string_channel.transformationPatternOut.description = 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. thing-type.config.mqtt.string_channel.transformationPatternOut.description = 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.
thing-type.config.mqtt.switch_channel.commandTopic.label = MQTT Command Topic thing-type.config.mqtt.switch_channel.commandTopic.label = MQTT Command Topic
@ -211,7 +211,7 @@ thing-type.config.mqtt.switch_channel.retained.description = The value will be p
thing-type.config.mqtt.switch_channel.stateTopic.label = MQTT State Topic thing-type.config.mqtt.switch_channel.stateTopic.label = MQTT State Topic
thing-type.config.mqtt.switch_channel.stateTopic.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. thing-type.config.mqtt.switch_channel.stateTopic.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.
thing-type.config.mqtt.switch_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.switch_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.switch_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.switch_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.
thing-type.config.mqtt.switch_channel.transformationPatternOut.label = Outgoing Value Transformation thing-type.config.mqtt.switch_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.switch_channel.transformationPatternOut.description = 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. thing-type.config.mqtt.switch_channel.transformationPatternOut.description = 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.
thing-type.config.mqtt.trigger_channel.group.transformations.label = Transform Values thing-type.config.mqtt.trigger_channel.group.transformations.label = Transform Values
@ -219,4 +219,4 @@ thing-type.config.mqtt.trigger_channel.group.transformations.description = These
thing-type.config.mqtt.trigger_channel.stateTopic.label = MQTT Trigger Topic thing-type.config.mqtt.trigger_channel.stateTopic.label = MQTT Trigger Topic
thing-type.config.mqtt.trigger_channel.stateTopic.description = An MQTT topic that this thing will subscribe to, to receive the trigger thing-type.config.mqtt.trigger_channel.stateTopic.description = An MQTT topic that this thing will subscribe to, to receive the trigger
thing-type.config.mqtt.trigger_channel.transformationPattern.label = Incoming Value Transformations thing-type.config.mqtt.trigger_channel.transformationPattern.label = Incoming Value Transformations
thing-type.config.mqtt.trigger_channel.transformationPattern.description = 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 ∩. thing-type.config.mqtt.trigger_channel.transformationPattern.description = 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 listing each transformation on a separate line, or by separating them with the intersection character ∩.

View File

@ -29,7 +29,7 @@
<description>Payload of the 'Availability Topic', when the device is *not* available. Default: 'OFF'</description> <description>Payload of the 'Availability Topic', when the device is *not* available. Default: 'OFF'</description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="transformationPattern" type="text"> <parameter name="transformationPattern" type="text" multiple="true">
<label>Availability Payload Transformations</label> <label>Availability Payload Transformations</label>
<description> <description>
<![CDATA[ <![CDATA[
@ -37,7 +37,8 @@
A transformation example for a received JSON would be "JSONPATH:$.status" for A transformation example for a received JSON would be "JSONPATH:$.status" for
a json {status: "Online"}. a json {status: "Online"}.
You can chain transformations by separating them with the intersection character ∩. You can chain transformations by listing each transformation on a separate line, or
by separating them with the intersection character ∩.
]]> ]]>
</description> </description>
<advanced>true</advanced> <advanced>true</advanced>

View File

@ -24,6 +24,7 @@ import java.math.BigDecimal;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
@ -33,9 +34,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy; import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
@ -56,8 +59,13 @@ import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units; import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.util.ColorUtil; import org.openhab.core.util.ColorUtil;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
/** /**
* Tests the {@link ChannelState} class. * Tests the {@link ChannelState} class.
@ -407,4 +415,97 @@ public class ChannelStateTests {
assertThat(value.getChannelState(), is(instanceOf(RawType.class))); assertThat(value.getChannelState(), is(instanceOf(RawType.class)));
assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg")); assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg"));
} }
@Nested
public class TransformationTests {
// Copied from org.openhab.core.thing.binding.generic.ChannelTransformationTest
private static final String T1_NAME = "TRANSFORM1";
private static final String T1_PATTERN = "T1Pattern";
private static final String T1_INPUT = "T1Input";
private static final String T1_RESULT = "T1Result";
private static final String NULL_INPUT = "nullInput";
private static final @Nullable String NULL_RESULT = null;
private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;
private @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
private @Mock @NonNullByDefault({}) ServiceReference<TransformationService> serviceRef1Mock;
private @NonNullByDefault({}) TransformationHelper transformationHelper;
@BeforeEach
public void init() throws TransformationException {
Mockito.when(transformationService1Mock.transform(eq(T1_PATTERN), eq(T1_INPUT)))
.thenAnswer(answer -> T1_RESULT);
Mockito.when(transformationService1Mock.transform(eq(T1_PATTERN), eq(NULL_INPUT)))
.thenAnswer(answer -> NULL_RESULT);
Mockito.when(serviceRef1Mock.getProperty(any())).thenReturn("TRANSFORM1");
Mockito.when(bundleContextMock.getService(serviceRef1Mock)).thenReturn(transformationService1Mock);
transformationHelper = new TransformationHelper(bundleContextMock);
transformationHelper.setTransformationService(serviceRef1Mock);
}
@AfterEach
public void tearDown() {
transformationHelper.deactivate();
}
@Test
public void transformationPatternTest() throws Exception {
ChannelConfig config = ChannelConfigBuilder.create("state", "command")
.withTransformationPattern(List.of(T1_NAME + ":" + T1_PATTERN)).build();
ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
c.processMessage("state", T1_INPUT.getBytes());
future.get(300, TimeUnit.MILLISECONDS);
assertThat(textValue.getChannelState().toString(), is(T1_RESULT));
verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
}
@Test
public void transformationPatternReturningNullTest() throws Exception {
ChannelConfig config = ChannelConfigBuilder.create("state", "command")
.withTransformationPattern(List.of(T1_NAME + ":" + T1_PATTERN)).build();
ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
// First, test with an input that doesn't get transformed to null
CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
c.processMessage("state", T1_INPUT.getBytes());
future.get(300, TimeUnit.MILLISECONDS);
assertThat(textValue.getChannelState().toString(), is(T1_RESULT));
verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
clearInvocations(channelStateUpdateListenerMock);
// now test with an input that gets transformed to null
future = c.start(connectionMock, scheduler, 100);
c.processMessage("state", NULL_INPUT.getBytes());
future.get(300, TimeUnit.MILLISECONDS);
// textValue should not have been updated
assertThat(textValue.getChannelState().toString(), is(T1_RESULT));
verify(channelStateUpdateListenerMock, never()).updateChannelState(eq(channelUIDMock), any());
}
@Test
public void transformationPatternOutTest() throws Exception {
ChannelConfig config = ChannelConfigBuilder.create("state", "command")
.withTransformationPatternOut(List.of(T1_NAME + ":" + T1_PATTERN)).build();
ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
verify(connectionMock).subscribe(eq("state"), eq(c));
c.publishValue(new StringType(T1_INPUT)).get();
verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, T1_RESULT.getBytes())),
anyInt(), eq(false));
}
}
} }

View File

@ -1,123 +0,0 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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.hamcrest.MatcherAssert.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.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
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.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
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class ChannelStateTransformationTests {
private @Mock @NonNullByDefault({}) TransformationService jsonPathServiceMock;
private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProviderMock;
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @Mock @NonNullByDefault({}) Thing thingMock;
private @Mock @NonNullByDefault({}) AbstractBrokerHandler bridgeHandlerMock;
private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
private @NonNullByDefault({}) GenericMQTTThingHandler thingHandler;
@BeforeEach
public void setUp() throws Exception {
ThingStatusInfo thingStatus = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
// Mock the thing: We need the thingUID and the bridgeUID
when(thingMock.getUID()).thenReturn(TEST_GENERIC_THING);
when(thingMock.getChannels()).thenReturn(THING_CHANNEL_LIST_WITH_JSON);
when(thingMock.getStatusInfo()).thenReturn(thingStatus);
when(thingMock.getConfiguration()).thenReturn(new Configuration());
// Return the mocked connection object if the bridge handler is asked for it
when(bridgeHandlerMock.getConnectionAsync()).thenReturn(CompletableFuture.completedFuture(connectionMock));
CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
voidFutureComplete.complete(null);
doReturn(voidFutureComplete).when(connectionMock).unsubscribeAll();
doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).subscribe(any(), any());
doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).unsubscribe(any(), any());
thingHandler = spy(new GenericMQTTThingHandler(thingMock, mock(MqttChannelStateDescriptionProvider.class),
transformationServiceProviderMock, 1500));
when(transformationServiceProviderMock.getTransformationService(anyString())).thenReturn(jsonPathServiceMock);
thingHandler.setCallback(callbackMock);
// Return the bridge handler if the thing handler asks for it
doReturn(bridgeHandlerMock).when(thingHandler).getBridgeHandler();
// We are by default online
doReturn(thingStatus).when(thingHandler).getBridgeStatus();
}
@SuppressWarnings("null")
@Test
public void initialize() throws Exception {
when(thingMock.getChannels()).thenReturn(THING_CHANNEL_LIST_WITH_JSON);
thingHandler.initialize();
ChannelState channelConfig = thingHandler.getChannelState(TEXT_CHANNEL_UID);
assertThat(channelConfig.transformationsIn.get(0).pattern, is(JSON_PATH_PATTERN));
}
@SuppressWarnings("null")
@Test
public void processMessageWithJSONPath() throws Exception {
when(jsonPathServiceMock.transform(JSON_PATH_PATTERN, JSON_PATH_JSON)).thenReturn("23.2");
thingHandler.initialize();
ChannelState channelConfig = thingHandler.getChannelState(TEXT_CHANNEL_UID);
channelConfig.setChannelStateUpdateListener(thingHandler);
ChannelStateTransformation transformation = channelConfig.transformationsIn.get(0);
byte[] payload = JSON_PATH_JSON.getBytes();
assertThat(transformation.pattern, is(JSON_PATH_PATTERN));
// Test process message
channelConfig.processMessage(channelConfig.getStateTopic(), payload);
verify(callbackMock).stateUpdated(eq(TEXT_CHANNEL_UID), argThat(arg -> "23.2".equals(arg.toString())));
assertThat(channelConfig.getCache().getChannelState().toString(), is("23.2"));
}
}

View File

@ -35,7 +35,6 @@ import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
import org.openhab.binding.mqtt.generic.ChannelState; import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.ThingHandlerHelper; 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.OnOffValue;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.ValueFactory; import org.openhab.binding.mqtt.generic.values.ValueFactory;
@ -90,8 +89,8 @@ public class GenericThingHandlerTests {
doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).publish(any(), any(), anyInt(), doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).publish(any(), any(), anyInt(),
anyBoolean()); anyBoolean());
thingHandler = spy(new GenericMQTTThingHandler(thingMock, mock(MqttChannelStateDescriptionProvider.class), thingHandler = spy(
mock(TransformationServiceProvider.class), 1500)); new GenericMQTTThingHandler(thingMock, mock(MqttChannelStateDescriptionProvider.class), 1500));
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
// Return the bridge handler if the thing handler asks for it // Return the bridge handler if the thing handler asks for it

View File

@ -49,11 +49,7 @@ public class ThingChannelConstants {
public static final ChannelUID TEXT_CHANNEL_UID = new ChannelUID(TEST_GENERIC_THING, "mytext"); public static final ChannelUID TEXT_CHANNEL_UID = new ChannelUID(TEST_GENERIC_THING, "mytext");
public static final String JSON_PATH_JSON = "{ \"device\": { \"status\": { \"temperature\": 23.2 }}}";
public static final String JSON_PATH_PATTERN = "$.device.status.temperature";
public static final List<Channel> THING_CHANNEL_LIST = new ArrayList<>(); public static final List<Channel> THING_CHANNEL_LIST = new ArrayList<>();
public static final List<Channel> THING_CHANNEL_LIST_WITH_JSON = new ArrayList<>();
/** /**
* Create a channel with exact the parameters we need for the tests * Create a channel with exact the parameters we need for the tests
@ -74,12 +70,6 @@ public class ThingChannelConstants {
THING_CHANNEL_LIST.add(cb("onoff", "Switch", onoffConfiguration(), ON_OFF_CHANNEL)); THING_CHANNEL_LIST.add(cb("onoff", "Switch", onoffConfiguration(), ON_OFF_CHANNEL));
THING_CHANNEL_LIST.add(cb("num", "Number", numberConfiguration(), NUMBER_CHANNEL)); THING_CHANNEL_LIST.add(cb("num", "Number", numberConfiguration(), NUMBER_CHANNEL));
THING_CHANNEL_LIST.add(cb("percent", "Number:Dimensionless", percentageConfiguration(), PERCENTAGE_CHANNEL)); THING_CHANNEL_LIST.add(cb("percent", "Number:Dimensionless", percentageConfiguration(), PERCENTAGE_CHANNEL));
THING_CHANNEL_LIST_WITH_JSON.add(cb("mytext", "String", textConfigurationWithJson(), TEXT_WITH_JSON_CHANNEL));
THING_CHANNEL_LIST_WITH_JSON.add(cb("onoff", "Switch", onoffConfiguration(), ON_OFF_CHANNEL));
THING_CHANNEL_LIST_WITH_JSON.add(cb("num", "Number", numberConfiguration(), NUMBER_CHANNEL));
THING_CHANNEL_LIST_WITH_JSON
.add(cb("percent", "Number:Dimensionless", percentageConfiguration(), PERCENTAGE_CHANNEL));
} }
static Configuration textConfiguration() { static Configuration textConfiguration() {
@ -89,14 +79,6 @@ public class ThingChannelConstants {
return new Configuration(data); 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:" + JSON_PATH_PATTERN);
return new Configuration(data);
}
private static Configuration numberConfiguration() { private static Configuration numberConfiguration() {
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("stateTopic", "test/state"); data.put("stateTopic", "test/state");

View File

@ -20,7 +20,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler; import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
@ -28,8 +27,6 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
@ -42,7 +39,7 @@ import org.osgi.service.component.annotations.Reference;
*/ */
@Component(service = ThingHandlerFactory.class) @Component(service = ThingHandlerFactory.class)
@NonNullByDefault @NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider { public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelTypeProvider typeProvider; private final MqttChannelTypeProvider typeProvider;
private final MqttChannelStateDescriptionProvider stateDescriptionProvider; private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry; private final ChannelTypeRegistry channelTypeRegistry;
@ -75,13 +72,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements
if (supportsThingType(thingTypeUID)) { if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry, return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
this, 10000, 2000); 10000, 2000);
} }
return null; return null;
} }
@Override
public @Nullable TransformationService getTransformationService(String type) {
return TransformationHelper.getTransformationService(bundleContext, type);
}
} }

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.mqtt.homeassistant.internal; package org.openhab.binding.mqtt.homeassistant.internal;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -21,9 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelConfigBuilder; import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
import org.openhab.binding.mqtt.generic.ChannelState; 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.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent; import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
@ -226,10 +225,19 @@ public class ComponentChannel {
Channel channel; Channel channel;
channelUID = component.buildChannelUID(channelID); channelUID = component.buildChannelUID(channelID);
channelState = new HomeAssistantChannelState( ChannelConfigBuilder channelConfigBuilder = ChannelConfigBuilder.create().withRetain(retain).withQos(qos)
ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic) .withStateTopic(stateTopic).withCommandTopic(commandTopic).makeTrigger(trigger)
.withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(), .withFormatter(format);
channelUID, valueState, channelStateUpdateListener, commandFilter);
if (templateIn != null) {
channelConfigBuilder.withTransformationPattern(List.of(JINJA + ":" + templateIn));
}
if (templateOut != null) {
channelConfigBuilder.withTransformationPatternOut(List.of(JINJA + ":" + templateOut));
}
channelState = new HomeAssistantChannelState(channelConfigBuilder.build(), channelUID, valueState,
channelStateUpdateListener, commandFilter);
// disabled by default components should always show up as advanced // disabled by default components should always show up as advanced
if (!component.isEnabledByDefault()) { if (!component.isEnabledByDefault()) {
@ -262,18 +270,6 @@ public class ComponentChannel {
ComponentChannel result = new ComponentChannel(channelState, channel, stateDescription, commandDescription, ComponentChannel result = new ComponentChannel(channelState, channel, stateDescription, commandDescription,
channelStateUpdateListener); channelStateUpdateListener);
TransformationServiceProvider transformationProvider = component.getTransformationServiceProvider();
final String templateIn = this.templateIn;
if (templateIn != null && transformationProvider != null) {
channelState
.addTransformation(new ChannelStateTransformation(JINJA, templateIn, transformationProvider));
}
final String templateOut = this.templateOut;
if (templateOut != null && transformationProvider != null) {
channelState.addTransformationOut(
new ChannelStateTransformation(JINJA, templateOut, transformationProvider));
}
if (addToComponent) { if (addToComponent) {
component.getChannelMap().put(channelID, result); component.getChannelMap().put(channelID, result);
} }

View File

@ -25,7 +25,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.utils.FutureCollector; import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent; import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory; import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
@ -52,7 +51,6 @@ public class DiscoverComponents implements MqttMessageSubscriber {
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private final ChannelStateUpdateListener updateListener; private final ChannelStateUpdateListener updateListener;
private final AvailabilityTracker tracker; private final AvailabilityTracker tracker;
private final TransformationServiceProvider transformationServiceProvider;
private final boolean newStyleChannels; private final boolean newStyleChannels;
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>(); protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
@ -80,13 +78,12 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/ */
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler, public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson, ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
TransformationServiceProvider transformationServiceProvider, boolean newStyleChannels) { boolean newStyleChannels) {
this.thingUID = thingUID; this.thingUID = thingUID;
this.scheduler = scheduler; this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener; this.updateListener = channelStateUpdateListener;
this.gson = gson; this.gson = gson;
this.tracker = tracker; this.tracker = tracker;
this.transformationServiceProvider = transformationServiceProvider;
this.newStyleChannels = newStyleChannels; this.newStyleChannels = newStyleChannels;
} }
@ -103,7 +100,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
if (config.length() > 0) { if (config.length() > 0) {
try { try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler, component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, transformationServiceProvider, newStyleChannels); gson, newStyleChannels);
component.setConfigSeen(); component.setConfigSeen();
logger.trace("Found HomeAssistant component {}", haID); logger.trace("Found HomeAssistant component {}", haID);

View File

@ -26,7 +26,6 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
@ -132,24 +131,27 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode); componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode);
for (Availability availability : availabilities) { for (Availability availability : availabilities) {
String availabilityTemplate = availability.getValueTemplate(); String availabilityTemplate = availability.getValueTemplate();
List<String> availabilityTemplates = List.of();
if (availabilityTemplate != null) { if (availabilityTemplate != null) {
availabilityTemplate = JINJA_PREFIX + availabilityTemplate; availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
availabilityTemplates = List.of(availabilityTemplate);
} }
componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(), componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), availabilityTemplate, availability.getPayloadAvailable(), availability.getPayloadNotAvailable(),
componentConfiguration.getTransformationServiceProvider()); availabilityTemplates);
} }
} else { } else {
String availabilityTopic = this.channelConfiguration.getAvailabilityTopic(); String availabilityTopic = this.channelConfiguration.getAvailabilityTopic();
if (availabilityTopic != null) { if (availabilityTopic != null) {
String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate(); String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
List<String> availabilityTemplates = List.of();
if (availabilityTemplate != null) { if (availabilityTemplate != null) {
availabilityTemplate = JINJA_PREFIX + availabilityTemplate; availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
availabilityTemplates = List.of(availabilityTemplate);
} }
componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic, componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
this.channelConfiguration.getPayloadAvailable(), this.channelConfiguration.getPayloadAvailable(),
this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplate, this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplates);
componentConfiguration.getTransformationServiceProvider());
} }
} }
} }
@ -324,11 +326,6 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
return channelConfigurationJson; return channelConfigurationJson;
} }
@Nullable
public TransformationServiceProvider getTransformationServiceProvider() {
return componentConfiguration.getTransformationServiceProvider();
}
public boolean isEnabledByDefault() { public boolean isEnabledByDefault() {
return channelConfiguration.isEnabledByDefault(); return channelConfiguration.isEnabledByDefault();
} }

View File

@ -15,10 +15,8 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.HaID; import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -48,11 +46,9 @@ public class ComponentFactory {
*/ */
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON, public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler, ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler,
Gson gson, TransformationServiceProvider transformationServiceProvider, boolean newStyleChannels) Gson gson, boolean newStyleChannels) throws ConfigurationException {
throws ConfigurationException {
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID, ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, updateListener, tracker, scheduler) channelConfigurationJSON, gson, updateListener, tracker, scheduler);
.transformationProvider(transformationServiceProvider);
switch (haID.component) { switch (haID.component) {
case "alarm_control_panel": case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration, newStyleChannels); return new AlarmControlPanel(componentConfiguration, newStyleChannels);
@ -101,7 +97,6 @@ public class ComponentFactory {
private final AvailabilityTracker tracker; private final AvailabilityTracker tracker;
private final Gson gson; private final Gson gson;
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private @Nullable TransformationServiceProvider transformationServiceProvider;
/** /**
* Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type. * Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
@ -123,12 +118,6 @@ public class ComponentFactory {
this.scheduler = scheduler; this.scheduler = scheduler;
} }
public ComponentConfiguration transformationProvider(
TransformationServiceProvider transformationServiceProvider) {
this.transformationServiceProvider = transformationServiceProvider;
return this;
}
public ThingUID getThingUID() { public ThingUID getThingUID() {
return thingUID; return thingUID;
} }
@ -145,11 +134,6 @@ public class ComponentFactory {
return updateListener; return updateListener;
} }
@Nullable
public TransformationServiceProvider getTransformationServiceProvider() {
return transformationServiceProvider;
}
public Gson getGson() { public Gson getGson() {
return gson; return gson;
} }

View File

@ -30,7 +30,6 @@ import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
import org.openhab.binding.mqtt.generic.ChannelState; import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing; import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing;
import org.openhab.binding.mqtt.generic.utils.FutureCollector; import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
@ -101,8 +100,6 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected HandlerConfiguration config = new HandlerConfiguration(); protected HandlerConfiguration config = new HandlerConfiguration();
private Set<HaID> discoveryHomeAssistantIDs = new HashSet<>(); private Set<HaID> discoveryHomeAssistantIDs = new HashSet<>();
protected final TransformationServiceProvider transformationServiceProvider;
private boolean started; private boolean started;
private boolean newStyleChannels; private boolean newStyleChannels;
private @Nullable Update updateComponent; private @Nullable Update updateComponent;
@ -118,21 +115,18 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
*/ */
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry, MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout, int subscribeTimeout, int attributeReceiveTimeout) {
int attributeReceiveTimeout) {
super(thing, subscribeTimeout); super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider; this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider; this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.transformationServiceProvider = transformationServiceProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout; this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler); this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels")); newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels"));
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson, this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson, newStyleChannels);
this.transformationServiceProvider, newStyleChannels);
} }
@Override @Override
@ -162,7 +156,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
} else { } else {
try { try {
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
scheduler, gson, transformationServiceProvider, newStyleChannels); scheduler, gson, newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) { if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component); typeID = calculateThingTypeUID(component);
} }

View File

@ -32,12 +32,12 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.handler.BrokerHandler; import org.openhab.binding.mqtt.handler.BrokerHandler;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
@ -57,8 +57,12 @@ import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ThingType; import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeBuilder; import org.openhab.core.thing.type.ThingTypeBuilder;
import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.thing.type.ThingTypeRegistry;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.transform.jinja.internal.JinjaTransformationService; import org.openhab.transform.jinja.internal.JinjaTransformationService;
import org.openhab.transform.jinja.internal.profiles.JinjaTransformationProfile; import org.openhab.transform.jinja.internal.profiles.JinjaTransformationProfile;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
/** /**
* Abstract class for HomeAssistant unit tests. * Abstract class for HomeAssistant unit tests.
@ -86,7 +90,6 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection; protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection;
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry; protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider; protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider;
protected @NonNullByDefault({}) MqttChannelStateDescriptionProvider stateDescriptionProvider; protected @NonNullByDefault({}) MqttChannelStateDescriptionProvider stateDescriptionProvider;
@ -97,16 +100,27 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
protected final Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build(); protected final Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
protected final ConcurrentMap<String, Set<MqttMessageSubscriber>> subscriptions = new ConcurrentHashMap<>(); protected final ConcurrentMap<String, Set<MqttMessageSubscriber>> subscriptions = new ConcurrentHashMap<>();
private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;
private @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
private @Mock @NonNullByDefault({}) ServiceReference<TransformationService> serviceRefMock;
private @NonNullByDefault({}) TransformationHelper transformationHelper;
private final JinjaTransformationService jinjaTransformationService = new JinjaTransformationService(); private final JinjaTransformationService jinjaTransformationService = new JinjaTransformationService();
@BeforeEach @BeforeEach
public void beforeEachAbstractHomeAssistantTests() { public void beforeEachAbstractHomeAssistantTests() {
Mockito.when(serviceRefMock.getProperty(any())).thenReturn(JinjaTransformationProfile.PROFILE_TYPE_UID.getId());
Mockito.when(bundleContextMock.getService(serviceRefMock)).thenReturn(jinjaTransformationService);
transformationHelper = new TransformationHelper(bundleContextMock);
transformationHelper.setTransformationService(serviceRefMock);
when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID)) when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
.thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build()); .thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE); when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE);
when(transformationServiceProvider
.getTransformationService(JinjaTransformationProfile.PROFILE_TYPE_UID.getId()))
.thenReturn(jinjaTransformationService);
channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry, new VolatileStorageService())); channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry, new VolatileStorageService()));
stateDescriptionProvider = spy(new MqttChannelStateDescriptionProvider()); stateDescriptionProvider = spy(new MqttChannelStateDescriptionProvider());

View File

@ -34,7 +34,6 @@ import org.mockito.ArgumentMatchers;
import org.mockito.Mock; import org.mockito.Mock;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests; import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
@ -79,7 +78,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing); when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider, thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, transformationServiceProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT); channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection); thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
thingHandler = spy(thingHandler); thingHandler = spy(thingHandler);
@ -288,10 +287,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry, MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout, int subscribeTimeout, int attributeReceiveTimeout) {
int attributeReceiveTimeout) { super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, subscribeTimeout,
super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, attributeReceiveTimeout);
transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
} }
@Override @Override

View File

@ -75,7 +75,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing); when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider, thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, transformationServiceProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT); channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection); thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler; nonSpyThingHandler = thingHandler;

View File

@ -18,7 +18,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homie.internal.handler.HomieThingHandler; import org.openhab.binding.mqtt.homie.internal.handler.HomieThingHandler;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
@ -26,8 +25,6 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
@ -40,7 +37,7 @@ import org.osgi.service.component.annotations.Reference;
*/ */
@Component(service = ThingHandlerFactory.class) @Component(service = ThingHandlerFactory.class)
@NonNullByDefault @NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider { public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelTypeProvider typeProvider; private final MqttChannelTypeProvider typeProvider;
private final MqttChannelStateDescriptionProvider stateDescriptionProvider; private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry; private final ChannelTypeRegistry channelTypeRegistry;
@ -78,9 +75,4 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements
} }
return null; return null;
} }
@Override
public @Nullable TransformationService getTransformationService(String type) {
return TransformationHelper.getTransformationService(bundleContext, type);
}
} }

View File

@ -36,7 +36,6 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
import org.openhab.binding.mqtt.homeassistant.internal.HaID; import org.openhab.binding.mqtt.homeassistant.internal.HaID;
@ -60,7 +59,6 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
private @Mock @NonNullByDefault({}) MqttBrokerConnection connection; private @Mock @NonNullByDefault({}) MqttBrokerConnection connection;
private @Mock @NonNullByDefault({}) ComponentDiscovered discovered; private @Mock @NonNullByDefault({}) ComponentDiscovered discovered;
private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener; private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener;
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker; private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
@ -73,7 +71,6 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any()); doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(), doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(),
anyBoolean()); anyBoolean());
doReturn(null).when(transformationServiceProvider).getTransformationService(any());
} }
@Test @Test
@ -84,7 +81,7 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING,
scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider, true)); scheduler, channelStateUpdateListener, availabilityTracker, gson, true));
HandlerConfiguration config = new HandlerConfiguration("homeassistant", List.of("switch/object")); HandlerConfiguration config = new HandlerConfiguration("homeassistant", List.of("switch/object"));

View File

@ -42,7 +42,6 @@ import org.mockito.quality.Strictness;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
import org.openhab.binding.mqtt.homeassistant.internal.HaID; import org.openhab.binding.mqtt.homeassistant.internal.HaID;
@ -76,7 +75,6 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener; private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener;
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker; private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
/** /**
* Create an observer that fails the test as soon as the broker client connection changes its connection state * Create an observer that fails the test as soon as the broker client connection changes its connection state
@ -110,8 +108,6 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(2, TimeUnit.SECONDS); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(2, TimeUnit.SECONDS);
failure = null; failure = null;
doReturn(null).when(transformationServiceProvider).getTransformationService(any());
} }
@Override @Override
@ -150,7 +146,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING,
scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider, true)); scheduler, channelStateUpdateListener, availabilityTracker, gson, true));
// The DiscoverComponents object calls ComponentDiscovered callbacks. // The DiscoverComponents object calls ComponentDiscovered callbacks.
// In the following implementation we add the found component to the `haComponents` map // In the following implementation we add the found component to the `haComponents` map