diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index 8862acbdead..0f118c6079b 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -320,7 +320,7 @@
org.apache.aries.spifly
org.apache.aries.spifly.dynamic.bundle
- 1.3.4
+ 1.3.6
diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java
index b07f57086dd..95f4b79f9e4 100644
--- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java
+++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonService.java
@@ -20,6 +20,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -29,6 +30,8 @@
import org.openhab.core.OpenHAB;
import org.openhab.core.addon.Addon;
import org.openhab.core.addon.AddonEventFactory;
+import org.openhab.core.addon.AddonInfo;
+import org.openhab.core.addon.AddonInfoRegistry;
import org.openhab.core.addon.AddonService;
import org.openhab.core.addon.AddonType;
import org.openhab.core.cache.ExpiringCache;
@@ -66,13 +69,15 @@ public abstract class AbstractRemoteAddonService implements AddonService {
protected final ConfigurationAdmin configurationAdmin;
protected final ExpiringCache> cachedRemoteAddons = new ExpiringCache<>(Duration.ofMinutes(15),
this::getRemoteAddons);
+ protected final AddonInfoRegistry addonInfoRegistry;
protected List cachedAddons = List.of();
protected List installedAddons = List.of();
private final Logger logger = LoggerFactory.getLogger(AbstractRemoteAddonService.class);
public AbstractRemoteAddonService(EventPublisher eventPublisher, ConfigurationAdmin configurationAdmin,
- StorageService storageService, String servicePid) {
+ StorageService storageService, AddonInfoRegistry addonInfoRegistry, String servicePid) {
+ this.addonInfoRegistry = addonInfoRegistry;
this.eventPublisher = eventPublisher;
this.configurationAdmin = configurationAdmin;
this.installedAddonStorage = storageService.getStorage(servicePid);
@@ -83,6 +88,16 @@ protected BundleVersion getCoreVersion() {
return new BundleVersion(FrameworkUtil.getBundle(OpenHAB.class).getVersion().toString());
}
+ private Addon convertFromStorage(Map.Entry entry) {
+ Addon storedAddon = Objects.requireNonNull(gson.fromJson(entry.getValue(), Addon.class));
+ AddonInfo addonInfo = addonInfoRegistry.getAddonInfo(storedAddon.getType() + "-" + storedAddon.getId());
+ if (addonInfo != null && storedAddon.getConfigDescriptionURI().isBlank()) {
+ return Addon.create(storedAddon).withConfigDescriptionURI(addonInfo.getConfigDescriptionURI()).build();
+ } else {
+ return storedAddon;
+ }
+ }
+
@Override
public void refreshSource() {
if (!addonHandlers.stream().allMatch(MarketplaceAddonHandler::isReady)) {
@@ -92,8 +107,7 @@ public void refreshSource() {
}
List addons = new ArrayList<>();
try {
- installedAddonStorage.stream().map(e -> Objects.requireNonNull(gson.fromJson(e.getValue(), Addon.class)))
- .forEach(addons::add);
+ installedAddonStorage.stream().map(this::convertFromStorage).forEach(addons::add);
} catch (JsonSyntaxException e) {
List.copyOf(installedAddonStorage.getKeys()).forEach(installedAddonStorage::remove);
logger.error(
diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java
index 7054b6f94bd..69104bec7e0 100644
--- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java
+++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/community/CommunityMarketplaceAddonService.java
@@ -36,6 +36,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
+import org.openhab.core.addon.AddonInfoRegistry;
import org.openhab.core.addon.AddonService;
import org.openhab.core.addon.AddonType;
import org.openhab.core.addon.marketplace.AbstractRemoteAddonService;
@@ -115,8 +116,8 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
@Activate
public CommunityMarketplaceAddonService(final @Reference EventPublisher eventPublisher,
@Reference ConfigurationAdmin configurationAdmin, @Reference StorageService storageService,
- Map config) {
- super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
+ @Reference AddonInfoRegistry addonInfoRegistry, Map config) {
+ super(eventPublisher, configurationAdmin, storageService, addonInfoRegistry, SERVICE_PID);
modified(config);
}
@@ -200,10 +201,13 @@ protected List getRemoteAddons() {
@Override
public @Nullable Addon getAddon(String uid, @Nullable Locale locale) {
+ String queryId = uid.startsWith(ADDON_ID_PREFIX) ? uid : ADDON_ID_PREFIX + uid;
+
// check if it is an installed add-on (cachedAddons also contains possibly incomplete results from the remote
// side, we need to retrieve them from Discourse)
- if (installedAddons.contains(uid)) {
- return cachedAddons.stream().filter(e -> uid.equals(e.getUid())).findAny().orElse(null);
+
+ if (installedAddons.contains(queryId)) {
+ return cachedAddons.stream().filter(e -> queryId.equals(e.getUid())).findAny().orElse(null);
}
if (!remoteEnabled()) {
@@ -437,11 +441,13 @@ private Addon convertTopicToAddon(DiscourseTopicResponseDTO topic) {
boolean installed = addonHandlers.stream()
.anyMatch(handler -> handler.supports(type, contentType) && handler.isInstalled(uid));
- return Addon.create(uid).withType(type).withId(id).withContentType(contentType).withLabel(topic.title)
- .withImageLink(topic.imageUrl).withLink(COMMUNITY_TOPIC_URL + topic.id.toString())
+ Addon.Builder builder = Addon.create(uid).withType(type).withId(id).withContentType(contentType)
+ .withLabel(topic.title).withImageLink(topic.imageUrl)
+ .withLink(COMMUNITY_TOPIC_URL + topic.id.toString())
.withAuthor(topic.postStream.posts[0].displayUsername).withMaturity(maturity)
- .withDetailedDescription(detailedDescription).withInstalled(installed).withProperties(properties)
- .build();
+ .withDetailedDescription(detailedDescription).withInstalled(installed).withProperties(properties);
+
+ return builder.build();
}
private @Nullable String determineIdFromUrl(String url) {
diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java
index 626c6852724..0b39fd1460c 100644
--- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java
+++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/internal/json/JsonAddonService.java
@@ -30,6 +30,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
+import org.openhab.core.addon.AddonInfoRegistry;
import org.openhab.core.addon.AddonService;
import org.openhab.core.addon.marketplace.AbstractRemoteAddonService;
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
@@ -78,8 +79,9 @@ public class JsonAddonService extends AbstractRemoteAddonService {
@Activate
public JsonAddonService(@Reference EventPublisher eventPublisher, @Reference StorageService storageService,
- @Reference ConfigurationAdmin configurationAdmin, Map config) {
- super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
+ @Reference ConfigurationAdmin configurationAdmin, @Reference AddonInfoRegistry addonInfoRegistry,
+ Map config) {
+ super(eventPublisher, configurationAdmin, storageService, addonInfoRegistry, SERVICE_PID);
modified(config);
}
diff --git a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java
index 58004557856..dac9fa3fc09 100644
--- a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java
+++ b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/AbstractRemoteAddonServiceTest.java
@@ -43,6 +43,7 @@
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.addon.Addon;
+import org.openhab.core.addon.AddonInfoRegistry;
import org.openhab.core.addon.marketplace.test.TestAddonHandler;
import org.openhab.core.addon.marketplace.test.TestAddonService;
import org.openhab.core.events.Event;
@@ -64,6 +65,7 @@
@NonNullByDefault
public class AbstractRemoteAddonServiceTest {
private @Mock @NonNullByDefault({}) StorageService storageService;
+ private @Mock @NonNullByDefault({}) AddonInfoRegistry addonInfoRegistry;
private @Mock @NonNullByDefault({}) ConfigurationAdmin configurationAdmin;
private @Mock @NonNullByDefault({}) EventPublisher eventPublisher;
private @Mock @NonNullByDefault({}) Configuration configuration;
@@ -82,7 +84,7 @@ public void initialize() throws IOException {
addonHandler = new TestAddonHandler();
- addonService = new TestAddonService(eventPublisher, configurationAdmin, storageService);
+ addonService = new TestAddonService(eventPublisher, configurationAdmin, storageService, addonInfoRegistry);
addonService.addAddonHandler(addonHandler);
}
@@ -93,7 +95,7 @@ public void testSourceNotRefreshedIfAddonHandlerNotReady() {
addonHandler = new TestAddonHandler();
addonHandler.setReady(false);
- addonService = new TestAddonService(eventPublisher, configurationAdmin, storageService);
+ addonService = new TestAddonService(eventPublisher, configurationAdmin, storageService, addonInfoRegistry);
addonService.addAddonHandler(addonHandler);
List addons = addonService.getAddons(null);
diff --git a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java
index 73ffa5cead0..fe5bc672a5a 100644
--- a/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java
+++ b/bundles/org.openhab.core.addon.marketplace/src/test/java/org/openhab/core/addon/marketplace/test/TestAddonService.java
@@ -21,6 +21,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.Addon;
+import org.openhab.core.addon.AddonInfoRegistry;
import org.openhab.core.addon.marketplace.AbstractRemoteAddonService;
import org.openhab.core.addon.marketplace.BundleVersion;
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
@@ -51,8 +52,8 @@ public class TestAddonService extends AbstractRemoteAddonService {
private int remoteCalls = 0;
public TestAddonService(EventPublisher eventPublisher, ConfigurationAdmin configurationAdmin,
- StorageService storageService) {
- super(eventPublisher, configurationAdmin, storageService, SERVICE_PID);
+ StorageService storageService, AddonInfoRegistry addonInfoRegistry) {
+ super(eventPublisher, configurationAdmin, storageService, addonInfoRegistry, SERVICE_PID);
}
@Override
diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/Addon.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/Addon.java
index 84019c79fb9..e1d06175ae9 100644
--- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/Addon.java
+++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/Addon.java
@@ -304,6 +304,33 @@ public static Builder create(String uid) {
return new Builder(uid);
}
+ public static Builder create(Addon addon) {
+ Addon.Builder builder = new Builder(addon.uid);
+ builder.id = addon.id;
+ builder.label = addon.label;
+ builder.version = addon.version;
+ builder.maturity = addon.maturity;
+ builder.compatible = addon.compatible;
+ builder.contentType = addon.contentType;
+ builder.link = addon.link;
+ builder.author = addon.author;
+ builder.verifiedAuthor = addon.verifiedAuthor;
+ builder.installed = addon.installed;
+ builder.type = addon.type;
+ builder.description = addon.description;
+ builder.detailedDescription = addon.detailedDescription;
+ builder.configDescriptionURI = addon.configDescriptionURI;
+ builder.keywords = addon.keywords;
+ builder.countries = addon.countries;
+ builder.license = addon.license;
+ builder.connection = addon.connection;
+ builder.backgroundColor = addon.backgroundColor;
+ builder.imageLink = addon.imageLink;
+ builder.properties = addon.properties;
+ builder.loggerPackages = addon.loggerPackages;
+ return builder;
+ }
+
public static class Builder {
private final String uid;
private String id;
diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/JarFileAddonService.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/JarFileAddonService.java
index 2b7819900b6..8b8fe68bcbe 100644
--- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/JarFileAddonService.java
+++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/JarFileAddonService.java
@@ -53,6 +53,7 @@
public class JarFileAddonService extends BundleTracker implements AddonService {
public static final String SERVICE_ID = "jar";
public static final String SERVICE_NAME = "JAR-File add-on service";
+ private static final String ADDONS_CONTENT_TYPE = "application/vnd.openhab.bundle";
private static final Map ADDON_TYPE_MAP = Map.of( //
"automation", new AddonType("automation", "Automation"), //
@@ -155,7 +156,7 @@ private Addon toAddon(Bundle bundle, AddonInfo addonInfo) {
.withVersion(bundle.getVersion().toString()).withLabel(addonInfo.getName())
.withConfigDescriptionURI(addonInfo.getConfigDescriptionURI())
.withDescription(Objects.requireNonNullElse(addonInfo.getDescription(), bundle.getSymbolicName()))
- .build();
+ .withContentType(ADDONS_CONTENT_TYPE).build();
}
@Override
@@ -168,7 +169,8 @@ public List getAddons(@Nullable Locale locale) {
@Override
public @Nullable Addon getAddon(String id, @Nullable Locale locale) {
- return addons.get(id);
+ String queryId = id.startsWith(ADDON_ID_PREFIX) ? id : ADDON_ID_PREFIX + id;
+ return addons.get(queryId);
}
@Override
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioFormat.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioFormat.java
index 2a6942b6b97..0eeb7ab2e2b 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioFormat.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioFormat.java
@@ -484,7 +484,7 @@ public String toString() {
+ (bigEndian != null ? "bigEndian=" + bigEndian + ", " : "")
+ (bitDepth != null ? "bitDepth=" + bitDepth + ", " : "")
+ (bitRate != null ? "bitRate=" + bitRate + ", " : "")
- + (frequency != null ? "frequency=" + frequency : "") + (channels != null ? "channels=" + channels : "")
- + "]";
+ + (frequency != null ? "frequency=" + frequency + ", " : "")
+ + (channels != null ? "channels=" + channels : "") + "]";
}
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java
index 2cb85ee5833..d74b88ef989 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java
@@ -31,7 +31,7 @@ public interface AudioHTTPServer {
/**
* Creates a relative url for a given {@link AudioStream} where it can be requested a single time.
* Note that the HTTP header only contains "Content-length", if the passed stream is an instance of
- * {@link FixedLengthAudioStream}.
+ * {@link SizeableAudioStream}.
* If the client that requests the url expects this header field to be present, make sure to pass such an instance.
* Streams are closed after having been served.
*
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ByteArrayAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ByteArrayAudioStream.java
index 6dfcae37ff2..6386e39db53 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ByteArrayAudioStream.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ByteArrayAudioStream.java
@@ -19,7 +19,8 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
- * This is an implementation of a {@link FixedLengthAudioStream}, which is based on a simple byte array.
+ * This is an implementation of an {@link AudioStream} with known length and a clone method, which is based on a simple
+ * byte array.
*
* @author Kai Kreuzer - Initial contribution
*/
@@ -60,4 +61,19 @@ public long length() {
public InputStream getClonedStream() {
return new ByteArrayAudioStream(bytes, format);
}
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ stream.mark(readlimit);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ stream.reset();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ClonableAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ClonableAudioStream.java
index 111e1c1d2d9..57dfcdc3c80 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ClonableAudioStream.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ClonableAudioStream.java
@@ -17,12 +17,12 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
- * This is an {@link AudioStream}, that can be cloned
+ * This is for an {@link AudioStream}, that can be cloned
*
- * @author Gwendal Roulleau - Initial contribution, separation from FixedLengthAudioStream
+ * @author Gwendal Roulleau - Initial contribution, separation from {@link FixedLengthAudioStream}
*/
@NonNullByDefault
-public abstract class ClonableAudioStream extends AudioStream {
+public interface ClonableAudioStream {
/**
* Returns a new, fully independent stream instance, which can be read and closed without impacting the original
@@ -31,5 +31,5 @@ public abstract class ClonableAudioStream extends AudioStream {
* @return a new input stream that can be consumed by the caller
* @throws AudioException if stream cannot be created
*/
- public abstract InputStream getClonedStream() throws AudioException;
+ public InputStream getClonedStream() throws AudioException;
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FileAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FileAudioStream.java
index 7247742aaa6..ab35257b6f5 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FileAudioStream.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FileAudioStream.java
@@ -42,9 +42,11 @@ public class FileAudioStream extends FixedLengthAudioStream implements Disposabl
private final File file;
private final AudioFormat audioFormat;
- private InputStream inputStream;
+ private FileInputStream inputStream;
private final long length;
private final boolean isTemporaryFile;
+ private int markedOffset = 0;
+ private int alreadyRead = 0;
public FileAudioStream(File file) throws AudioException {
this(file, getAudioFormat(file));
@@ -87,7 +89,7 @@ private static AudioFormat parseWavFormat(File file) throws AudioException {
}
}
- private static InputStream getInputStream(File file) throws AudioException {
+ private static FileInputStream getInputStream(File file) throws AudioException {
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
@@ -102,7 +104,9 @@ public AudioFormat getFormat() {
@Override
public int read() throws IOException {
- return inputStream.read();
+ int read = inputStream.read();
+ alreadyRead++;
+ return read;
}
@Override
@@ -124,11 +128,23 @@ public synchronized void reset() throws IOException {
}
try {
inputStream = getInputStream(file);
+ inputStream.skipNBytes(markedOffset);
+ alreadyRead = markedOffset;
} catch (AudioException e) {
throw new IOException("Cannot reset file input stream: " + e.getMessage(), e);
}
}
+ @Override
+ public synchronized void mark(int readlimit) {
+ markedOffset = alreadyRead;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
@Override
public InputStream getClonedStream() throws AudioException {
return getInputStream(file);
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FixedLengthAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FixedLengthAudioStream.java
index 4a737d50f99..daaf6657a33 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FixedLengthAudioStream.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/FixedLengthAudioStream.java
@@ -15,18 +15,15 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
- * This is a {@link ClonableAudioStream}, which can also provide information about its absolute length.
+ * This is a {@link AudioStream}, which can also provide information about its absolute length and get cloned.
*
* @author Kai Kreuzer - Initial contribution
- * @author Gwendal Roulleau - Separate getClonedStream into its own class
+ * @author Gwendal Roulleau - Separate getClonedStream and length into their own interface.
+ * @deprecated You should consider using {@link ClonableAudioStream} and/or {@link SizeableAudioStream} to detect audio
+ * stream capabilities
*/
@NonNullByDefault
-public abstract class FixedLengthAudioStream extends ClonableAudioStream {
+@Deprecated
+public abstract class FixedLengthAudioStream extends AudioStream implements SizeableAudioStream, ClonableAudioStream {
- /**
- * Provides the length of the stream in bytes.
- *
- * @return absolute length in bytes
- */
- public abstract long length();
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/SizeableAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/SizeableAudioStream.java
new file mode 100644
index 00000000000..6074f2424c2
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/SizeableAudioStream.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.audio;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is for an {@link AudioStream}, which size is known
+ *
+ * @author Gwendal Roulleau - Initial contribution, separation from {@link FixedLengthAudioStream}
+ */
+@NonNullByDefault
+public interface SizeableAudioStream {
+
+ /**
+ * Provides the length of the stream in bytes.
+ *
+ * @return absolute length in bytes
+ */
+ public long length();
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/URLAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/URLAudioStream.java
index e43ab639760..d365829097a 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/URLAudioStream.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/URLAudioStream.java
@@ -40,7 +40,7 @@
* @author Christoph Weitkamp - Refactored use of filename extension
*/
@NonNullByDefault
-public class URLAudioStream extends ClonableAudioStream {
+public class URLAudioStream extends AudioStream implements ClonableAudioStream {
private static final Pattern PLS_STREAM_PATTERN = Pattern.compile("^File[0-9]=(.+)$");
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java
index e7acbb867ee..3a5ac9377ab 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java
@@ -48,7 +48,7 @@
import org.openhab.core.audio.ByteArrayAudioStream;
import org.openhab.core.audio.ClonableAudioStream;
import org.openhab.core.audio.FileAudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.SizeableAudioStream;
import org.openhab.core.audio.StreamServed;
import org.openhab.core.audio.utils.AudioSinkUtils;
import org.openhab.core.common.ThreadPoolManager;
@@ -135,8 +135,8 @@ private InputStream prepareInputStream(final StreamServed streamServed, final Ht
}
// try to set the content-length, if possible
- if (streamServed.audioStream() instanceof FixedLengthAudioStream fixedLengthServedStream) {
- final long size = fixedLengthServedStream.length();
+ if (streamServed.audioStream() instanceof SizeableAudioStream sizeableServedStream) {
+ final long size = sizeableServedStream.length();
resp.setContentLength((int) size);
}
@@ -285,9 +285,9 @@ public StreamServed serve(AudioStream originalStream, int seconds, boolean multi
return streamToServe;
}
- private ClonableAudioStream createClonableInputStream(AudioStream stream, String streamId) throws IOException {
+ private AudioStream createClonableInputStream(AudioStream stream, String streamId) throws IOException {
byte[] dataBytes = stream.readNBytes(ONETIME_STREAM_BUFFER_MAX_SIZE + 1);
- ClonableAudioStream clonableAudioStreamResult;
+ AudioStream clonableAudioStreamResult;
if (dataBytes.length <= ONETIME_STREAM_BUFFER_MAX_SIZE) {
// we will use an in memory buffer to avoid disk operation
clonableAudioStreamResult = new ByteArrayAudioStream(dataBytes, stream.getFormat());
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java
index f49da0ce8e0..069096baee8 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java
@@ -66,11 +66,20 @@ public class AudioSinkUtilsImpl implements AudioSinkUtils {
.longValue();
return startTime + computedDuration;
} catch (IOException | UnsupportedAudioFileException e) {
- logger.debug("Cannot compute the duration of input stream", e);
+ logger.debug("Cannot compute the duration of input stream with method java stream sound analysis",
+ e);
+ Integer bitRate = audioFormat.getBitRate();
+ if (bitRate != null && bitRate != 0) {
+ long computedDuration = Float.valueOf((1f * dataTransferedLength / bitRate) * 1000000000)
+ .longValue();
+ return startTime + computedDuration;
+ } else {
+ logger.debug("Cannot compute the duration of input stream by using audio format information");
+ }
return null;
}
} else if (AudioFormat.CODEC_MP3.equals(audioFormat.getCodec())) {
- // not precise, no VBR, but better than nothing
+ // not accurate, no VBR support, but better than nothing
Bitstream bitstream = new Bitstream(new ByteArrayInputStream(dataBytes));
try {
Header h = bitstream.readFrame();
diff --git a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java
index 6f7200604a6..da5eafc1d0e 100644
--- a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java
+++ b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java
@@ -29,7 +29,6 @@
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.ByteArrayAudioStream;
import org.openhab.core.audio.FileAudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
import org.openhab.core.audio.StreamServed;
import org.openhab.core.audio.internal.utils.BundledSoundFileHandler;
@@ -214,7 +213,7 @@ public void oneTimeStreamIsClosedAndRemovedAfterServed() throws Exception {
@Test
public void multiTimeStreamIsClosedAfterExpired() throws Exception {
AtomicInteger cloneCounter = new AtomicInteger();
- FixedLengthAudioStream audioStream = mock(FixedLengthAudioStream.class);
+ ByteArrayAudioStream audioStream = mock(ByteArrayAudioStream.class);
AudioStream clonedStream = mock(AudioStream.class);
AudioFormat audioFormat = mock(AudioFormat.class);
when(audioStream.getFormat()).thenReturn(audioFormat);
@@ -250,7 +249,7 @@ public void multiTimeStreamIsClosedAfterExpired() throws Exception {
@Test
public void streamsAreClosedOnDeactivate() throws Exception {
AudioStream oneTimeStream = mock(AudioStream.class);
- FixedLengthAudioStream multiTimeStream = mock(FixedLengthAudioStream.class);
+ ByteArrayAudioStream multiTimeStream = mock(ByteArrayAudioStream.class);
serveStream(oneTimeStream);
serveStream(multiTimeStream, 10);
diff --git a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/fake/AudioSinkFake.java b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/fake/AudioSinkFake.java
index 89d189c12b8..6bf2a61d40b 100644
--- a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/fake/AudioSinkFake.java
+++ b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/fake/AudioSinkFake.java
@@ -21,7 +21,7 @@
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.ByteArrayAudioStream;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
@@ -49,8 +49,8 @@ public class AudioSinkFake implements AudioSink {
public boolean isUnsupportedAudioStreamExceptionExpected;
private static final Set SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV);
- private static final Set> SUPPORTED_AUDIO_STREAMS = Set
- .of(FixedLengthAudioStream.class, URLAudioStream.class);
+ private static final Set> SUPPORTED_AUDIO_STREAMS = Set.of(ByteArrayAudioStream.class,
+ URLAudioStream.class);
@Override
public String getId() {
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryBundleTracker.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryBundleTracker.java
index 3b4f6f4dd9f..69c23fecc66 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryBundleTracker.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryBundleTracker.java
@@ -50,22 +50,19 @@ public class ScriptEngineFactoryBundleTracker extends BundleTracker impl
private final Logger logger = LoggerFactory.getLogger(ScriptEngineFactoryBundleTracker.class);
private final ReadyService readyService;
- private final StartLevelService startLevelService;
private final Map bundles = new ConcurrentHashMap<>();
+ private boolean startLevel = false;
private boolean ready = false;
@Activate
- public ScriptEngineFactoryBundleTracker(final @Reference ReadyService readyService,
- final @Reference StartLevelService startLevelService, BundleContext bc) {
+ public ScriptEngineFactoryBundleTracker(final @Reference ReadyService readyService, BundleContext bc) {
super(bc, STATE_MASK, null);
this.readyService = readyService;
- this.startLevelService = startLevelService;
-
this.open();
readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)
- .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_OSGI)));
+ .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL)));
}
@Deactivate
@@ -85,8 +82,8 @@ public Bundle addingBundle(@NonNullByDefault({}) Bundle bundle, @Nullable Bundle
if (isScriptingBundle(bundle)) {
logger.debug("Added {}: {} ", bsn, stateToString(state));
bundles.put(bsn, state);
+ checkReady();
}
- checkReady();
return bundle;
}
@@ -99,8 +96,8 @@ public void modifiedBundle(@NonNullByDefault({}) Bundle bundle, @Nullable Bundle
if (isScriptingBundle(bundle)) {
logger.debug("Modified {}: {}", bsn, stateToString(state));
bundles.put(bsn, state);
+ checkReady();
}
- checkReady();
}
@Override
@@ -110,27 +107,35 @@ public void removedBundle(@NonNullByDefault({}) Bundle bundle, @Nullable BundleE
if (isScriptingBundle(bundle)) {
logger.debug("Removed {}", bsn);
bundles.remove(bsn);
+ checkReady();
}
- checkReady();
}
@Override
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
+ logger.debug("Readymarker {} added", readyMarker);
+ startLevel = true;
checkReady();
}
@Override
public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
+ logger.debug("Readymarker {} removed", readyMarker);
+ startLevel = false;
ready = false;
readyService.unmarkReady(READY_MARKER);
}
- private void checkReady() {
- if (!ready && startLevelService.getStartLevel() > StartLevelService.STARTLEVEL_OSGI && allBundlesActive()) {
- logger.info("All automation bundles ready.");
+ private synchronized void checkReady() {
+ boolean allBundlesActive = allBundlesActive();
+ logger.trace("ready: {}, startlevel: {}, allActive: {}", ready, startLevel, allBundlesActive);
+
+ if (!ready && startLevel && allBundlesActive) {
+ logger.debug("Adding ready marker: All automation bundles ready ({})", bundles);
readyService.markReady(READY_MARKER);
ready = true;
- } else if (ready && !allBundlesActive()) {
+ } else if (ready && !allBundlesActive) {
+ logger.debug("Removing ready marker: Not all automation bundles ready ({})", bundles);
readyService.unmarkReady(READY_MARKER);
ready = false;
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java
index 1585d0be863..9d3adc6e14c 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java
@@ -266,7 +266,7 @@ public RuleEngineImpl(final @Reference ModuleTypeRegistry moduleTypeRegistry,
this.ruleRegistry = ruleRegistry;
this.readyService = readyService;
- listener = new RegistryChangeListener() {
+ listener = new RegistryChangeListener<>() {
@Override
public void added(Rule rule) {
RuleEngineImpl.this.addRule(rule);
@@ -337,8 +337,7 @@ public void added(ModuleType moduleType) {
synchronized (this) {
Set rulesPerModule = mapModuleTypeToRules.get(moduleTypeName);
if (rulesPerModule != null) {
- rules = new HashSet<>();
- rules.addAll(rulesPerModule);
+ rules = new HashSet<>(rulesPerModule);
}
}
if (rules != null) {
@@ -366,8 +365,7 @@ public void updated(ModuleType oldElement, ModuleType moduleType) {
synchronized (this) {
Set rulesPerModule = mapModuleTypeToRules.get(moduleTypeName);
if (rulesPerModule != null) {
- rules = new HashSet<>();
- rules.addAll(rulesPerModule);
+ rules = new HashSet<>(rulesPerModule);
}
}
if (rules != null) {
@@ -402,8 +400,7 @@ protected void addModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory
moduleHandlerFactories.put(moduleTypeName, moduleHandlerFactory);
Set rulesPerModule = mapModuleTypeToRules.get(moduleTypeName);
if (rulesPerModule != null) {
- rules = new HashSet<>();
- rules.addAll(rulesPerModule);
+ rules = new HashSet<>(rulesPerModule);
}
}
if (rules != null) {
@@ -505,10 +502,8 @@ private void setRule(WrappedRule rule) {
final boolean activated = activateRule(rule);
if (activated) {
Future> f = scheduleTasks.remove(rUID);
- if (f != null) {
- if (!f.isDone()) {
- f.cancel(true);
- }
+ if ((f != null) && !f.isDone()) {
+ f.cancel(true);
}
}
}
@@ -942,7 +937,7 @@ private void removeMissingModuleTypes(Collection moduleTypes) {
for (Entry> e : mapMissingHandlers.entrySet()) {
String rUID = e.getKey();
List missingTypes = e.getValue();
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
sb.append("Missing handlers: ");
for (String typeUID : missingTypes) {
sb.append(typeUID).append(", ");
@@ -1097,7 +1092,7 @@ private Map getContext(String ruleUID, @Nullable Set
throw new IllegalStateException("context cannot be null at that point - please report a bug.");
}
if (connections != null) {
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
for (Connection c : connections) {
String outputModuleId = c.getOutputModuleId();
if (outputModuleId != null) {
@@ -1438,7 +1433,7 @@ public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
private void executeRulesWithStartLevel() {
getScheduledExecutor().submit(() -> {
ruleRegistry.getAll().stream() //
- .filter(r -> mustTrigger(r)) //
+ .filter(this::mustTrigger) //
.forEach(r -> runNow(r.getUID(), true,
Map.of(SystemTriggerHandler.OUT_STARTLEVEL, StartLevelService.STARTLEVEL_RULES)));
started = true;
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java
index c7c5971823a..f50a9f5d081 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java
@@ -500,7 +500,7 @@ private void validateConfiguration(List configDescri
if (isOptionalConfig(configDescriptions)) {
return;
} else {
- StringBuffer statusDescription = new StringBuffer();
+ StringBuilder statusDescription = new StringBuilder();
String msg = " '%s';";
for (ConfigDescriptionParameter configParameter : configDescriptions) {
if (configParameter.isRequired()) {
@@ -517,7 +517,7 @@ private void validateConfiguration(List configDescri
processValue(configurations.remove(configParameterName), configParameter);
}
if (!configurations.isEmpty()) {
- StringBuffer statusDescription = new StringBuffer();
+ StringBuilder statusDescription = new StringBuilder();
String msg = " '%s';";
for (String name : configurations.keySet()) {
statusDescription.append(String.format(msg, name));
@@ -610,7 +610,7 @@ private boolean checkType(Type type, Object configValue) {
*/
private void resolveModuleConfigReferences(List extends Module> modules, Map ruleConfiguration) {
if (modules != null) {
- StringBuffer statusDescription = new StringBuffer();
+ StringBuilder statusDescription = new StringBuilder();
for (Module module : modules) {
try {
ReferenceResolver.updateConfiguration(module.getConfiguration(), ruleConfiguration, logger);
diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java
index d4f94db622a..01c8d076eb4 100644
--- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java
+++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java
@@ -33,7 +33,7 @@ public class ConsoleInterpreter {
public static String getHelp(final String base, final String separator,
Collection extensions) {
final List usages = ConsoleInterpreter.getUsages(extensions);
- final StringBuffer buffer = new StringBuffer();
+ final StringBuilder buffer = new StringBuilder();
buffer.append("---openHAB commands---\n\t");
for (int i = 0; i < usages.size(); i++) {
diff --git a/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java b/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java
index b74cd99416a..4501bbacfed 100644
--- a/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java
+++ b/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java
@@ -40,16 +40,18 @@
import io.micrometer.core.instrument.Tags;
/**
- * The {@link ThingStateMetric} class implements a metric for the openHAB things states.
+ * The {@link ThingStateMetric} class implements a metric for the openHAB things
+ * states.
*
* @author Robert Bach - Initial contribution
+ * @author Scott Hraban - Create Meter using thingUid instead of thingId during
+ * bind phase
*/
@NonNullByDefault
public class ThingStateMetric implements OpenhabCoreMeterBinder, EventSubscriber {
private final Logger logger = LoggerFactory.getLogger(ThingStateMetric.class);
public static final String METRIC_NAME = "openhab.thing.state";
private static final String THING_TAG_NAME = "thing";
- private static final String THINGSTATUS_TOPIC_PREFIX = "openhab/things/";
private final ThingRegistry thingRegistry;
private final Meter.Id commonMeterId;
private final Map registeredMeters = new HashMap<>();
@@ -70,7 +72,7 @@ public void bindTo(@NonNullByDefault({}) MeterRegistry meterRegistry) {
logger.debug("ThingStateMetric is being bound...");
this.meterRegistry = meterRegistry;
thingRegistry.getAll().forEach(
- thing -> createOrUpdateMetricForBundleState(thing.getUID().getId(), thing.getStatus().ordinal()));
+ thing -> createOrUpdateMetricForBundleState(thing.getUID().getAsString(), thing.getStatus().ordinal()));
eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, null);
}
@@ -111,10 +113,13 @@ public Set getSubscribedEventTypes() {
@Override
public void receive(Event event) {
- logger.trace("Received ThingStatusInfo(Changed)Event...");
- String thingId = event.getTopic().substring(THINGSTATUS_TOPIC_PREFIX.length(),
- event.getTopic().lastIndexOf('/'));
- ThingStatus status = gson.fromJson(event.getPayload(), ThingStatusInfo.class).getStatus();
- createOrUpdateMetricForBundleState(thingId, status.ordinal());
+ if (event instanceof ThingStatusInfoEvent thingEvent) {
+ logger.trace("Received ThingStatusInfo(Changed)Event...");
+ String thingUid = thingEvent.getThingUID().getAsString();
+ ThingStatus status = gson.fromJson(event.getPayload(), ThingStatusInfo.class).getStatus();
+ createOrUpdateMetricForBundleState(thingUid, status.ordinal());
+ } else {
+ logger.trace("Received unsubscribed for event type {}", event.getClass().getSimpleName());
+ }
}
}
diff --git a/bundles/org.openhab.core.io.monitor/src/test/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetricTest.java b/bundles/org.openhab.core.io.monitor/src/test/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetricTest.java
new file mode 100644
index 00000000000..61118050546
--- /dev/null
+++ b/bundles/org.openhab.core.io.monitor/src/test/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetricTest.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.io.monitor.internal.metrics;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.events.ThingEventFactory;
+import org.openhab.core.thing.internal.ThingImpl;
+import org.osgi.framework.BundleContext;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+
+/**
+ * Tests for ThingStateMetric class
+ *
+ * @author Scott Hraban - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class ThingStateMetricTest {
+
+ @Test
+ public void testThingUidAlwaysUsedToCreateMeter() {
+ final String strThingTypeUid = "sonos:Amp";
+
+ final String strThingUid = strThingTypeUid + ":RINCON_347E5C0D150501400";
+ ThingUID thingUid = new ThingUID(strThingUid);
+ Thing thing = new ThingImpl(new ThingTypeUID(strThingTypeUid), thingUid);
+
+ final String strThingUid2 = strThingTypeUid + ":foo";
+ ThingUID thingUid2 = new ThingUID(strThingUid2);
+
+ ThingRegistry thingRegistry = mock(ThingRegistry.class);
+
+ SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
+
+ ThingStateMetric thingStateMetric = new ThingStateMetric(mock(BundleContext.class), thingRegistry,
+ new HashSet());
+
+ // Only one meter registered at bind time
+ doReturn(Collections.singleton(thing)).when(thingRegistry).getAll();
+ thingStateMetric.bindTo(meterRegistry);
+
+ List meters = meterRegistry.getMeters();
+ assertEquals(1, meters.size());
+ assertEquals(strThingUid, meters.get(0).getId().getTag("thing"));
+
+ // Still only one meter registered after receiving an event
+ ThingStatusInfo thingStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
+ thingStateMetric.receive(ThingEventFactory.createStatusInfoEvent(thingUid, thingStatusInfo));
+
+ meters = meterRegistry.getMeters();
+ assertEquals(1, meters.size());
+ assertEquals(strThingUid, meters.get(0).getId().getTag("thing"));
+
+ // Now another one is added
+ thingStateMetric.receive(ThingEventFactory.createStatusInfoEvent(thingUid2, thingStatusInfo));
+
+ meters = meterRegistry.getMeters();
+ assertEquals(2, meters.size());
+ }
+}
diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java
index dfa37e601a3..c17ce4df1b4 100644
--- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java
+++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java
@@ -31,7 +31,7 @@ public class EnrichedThingDTO extends AbstractThingDTO {
public List channels;
public ThingStatusInfo statusInfo;
- public final FirmwareStatusDTO firmwareStatus;
+ public FirmwareStatusDTO firmwareStatus;
public boolean editable;
/**
diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java
index 70bafa9e4bf..d788c1762ea 100644
--- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java
+++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java
@@ -42,6 +42,8 @@
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.thing.events.ChannelDescriptionChangedEvent;
import org.openhab.core.ui.items.ItemUIRegistry;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
@@ -72,6 +74,7 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener
private static final int DEFAULT_MAX_SUBSCRIPTIONS = 50;
private final Logger logger = LoggerFactory.getLogger(SitemapSubscriptionService.class);
+ private final BundleContext bundleContext;
public interface SitemapSubscriptionCallback {
@@ -94,15 +97,17 @@ public interface SitemapSubscriptionCallback {
private final Map creationInstants = new ConcurrentHashMap<>();
/* sitemap+page -> listener */
- private final Map pageChangeListeners = new ConcurrentHashMap<>();
+ private final Map pageChangeListeners = new ConcurrentHashMap<>();
/* Max number of subscriptions at the same time */
private int maxSubscriptions = DEFAULT_MAX_SUBSCRIPTIONS;
@Activate
- public SitemapSubscriptionService(Map config, final @Reference ItemUIRegistry itemUIRegistry) {
- applyConfig(config);
+ public SitemapSubscriptionService(Map config, final @Reference ItemUIRegistry itemUIRegistry,
+ BundleContext bundleContext) {
this.itemUIRegistry = itemUIRegistry;
+ this.bundleContext = bundleContext;
+ applyConfig(config);
}
@Deactivate
@@ -110,9 +115,7 @@ protected void deactivate() {
pageOfSubscription.clear();
callbacks.clear();
creationInstants.clear();
- for (PageChangeListener listener : pageChangeListeners.values()) {
- listener.dispose();
- }
+ pageChangeListeners.values().forEach(l -> l.serviceRegistration.unregister());
pageChangeListeners.clear();
}
@@ -150,7 +153,7 @@ protected void removeSitemapProvider(SitemapProvider provider) {
* Creates a new subscription with the given id.
*
* @param callback an instance that should receive the events
- * @returns a unique id that identifies the subscription or null if the limit of subscriptions is already reached
+ * @return a unique id that identifies the subscription or null if the limit of subscriptions is already reached
*/
public @Nullable String createSubscription(SitemapSubscriptionCallback callback) {
if (maxSubscriptions >= 0 && callbacks.size() >= maxSubscriptions) {
@@ -176,9 +179,9 @@ public void removeSubscription(String subscriptionId) {
String sitemapPage = pageOfSubscription.remove(subscriptionId);
if (sitemapPage != null && !pageOfSubscription.values().contains(sitemapPage)) {
// this was the only subscription listening on this page, so we can dispose the listener
- PageChangeListener listener = pageChangeListeners.remove(sitemapPage);
+ ListenerRecord listener = pageChangeListeners.remove(sitemapPage);
if (listener != null) {
- listener.dispose();
+ listener.serviceRegistration().unregister();
}
}
logger.debug("Removed subscription with id {} ({} active subscriptions)", subscriptionId, callbacks.size());
@@ -249,13 +252,14 @@ public void setPageId(String subscriptionId, String sitemapName, String pageId)
}
private void addCallbackToListener(String sitemapName, String pageId, SitemapSubscriptionCallback callback) {
- PageChangeListener listener = pageChangeListeners.get(getValue(sitemapName, pageId));
- if (listener == null) {
- // there is no listener for this page yet, so let's try to create one
- listener = new PageChangeListener(sitemapName, pageId, itemUIRegistry, collectWidgets(sitemapName, pageId));
- pageChangeListeners.put(getValue(sitemapName, pageId), listener);
- }
- listener.addCallback(callback);
+ ListenerRecord listener = pageChangeListeners.computeIfAbsent(getValue(sitemapName, pageId), v -> {
+ PageChangeListener newListener = new PageChangeListener(sitemapName, pageId, itemUIRegistry,
+ collectWidgets(sitemapName, pageId));
+ ServiceRegistration> registration = bundleContext.registerService(EventSubscriber.class.getName(),
+ newListener, null);
+ return new ListenerRecord(newListener, registration);
+ });
+ listener.pageChangeListener().addCallback(callback);
}
private EList collectWidgets(String sitemapName, String pageId) {
@@ -278,12 +282,12 @@ private EList collectWidgets(String sitemapName, String pageId) {
}
private void removeCallbackFromListener(String sitemapPage, SitemapSubscriptionCallback callback) {
- PageChangeListener oldListener = pageChangeListeners.get(sitemapPage);
+ ListenerRecord oldListener = pageChangeListeners.get(sitemapPage);
if (oldListener != null) {
- oldListener.removeCallback(callback);
- if (!pageOfSubscription.values().contains(sitemapPage)) {
+ oldListener.pageChangeListener().removeCallback(callback);
+ if (!pageOfSubscription.containsValue(sitemapPage)) {
// no other callbacks are left here, so we can safely dispose the listener
- oldListener.dispose();
+ oldListener.serviceRegistration().unregister();
pageChangeListeners.remove(sitemapPage);
}
}
@@ -311,14 +315,14 @@ public void modelChanged(String modelName, EventType type) {
String changedSitemapName = modelName.substring(0, modelName.length() - SITEMAP_SUFFIX.length());
- for (Entry listenerEntry : pageChangeListeners.entrySet()) {
+ for (Entry listenerEntry : pageChangeListeners.entrySet()) {
String sitemapWithPage = listenerEntry.getKey();
String sitemapName = extractSitemapName(sitemapWithPage);
String pageId = extractPageId(sitemapWithPage);
if (sitemapName.equals(changedSitemapName)) {
EList widgets = collectWidgets(sitemapName, pageId);
- listenerEntry.getValue().sitemapContentChanged(widgets);
+ listenerEntry.getValue().pageChangeListener().sitemapContentChanged(widgets);
}
}
}
@@ -336,9 +340,7 @@ public void checkAliveClients() {
}
}
// Send an ALIVE event to all subscribers to trigger an exception for dead subscribers
- for (Entry listenerEntry : pageChangeListeners.entrySet()) {
- listenerEntry.getValue().sendAliveEvent();
- }
+ pageChangeListeners.values().forEach(l -> l.pageChangeListener().sendAliveEvent());
}
@Override
@@ -355,19 +357,22 @@ public void receive(Event event) {
// members and predictions aren't really possible in that case (or at least would be highly complex).
return;
}
- for (PageChangeListener pageChangeListener : pageChangeListeners.values()) {
+ for (ListenerRecord listener : pageChangeListeners.values()) {
if (prediction.isConfirmation()) {
- pageChangeListener.keepCurrentState(item);
+ listener.pageChangeListener().keepCurrentState(item);
} else {
- pageChangeListener.changeStateTo(item, prediction.getPredictedState());
+ listener.pageChangeListener().changeStateTo(item, prediction.getPredictedState());
}
}
} else if (event instanceof ChannelDescriptionChangedEvent channelDescriptionChangedEvent) {
channelDescriptionChangedEvent.getLinkedItemNames().forEach(itemName -> {
- for (PageChangeListener pageChangeListener : pageChangeListeners.values()) {
- pageChangeListener.descriptionChanged(itemName);
+ for (ListenerRecord listener : pageChangeListeners.values()) {
+ listener.pageChangeListener().descriptionChanged(itemName);
}
});
}
}
+
+ private record ListenerRecord(PageChangeListener pageChangeListener, ServiceRegistration> serviceRegistration) {
+ }
}
diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java
index 3252af2f6bd..30aa6bc5e92 100644
--- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java
+++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java
@@ -20,16 +20,19 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import org.eclipse.emf.common.util.EList;
import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.events.Event;
+import org.openhab.core.events.EventSubscriber;
import org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper;
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback;
-import org.openhab.core.items.GenericItem;
-import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
-import org.openhab.core.items.StateChangeListener;
+import org.openhab.core.items.events.GroupStateUpdatedEvent;
+import org.openhab.core.items.events.ItemEvent;
+import org.openhab.core.items.events.ItemStateChangedEvent;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
@@ -45,7 +48,7 @@
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - Added support for icon color
*/
-public class PageChangeListener implements StateChangeListener {
+public class PageChangeListener implements EventSubscriber {
private static final int REVERT_INTERVAL = 300;
private final ScheduledExecutorService scheduler = ThreadPoolManager
@@ -55,6 +58,7 @@ public class PageChangeListener implements StateChangeListener {
private final ItemUIRegistry itemUIRegistry;
private EList widgets;
private Set- items;
+ private final HashSet filterItems = new HashSet<>();
private final List callbacks = Collections.synchronizedList(new ArrayList<>());
private Set distinctCallbacks = Collections.emptySet();
@@ -75,23 +79,10 @@ public PageChangeListener(String sitemapName, String pageId, ItemUIRegistry item
}
private void updateItemsAndWidgets(EList widgets) {
- if (this.widgets != null) {
- // cleanup statechange listeners in case widgets were removed
- items = getAllItems(this.widgets);
- for (Item item : items) {
- if (item instanceof GenericItem genericItem) {
- genericItem.removeStateChangeListener(this);
- }
- }
- }
-
this.widgets = widgets;
items = getAllItems(widgets);
- for (Item item : items) {
- if (item instanceof GenericItem genericItem) {
- genericItem.addStateChangeListener(this);
- }
- }
+ filterItems.clear();
+ filterItems.addAll(items.stream().map(Item::getName).collect(Collectors.toSet()));
}
public String getSitemapName() {
@@ -113,19 +104,6 @@ public void removeCallback(SitemapSubscriptionCallback callback) {
distinctCallbacks = new HashSet<>(callbacks);
}
- /**
- * Disposes this instance and releases all resources.
- */
- public void dispose() {
- for (Item item : items) {
- if (item instanceof GenericItem genericItem) {
- genericItem.removeStateChangeListener(this);
- } else if (item instanceof GroupItem groupItem) {
- groupItem.removeStateChangeListener(this);
- }
- }
- }
-
/**
* Collects all items that are represented by a given list of widgets
*
@@ -182,26 +160,6 @@ private void constructAndSendEvents(Item item, State newState) {
}
}
- @Override
- public void stateChanged(Item item, State oldState, State newState) {
- // For all items except group, send an event only when the event state is changed.
- if (item instanceof GroupItem) {
- return;
- }
- constructAndSendEvents(item, newState);
- }
-
- @Override
- public void stateUpdated(Item item, State state) {
- // For group item only, send an event each time the event state is updated.
- // It allows updating the group label while the group state is unchanged,
- // for example the count in label for Group:Switch:OR
- if (!(item instanceof GroupItem)) {
- return;
- }
- constructAndSendEvents(item, state);
- }
-
public void keepCurrentState(Item item) {
scheduler.schedule(() -> {
constructAndSendEvents(item, item.getState());
@@ -338,4 +296,24 @@ private Set constructSitemapEventsForUpdatedDescr(Item item, List<
}
return events;
}
+
+ @Override
+ public Set getSubscribedEventTypes() {
+ return Set.of(ItemStateChangedEvent.TYPE, GroupStateUpdatedEvent.TYPE);
+ }
+
+ @Override
+ public void receive(Event event) {
+ if (event instanceof ItemEvent itemEvent && filterItems.contains(itemEvent.getItemName())) {
+ Item item = itemUIRegistry.get(itemEvent.getItemName());
+ if (item == null) {
+ return;
+ }
+ if (event instanceof GroupStateUpdatedEvent groupStateUpdatedEvent) {
+ constructAndSendEvents(item, groupStateUpdatedEvent.getItemState());
+ } else if (event instanceof ItemStateChangedEvent itemStateChangedEvent) {
+ constructAndSendEvents(item, itemStateChangedEvent.getItemState());
+ }
+ }
+ }
}
diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java
index 478f244a141..2c92feed0f7 100644
--- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java
+++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java
@@ -21,10 +21,12 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
@@ -56,6 +58,8 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role;
import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.events.Event;
+import org.openhab.core.events.EventSubscriber;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
@@ -67,7 +71,8 @@
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
-import org.openhab.core.items.StateChangeListener;
+import org.openhab.core.items.events.ItemEvent;
+import org.openhab.core.items.events.ItemStateChangedEvent;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.model.sitemap.SitemapProvider;
@@ -125,8 +130,9 @@
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
* @author Wouter Born - Migrated to OpenAPI annotations
* @author Laurent Garnier - Added support for icon color
+ * @author Mark Herwege - Added pattern and unit fields
*/
-@Component(service = RESTResource.class)
+@Component(service = { RESTResource.class, EventSubscriber.class })
@JaxrsResource
@JaxrsName(SitemapResource.PATH_SITEMAPS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@@ -136,7 +142,7 @@
@Tag(name = SitemapResource.PATH_SITEMAPS)
@NonNullByDefault
public class SitemapResource
- implements RESTResource, SitemapSubscriptionCallback, SseBroadcaster.Listener {
+ implements RESTResource, SitemapSubscriptionCallback, SseBroadcaster.Listener, EventSubscriber {
private final Logger logger = LoggerFactory.getLogger(SitemapResource.class);
@@ -176,6 +182,7 @@ public class SitemapResource
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
private @Nullable ScheduledFuture> cleanSubscriptionsJob;
+ private Set stateChangeListeners = new CopyOnWriteArraySet<>();
@Activate
public SitemapResource( //
@@ -520,6 +527,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState);
bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState);
bean.label = itemUIRegistry.getLabel(widget);
+ bean.pattern = itemUIRegistry.getFormatPattern(widget);
+ bean.unit = itemUIRegistry.getUnitForWidget(widget);
bean.type = widget.eClass().getName();
bean.visibility = itemUIRegistry.getVisiblity(widget);
if (widget instanceof LinkableWidget linkableWidget) {
@@ -683,13 +692,11 @@ private boolean blockUnlessChangeOccurs(String sitemapname, String pageId) {
private boolean waitForChanges(EList widgets) {
long startTime = (new Date()).getTime();
boolean timeout = false;
- BlockingStateChangeListener listener = new BlockingStateChangeListener();
- // let's get all items for these widgets
- Set items = getAllItems(widgets);
- for (GenericItem item : items) {
- item.addStateChangeListener(listener);
- }
- while (!listener.hasChangeOccurred() && !timeout) {
+ Set items = getAllItems(widgets).stream().map(Item::getName).collect(Collectors.toSet());
+ BlockingStateChangeListener listener = new BlockingStateChangeListener(items);
+ stateChangeListeners.add(listener);
+
+ while (!listener.hasChanged() && !timeout) {
timeout = (new Date()).getTime() - startTime > TIMEOUT_IN_MS;
try {
Thread.sleep(300);
@@ -698,9 +705,8 @@ private boolean waitForChanges(EList widgets) {
break;
}
}
- for (GenericItem item : items) {
- item.removeStateChangeListener(listener);
- }
+
+ stateChangeListeners.remove(listener);
return timeout;
}
@@ -779,34 +785,16 @@ private Set getItemsInColorCond(EList colorList) {
return items;
}
- /**
- * This is a state change listener, which is merely used to determine, if a
- * state change has occurred on one of a list of items.
- *
- * @author Kai Kreuzer - Initial contribution
- *
- */
- private static class BlockingStateChangeListener implements StateChangeListener {
-
- private boolean changed = false;
-
- @Override
- public void stateChanged(Item item, State oldState, State newState) {
- changed = true;
- }
-
- /**
- * determines, whether a state change has occurred since its creation
- *
- * @return true, if a state has changed
- */
- public boolean hasChangeOccurred() {
- return changed;
- }
+ @Override
+ public Set getSubscribedEventTypes() {
+ return Set.of(ItemStateChangedEvent.TYPE);
+ }
- @Override
- public void stateUpdated(Item item, State state) {
- // ignore if the state did not change
+ @Override
+ public void receive(Event event) {
+ if (event instanceof ItemEvent itemEvent) {
+ String itemName = itemEvent.getItemName();
+ stateChangeListeners.forEach(l -> l.itemChanged(itemName));
}
}
@@ -852,4 +840,23 @@ public void sseEventSinkRemoved(SseEventSink sink, SseSinkInfo info) {
subscriptions.removeSubscription(info.subscriptionId);
knownSubscriptions.remove(info.subscriptionId);
}
+
+ private static class BlockingStateChangeListener {
+ private final Set items;
+ private boolean changed = false;
+
+ public BlockingStateChangeListener(Set items) {
+ this.items = items;
+ }
+
+ public void itemChanged(String item) {
+ if (items.contains(item)) {
+ changed = true;
+ }
+ }
+
+ public boolean hasChanged() {
+ return changed;
+ }
+ }
}
diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java
index db47ddd27e8..c9dc713b082 100644
--- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java
+++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java
@@ -24,6 +24,7 @@
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Initial contribution
* @author Laurent Garnier - New field iconcolor
+ * @author Mark herwege - New fields pattern, unit
*/
public class WidgetDTO {
@@ -38,6 +39,9 @@ public class WidgetDTO {
public String valuecolor;
public String iconcolor;
+ public String pattern;
+ public String unit;
+
// widget-specific attributes
public final List mappings = new ArrayList<>();
public Boolean switchSupport;
diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java
index fe384aeabb0..b8a0b0da6ab 100644
--- a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java
+++ b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java
@@ -18,7 +18,6 @@
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.mockito.Mockito.*;
-import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
@@ -45,7 +44,7 @@
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemNotFoundException;
-import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.items.events.ItemEvent;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.model.sitemap.SitemapProvider;
@@ -158,11 +157,13 @@ public void whenSitemapsAreProvidedShouldReturnSitemapBeans() {
@Test
public void whenLongPollingShouldObserveItems() {
+ ItemEvent itemEvent = mock(ItemEvent.class);
+ when(itemEvent.getItemName()).thenReturn(item.getName());
new Thread(() -> {
try {
Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the
// item
- item.setState(PercentType.ZERO);
+ sitemapResource.receive(itemEvent);
} catch (InterruptedException e) {
}
}).start();
@@ -180,11 +181,13 @@ public void whenLongPollingShouldObserveItems() {
@Test
public void whenLongPollingShouldObserveItemsFromVisibilityRules() {
+ ItemEvent itemEvent = mock(ItemEvent.class);
+ when(itemEvent.getItemName()).thenReturn(visibilityRuleItem.getName());
new Thread(() -> {
try {
Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the
// item
- visibilityRuleItem.setState(new DecimalType(BigDecimal.ONE));
+ sitemapResource.receive(itemEvent);
} catch (InterruptedException e) {
}
}).start();
@@ -202,11 +205,13 @@ public void whenLongPollingShouldObserveItemsFromVisibilityRules() {
@Test
public void whenLongPollingShouldObserveItemsFromLabelColorConditions() {
+ ItemEvent itemEvent = mock(ItemEvent.class);
+ when(itemEvent.getItemName()).thenReturn(labelColorItem.getName());
new Thread(() -> {
try {
Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the
// item
- labelColorItem.setState(new DecimalType(BigDecimal.ONE));
+ sitemapResource.receive(itemEvent);
} catch (InterruptedException e) {
}
}).start();
@@ -224,11 +229,13 @@ public void whenLongPollingShouldObserveItemsFromLabelColorConditions() {
@Test
public void whenLongPollingShouldObserveItemsFromValueColorConditions() {
+ ItemEvent itemEvent = mock(ItemEvent.class);
+ when(itemEvent.getItemName()).thenReturn(valueColorItem.getName());
new Thread(() -> {
try {
Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the
// item
- valueColorItem.setState(new DecimalType(BigDecimal.ONE));
+ sitemapResource.receive(itemEvent);
} catch (InterruptedException e) {
}
}).start();
diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext
index 4e34511c2e1..7699c6e7405 100644
--- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext
+++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext
@@ -27,17 +27,34 @@ Filter:
;
FilterDetails:
- ThresholdFilter | TimeFilter
+ ThresholdFilter | TimeFilter | EqualsFilter | NotEqualsFilter | IncludeFilter | NotIncludeFilter
;
ThresholdFilter:
- '>' value=DECIMAL unit=STRING
+ '>' (relative?='%')? value=DECIMAL unit=STRING?
;
TimeFilter:
- value=INT unit=('s' | 'm' | 'h' | 'd')
+ 'T' value=INT unit=('s' | 'm' | 'h' | 'd')
;
+EqualsFilter:
+ '=' values+=STRING (',' values+=STRING)*
+;
+
+NotEqualsFilter:
+ '!' values+=STRING (',' values+=STRING)*
+;
+
+IncludeFilter:
+ '[]' lower=DECIMAL upper=DECIMAL unit=STRING?
+;
+
+NotIncludeFilter:
+ '][' lower=DECIMAL upper=DECIMAL unit=STRING?
+;
+
+
PersistenceConfiguration:
items+=(AllConfig | ItemConfig | GroupConfig) (',' items+=(AllConfig | ItemConfig | GroupConfig))* ('->' alias=STRING)?
((':' ('strategy' '=' strategies+=[Strategy|ID] (',' strategies+=[Strategy|ID])*)?
diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java
index 3531eb4976c..9b5d2b24de6 100644
--- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java
+++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java
@@ -26,9 +26,13 @@
import org.openhab.core.model.core.ModelRepositoryChangeListener;
import org.openhab.core.model.persistence.persistence.AllConfig;
import org.openhab.core.model.persistence.persistence.CronStrategy;
+import org.openhab.core.model.persistence.persistence.EqualsFilter;
import org.openhab.core.model.persistence.persistence.Filter;
import org.openhab.core.model.persistence.persistence.GroupConfig;
+import org.openhab.core.model.persistence.persistence.IncludeFilter;
import org.openhab.core.model.persistence.persistence.ItemConfig;
+import org.openhab.core.model.persistence.persistence.NotEqualsFilter;
+import org.openhab.core.model.persistence.persistence.NotIncludeFilter;
import org.openhab.core.model.persistence.persistence.PersistenceConfiguration;
import org.openhab.core.model.persistence.persistence.PersistenceModel;
import org.openhab.core.model.persistence.persistence.Strategy;
@@ -40,7 +44,9 @@
import org.openhab.core.persistence.config.PersistenceConfig;
import org.openhab.core.persistence.config.PersistenceGroupConfig;
import org.openhab.core.persistence.config.PersistenceItemConfig;
+import org.openhab.core.persistence.filter.PersistenceEqualsFilter;
import org.openhab.core.persistence.filter.PersistenceFilter;
+import org.openhab.core.persistence.filter.PersistenceIncludeFilter;
import org.openhab.core.persistence.filter.PersistenceThresholdFilter;
import org.openhab.core.persistence.filter.PersistenceTimeFilter;
import org.openhab.core.persistence.registry.PersistenceServiceConfiguration;
@@ -175,13 +181,21 @@ private List mapFilters(List filters) {
}
private PersistenceFilter mapFilter(Filter filter) {
- if (filter.getDefinition() instanceof TimeFilter) {
- TimeFilter timeFilter = (TimeFilter) filter.getDefinition();
+ if (filter.getDefinition() instanceof TimeFilter timeFilter) {
return new PersistenceTimeFilter(filter.getName(), timeFilter.getValue(), timeFilter.getUnit());
- } else if (filter.getDefinition() instanceof ThresholdFilter) {
- ThresholdFilter thresholdFilter = (ThresholdFilter) filter.getDefinition();
+ } else if (filter.getDefinition() instanceof ThresholdFilter thresholdFilter) {
return new PersistenceThresholdFilter(filter.getName(), thresholdFilter.getValue(),
- thresholdFilter.getUnit());
+ thresholdFilter.getUnit(), thresholdFilter.isRelative());
+ } else if (filter.getDefinition() instanceof EqualsFilter equalsFilter) {
+ return new PersistenceEqualsFilter(filter.getName(), equalsFilter.getValues(), false);
+ } else if (filter.getDefinition() instanceof NotEqualsFilter notEqualsFilter) {
+ return new PersistenceEqualsFilter(filter.getName(), notEqualsFilter.getValues(), true);
+ } else if (filter.getDefinition() instanceof IncludeFilter includeFilter) {
+ return new PersistenceIncludeFilter(filter.getName(), includeFilter.getLower(), includeFilter.getUpper(),
+ includeFilter.getUnit(), false);
+ } else if (filter.getDefinition() instanceof NotIncludeFilter notIncludeFilter) {
+ return new PersistenceIncludeFilter(filter.getName(), notIncludeFilter.getLower(),
+ notIncludeFilter.getUpper(), notIncludeFilter.getUnit(), true);
}
throw new IllegalArgumentException("Unknown filter type " + filter.getClass());
}
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java
index d59bc9a480d..097e92204d7 100644
--- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java
@@ -13,6 +13,7 @@
package org.openhab.core.persistence.dto;
import java.math.BigDecimal;
+import java.util.List;
/**
* The {@link org.openhab.core.persistence.dto.PersistenceFilterDTO} is used for transferring persistence filter
@@ -21,7 +22,24 @@
* @author Jan N. Klug - Initial contribution
*/
public class PersistenceFilterDTO {
- public String name = "";
- public BigDecimal value = BigDecimal.ZERO;
- public String unit = "";
+ public String name;
+
+ // threshold and time
+ public BigDecimal value;
+
+ // threshold
+ public Boolean relative;
+
+ // threshold, include/exclude
+ public String unit;
+
+ // include/exclude
+ public BigDecimal lower;
+ public BigDecimal upper;
+
+ // equals/not equals
+ public List values;
+
+ // equals/not equals, include/exclude
+ public Boolean inverted;
}
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java
index bc796a36871..e4faa9ed20f 100644
--- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java
@@ -30,6 +30,8 @@ public class PersistenceServiceConfigurationDTO {
public Collection cronStrategies = List.of();
public Collection thresholdFilters = List.of();
public Collection timeFilters = List.of();
+ public Collection equalsFilters = List.of();
+ public Collection includeFilters = List.of();
public boolean editable = false;
}
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceEqualsFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceEqualsFilter.java
new file mode 100644
index 00000000000..fdceb2863a8
--- /dev/null
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceEqualsFilter.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.persistence.filter;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.items.Item;
+
+/**
+ * The {@link PersistenceEqualsFilter} is a filter that allows only specific values to pass
+ *
+ * The filter returns {@code false} if the string representation of the item's state is not in the given list
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class PersistenceEqualsFilter extends PersistenceFilter {
+ private final Collection values;
+ private final boolean inverted;
+
+ public PersistenceEqualsFilter(String name, Collection values, boolean inverted) {
+ super(name);
+ this.values = values;
+ this.inverted = inverted;
+ }
+
+ public Collection getValues() {
+ return values;
+ }
+
+ public boolean getInverted() {
+ return inverted;
+ }
+
+ @Override
+ public boolean apply(Item item) {
+ return values.contains(item.getState().toFullString()) != inverted;
+ }
+
+ @Override
+ public void persisted(Item item) {
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s [name=%s, value=%s, inverted=]", getClass().getSimpleName(), getName(), values);
+ }
+}
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceIncludeFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceIncludeFilter.java
new file mode 100644
index 00000000000..c25aa181a3d
--- /dev/null
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceIncludeFilter.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.persistence.filter;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.items.Item;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PersistenceIncludeFilter} is a filter that allows only specific values to pass
+ *
+ * The filter returns {@code false} if the string representation of the item's state is not in the given list
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class PersistenceIncludeFilter extends PersistenceFilter {
+ private final Logger logger = LoggerFactory.getLogger(PersistenceIncludeFilter.class);
+
+ private final BigDecimal lower;
+ private final BigDecimal upper;
+ private final String unit;
+ private final boolean inverted;
+
+ public PersistenceIncludeFilter(String name, BigDecimal lower, BigDecimal upper, String unit, boolean inverted) {
+ super(name);
+ this.lower = lower;
+ this.upper = upper;
+ this.unit = unit;
+ this.inverted = inverted;
+ }
+
+ public BigDecimal getLower() {
+ return lower;
+ }
+
+ public BigDecimal getUpper() {
+ return upper;
+ }
+
+ public String getUnit() {
+ return unit;
+ }
+
+ public boolean getInverted() {
+ return inverted;
+ }
+
+ @Override
+ public boolean apply(Item item) {
+ State state = item.getState();
+ BigDecimal compareValue = null;
+ if (state instanceof DecimalType decimalType) {
+ compareValue = decimalType.toBigDecimal();
+ } else if (state instanceof QuantityType> quantityType) {
+ if (!unit.isBlank()) {
+ QuantityType> convertedQuantity = quantityType.toUnit(unit);
+ if (convertedQuantity != null) {
+ compareValue = convertedQuantity.toBigDecimal();
+ }
+ }
+ }
+ if (compareValue == null) {
+ logger.warn("Cannot compare {} to range {}{} - {}{} ", state, lower, unit, upper, unit);
+ return true;
+ }
+
+ if (inverted) {
+ return compareValue.compareTo(lower) <= 0 || compareValue.compareTo(upper) >= 0;
+ } else {
+ return compareValue.compareTo(lower) >= 0 && compareValue.compareTo(upper) <= 0;
+ }
+ }
+
+ @Override
+ public void persisted(Item item) {
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s [name=%s, lower=%s, upper=%s, unit=%s, inverted=%b]", getClass().getSimpleName(),
+ getName(), lower, upper, unit, inverted);
+ }
+}
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java
index efd3d6d968c..269c40db9d8 100644
--- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java
@@ -29,7 +29,7 @@
/**
* The {@link PersistenceThresholdFilter} is a filter to prevent persistence based on a threshold.
- *
+ *
* The filter returns {@code false} if the new value deviates by less than {@link #value}. If unit is "%" is
* {@code true}, the filter returns {@code false} if the relative deviation is less than {@link #value}.
*
@@ -43,13 +43,15 @@ public class PersistenceThresholdFilter extends PersistenceFilter {
private final BigDecimal value;
private final String unit;
+ private final boolean relative;
private final transient Map valueCache = new HashMap<>();
- public PersistenceThresholdFilter(String name, BigDecimal value, String unit) {
+ public PersistenceThresholdFilter(String name, BigDecimal value, String unit, boolean relative) {
super(name);
this.value = value;
this.unit = unit;
+ this.relative = relative;
}
public BigDecimal getValue() {
@@ -60,6 +62,10 @@ public String getUnit() {
return unit;
}
+ public boolean isRelative() {
+ return relative;
+ }
+
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public boolean apply(Item item) {
@@ -78,7 +84,7 @@ public boolean apply(Item item) {
if (state instanceof DecimalType) {
BigDecimal oldState = ((DecimalType) cachedState).toBigDecimal();
BigDecimal delta = oldState.subtract(((DecimalType) state).toBigDecimal());
- if ("%".equals(unit) && !BigDecimal.ZERO.equals(oldState)) {
+ if (relative && !BigDecimal.ZERO.equals(oldState)) {
delta = delta.multiply(HUNDRED).divide(oldState, 2, RoundingMode.HALF_UP);
}
return delta.abs().compareTo(value) > 0;
@@ -86,7 +92,7 @@ public boolean apply(Item item) {
try {
QuantityType oldState = (QuantityType) cachedState;
QuantityType delta = oldState.subtract((QuantityType) state);
- if ("%".equals(unit)) {
+ if (relative) {
if (BigDecimal.ZERO.equals(oldState.toBigDecimal())) {
// value is different and old value is 0 -> always above relative threshold
return true;
@@ -117,6 +123,7 @@ public void persisted(Item item) {
@Override
public String toString() {
- return String.format("%s [name=%s, value=%s, unit=%s]", getClass().getSimpleName(), getName(), value, unit);
+ return String.format("%s [name=%s, value=%s, unit=%s, relative=%b]", getClass().getSimpleName(), getName(),
+ value, unit, relative);
}
}
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java
index c3edcfdc6f3..524ef2bc342 100644
--- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java
@@ -24,8 +24,8 @@
/**
* The {@link PersistenceTimeFilter} is a filter to prevent persistence base on intervals.
- *
- * The filter returns {@link false} if the time between now and the time of the last persisted value is less than
+ *
+ * The filter returns {@code false} if the time between now and the time of the last persisted value is less than
* {@link #duration} {@link #unit}
*
* @author Jan N. Klug - Initial contribution
diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java
index 2b9b513ed18..a20dbcc6d39 100644
--- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java
+++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java
@@ -32,7 +32,9 @@
import org.openhab.core.persistence.dto.PersistenceFilterDTO;
import org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO;
import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO;
+import org.openhab.core.persistence.filter.PersistenceEqualsFilter;
import org.openhab.core.persistence.filter.PersistenceFilter;
+import org.openhab.core.persistence.filter.PersistenceIncludeFilter;
import org.openhab.core.persistence.filter.PersistenceThresholdFilter;
import org.openhab.core.persistence.filter.PersistenceTimeFilter;
import org.openhab.core.persistence.strategy.PersistenceCronStrategy;
@@ -65,6 +67,10 @@ public static PersistenceServiceConfigurationDTO map(
PersistenceServiceConfigurationDTOMapper::mapPersistenceThresholdFilter);
dto.timeFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceTimeFilter.class,
PersistenceServiceConfigurationDTOMapper::mapPersistenceTimeFilter);
+ dto.equalsFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceEqualsFilter.class,
+ PersistenceServiceConfigurationDTOMapper::mapPersistenceEqualsFilter);
+ dto.includeFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceIncludeFilter.class,
+ PersistenceServiceConfigurationDTOMapper::mapPersistenceIncludeFilter);
return dto;
}
@@ -73,11 +79,14 @@ public static PersistenceServiceConfiguration map(PersistenceServiceConfiguratio
Map strategyMap = dto.cronStrategies.stream()
.collect(Collectors.toMap(e -> e.name, e -> new PersistenceCronStrategy(e.name, e.cronExpression)));
- Map filterMap = Stream
- .concat(dto.thresholdFilters.stream().map(f -> new PersistenceThresholdFilter(f.name, f.value, f.unit)),
- dto.timeFilters.stream()
- .map(f -> new PersistenceTimeFilter(f.name, f.value.intValue(), f.unit)))
- .collect(Collectors.toMap(PersistenceFilter::getName, e -> e));
+ Map filterMap = Stream.of(
+ dto.thresholdFilters.stream()
+ .map(f -> new PersistenceThresholdFilter(f.name, f.value, f.unit, f.relative)),
+ dto.timeFilters.stream().map(f -> new PersistenceTimeFilter(f.name, f.value.intValue(), f.unit)),
+ dto.equalsFilters.stream().map(f -> new PersistenceEqualsFilter(f.name, f.values, f.inverted)),
+ dto.includeFilters.stream()
+ .map(f -> new PersistenceIncludeFilter(f.name, f.lower, f.upper, f.unit, f.inverted)))
+ .flatMap(Function.identity()).collect(Collectors.toMap(PersistenceFilter::getName, e -> e));
List defaults = dto.defaults.stream()
.map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList();
@@ -87,7 +96,9 @@ public static PersistenceServiceConfiguration map(PersistenceServiceConfiguratio
.map(PersistenceServiceConfigurationDTOMapper::stringToPersistenceConfig).toList();
List strategies = config.strategies.stream()
.map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList();
- return new PersistenceItemConfiguration(items, config.alias, strategies, List.of());
+ List filters = config.filters.stream()
+ .map(str -> stringToPersistenceFilter(str, filterMap, dto.serviceId)).toList();
+ return new PersistenceItemConfiguration(items, config.alias, strategies, filters);
}).toList();
return new PersistenceServiceConfiguration(dto.serviceId, configs, defaults, strategyMap.values(),
@@ -121,6 +132,16 @@ private static PersistenceStrategy stringToPersistenceStrategy(String string,
throw new IllegalArgumentException("Strategy '" + string + "' unknown for service '" + serviceId + "'");
}
+ private static PersistenceFilter stringToPersistenceFilter(String string, Map filterMap,
+ String serviceId) {
+ PersistenceFilter filter = filterMap.get(string);
+ if (filter != null) {
+ return filter;
+ }
+
+ throw new IllegalArgumentException("Filter '" + string + "' unknown for service '" + serviceId + "'");
+ }
+
private static String persistenceConfigToString(PersistenceConfig config) {
if (config instanceof PersistenceAllConfig) {
return "*";
@@ -137,6 +158,7 @@ private static PersistenceItemConfigurationDTO mapPersistenceItemConfig(Persiste
itemDto.items = config.items().stream().map(PersistenceServiceConfigurationDTOMapper::persistenceConfigToString)
.toList();
itemDto.strategies = config.strategies().stream().map(PersistenceStrategy::getName).toList();
+ itemDto.filters = config.filters().stream().map(PersistenceFilter::getName).toList();
itemDto.alias = config.alias();
return itemDto;
}
@@ -153,6 +175,7 @@ private static PersistenceFilterDTO mapPersistenceThresholdFilter(PersistenceThr
filterDTO.name = thresholdFilter.getName();
filterDTO.value = thresholdFilter.getValue();
filterDTO.unit = thresholdFilter.getUnit();
+ filterDTO.relative = thresholdFilter.isRelative();
return filterDTO;
}
@@ -163,4 +186,22 @@ private static PersistenceFilterDTO mapPersistenceTimeFilter(PersistenceTimeFilt
filterDTO.unit = persistenceTimeFilter.getUnit();
return filterDTO;
}
+
+ private static PersistenceFilterDTO mapPersistenceEqualsFilter(PersistenceEqualsFilter persistenceEqualsFilter) {
+ PersistenceFilterDTO filterDTO = new PersistenceFilterDTO();
+ filterDTO.name = persistenceEqualsFilter.getName();
+ filterDTO.values = persistenceEqualsFilter.getValues().stream().toList();
+ filterDTO.inverted = persistenceEqualsFilter.getInverted();
+ return filterDTO;
+ }
+
+ private static PersistenceFilterDTO mapPersistenceIncludeFilter(PersistenceIncludeFilter persistenceIncludeFilter) {
+ PersistenceFilterDTO filterDTO = new PersistenceFilterDTO();
+ filterDTO.name = persistenceIncludeFilter.getName();
+ filterDTO.lower = persistenceIncludeFilter.getLower();
+ filterDTO.upper = persistenceIncludeFilter.getUpper();
+ filterDTO.unit = persistenceIncludeFilter.getUnit();
+ filterDTO.inverted = persistenceIncludeFilter.getInverted();
+ return filterDTO;
+ }
}
diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceEqualsFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceEqualsFilterTest.java
new file mode 100644
index 00000000000..2376cc59b47
--- /dev/null
+++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceEqualsFilterTest.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.persistence.filter;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.items.GenericItem;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link PersistenceEqualsFilterTest} contains tests for {@link PersistenceEqualsFilter}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class PersistenceEqualsFilterTest {
+ private static final String ITEM_NAME = "itemName";
+
+ private @NonNullByDefault({}) @Mock GenericItem item;
+
+ @ParameterizedTest
+ @MethodSource("argumentProvider")
+ public void equalsFilterTest(State state, Collection values, boolean expected) {
+ when(item.getState()).thenReturn(state);
+
+ PersistenceEqualsFilter filter = new PersistenceEqualsFilter("filter", values, false);
+ assertThat(filter.apply(item), is(expected));
+ }
+
+ @ParameterizedTest
+ @MethodSource("argumentProvider")
+ public void notEqualsFilterTest(State state, Collection values, boolean expected) {
+ when(item.getState()).thenReturn(state);
+
+ PersistenceEqualsFilter filter = new PersistenceEqualsFilter("filter", values, true);
+ assertThat(filter.apply(item), is(not(expected)));
+ }
+
+ private static Stream argumentProvider() {
+ return Stream.of(//
+ // item state, values, result
+ Arguments.of(new StringType("value1"), List.of("value1", "value2"), true),
+ Arguments.of(new StringType("value3"), List.of("value1", "value2"), false),
+ Arguments.of(new DecimalType(5), List.of("3", "5", "9"), true),
+ Arguments.of(new DecimalType(7), List.of("3", "5", "9"), false),
+ Arguments.of(new QuantityType<>(10, SIUnits.CELSIUS), List.of("5 °C", "10 °C", "15 °C"), true),
+ Arguments.of(new QuantityType<>(20, SIUnits.CELSIUS), List.of("5 °C", "10 °C", "15 °C"), false),
+ Arguments.of(OnOffType.ON, List.of("ON", "UNDEF", "NULL"), true),
+ Arguments.of(OnOffType.OFF, List.of("ON", "UNDEF", "NULL"), false));
+ }
+}
diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceIncludeFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceIncludeFilterTest.java
new file mode 100644
index 00000000000..a43b85cab41
--- /dev/null
+++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceIncludeFilterTest.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.persistence.filter;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.items.GenericItem;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link PersistenceIncludeFilterTest} contains tests for {@link PersistenceIncludeFilter}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class PersistenceIncludeFilterTest {
+ private static final String ITEM_NAME = "itemName";
+
+ private @NonNullByDefault({}) @Mock GenericItem item;
+
+ @ParameterizedTest
+ @MethodSource("argumentProvider")
+ public void includeFilterTest(State state, BigDecimal lower, BigDecimal upper, String unit, boolean expected) {
+ when(item.getState()).thenReturn(state);
+
+ PersistenceIncludeFilter filter = new PersistenceIncludeFilter("filter", lower, upper, unit, false);
+ assertThat(filter.apply(item), is(expected));
+ }
+
+ @ParameterizedTest
+ @MethodSource("notArgumentProvider")
+ public void notIncludeFilterTest(State state, BigDecimal lower, BigDecimal upper, String unit, boolean expected) {
+ when(item.getState()).thenReturn(state);
+
+ PersistenceIncludeFilter filter = new PersistenceIncludeFilter("filter", lower, upper, unit, true);
+ assertThat(filter.apply(item), is(expected));
+ }
+
+ private static Stream argumentProvider() {
+ return Stream.of(//
+ // item state, lower, upper, unit, result
+ // QuantityType
+ Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true),
+ Arguments.of(new QuantityType<>("10 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", false),
+ Arguments.of(new QuantityType<>("20 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", false),
+ Arguments.of(new QuantityType<>("0 °C"), BigDecimal.valueOf(270), BigDecimal.valueOf(275), "K", true),
+ // invalid or missing units
+ Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true),
+ Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true),
+ // DecimalType
+ Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true),
+ Arguments.of(new DecimalType("17"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "", true),
+ Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true),
+ Arguments.of(new DecimalType("10"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", false),
+ Arguments.of(new DecimalType("20"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", false));
+ }
+
+ private static Stream notArgumentProvider() {
+ return Stream.of(//
+ // item state, lower, upper, unit, result
+ // QuantityType
+ Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", false),
+ Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true),
+ Arguments.of(new QuantityType<>("10 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true),
+ Arguments.of(new QuantityType<>("20 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true),
+ Arguments.of(new QuantityType<>("0 °C"), BigDecimal.valueOf(270), BigDecimal.valueOf(275), "K", false),
+ // invalid or missing units
+ Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true),
+ Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true),
+ Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true),
+ // DecimalType
+ Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", false),
+ Arguments.of(new DecimalType("17"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "", true),
+ Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true),
+ Arguments.of(new DecimalType("10"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true),
+ Arguments.of(new DecimalType("20"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true));
+ }
+}
diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java
index 15cef6acc65..e37e22c5030 100644
--- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java
+++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java
@@ -70,42 +70,43 @@ public void setup() {
@Test
public void differentItemSameValue() {
- filterTest(ITEM_NAME_2, DecimalType.ZERO, DecimalType.ZERO, "", true);
+ filterTest(ITEM_NAME_2, DecimalType.ZERO, DecimalType.ZERO, "", false, true);
}
@ParameterizedTest
@MethodSource("argumentProvider")
- public void filterTest(State state1, State state2, String unit, boolean expected) {
- filterTest(ITEM_NAME_1, state1, state2, unit, expected);
+ public void filterTest(State state1, State state2, String unit, boolean relative, boolean expected) {
+ filterTest(ITEM_NAME_1, state1, state2, unit, relative, expected);
}
private static Stream argumentProvider() {
return Stream.of(//
// same item, same value -> false
- Arguments.of(DecimalType.ZERO, DecimalType.ZERO, "", false),
+ Arguments.of(DecimalType.ZERO, DecimalType.ZERO, "", false, false),
// plain decimal, below threshold, absolute
- Arguments.of(DecimalType.ZERO, DecimalType.valueOf("5"), "", false),
+ Arguments.of(DecimalType.ZERO, DecimalType.valueOf("5"), "", false, false),
// plain decimal, above threshold, absolute
- Arguments.of(DecimalType.ZERO, DecimalType.valueOf("15"), "", true),
+ Arguments.of(DecimalType.ZERO, DecimalType.valueOf("15"), "", false, true),
// plain decimal, below threshold, relative
- Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("9.5"), "%", false),
+ Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("9.5"), "", true, false),
// plain decimal, above threshold, relative
- Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("11.5"), "%", true),
+ Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("11.5"), "", true, true),
// quantity type, below threshold, relative
- Arguments.of(new QuantityType<>("15 A"), new QuantityType<>("14000 mA"), "%", false),
+ Arguments.of(new QuantityType<>("15 A"), new QuantityType<>("14000 mA"), "", true, false),
// quantity type, above threshold, relative
- Arguments.of(new QuantityType<>("2000 mbar"), new QuantityType<>("2.6 bar"), "%", true),
+ Arguments.of(new QuantityType<>("2000 mbar"), new QuantityType<>("2.6 bar"), "", true, true),
// quantity type, below threshold, absolute, no unit
- Arguments.of(new QuantityType<>("100 K"), new QuantityType<>("105 K"), "", false),
+ Arguments.of(new QuantityType<>("100 K"), new QuantityType<>("105 K"), "", false, false),
// quantity type, above threshold, absolute, no unit
- Arguments.of(new QuantityType<>("20 V"), new QuantityType<>("9000 mV"), "", true),
+ Arguments.of(new QuantityType<>("20 V"), new QuantityType<>("9000 mV"), "", false, true),
// quantity type, below threshold, absolute, with unit
- Arguments.of(new QuantityType<>("10 m"), new QuantityType<>("10.002 m"), "mm", false),
+ Arguments.of(new QuantityType<>("10 m"), new QuantityType<>("10.002 m"), "mm", false, false),
// quantity type, above threshold, absolute, with unit
- Arguments.of(new QuantityType<>("-10 °C"), new QuantityType<>("5 °C"), "K", true));
+ Arguments.of(new QuantityType<>("-10 °C"), new QuantityType<>("5 °C"), "K", false, true));
}
- private void filterTest(String item2name, State state1, State state2, String unit, boolean expected) {
+ private void filterTest(String item2name, State state1, State state2, String unit, boolean relative,
+ boolean expected) {
String itemType = "Number";
if (state1 instanceof QuantityType> q) {
itemType += ":" + UnitUtils.getDimensionName(q.getUnit());
@@ -117,7 +118,7 @@ private void filterTest(String item2name, State state1, State state2, String uni
item1.setState(state1);
item2.setState(state2);
- PersistenceFilter filter = new PersistenceThresholdFilter("test", BigDecimal.TEN, unit);
+ PersistenceFilter filter = new PersistenceThresholdFilter("test", BigDecimal.TEN, unit, relative);
assertThat(filter.apply(item1), is(true));
filter.persisted(item1);
diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java
index 9172dd3e83e..b33766fbd4a 100644
--- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java
+++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java
@@ -308,7 +308,7 @@ public void cronStrategyIsScheduledAndCancelledAndPersistsValue() throws Excepti
new PersistenceCronStrategy("withoutFilter", "0 0 * * * ?"), null);
addConfiguration(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM3_NAME),
new PersistenceCronStrategy("withFilter", "0 * * * * ?"),
- new PersistenceThresholdFilter("test", BigDecimal.TEN, ""));
+ new PersistenceThresholdFilter("test", BigDecimal.TEN, "", false));
manager.onReadyMarkerAdded(new ReadyMarker("", ""));
@@ -352,7 +352,7 @@ public void cronStrategyIsProperlyUpdated() {
@Test
public void filterAppliesOnStateUpdate() {
addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE,
- new PersistenceThresholdFilter("test", BigDecimal.TEN, ""));
+ new PersistenceThresholdFilter("test", BigDecimal.TEN, "", false));
manager.stateUpdated(TEST_ITEM3, DecimalType.ZERO);
manager.stateUpdated(TEST_ITEM3, DecimalType.ZERO);
diff --git a/bundles/org.openhab.core.semantics/pom.xml b/bundles/org.openhab.core.semantics/pom.xml
index 54962c3b240..1ebf3cec206 100644
--- a/bundles/org.openhab.core.semantics/pom.xml
+++ b/bundles/org.openhab.core.semantics/pom.xml
@@ -23,7 +23,7 @@
org.ow2.asm
asm
- 9.2
+ 9.4
provided
diff --git a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java
index ce8895e0774..1e46d4bf392 100644
--- a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java
+++ b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java
@@ -46,7 +46,7 @@ public class MagicServiceConfig {
@Override
public String toString() {
- StringBuffer b = new StringBuffer();
+ StringBuilder b = new StringBuilder();
for (Field field : this.getClass().getDeclaredFields()) {
Object value;
try {
diff --git a/bundles/org.openhab.core.thing/pom.xml b/bundles/org.openhab.core.thing/pom.xml
index 58cd26a9443..f969dcd2100 100644
--- a/bundles/org.openhab.core.thing/pom.xml
+++ b/bundles/org.openhab.core.thing/pom.xml
@@ -25,6 +25,11 @@
org.openhab.core.io.console
${project.version}
+
+ org.openhab.core.bundles
+ org.openhab.core.transform
+ ${project.version}
+
org.openhab.core.bundles
org.openhab.core.test
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java
new file mode 100644
index 00000000000..8839167d4f9
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link ChannelHandler} defines the interface for converting received {@link ChannelHandlerContent}
+ * to {@link org.openhab.core.types.State}s for posting updates to {@link org.openhab.core.thing.Channel}s and
+ * {@link Command}s to values for sending
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ChannelHandler {
+
+ /**
+ * called to process a given content for this channel
+ *
+ * @param content raw content to process (
null
results in
+ * {@link org.openhab.core.types.UnDefType#UNDEF})
+ */
+ void process(@Nullable ChannelHandlerContent content);
+
+ /**
+ * called to send a command to this channel
+ *
+ * @param command
+ */
+ void send(Command command);
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java
new file mode 100644
index 00000000000..ed5b4ad3e15
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link ChannelHandlerContent} defines the pre-processed response
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelHandlerContent {
+ private final byte[] rawContent;
+ private final Charset encoding;
+ private final @Nullable String mediaType;
+
+ public ChannelHandlerContent(byte[] rawContent, String encoding, @Nullable String mediaType) {
+ this.rawContent = rawContent;
+ this.mediaType = mediaType;
+
+ Charset finalEncoding = StandardCharsets.UTF_8;
+ try {
+ finalEncoding = Charset.forName(encoding);
+ } catch (IllegalArgumentException e) {
+ }
+ this.encoding = finalEncoding;
+ }
+
+ public byte[] getRawContent() {
+ return rawContent;
+ }
+
+ public String getAsString() {
+ return new String(rawContent, encoding);
+ }
+
+ public @Nullable String getMediaType() {
+ return mediaType;
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java
new file mode 100644
index 00000000000..63353866747
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ChannelMode} enum defines control modes for channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum ChannelMode {
+ READONLY,
+ READWRITE,
+ WRITEONLY
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java
new file mode 100644
index 00000000000..e4187cdae13
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.transform.TransformationException;
+import org.openhab.core.transform.TransformationHelper;
+import org.openhab.core.transform.TransformationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ChannelTransformation} can be used to transform an input value using one or more transformations.
+ * Individual transformations can be chained with ∩
and must follow the pattern
+ * serviceName:function
where serviceName
refers to a {@link TransformationService} and
+ * function
has to be a valid transformation function for this service
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+public class ChannelTransformation {
+ private final Logger logger = LoggerFactory.getLogger(ChannelTransformation.class);
+ private List transformationSteps;
+
+ public ChannelTransformation(@Nullable String transformationString) {
+ if (transformationString != null) {
+ try {
+ transformationSteps = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isBlank())
+ .map(TransformationStep::new).toList();
+ return;
+ } catch (IllegalArgumentException e) {
+ logger.warn("Transformation ignored, failed to parse {}: {}", transformationString, e.getMessage());
+ }
+ }
+ transformationSteps = List.of();
+ }
+
+ public Optional apply(String value) {
+ Optional valueOptional = Optional.of(value);
+
+ // process all transformations
+ for (TransformationStep transformationStep : transformationSteps) {
+ valueOptional = valueOptional.flatMap(transformationStep::apply);
+ }
+
+ logger.trace("Transformed '{}' to '{}' using '{}'", value, valueOptional, transformationSteps);
+ return valueOptional;
+ }
+
+ private static class TransformationStep {
+ private final Logger logger = LoggerFactory.getLogger(TransformationStep.class);
+ private final String serviceName;
+ private final String function;
+
+ public TransformationStep(String pattern) throws IllegalArgumentException {
+ 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");
+ }
+ this.serviceName = pattern.substring(0, index).toUpperCase().trim();
+ this.function = pattern.substring(index + 1).trim();
+ }
+
+ public Optional apply(String value) {
+ TransformationService service = TransformationHelper.getTransformationService(serviceName);
+ if (service != null) {
+ try {
+ return Optional.ofNullable(service.transform(function, value));
+ } catch (TransformationException e) {
+ logger.debug("Applying {} failed: {}", this, e.getMessage());
+ }
+ } else {
+ logger.warn("Failed to use {}, service not found", this);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public String toString() {
+ return "TransformationStep{serviceName='" + serviceName + "', function='" + function + "'}";
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java
new file mode 100644
index 00000000000..72dd8a8a053
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.binding.generic.converter.ColorChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ChannelValueConverterConfig} is a base class for the channel configuration of things
+ * using the {@link ChannelHandler}s
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ChannelValueConverterConfig {
+ private final Map stringStateMap = new HashMap<>();
+ private final Map commandStringMap = new HashMap<>();
+
+ public ChannelMode mode = ChannelMode.READWRITE;
+
+ // number
+ public @Nullable String unit;
+
+ // switch, dimmer, color
+ public @Nullable String onValue;
+ public @Nullable String offValue;
+
+ // dimmer, color
+ public BigDecimal step = BigDecimal.ONE;
+ public @Nullable String increaseValue;
+ public @Nullable String decreaseValue;
+
+ // color
+ public ColorChannelHandler.ColorMode colorMode = ColorChannelHandler.ColorMode.RGB;
+
+ // contact
+ public @Nullable String openValue;
+ public @Nullable String closedValue;
+
+ // rollershutter
+ public @Nullable String upValue;
+ public @Nullable String downValue;
+ public @Nullable String stopValue;
+ public @Nullable String moveValue;
+
+ // player
+ public @Nullable String playValue;
+ public @Nullable String pauseValue;
+ public @Nullable String nextValue;
+ public @Nullable String previousValue;
+ public @Nullable String rewindValue;
+ public @Nullable String fastforwardValue;
+
+ private boolean initialized = false;
+
+ /**
+ * maps a command to a user-defined string
+ *
+ * @param command the command to map
+ * @return a string or null if no mapping found
+ */
+ public @Nullable String commandToFixedValue(Command command) {
+ if (!initialized) {
+ createMaps();
+ }
+
+ return commandStringMap.get(command);
+ }
+
+ /**
+ * maps a user-defined string to a state
+ *
+ * @param string the string to map
+ * @return the state or null if no mapping found
+ */
+ public @Nullable State fixedValueToState(String string) {
+ if (!initialized) {
+ createMaps();
+ }
+
+ return stringStateMap.get(string);
+ }
+
+ private void createMaps() {
+ addToMaps(this.onValue, OnOffType.ON);
+ addToMaps(this.offValue, OnOffType.OFF);
+ addToMaps(this.openValue, OpenClosedType.OPEN);
+ addToMaps(this.closedValue, OpenClosedType.CLOSED);
+ addToMaps(this.upValue, UpDownType.UP);
+ addToMaps(this.downValue, UpDownType.DOWN);
+
+ commandStringMap.put(IncreaseDecreaseType.INCREASE, increaseValue);
+ commandStringMap.put(IncreaseDecreaseType.DECREASE, decreaseValue);
+ commandStringMap.put(StopMoveType.STOP, stopValue);
+ commandStringMap.put(StopMoveType.MOVE, moveValue);
+ commandStringMap.put(PlayPauseType.PLAY, playValue);
+ commandStringMap.put(PlayPauseType.PAUSE, pauseValue);
+ commandStringMap.put(NextPreviousType.NEXT, nextValue);
+ commandStringMap.put(NextPreviousType.PREVIOUS, previousValue);
+ commandStringMap.put(RewindFastforwardType.REWIND, rewindValue);
+ commandStringMap.put(RewindFastforwardType.FASTFORWARD, fastforwardValue);
+
+ initialized = true;
+ }
+
+ private void addToMaps(@Nullable String value, State state) {
+ if (value != null) {
+ commandStringMap.put((Command) state, value);
+ stringStateMap.put(value, state);
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java
new file mode 100644
index 00000000000..c2dff0ef26f
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ColorChannelHandler} implements {@link org.openhab.core.library.items.ColorItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ColorChannelHandler extends AbstractTransformingChannelHandler {
+ private static final BigDecimal BYTE_FACTOR = BigDecimal.valueOf(2.55);
+ private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+ private static final Pattern TRIPLE_MATCHER = Pattern.compile("(?\\d+),(?\\d+),(?\\d+)");
+
+ private State state = UnDefType.UNDEF;
+
+ public ColorChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ if (command instanceof HSBType newState) {
+ state = newState;
+ return hsbToString(newState);
+ } else if (command instanceof PercentType percentCommand && state instanceof HSBType colorState) {
+ HSBType newState = new HSBType(colorState.getBrightness(), colorState.getSaturation(), percentCommand);
+ state = newState;
+ return hsbToString(newState);
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State newState = UnDefType.UNDEF;
+ if (string.equals(channelConfig.onValue)) {
+ if (state instanceof HSBType) {
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+ PercentType.HUNDRED);
+ } else {
+ newState = HSBType.WHITE;
+ }
+ } else if (string.equals(channelConfig.offValue)) {
+ if (state instanceof HSBType) {
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), PercentType.ZERO);
+ } else {
+ newState = HSBType.BLACK;
+ }
+ } else if (string.equals(channelConfig.increaseValue) && state instanceof HSBType) {
+ BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().add(channelConfig.step);
+ if (HUNDRED.compareTo(newBrightness) < 0) {
+ newBrightness = HUNDRED;
+ }
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+ new PercentType(newBrightness));
+ } else if (string.equals(channelConfig.decreaseValue) && state instanceof HSBType) {
+ BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().subtract(channelConfig.step);
+ if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
+ newBrightness = BigDecimal.ZERO;
+ }
+ newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+ new PercentType(newBrightness));
+ } else {
+ Matcher matcher = TRIPLE_MATCHER.matcher(string);
+ if (matcher.matches()) {
+ switch (channelConfig.colorMode) {
+ case RGB -> {
+ int r = Integer.parseInt(matcher.group("r"));
+ int g = Integer.parseInt(matcher.group("g"));
+ int b = Integer.parseInt(matcher.group("b"));
+ newState = HSBType.fromRGB(r, g, b);
+ }
+ case HSB -> newState = new HSBType(string);
+ }
+ }
+ }
+
+ state = newState;
+ return Optional.of(newState);
+ }
+
+ private String hsbToString(HSBType state) {
+ switch (channelConfig.colorMode) {
+ case RGB:
+ PercentType[] rgb = state.toRGB();
+ return String.format("%1$d,%2$d,%3$d", rgb[0].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
+ rgb[1].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
+ rgb[2].toBigDecimal().multiply(BYTE_FACTOR).intValue());
+ case HSB:
+ return state.toString();
+ }
+ throw new IllegalStateException("Invalid colorMode setting");
+ }
+
+ public enum ColorMode {
+ RGB,
+ HSB
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java
new file mode 100644
index 00000000000..f9980de7831
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link DimmerChannelHandler} implements {@link org.openhab.core.library.items.DimmerItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class DimmerChannelHandler extends AbstractTransformingChannelHandler {
+ private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+
+ private State state = UnDefType.UNDEF;
+
+ public DimmerChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ if (command instanceof PercentType) {
+ return ((PercentType) command).toString();
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State newState = UnDefType.UNDEF;
+
+ if (string.equals(channelConfig.onValue)) {
+ newState = PercentType.HUNDRED;
+ } else if (string.equals(channelConfig.offValue)) {
+ newState = PercentType.ZERO;
+ } else if (string.equals(channelConfig.increaseValue) && state instanceof PercentType) {
+ BigDecimal newBrightness = ((PercentType) state).toBigDecimal().add(channelConfig.step);
+ if (HUNDRED.compareTo(newBrightness) < 0) {
+ newBrightness = HUNDRED;
+ }
+ newState = new PercentType(newBrightness);
+ } else if (string.equals(channelConfig.decreaseValue) && state instanceof PercentType) {
+ BigDecimal newBrightness = ((PercentType) state).toBigDecimal().subtract(channelConfig.step);
+ if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
+ newBrightness = BigDecimal.ZERO;
+ }
+ newState = new PercentType(newBrightness);
+ } else {
+ try {
+ BigDecimal value = new BigDecimal(string);
+ if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
+ value = PercentType.HUNDRED.toBigDecimal();
+ }
+ if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
+ value = PercentType.ZERO.toBigDecimal();
+ }
+ newState = new PercentType(value);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ }
+
+ state = newState;
+ return Optional.of(newState);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java
new file mode 100644
index 00000000000..47ea726d3b6
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link FixedValueMappingChannelHandler} implements mapping conversions for different item-types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class FixedValueMappingChannelHandler extends AbstractTransformingChannelHandler {
+
+ public FixedValueMappingChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ public String toString(Command command) {
+ String value = channelConfig.commandToFixedValue(command);
+ if (value != null) {
+ return value;
+ }
+
+ throw new IllegalArgumentException(
+ "Command type '" + command.toString() + "' not supported or mapping not defined.");
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State state = channelConfig.fixedValueToState(string);
+
+ return Optional.of(Objects.requireNonNullElse(state, UnDefType.UNDEF));
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java
new file mode 100644
index 00000000000..aa25055270c
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link GenericChannelHandler} implements simple conversions for different item types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GenericChannelHandler extends AbstractTransformingChannelHandler {
+ private final Function toState;
+
+ public GenericChannelHandler(Function toState, Consumer updateState,
+ Consumer postCommand, @Nullable Consumer sendValue,
+ ChannelTransformation stateTransformations, ChannelTransformation commandTransformations,
+ ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ this.toState = toState;
+ }
+
+ protected Optional toState(String value) {
+ try {
+ return Optional.of(toState.apply(value));
+ } catch (IllegalArgumentException e) {
+ return Optional.of(UnDefType.UNDEF);
+ }
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ protected String toString(Command command) {
+ return command.toString();
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java
new file mode 100644
index 00000000000..ff23ea3289b
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.thing.binding.generic.ChannelHandler;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ImageChannelHandler} implements {@link org.openhab.core.library.items.ImageItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ImageChannelHandler implements ChannelHandler {
+ private final Consumer updateState;
+
+ public ImageChannelHandler(Consumer updateState) {
+ this.updateState = updateState;
+ }
+
+ @Override
+ public void process(@Nullable ChannelHandlerContent content) {
+ if (content == null) {
+ updateState.accept(UnDefType.UNDEF);
+ return;
+ }
+ String mediaType = content.getMediaType();
+ updateState.accept(
+ new RawType(content.getRawContent(), mediaType != null ? mediaType : RawType.DEFAULT_MIME_TYPE));
+ }
+
+ @Override
+ public void send(Command command) {
+ throw new IllegalStateException("Read-only channel");
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java
new file mode 100644
index 00000000000..91a48514d4c
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import javax.measure.format.MeasurementParseException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link NumberChannelHandler} implements {@link org.openhab.core.library.items.NumberItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class NumberChannelHandler extends AbstractTransformingChannelHandler {
+
+ public NumberChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return null;
+ }
+
+ @Override
+ protected Optional toState(String value) {
+ String trimmedValue = value.trim();
+ State newState = UnDefType.UNDEF;
+ if (!trimmedValue.isEmpty()) {
+ try {
+ if (channelConfig.unit != null) {
+ // we have a given unit - use that
+ newState = new QuantityType<>(trimmedValue + " " + channelConfig.unit);
+ } else {
+ try {
+ // try if we have a simple number
+ newState = new DecimalType(trimmedValue);
+ } catch (IllegalArgumentException e1) {
+ // not a plain number, maybe with unit?
+ newState = new QuantityType<>(trimmedValue);
+ }
+ }
+ } catch (IllegalArgumentException | MeasurementParseException e) {
+ // finally failed
+ }
+ }
+ return Optional.of(newState);
+ }
+
+ @Override
+ protected String toString(Command command) {
+ return command.toString();
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java
new file mode 100644
index 00000000000..0afe6f41c18
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link PlayerChannelHandler} implements {@link org.openhab.core.library.items.RollershutterItem}
+ * conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class PlayerChannelHandler extends AbstractTransformingChannelHandler {
+ private @Nullable String lastCommand; // store last command to prevent duplicate commands
+
+ public PlayerChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String string) {
+ if (string.equals(lastCommand)) {
+ // only send commands once
+ return null;
+ }
+ lastCommand = string;
+
+ if (string.equals(channelConfig.playValue)) {
+ return PlayPauseType.PLAY;
+ } else if (string.equals(channelConfig.pauseValue)) {
+ return PlayPauseType.PAUSE;
+ } else if (string.equals(channelConfig.nextValue)) {
+ return NextPreviousType.NEXT;
+ } else if (string.equals(channelConfig.previousValue)) {
+ return NextPreviousType.PREVIOUS;
+ } else if (string.equals(channelConfig.rewindValue)) {
+ return RewindFastforwardType.REWIND;
+ } else if (string.equals(channelConfig.fastforwardValue)) {
+ return RewindFastforwardType.FASTFORWARD;
+ }
+
+ return null;
+ }
+
+ @Override
+ public Optional toState(String string) {
+ // no value - we ignore state updates
+ return Optional.empty();
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java
new file mode 100644
index 00000000000..cf0ef1df0a6
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link RollershutterChannelHandler} implements {@link org.openhab.core.library.items.RollershutterItem}
+ * conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class RollershutterChannelHandler extends AbstractTransformingChannelHandler {
+
+ public RollershutterChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig);
+ }
+
+ @Override
+ public String toString(Command command) {
+ String string = channelConfig.commandToFixedValue(command);
+ if (string != null) {
+ return string;
+ }
+
+ if (command instanceof PercentType) {
+ final String downValue = channelConfig.downValue;
+ final String upValue = channelConfig.upValue;
+ if (command.equals(PercentType.HUNDRED) && downValue != null) {
+ return downValue;
+ } else if (command.equals(PercentType.ZERO) && upValue != null) {
+ return upValue;
+ } else {
+ return ((PercentType) command).toString();
+ }
+ }
+
+ throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String string) {
+ if (string.equals(channelConfig.upValue)) {
+ return UpDownType.UP;
+ } else if (string.equals(channelConfig.downValue)) {
+ return UpDownType.DOWN;
+ } else if (string.equals(channelConfig.moveValue)) {
+ return StopMoveType.MOVE;
+ } else if (string.equals(channelConfig.stopValue)) {
+ return StopMoveType.STOP;
+ }
+
+ return null;
+ }
+
+ @Override
+ public Optional toState(String string) {
+ State newState = UnDefType.UNDEF;
+ try {
+ BigDecimal value = new BigDecimal(string);
+ if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
+ value = PercentType.HUNDRED.toBigDecimal();
+ }
+ if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
+ value = PercentType.ZERO.toBigDecimal();
+ }
+ newState = new PercentType(value);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+
+ return Optional.of(newState);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java
new file mode 100644
index 00000000000..b4e332f0c9a
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.internal.binding.generic.converter;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.binding.generic.ChannelHandler;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.thing.binding.generic.ChannelMode;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link AbstractTransformingChannelHandler} is a base class for an item converter with transformations
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractTransformingChannelHandler implements ChannelHandler {
+ private final Consumer updateState;
+ private final Consumer postCommand;
+ private final @Nullable Consumer sendValue;
+ private final ChannelTransformation stateTransformations;
+ private final ChannelTransformation commandTransformations;
+
+ protected final ChannelValueConverterConfig channelConfig;
+
+ public AbstractTransformingChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) {
+ this.updateState = updateState;
+ this.postCommand = postCommand;
+ this.sendValue = sendValue;
+ this.stateTransformations = stateTransformations;
+ this.commandTransformations = commandTransformations;
+ this.channelConfig = channelConfig;
+ }
+
+ @Override
+ public void process(@Nullable ChannelHandlerContent content) {
+ if (content == null) {
+ updateState.accept(UnDefType.UNDEF);
+ return;
+ }
+ if (channelConfig.mode != ChannelMode.WRITEONLY) {
+ stateTransformations.apply(content.getAsString()).ifPresent(transformedValue -> {
+ Command command = toCommand(transformedValue);
+ if (command != null) {
+ postCommand.accept(command);
+ } else {
+ toState(transformedValue).ifPresent(updateState);
+ }
+ });
+ } else {
+ throw new IllegalStateException("Write-only channel");
+ }
+ }
+
+ @Override
+ public void send(Command command) {
+ Consumer sendHttpValue = this.sendValue;
+ if (sendHttpValue != null && channelConfig.mode != ChannelMode.READONLY) {
+ commandTransformations.apply(toString(command)).ifPresent(sendHttpValue);
+ } else {
+ throw new IllegalStateException("Read-only channel");
+ }
+ }
+
+ /**
+ * check if this converter received a value that needs to be sent as command
+ *
+ * @param value the value
+ * @return the command or null
+ */
+ protected abstract @Nullable Command toCommand(String value);
+
+ /**
+ * convert the received value to a state
+ *
+ * @param value the value
+ * @return the state that represents the value of UNDEF if conversion failed
+ */
+ protected abstract Optional toState(String value);
+
+ /**
+ * convert a command to a string
+ *
+ * @param command the command
+ * @return the string representation of the command
+ */
+ protected abstract String toString(Command command);
+
+ @FunctionalInterface
+ public interface Factory {
+ ChannelHandler create(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendHttpValue, ChannelTransformation stateTransformations,
+ ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java
index 3e91ea4b99d..b102afba530 100644
--- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java
+++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java
@@ -132,7 +132,7 @@ private void listFirmwareStatus(Console console, String[] args) {
FirmwareStatusInfo firmwareStatusInfo = firmwareUpdateService.getFirmwareStatusInfo(thingUID);
if (firmwareStatusInfo != null) {
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
sb.append(String.format("Firmware status for thing with UID %s is %s.", thingUID,
firmwareStatusInfo.getFirmwareStatus()));
diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java
new file mode 100644
index 00000000000..d7847b63228
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.AfterEach;
+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.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.transform.TransformationException;
+import org.openhab.core.transform.TransformationHelper;
+import org.openhab.core.transform.TransformationService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * The {@link ChannelTransformationTest} contains tests for the {@link ChannelTransformation}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class 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 T2_NAME = "TRANSFORM2";
+ private static final String T2_PATTERN = "T2Pattern";
+ private static final String T2_INPUT = T1_RESULT;
+ private static final String T2_RESULT = "T2Result";
+
+ private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;
+ private @Mock @NonNullByDefault({}) TransformationService transformationService2Mock;
+
+ private @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
+ private @Mock @NonNullByDefault({}) ServiceReference serviceRef1Mock;
+ private @Mock @NonNullByDefault({}) ServiceReference serviceRef2Mock;
+
+ 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(transformationService2Mock.transform(eq(T2_PATTERN), eq(T1_INPUT)))
+ .thenAnswer(answer -> T2_RESULT);
+ Mockito.when(transformationService2Mock.transform(eq(T2_PATTERN), eq(T2_INPUT)))
+ .thenAnswer(answer -> T2_RESULT);
+
+ Mockito.when(serviceRef1Mock.getProperty(any())).thenReturn("TRANSFORM1");
+ Mockito.when(serviceRef2Mock.getProperty(any())).thenReturn("TRANSFORM2");
+
+ Mockito.when(bundleContextMock.getService(serviceRef1Mock)).thenReturn(transformationService1Mock);
+ Mockito.when(bundleContextMock.getService(serviceRef2Mock)).thenReturn(transformationService2Mock);
+
+ transformationHelper = new TransformationHelper(bundleContextMock);
+ transformationHelper.setTransformationService(serviceRef1Mock);
+ transformationHelper.setTransformationService(serviceRef2Mock);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ transformationHelper.deactivate();
+ }
+
+ @Test
+ public void testMissingTransformation() {
+ String pattern = "TRANSFORM:pattern";
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testSingleTransformation() {
+ String pattern = T1_NAME + ":" + T1_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertEquals(T1_RESULT, result);
+ }
+
+ @Test
+ public void testInvalidFirstTransformation() {
+ String pattern = T1_NAME + "X:" + T1_PATTERN + "∩" + T2_NAME + ":" + T2_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testInvalidSecondTransformation() {
+ String pattern = T1_NAME + ":" + T1_PATTERN + "∩" + T2_NAME + "X:" + T2_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testDoubleTransformationWithoutSpaces() {
+ String pattern = T1_NAME + ":" + T1_PATTERN + "∩" + T2_NAME + ":" + T2_PATTERN;
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertEquals(T2_RESULT, result);
+ }
+
+ @Test
+ public void testDoubleTransformationWithSpaces() {
+ String pattern = " " + T1_NAME + " : " + T1_PATTERN + " ∩ " + T2_NAME + " : " + T2_PATTERN + " ";
+
+ ChannelTransformation transformation = new ChannelTransformation(pattern);
+ String result = transformation.apply(T1_INPUT).orElse(null);
+
+ assertEquals(T2_RESULT, result);
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java
new file mode 100644
index 00000000000..1d5e2e6b24e
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.binding.generic.converter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ConverterTest} is a test class for state converters
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class ConverterTest {
+
+ private @Mock @NonNullByDefault({}) Consumer sendValueMock;
+
+ private @Mock @NonNullByDefault({}) Consumer updateStateMock;
+
+ private @Mock @NonNullByDefault({}) Consumer postCommandMock;
+
+ @Test
+ public void numberItemConverter() {
+ NumberChannelHandler converter = new NumberChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig());
+
+ // without unit
+ Assertions.assertEquals(Optional.of(new DecimalType(1234)), converter.toState("1234"));
+
+ // unit in transformation result
+ Assertions.assertEquals(Optional.of(new QuantityType<>(100, SIUnits.CELSIUS)), converter.toState("100°C"));
+
+ // no valid value
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("W"));
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState(""));
+ }
+
+ @Test
+ public void numberItemConverterWithUnit() {
+ ChannelValueConverterConfig channelConfig = new ChannelValueConverterConfig();
+ channelConfig.unit = "W";
+ NumberChannelHandler converter = new NumberChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), channelConfig);
+
+ // without unit
+ Assertions.assertEquals(Optional.of(new QuantityType<>(500, Units.WATT)), converter.toState("500"));
+
+ // no valid value
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("foo"));
+ Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState(""));
+ }
+
+ @Test
+ public void stringTypeConverter() {
+ GenericChannelHandler converter = createConverter(StringType::new);
+ Assertions.assertEquals(Optional.of(new StringType("Test")), converter.toState("Test"));
+ }
+
+ @Test
+ public void decimalTypeConverter() {
+ GenericChannelHandler converter = createConverter(DecimalType::new);
+ Assertions.assertEquals(Optional.of(new DecimalType(15.6)), converter.toState("15.6"));
+ }
+
+ @Test
+ public void pointTypeConverter() {
+ GenericChannelHandler converter = createConverter(PointType::new);
+ Assertions.assertEquals(
+ Optional.of(new PointType(new DecimalType(51.1), new DecimalType(7.2), new DecimalType(100))),
+ converter.toState("51.1, 7.2, 100"));
+ }
+
+ @Test
+ public void playerItemTypeConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ cfg.playValue = "PLAY";
+ ChannelHandlerContent content = new ChannelHandlerContent("PLAY".getBytes(StandardCharsets.UTF_8), "UTF-8",
+ null);
+ PlayerChannelHandler converter = new PlayerChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+ converter.process(content);
+ converter.process(content);
+
+ Mockito.verify(postCommandMock).accept(PlayPauseType.PLAY);
+ Mockito.verify(updateStateMock, Mockito.never()).accept(ArgumentMatchers.any());
+ }
+
+ @Test
+ public void colorItemTypeRGBConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ cfg.colorMode = ColorChannelHandler.ColorMode.RGB;
+ ChannelHandlerContent content = new ChannelHandlerContent("123,34,47".getBytes(StandardCharsets.UTF_8), "UTF-8",
+ null);
+ ColorChannelHandler converter = new ColorChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(HSBType.fromRGB(123, 34, 47));
+ }
+
+ @Test
+ public void colorItemTypeHSBConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ cfg.colorMode = ColorChannelHandler.ColorMode.HSB;
+ ChannelHandlerContent content = new ChannelHandlerContent("123,34,47".getBytes(StandardCharsets.UTF_8), "UTF-8",
+ null);
+ ColorChannelHandler converter = new ColorChannelHandler(updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(new HSBType("123,34,47"));
+ }
+
+ @Test
+ public void rollerSHutterConverter() {
+ ChannelValueConverterConfig cfg = new ChannelValueConverterConfig();
+ RollershutterChannelHandler converter = new RollershutterChannelHandler(updateStateMock, postCommandMock,
+ sendValueMock, new ChannelTransformation(null), new ChannelTransformation(null), cfg);
+
+ // test 0 and 100
+ ChannelHandlerContent content = new ChannelHandlerContent("0".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(PercentType.ZERO);
+ content = new ChannelHandlerContent("100".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(PercentType.HUNDRED);
+
+ // test under/over-range (expect two times total for zero/100
+ content = new ChannelHandlerContent("-1".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock, Mockito.times(2)).accept(PercentType.ZERO);
+ content = new ChannelHandlerContent("105".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock, Mockito.times(2)).accept(PercentType.HUNDRED);
+
+ // test value
+ content = new ChannelHandlerContent("67".getBytes(StandardCharsets.UTF_8), "UTF-8", null);
+ converter.process(content);
+ Mockito.verify(updateStateMock).accept(new PercentType(67));
+ }
+
+ public GenericChannelHandler createConverter(Function fcn) {
+ return new GenericChannelHandler(fcn, updateStateMock, postCommandMock, sendValueMock,
+ new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig());
+ }
+}
diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java
new file mode 100644
index 00000000000..08207319d4a
--- /dev/null
+++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.thing.internal.binding.generic.converter;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
+import org.openhab.core.thing.binding.generic.ChannelTransformation;
+import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link AbstractTransformingItemConverterTest} is a test class for the
+ * {@link AbstractTransformingChannelHandler}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class AbstractTransformingItemConverterTest {
+
+ @Mock
+ private @NonNullByDefault({}) Consumer sendHttpValue;
+
+ @Mock
+ private @NonNullByDefault({}) Consumer updateState;
+
+ @Mock
+ private @NonNullByDefault({}) Consumer postCommand;
+
+ private @NonNullByDefault({}) AutoCloseable closeable;
+
+ @Spy
+ private ChannelTransformation stateChannelTransformation = new ChannelTransformation(null);
+
+ @Spy
+ private ChannelTransformation commandChannelTransformation = new ChannelTransformation(null);
+
+ @BeforeEach
+ public void init() {
+ closeable = MockitoAnnotations.openMocks(this);
+ }
+
+ @AfterEach
+ public void close() throws Exception {
+ closeable.close();
+ }
+
+ @Test
+ public void undefOnNullContentTest() {
+ TestChannelHandler realConverter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, false);
+ TestChannelHandler converter = spy(realConverter);
+
+ converter.process(null);
+ // make sure UNDEF is send as state update
+ verify(updateState, only()).accept(UnDefType.UNDEF);
+ verify(postCommand, never()).accept(any());
+ verify(sendHttpValue, never()).accept(any());
+
+ // make sure no other processing applies
+ verify(converter, never()).toState(any());
+ verify(converter, never()).toCommand(any());
+ verify(converter, never()).toString(any());
+ }
+
+ @Test
+ public void commandIsPostedAsCommand() {
+ TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, true);
+
+ converter.process(new ChannelHandlerContent("TEST".getBytes(StandardCharsets.UTF_8), "", null));
+
+ // check state transformation is applied
+ verify(stateChannelTransformation).apply(any());
+ verify(commandChannelTransformation, never()).apply(any());
+
+ // check only postCommand is applied
+ verify(updateState, never()).accept(any());
+ verify(postCommand, only()).accept(new StringType("TEST"));
+ verify(sendHttpValue, never()).accept(any());
+ }
+
+ @Test
+ public void updateIsPostedAsUpdate() {
+ TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, false);
+
+ converter.process(new ChannelHandlerContent("TEST".getBytes(StandardCharsets.UTF_8), "", null));
+
+ // check state transformation is applied
+ verify(stateChannelTransformation).apply(any());
+ verify(commandChannelTransformation, never()).apply(any());
+
+ // check only updateState is called
+ verify(updateState, only()).accept(new StringType("TEST"));
+ verify(postCommand, never()).accept(any());
+ verify(sendHttpValue, never()).accept(any());
+ }
+
+ @Test
+ public void sendCommandSendsCommand() {
+ TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue,
+ stateChannelTransformation, commandChannelTransformation, false);
+
+ converter.send(new StringType("TEST"));
+
+ // check command transformation is applied
+ verify(stateChannelTransformation, never()).apply(any());
+ verify(commandChannelTransformation).apply(any());
+
+ // check only sendHttpValue is applied
+ verify(updateState, never()).accept(any());
+ verify(postCommand, never()).accept(any());
+ verify(sendHttpValue, only()).accept("TEST");
+ }
+
+ private static class TestChannelHandler extends AbstractTransformingChannelHandler {
+ private boolean hasCommand;
+
+ public TestChannelHandler(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer sendValue, ChannelTransformation stateChannelTransformation,
+ ChannelTransformation commandChannelTransformation, boolean hasCommand) {
+ super(updateState, postCommand, sendValue, stateChannelTransformation, commandChannelTransformation,
+ new ChannelValueConverterConfig());
+ this.hasCommand = hasCommand;
+ }
+
+ @Override
+ protected @Nullable Command toCommand(String value) {
+ return hasCommand ? new StringType(value) : null;
+ }
+
+ @Override
+ protected Optional toState(String value) {
+ return Optional.of(new StringType(value));
+ }
+
+ @Override
+ protected String toString(Command command) {
+ return command.toString();
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java
index 93500be5e19..94e953d7253 100644
--- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java
+++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java
@@ -12,25 +12,34 @@
*/
package org.openhab.core.transform;
-import java.util.Collection;
import java.util.IllegalFormatException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.framework.BundleContext;
-import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Kai Kreuzer - Initial contribution
+ * @author Jan N. Klug - Refactored to OSGi service
*/
+@Component(immediate = true)
@NonNullByDefault
public class TransformationHelper {
+ private static final Map SERVICES = new ConcurrentHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(TransformationHelper.class);
@@ -40,6 +49,35 @@ public class TransformationHelper {
protected static final Pattern EXTRACT_TRANSFORMFUNCTION_PATTERN = Pattern
.compile("(.*?)\\((.*)\\)" + FUNCTION_VALUE_DELIMITER + "(.*)");
+ private final BundleContext bundleContext;
+
+ @Activate
+ public TransformationHelper(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ @Deactivate
+ public void deactivate() {
+ SERVICES.clear();
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ public void setTransformationService(ServiceReference ref) {
+ String key = (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME);
+ TransformationService service = bundleContext.getService(ref);
+ if (service != null) {
+ SERVICES.put(key, service);
+ LOGGER.debug("Added transformation service {}", key);
+ }
+ }
+
+ public void unsetTransformationService(ServiceReference ref) {
+ String key = (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME);
+ if (SERVICES.remove(key) != null) {
+ LOGGER.debug("Removed transformation service {}", key);
+ }
+ }
+
/**
* determines whether a pattern refers to a transformation service
*
@@ -50,52 +88,57 @@ public static boolean isTransform(String pattern) {
return EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(pattern).matches();
}
+ public static @Nullable TransformationService getTransformationService(String serviceName) {
+ return SERVICES.get(serviceName);
+ }
+
/**
- * Queries the OSGi service registry for a service that provides a transformation service of
- * a given transformation type (e.g. REGEX, XSLT, etc.)
+ * Return the transformation service that provides a given transformation type (e.g. REGEX, XSLT, etc.)
*
* @param context the bundle context which can be null
* @param transformationType the desired transformation type
* @return a service instance or null, if none could be found
+ *
+ * @deprecated use {@link #getTransformationService(String)} instead
*/
+ @Deprecated
public static @Nullable TransformationService getTransformationService(@Nullable BundleContext context,
String transformationType) {
- if (context != null) {
- String filter = "(" + TransformationService.SERVICE_PROPERTY_NAME + "=" + transformationType + ")";
- try {
- Collection> refs = context
- .getServiceReferences(TransformationService.class, filter);
- if (refs != null && !refs.isEmpty()) {
- return context.getService(refs.iterator().next());
- } else {
- LOGGER.debug("Cannot get service reference for transformation service of type {}",
- transformationType);
- }
- } catch (InvalidSyntaxException e) {
- LOGGER.debug("Cannot get service reference for transformation service of type {}", transformationType,
- e);
- }
- }
- return null;
+ return getTransformationService(transformationType);
}
/**
* Transforms a state string using transformation functions within a given pattern.
*
* @param context a valid bundle context, required for accessing the services
- * @param stateDescPattern the pattern that contains the transformation instructions
+ * @param transformationString the pattern that contains the transformation instructions
* @param state the state to be formatted before being passed into the transformation function
* @return the result of the transformation. If no transformation was done, null
is returned
* @throws TransformationException if transformation service is not available or the transformation failed
+ *
+ * @deprecated Use {@link #transform(String, String)} instead
*/
- public static @Nullable String transform(BundleContext context, String stateDescPattern, String state)
+ @Deprecated
+ public static @Nullable String transform(BundleContext context, String transformationString, String state)
throws TransformationException {
- Matcher matcher = EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(stateDescPattern);
+ return transform(transformationString, state);
+ }
+
+ /**
+ * Transforms a state string using transformation functions within a given pattern.
+ *
+ * @param transformationString the pattern that contains the transformation instructions
+ * @param state the state to be formatted before being passed into the transformation function
+ * @return the result of the transformation. If no transformation was done, null
is returned
+ * @throws TransformationException if transformation service is not available or the transformation failed
+ */
+ public static @Nullable String transform(String transformationString, String state) throws TransformationException {
+ Matcher matcher = EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(transformationString);
if (matcher.find()) {
String type = matcher.group(1);
String pattern = matcher.group(2);
String value = matcher.group(3);
- TransformationService transformation = TransformationHelper.getTransformationService(context, type);
+ TransformationService transformation = SERVICES.get(type);
if (transformation != null) {
return transform(transformation, pattern, value, state);
} else {
diff --git a/bundles/org.openhab.core.ui/NOTICE b/bundles/org.openhab.core.ui/NOTICE
index f4cf4cb3fa1..c9355858dd6 100644
--- a/bundles/org.openhab.core.ui/NOTICE
+++ b/bundles/org.openhab.core.ui/NOTICE
@@ -15,10 +15,10 @@ https://github.com/openhab/openhab-core
== Third-party Content
-xchart-2.6.1
+xchart-3.8.4
* License: Apache License, 2.0
* Project: http://knowm.org/open-source/xchart
-* Source: https://github.com/timmolter/XChart/tree/xchart-2.6.1
+* Source: https://github.com/timmolter/XChart/tree/xchart-3.8.4
== Third-party license(s)
diff --git a/bundles/org.openhab.core.ui/bnd.bnd b/bundles/org.openhab.core.ui/bnd.bnd
index c13a3330e6a..d6f226152e2 100644
--- a/bundles/org.openhab.core.ui/bnd.bnd
+++ b/bundles/org.openhab.core.ui/bnd.bnd
@@ -4,4 +4,7 @@ Private-Package: \
org.knowm.xchart.*
Import-Package: \
de.erichseifert.vectorgraphics2d.*;resolution:=optional,\
+ de.rototor.pdfbox.*;resolution:=optional,\
+ com.madgag.*;resolution:=optional,\
+ org.apache.pdfbox.*;resolution:=optional,\
*
\ No newline at end of file
diff --git a/bundles/org.openhab.core.ui/pom.xml b/bundles/org.openhab.core.ui/pom.xml
index 575615ef393..2eef0e13fbb 100644
--- a/bundles/org.openhab.core.ui/pom.xml
+++ b/bundles/org.openhab.core.ui/pom.xml
@@ -18,8 +18,7 @@
org.knowm.xchart
xchart
-
- 3.1.0
+ 3.8.4
org.openhab.core.bundles
diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java
index 1c8304a336e..5f89cb04859 100644
--- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java
+++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java
@@ -32,7 +32,7 @@
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.XYSeries;
-import org.knowm.xchart.style.Styler;
+import org.knowm.xchart.style.AxesChartStyler;
import org.knowm.xchart.style.Styler.LegendPosition;
import org.knowm.xchart.style.XYStyler;
import org.knowm.xchart.style.markers.None;
@@ -195,7 +195,7 @@ public BufferedImage createChart(@Nullable String serviceId, @Nullable String th
styler.setYAxisDecimalPattern(yAxisDecimalPattern);
}
styler.setYAxisTickMarkSpacingHint(yAxisSpacing);
- styler.setYAxisLabelAlignment(Styler.TextAlignment.Right);
+ styler.setYAxisLabelAlignment(AxesChartStyler.TextAlignment.Right);
// chart
styler.setChartBackgroundColor(chartTheme.getChartBackgroundColor());
styler.setChartFontColor(chartTheme.getChartFontColor());
diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java
index 4be4185cddc..1862082e2e8 100644
--- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java
+++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java
@@ -108,6 +108,7 @@
* @author Stefan Triller - Method to convert a state into something a sitemap entity can understand
* @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
* @author Laurent Garnier - new method getIconColor
+ * @author Mark Herwege - new method getFormatPattern(widget), clean pattern
*/
@NonNullByDefault
@Component(immediate = true, configurationPid = "org.openhab.sitemap", //
@@ -121,9 +122,9 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
protected static final Pattern EXTRACT_TRANSFORM_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\):(.*)");
/* RegEx to identify format patterns. See java.util.Formatter#formatSpecifier (without the '%' at the very end). */
- protected static final String IDENTIFY_FORMAT_PATTERN_PATTERN = "%((unit%)|((\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z])))";
+ protected static final String IDENTIFY_FORMAT_PATTERN_PATTERN = "%(?:(unit%)|(?:(?:\\d+\\$)?(?:[-#+ 0,(<]*)?(?:\\d+)?(?:\\.\\d+)?(?:[tT])?(?:[a-zA-Z])))";
+ private static final Pattern FORMAT_PATTERN = Pattern.compile("(?:^|[^%])" + IDENTIFY_FORMAT_PATTERN_PATTERN);
- private static final Pattern LABEL_PATTERN = Pattern.compile(".*?\\[.*? (.*?)]");
private static final int MAX_BUTTONS = 4;
private static final String DEFAULT_SORTING = "NONE";
@@ -331,10 +332,14 @@ private Switch createPlayerButtons() {
String labelMappedOption = null;
State state = null;
StateDescription stateDescription = null;
- String formatPattern = getFormatPattern(label);
+ String formatPattern = getFormatPattern(w);
+
+ if (formatPattern != null && label.indexOf("[") < 0) {
+ label = label + " [" + formatPattern + "]";
+ }
// now insert the value, if the state is a string or decimal value and there is some formatting pattern defined
- // in the label (i.e. it contains at least a %)
+ // in the label or state description (i.e. it contains at least a %)
try {
final Item item = getItem(itemName);
@@ -348,13 +353,8 @@ private Switch createPlayerButtons() {
// returned StateDescription. What is expected is the display of a value using the pattern
// provided by the channel state description provider.
stateDescription = item.getStateDescription();
- if (formatPattern == null && stateDescription != null && stateDescription.getPattern() != null) {
- label = label + " [" + stateDescription.getPattern() + "]";
- }
- String updatedPattern = getFormatPattern(label);
- if (updatedPattern != null) {
- formatPattern = updatedPattern;
+ if (formatPattern != null) {
state = item.getState();
if (formatPattern.contains("%d")) {
@@ -371,7 +371,7 @@ private Switch createPlayerButtons() {
}
}
} catch (ItemNotFoundException e) {
- logger.error("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName());
+ logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName());
}
boolean considerTransform = false;
@@ -455,7 +455,10 @@ private Switch createPlayerButtons() {
}
label = label.trim();
- label = label.substring(0, label.indexOf("[") + 1) + formatPattern + "]";
+ int index = label.indexOf("[");
+ if (index >= 0) {
+ label = label.substring(0, index + 1) + formatPattern + "]";
+ }
}
}
@@ -463,7 +466,7 @@ private Switch createPlayerButtons() {
}
private QuantityType> convertStateToWidgetUnit(QuantityType> quantityState, Widget w) {
- Unit> widgetUnit = UnitUtils.parseUnit(getFormatPattern(w.getLabel()));
+ Unit> widgetUnit = UnitUtils.parseUnit(getFormatPattern(w));
if (widgetUnit != null && !widgetUnit.equals(quantityState.getUnit())) {
return Objects.requireNonNullElse(quantityState.toInvertibleUnit(widgetUnit), quantityState);
}
@@ -471,6 +474,56 @@ private QuantityType> convertStateToWidgetUnit(QuantityType> quantityState,
return quantityState;
}
+ @Override
+ public @Nullable String getFormatPattern(Widget w) {
+ String label = getLabelFromWidget(w);
+ String pattern = getFormatPattern(label);
+ String itemName = w.getItem();
+ try {
+ Item item = null;
+ if (itemName != null && !itemName.isBlank()) {
+ item = getItem(itemName);
+ }
+ if (item != null && pattern == null) {
+ StateDescription stateDescription = item.getStateDescription();
+ if (stateDescription != null) {
+ pattern = stateDescription.getPattern();
+ }
+ }
+
+ if (pattern == null) {
+ return null;
+ }
+
+ // remove last part of pattern, after unit, if it exists, as this is not valid and creates problems with
+ // updates
+ if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) {
+ Matcher m = FORMAT_PATTERN.matcher(pattern);
+ int matcherEnd = 0;
+ if (m.find() && m.group(1) == null) {
+ matcherEnd = m.end();
+ }
+ String unit = pattern.substring(matcherEnd).trim();
+ String postfix = "";
+ int unitEnd = unit.indexOf(" ");
+ if (unitEnd > -1) {
+ postfix = unit.substring(unitEnd + 1).trim();
+ unit = unit.substring(0, unitEnd);
+ }
+ if (!postfix.isBlank()) {
+ logger.warn(
+ "Item '{}' with unit, nothing allowed after unit in label pattern '{}', dropping postfix",
+ itemName, pattern);
+ }
+ pattern = pattern.substring(0, matcherEnd) + (!unit.isBlank() ? " " + unit : "");
+ }
+ } catch (ItemNotFoundException e) {
+ logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName());
+ }
+
+ return pattern;
+ }
+
private @Nullable String getFormatPattern(@Nullable String label) {
if (label == null) {
return null;
@@ -608,7 +661,7 @@ private String transform(String label, boolean matchTransform, @Nullable String
Item item = getItem(itemName);
return convertState(w, item, item.getState());
} catch (ItemNotFoundException e) {
- logger.error("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName());
+ logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName());
}
}
return UnDefType.UNDEF;
@@ -627,8 +680,8 @@ private String transform(String label, boolean matchTransform, @Nullable String
State returnState = null;
State itemState = i.getState();
- if (itemState instanceof QuantityType type) {
- itemState = convertStateToWidgetUnit(type, w);
+ if (itemState instanceof QuantityType> quantityTypeState) {
+ itemState = convertStateToWidgetUnit(quantityTypeState, w);
}
if (w instanceof Switch && i instanceof RollershutterItem) {
@@ -637,7 +690,7 @@ private String transform(String label, boolean matchTransform, @Nullable String
} else if (w instanceof Slider) {
if (i.getAcceptedDataTypes().contains(PercentType.class)) {
returnState = itemState.as(PercentType.class);
- } else {
+ } else if (!(itemState instanceof QuantityType>)) {
returnState = itemState.as(DecimalType.class);
}
} else if (w instanceof Switch sw) {
@@ -1208,7 +1261,7 @@ public boolean getVisiblity(Widget w) {
try {
item = itemRegistry.getItem(itemName);
} catch (ItemNotFoundException e) {
- logger.error("Cannot retrieve visibility item {} for widget {}", rule.getItem(),
+ logger.warn("Cannot retrieve visibility item {} for widget {}", rule.getItem(),
w.eClass().getInstanceTypeName());
// Default to visible!
@@ -1320,25 +1373,29 @@ public void removeRegistryHook(RegistryHook- hook) {
@Override
public @Nullable String getUnitForWidget(Widget w) {
- try {
- Item item = getItem(w.getItem());
+ String itemName = w.getItem();
+ if (itemName != null) {
+ try {
+ Item item = getItem(itemName);
- // we require the item to define a dimension, otherwise no unit will be reported to the UIs.
- if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) {
- if (w.getLabel() == null) {
- // if no Label was assigned to the Widget we fallback to the items unit
- return numberItem.getUnitSymbol();
- }
+ // we require the item to define a dimension, otherwise no unit will be reported to the UIs.
+ if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) {
+ String pattern = getFormatPattern(w);
+ if (pattern == null || pattern.isBlank()) {
+ // if no Label was assigned to the Widget we fallback to the items unit
+ return numberItem.getUnitSymbol();
+ }
- String unit = getUnitFromLabel(w.getLabel());
- if (!UnitUtils.UNIT_PLACEHOLDER.equals(unit)) {
- return unit;
- }
+ String unit = getUnitFromPattern(pattern);
+ if (!UnitUtils.UNIT_PLACEHOLDER.equals(unit)) {
+ return unit;
+ }
- return numberItem.getUnitSymbol();
+ return numberItem.getUnitSymbol();
+ }
+ } catch (ItemNotFoundException e) {
+ logger.warn("Failed to retrieve item during widget rendering, item does not exist: {}", e.getMessage());
}
- } catch (ItemNotFoundException e) {
- logger.debug("Failed to retrieve item during widget rendering: {}", e.getMessage());
}
return "";
@@ -1354,14 +1411,13 @@ public void removeRegistryHook(RegistryHook
- hook) {
return state;
}
- private @Nullable String getUnitFromLabel(@Nullable String label) {
- if (label == null || label.isBlank()) {
+ private @Nullable String getUnitFromPattern(@Nullable String format) {
+ if (format == null || format.isBlank()) {
return null;
}
- Matcher m = LABEL_PATTERN.matcher(label);
- if (m.matches()) {
- return m.group(1);
- }
- return null;
+ int index = format.lastIndexOf(" ");
+ String unit = index > 0 ? format.substring(index + 1) : null;
+ unit = UnitUtils.UNIT_PERCENT_FORMAT_STRING.equals(unit) ? "%" : unit;
+ return unit;
}
}
diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java
index 998a11d3c4c..a979543d4cf 100644
--- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java
+++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java
@@ -34,6 +34,7 @@
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Initial contribution
* @author Laurent Garnier - new method getIconColor
+ * @author Mark Herwege - new method getFormatPattern
*/
@NonNullByDefault
public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider {
@@ -130,6 +131,15 @@ public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider {
@Nullable
EObject getParent(Widget w);
+ /**
+ * Gets the format pattern for the widget value, retrieved from widget label, item label or item state description
+ *
+ * @param w Widget
+ * @return String with the format pattern
+ */
+ @Nullable
+ String getFormatPattern(Widget w);
+
/**
* Gets the label color for the widget. Checks conditional statements to
* find the color based on the item value
diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java
index 86ba0b8c04e..d34aa774b13 100644
--- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java
+++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java
@@ -135,6 +135,11 @@ public interface VoiceManager {
@Nullable
DialogContext getLastDialogContext();
+ /**
+ * Returns a list with the contexts of all running dialogs.
+ */
+ List getDialogsContexts();
+
/**
* Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve
* a question or a command (Speech to Text service), interpretation and handling of the command, and finally
diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java
index 2760edbd98e..f23b962140f 100644
--- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java
+++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java
@@ -223,6 +223,13 @@ public void stop() {
eventListener.onDialogStopped(dialogContext);
}
+ /**
+ * Returns the dialog context used to start this processor.
+ */
+ public DialogContext getContext() {
+ return dialogContext;
+ }
+
/**
* Indicates if voice recognition is running.
*/
diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java
index c29b0091b57..c4eb1ff0941 100644
--- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java
+++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java
@@ -20,6 +20,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -33,6 +34,7 @@
import org.openhab.core.items.ItemNotUniqueException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.voice.DialogContext;
+import org.openhab.core.voice.DialogRegistration;
import org.openhab.core.voice.KSService;
import org.openhab.core.voice.STTService;
import org.openhab.core.voice.TTSService;
@@ -60,7 +62,11 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio
private static final String SUBCMD_VOICES = "voices";
private static final String SUBCMD_START_DIALOG = "startdialog";
private static final String SUBCMD_STOP_DIALOG = "stopdialog";
+ private static final String SUBCMD_REGISTER_DIALOG = "registerdialog";
+ private static final String SUBCMD_UNREGISTER_DIALOG = "unregisterdialog";
private static final String SUBCMD_LISTEN_ANSWER = "listenandanswer";
+ private static final String SUBCMD_DIALOGS = "dialogs";
+ private static final String SUBCMD_DIALOG_REGS = "dialogregs";
private static final String SUBCMD_INTERPRETERS = "interpreters";
private static final String SUBCMD_KEYWORD_SPOTTERS = "keywordspotters";
private static final String SUBCMD_STT_SERVICES = "sttservices";
@@ -87,13 +93,21 @@ public List getUsages() {
return List.of(buildCommandUsage(SUBCMD_SAY + " ", "speaks a text"),
buildCommandUsage(SUBCMD_INTERPRET + " ", "interprets a human language command"),
buildCommandUsage(SUBCMD_VOICES, "lists available voices of the TTS services"),
+ buildCommandUsage(SUBCMD_DIALOGS, "lists the running dialog and their audio/voice services"),
+ buildCommandUsage(SUBCMD_DIALOG_REGS,
+ "lists the existing dialog registrations and their selected audio/voice services"),
+ buildCommandUsage(SUBCMD_REGISTER_DIALOG
+ + " [--source