diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000000..4c292a99f86
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,45 @@
+---
+name: "🐛 Bug report"
+about: Something isn't working correctly in the Core. This is the wrong place for openHAB user-interface or add-on issues
+labels: bug
+
+---
+
+
+
+
+
+
+
+
+## Expected Behavior
+
+
+
+## Current Behavior
+
+
+
+
+
+
+
+## Possible Solution
+
+
+
+## Steps to Reproduce (for Bugs)
+
+
+1.
+2.
+
+## Context
+
+
+
+## Your Environment
+
+* Version used: (e.g., openHAB and add-on versions)
+* Environment name and version (e.g. Chrome 111, Java 17, Node.js 18.15, ...):
+* Operating System and version (desktop or mobile, Windows 11, Raspbian Bullseye, ...):
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000000..b8c32b01222
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: true
+contact_links:
+ - name: 🤔 Support/Usage Question
+ url: https://community.openhab.org
+ about: Feel free to ask anything
+ - name: 📖 Documentation
+ url: https://www.openhab.org/docs/
+ about: Official documentation
+ - name: 📚 Translation feedback
+ url: https://crowdin.com/project/openhab-core
+ about: Share feedback on translations
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..16168137206
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: "✨ Feature request"
+about: You think that the Core should gain another feature
+labels: enhancement
+
+---
+
+
+
+
+
+
+## Your Environment
+
+* Version used: (e.g., openHAB and add-on versions)
+* Environment name and version (e.g. Chrome 111, Java 17, Node.js 18.15, ...):
+* Operating System and version (desktop or mobile, Windows 11, Raspbian Bullseye, ...):
diff --git a/.github/issue_template/sitemap_change.md b/.github/ISSUE_TEMPLATE/sitemap_change.md
similarity index 96%
rename from .github/issue_template/sitemap_change.md
rename to .github/ISSUE_TEMPLATE/sitemap_change.md
index 52975ceb27c..783ca757415 100644
--- a/.github/issue_template/sitemap_change.md
+++ b/.github/ISSUE_TEMPLATE/sitemap_change.md
@@ -1,5 +1,5 @@
---
-name: Suggest a Sitemap change
+name: 🗺 Sitemap change
about: 'Suggest a change in the Sitemap syntax and corresponding UIs'
labels: sitemap
---
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000000..c01cdb58c39
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,47 @@
+
diff --git a/bom/compile/pom.xml b/bom/compile/pom.xml
index 86a71ef2b1f..2cd40425121 100644
--- a/bom/compile/pom.xml
+++ b/bom/compile/pom.xml
@@ -183,6 +183,12 @@
${jetty.version}
compile
+
+ org.eclipse.jetty.http2
+ http2-server
+ ${jetty.version}
+ compile
+
@@ -407,7 +413,7 @@
org.bitbucket.b_c
jose4j
- 0.7.7
+ 0.9.3
compile
diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index cf8ae22d254..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
@@ -392,7 +392,7 @@
commons-fileupload
commons-fileupload
- 1.4
+ 1.5
compile
@@ -757,6 +757,12 @@
${jetty.version}
compile
+
+ org.eclipse.jetty.http2
+ http2-server
+ ${jetty.version}
+ compile
+
org.eclipse.jetty
jetty-alpn-java-client
@@ -967,7 +973,7 @@
org.bitbucket.b_c
jose4j
- 0.7.7
+ 0.9.3
compile
diff --git a/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java b/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java
index 1a301f5c70e..6dc61c0efad 100644
--- a/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java
+++ b/bundles/org.openhab.core.addon.marketplace.karaf/src/main/java/org/openhab/core/addon/marketplace/karaf/internal/community/CommunityKarafAddonHandler.java
@@ -78,8 +78,13 @@ public boolean supports(String type, String contentType) {
}
private Stream karFilesStream(Path addonDirectory) throws IOException {
- return Files.isDirectory(addonDirectory) ? Files.list(addonDirectory).map(Path::getFileName)
- .filter(path -> path.toString().endsWith(KAR_EXTENSION)) : Stream.empty();
+ if (Files.isDirectory(addonDirectory)) {
+ try (Stream files = Files.list(addonDirectory)) {
+ return files.map(Path::getFileName).filter(path -> path.toString().endsWith(KAR_EXTENSION)).toList()
+ .stream();
+ }
+ }
+ return Stream.empty();
}
private String pathToKarRepoName(Path path) {
@@ -153,8 +158,8 @@ private void addKarToCache(String addonId, URL sourceUrl) throws MarketplaceHand
private void installFromCache(String addonId) throws MarketplaceHandlerException {
Path addonPath = getAddonCacheDirectory(addonId);
if (Files.isDirectory(addonPath)) {
- try {
- List karFiles = Files.list(addonPath).collect(Collectors.toList());
+ try (Stream files = Files.list(addonPath)) {
+ List karFiles = files.toList();
if (karFiles.size() != 1) {
throw new MarketplaceHandlerException(
"The local cache folder doesn't contain a single file: " + addonPath, null);
@@ -172,10 +177,10 @@ private void installFromCache(String addonId) throws MarketplaceHandlerException
}
private void ensureCachedKarsAreInstalled() {
- try {
- if (Files.isDirectory(KAR_CACHE_PATH)) {
- Files.list(KAR_CACHE_PATH).filter(Files::isDirectory).map(this::addonIdFromPath)
- .filter(addonId -> !isInstalled(addonId)).forEach(addonId -> {
+ if (Files.isDirectory(KAR_CACHE_PATH)) {
+ try (Stream files = Files.list(KAR_CACHE_PATH)) {
+ files.filter(Files::isDirectory).map(this::addonIdFromPath).filter(addonId -> !isInstalled(addonId))
+ .forEach(addonId -> {
logger.info("Reinstalling missing marketplace KAR: {}", addonId);
try {
installFromCache(addonId);
@@ -183,9 +188,9 @@ private void ensureCachedKarsAreInstalled() {
logger.warn("Failed reinstalling add-on from cache", e);
}
});
+ } catch (IOException e) {
+ logger.warn("Failed to re-install KARs: {}", e.getMessage());
}
- } catch (IOException e) {
- logger.warn("Failed to re-install KARs: {}", e.getMessage());
}
isReady = true;
}
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 20736dd5028..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;
@@ -45,6 +48,7 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
/**
* The {@link AbstractRemoteAddonService} implements basic functionality of a remote add-on-service
@@ -65,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);
@@ -82,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)) {
@@ -90,8 +106,15 @@ public void refreshSource() {
return;
}
List addons = new ArrayList<>();
- installedAddonStorage.stream().map(e -> Objects.requireNonNull(gson.fromJson(e.getValue(), Addon.class)))
- .forEach(addons::add);
+ try {
+ installedAddonStorage.stream().map(this::convertFromStorage).forEach(addons::add);
+ } catch (JsonSyntaxException e) {
+ List.copyOf(installedAddonStorage.getKeys()).forEach(installedAddonStorage::remove);
+ logger.error(
+ "Failed to read JSON database, trying to purge it. You might need to re-install {} from the '{}' service.",
+ installedAddonStorage.getKeys(), getId());
+ refreshSource();
+ }
// create lookup list to make sure installed addons take precedence
List installedAddons = addons.stream().map(Addon::getUid).collect(Collectors.toList());
diff --git a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java
index a429e7a89bc..0b5a15fffc0 100644
--- a/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java
+++ b/bundles/org.openhab.core.addon.marketplace/src/main/java/org/openhab/core/addon/marketplace/MarketplaceBundleInstaller.java
@@ -22,7 +22,7 @@
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.OpenHAB;
@@ -79,8 +79,8 @@ protected void addBundleToCache(String addonId, URL sourceUrl) throws Marketplac
protected void installFromCache(BundleContext bundleContext, String addonId) throws MarketplaceHandlerException {
Path addonPath = getAddonCacheDirectory(addonId);
if (Files.isDirectory(addonPath)) {
- try {
- List bundleFiles = Files.list(addonPath).collect(Collectors.toList());
+ try (Stream files = Files.list(addonPath)) {
+ List bundleFiles = files.toList();
if (bundleFiles.size() != 1) {
throw new MarketplaceHandlerException(
"The local cache folder doesn't contain a single file: " + addonPath, null);
@@ -122,8 +122,10 @@ protected void uninstallBundle(BundleContext bundleContext, String addonId) thro
try {
Path addonPath = getAddonCacheDirectory(addonId);
if (Files.isDirectory(addonPath)) {
- for (Path bundleFile : Files.list(addonPath).collect(Collectors.toList())) {
- Files.delete(bundleFile);
+ try (Stream files = Files.list(addonPath)) {
+ for (Path path : files.toList()) {
+ Files.delete(path);
+ }
}
}
Files.delete(addonPath);
@@ -147,9 +149,9 @@ protected void uninstallBundle(BundleContext bundleContext, String addonId) thro
* @param bundleContext the {@link BundleContext} to use to look up the bundles
*/
protected void ensureCachedBundlesAreInstalled(BundleContext bundleContext) {
- try {
- if (Files.isDirectory(BUNDLE_CACHE_PATH)) {
- Files.list(BUNDLE_CACHE_PATH).filter(Files::isDirectory).map(this::addonIdFromPath)
+ if (Files.isDirectory(BUNDLE_CACHE_PATH)) {
+ try (Stream files = Files.list(BUNDLE_CACHE_PATH)) {
+ files.filter(Files::isDirectory).map(this::addonIdFromPath)
.filter(addonId -> !isBundleInstalled(bundleContext, addonId)).forEach(addonId -> {
logger.info("Reinstalling missing marketplace bundle: {}", addonId);
try {
@@ -158,9 +160,9 @@ protected void ensureCachedBundlesAreInstalled(BundleContext bundleContext) {
logger.warn("Failed reinstalling add-on from cache", e);
}
});
+ } catch (IOException e) {
+ logger.warn("Failed to re-install bundles: {}", e.getMessage());
}
- } catch (IOException e) {
- logger.warn("Failed to re-install bundles: {}", e.getMessage());
}
}
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 e5c699cacba..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;
@@ -90,7 +91,7 @@ public class CommunityMarketplaceAddonService extends AbstractRemoteAddonService
private static final String COMMUNITY_BASE_URL = "https://community.openhab.org";
private static final String COMMUNITY_MARKETPLACE_URL = COMMUNITY_BASE_URL + "/c/marketplace/69/l/latest";
private static final String COMMUNITY_TOPIC_URL = COMMUNITY_BASE_URL + "/t/";
- private static final Pattern BUNDLE_NAME_PATTERN = Pattern.compile(".*/(.*)-\\d+\\.\\d+\\.\\d+.*");
+ private static final Pattern BUNDLE_NAME_PATTERN = Pattern.compile(".*/(.*?)-\\d+\\.\\d+\\.\\d+.*");
private static final String SERVICE_ID = "marketplace";
private static final String ADDON_ID_PREFIX = SERVICE_ID + ":";
@@ -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/main/resources/OH-INF/i18n/marketplace_ro.properties b/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/i18n/marketplace_ro.properties
new file mode 100644
index 00000000000..995bde82834
--- /dev/null
+++ b/bundles/org.openhab.core.addon.marketplace/src/main/resources/OH-INF/i18n/marketplace_ro.properties
@@ -0,0 +1,13 @@
+system.config.jsonaddonservice.showUnstable.label = Arată suplimentele nestabilite
+system.config.jsonaddonservice.showUnstable.description = Include intrări care nu au fost etichetate ca "stabile". Aceste suplimente ar trebui să fie utilizate numai în scopuri de testare și nu sunt considerate ca fiind pregătite pentru sistemul de producție.
+system.config.jsonaddonservice.urls.label = URL-uri pentru servicii suplimentare
+system.config.jsonaddonservice.urls.description = Conducta (|) separă lista de URLS care oferă servicii suplimentare terțe prin fișiere Json. Atenție\: Pachetele distribuite prin servicii adiționale terțe ar putea să nu fie reexaminate corespunzător și ar putea conține un cod periculos și astfel să dăuneze sistemului dvs.
+system.config.marketplace.apiKey.label = API Key for community.openhab.org
+system.config.marketplace.apiKey.description = Specificați cheia API pe care să o utilizați pe forumul comunității (pentru personal și curatori - acest lucru permite, de exemplu, să vedeți conținut care nu este încă revizuit sau ascuns în alt mod de publicul larg). Lăsați necompletat dacă nu aveți unul.
+system.config.marketplace.enable.label = Activați bazarul comunitar
+system.config.marketplace.enable.description = Dacă este setat pe fals niciun supliment din piața comunitară nu va fi afișat. Suplimentele instalate vor fi încă disponibile.
+system.config.marketplace.showUnpublished.label = Arată intrările nepublicate
+system.config.marketplace.showUnpublished.description = Include intrările care nu au fost etichetate ca publicate. Avertisment\: aceasta poate include intrări care nu sunt gata și care ar putea să nu funcționeze sau să nu afecteze instalarea. Activează pe propriul risc doar în scopuri de testare.
+
+service.system.marketplace.label = Piață comunitară
+service.system.jsonaddonservice.label = Json 3rd party Add-on Service
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 8b09cf4bff7..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
@@ -60,10 +61,12 @@ protected BundleVersion getCoreVersion() {
return new BundleVersion("3.2.0");
}
+ @Override
public void addAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.add(handler);
}
+ @Override
public void removeAddonHandler(MarketplaceAddonHandler handler) {
this.addonHandlers.remove(handler);
}
diff --git a/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd b/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd
index 5af9bacf6bd..21142133d17 100644
--- a/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd
+++ b/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd
@@ -51,9 +51,26 @@
-
-
-
+
+
+ No interaction with external systems at all
+
+
+
+
+ Interaction with external systems, without internet access
+
+
+
+
+ Interaction with external systems, internet access required only for extended functionality (such as discovery)
+
+
+
+
+ Interaction with external systems, internet access required for normal operation
+
+
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 1f33c5aea00..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
@@ -298,13 +298,39 @@ public List getLoggerPackages() {
* Create a builder for an {@link Addon}
*
* @param uid the UID of the add-on (e.g. "binding-dmx", "json:transform-format" or "marketplace:123456")
- *
- * @return
+ * @return the builder
*/
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/AddonEventFactory.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonEventFactory.java
index 7fd9fe64d62..9465d7fa17c 100644
--- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonEventFactory.java
+++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonEventFactory.java
@@ -55,12 +55,10 @@ protected Event createEventByType(String eventType, String topic, String payload
throws Exception {
if (topic.endsWith(ADDON_FAILURE_EVENT_TOPIC_POSTFIX)) {
String[] properties = deserializePayload(payload, String[].class);
- Event event = new AddonEvent(topic, payload, properties[0], properties[1]);
- return event;
+ return new AddonEvent(topic, payload, properties[0], properties[1]);
} else {
String id = deserializePayload(payload, String.class);
- Event event = new AddonEvent(topic, payload, id);
- return event;
+ return new AddonEvent(topic, payload, id);
}
}
diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java
index 7094871fbc5..25c7593878b 100644
--- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java
+++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java
@@ -46,9 +46,9 @@ public class AddonInfo implements Identifiable {
private final String serviceId;
private @Nullable String sourceBundle;
- private AddonInfo(String id, String type, String name, String description, @Nullable String author,
- @Nullable String connection, List countries, @Nullable String configDescriptionURI,
- @Nullable String serviceId, @Nullable String sourceBundle) throws IllegalArgumentException {
+ private AddonInfo(String id, String type, String name, String description, @Nullable String connection,
+ List countries, @Nullable String configDescriptionURI, @Nullable String serviceId,
+ @Nullable String sourceBundle) throws IllegalArgumentException {
// mandatory fields
if (id.isBlank()) {
throw new IllegalArgumentException("The ID must neither be null nor empty!");
@@ -153,7 +153,6 @@ public static class Builder {
private final String type;
private String name = "";
private String description = "";
- private @Nullable String author;
private @Nullable String connection;
private List countries = List.of();
private @Nullable String configDescriptionURI = "";
@@ -187,11 +186,6 @@ public Builder withDescription(String description) {
return this;
}
- public Builder withAuthor(@Nullable String author) {
- this.author = author;
- return this;
- }
-
public Builder withConnection(@Nullable String connection) {
this.connection = connection;
return this;
@@ -229,8 +223,8 @@ public Builder withSourceBundle(@Nullable String sourceBundle) {
* @throws IllegalArgumentException if any of the information in this builder is invalid
*/
public AddonInfo build() throws IllegalArgumentException {
- return new AddonInfo(id, type, name, description, author, connection, countries, configDescriptionURI,
- serviceId, sourceBundle);
+ return new AddonInfo(id, type, name, description, connection, countries, configDescriptionURI, serviceId,
+ sourceBundle);
}
}
}
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 2d1769eaa88..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
@@ -435,8 +435,7 @@ public boolean isCompatible(@Nullable AudioFormat audioFormat) {
@Override
public boolean equals(@Nullable Object obj) {
- if (obj instanceof AudioFormat) {
- AudioFormat format = (AudioFormat) obj;
+ if (obj instanceof AudioFormat format) {
if (!(null == getCodec() ? null == format.getCodec() : getCodec().equals(format.getCodec()))) {
return false;
}
@@ -485,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 9788b3acbdd..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
@@ -12,6 +12,9 @@
*/
package org.openhab.core.audio;
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.audio.internal.AudioServlet;
@@ -28,25 +31,54 @@ 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.
*
* @param stream the stream to serve on HTTP
* @return the relative URL to access the stream starting with a '/'
+ * @deprecated Use {@link AudioHTTPServer#serve(AudioStream, int, boolean, CompletableFuture)}
*/
+ @Deprecated
String serve(AudioStream stream);
/**
* Creates a relative url for a given {@link AudioStream} where it can be requested multiple times within the given
* time frame.
- * This method only accepts {@link FixedLengthAudioStream}s, since it needs to be able to create multiple concurrent
- * streams from it, which isn't possible with a regular {@link AudioStream}.
+ * This method accepts all {@link AudioStream}s, but it is better to use {@link ClonableAudioStream}s. If generic
+ * {@link AudioStream} is used, the method tries to add the Clonable capability by storing it in a small memory
+ * buffer, e.g {@link ByteArrayAudioStream}, or in a cached file if the stream reached the buffer capacity,
+ * or fails if the stream is too long.
* Streams are closed, once they expire.
*
* @param stream the stream to serve on HTTP
* @param seconds number of seconds for which the stream is available through HTTP
* @return the relative URL to access the stream starting with a '/'
+ * @deprecated Use {@link AudioHTTPServer#serve(AudioStream, int, boolean, CompletableFuture)}
+ */
+ @Deprecated
+ String serve(AudioStream stream, int seconds);
+
+ /**
+ * Creates a relative url for a given {@link AudioStream} where it can be requested one or multiple times within the
+ * given time frame.
+ * This method accepts all {@link AudioStream}s, but if multiTimeStream is set to true it is better to use
+ * {@link ClonableAudioStream}s. Otherwise, if a generic {@link AudioStream} is used, the method will then try
+ * to add the Clonable capability by storing it in a small memory buffer, e.g {@link ByteArrayAudioStream}, or in a
+ * cached file if the stream reached the buffer capacity, or fails to render the sound completely if the stream is
+ * too long.
+ * A {@link CompletableFuture} is used to inform the caller that the playback ends in order to clean
+ * resources and run delayed task, such as restoring volume.
+ * Streams are closed, once they expire.
+ *
+ * @param stream the stream to serve on HTTP
+ * @param seconds number of seconds for which the stream is available through HTTP. The stream will be deleted only
+ * if not started, so you can set a duration shorter than the track's duration.
+ * @param multiTimeStream set to true if this stream should be played multiple time, and thus needs to be made
+ * Cloneable if it is not already.
+ * @return information about the {@link StreamServed}, including the relative URL to access the stream starting with
+ * a '/', and a CompletableFuture to know when the playback ends.
+ * @throws IOException when the stream is not a {@link ClonableAudioStream} and we cannot get or store it on disk.
*/
- String serve(FixedLengthAudioStream stream, int seconds);
+ StreamServed serve(AudioStream stream, int seconds, boolean multiTimeStream) throws IOException;
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java
index a60e5fe4172..d640c0f3996 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java
@@ -252,4 +252,15 @@ public interface AudioManager {
* @return ids of matching sinks
*/
Set getSinkIds(String pattern);
+
+ /**
+ * Handles a volume command change and returns a Runnable to restore it.
+ * Returning a Runnable allows us to have a no-op Runnable if changing volume back is not needed, and conveniently
+ * keeping it as one liner usable in a chain for the caller.
+ *
+ * @param volume The volume to set
+ * @param sink The sink to set the volume to
+ * @return A runnable to restore the volume to its previous value, or no-operation if no change is required.
+ */
+ Runnable handleVolumeCommand(@Nullable PercentType volume, AudioSink sink);
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java
index f1ecca1d43c..adc542a5ce4 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java
@@ -15,6 +15,7 @@
import java.io.IOException;
import java.util.Locale;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -58,13 +59,47 @@ public interface AudioSink {
*
* In case the audioStream is null, this should be interpreted as a request to end any currently playing stream.
*
+ * When the stream is not needed anymore, if the stream implements the {@link org.openhab.core.common.Disposable}
+ * interface, the sink should hereafter get rid of it by calling the dispose method.
+ *
* @param audioStream the audio stream to play or null to keep quiet
* @throws UnsupportedAudioFormatException If audioStream format is not supported
* @throws UnsupportedAudioStreamException If audioStream is not supported
+ * @deprecated Use {@link AudioSink#processAndComplete(AudioStream)}
*/
+ @Deprecated
void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException;
+ /**
+ * Processes the passed {@link AudioStream}, and returns a CompletableFuture that should complete when the sound is
+ * fully played. It is the sink responsibility to complete this future.
+ *
+ * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException}
+ * is thrown.
+ *
+ * If the passed {@link AudioStream} has an {@link AudioFormat} not supported by this instance,
+ * an {@link UnsupportedAudioFormatException} is thrown.
+ *
+ * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream.
+ *
+ * When the stream is not needed anymore, if the stream implements the {@link org.openhab.core.common.Disposable}
+ * interface, the sink should hereafter get rid of it by calling the dispose method.
+ *
+ * @param audioStream the audio stream to play or null to keep quiet
+ * @return A future completed when the sound is fully played. The method can instead complete with
+ * UnsupportedAudioFormatException if the audioStream format is not supported, or
+ * UnsupportedAudioStreamException If audioStream is not supported
+ */
+ default CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
+ try {
+ process(audioStream);
+ } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
+ return CompletableFuture.failedFuture(e);
+ }
+ return CompletableFuture.completedFuture(null);
+ }
+
/**
* Gets a set containing all supported audio formats
*
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java
new file mode 100644
index 00000000000..c88d20c93c9
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java
@@ -0,0 +1,113 @@
+/**
+ * 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 java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.common.Disposable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Definition of an audio output like headphones, a speaker or for writing to
+ * a file / clip.
+ * Helper class for asynchronous sink : when the process() method returns, the {@link AudioStream}
+ * may or may not be played. It is the responsibility of the implementing AudioSink class to
+ * complete the CompletableFuture when playing is done. Any delayed tasks will then be performed, such as volume
+ * restoration.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AudioSinkAsync implements AudioSink {
+
+ private final Logger logger = LoggerFactory.getLogger(AudioSinkAsync.class);
+
+ protected final Map> runnableByAudioStream = new HashMap<>();
+
+ @Override
+ public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
+ CompletableFuture<@Nullable Void> completableFuture = new CompletableFuture<@Nullable Void>();
+ if (audioStream != null) {
+ runnableByAudioStream.put(audioStream, completableFuture);
+ }
+ try {
+ processAsynchronously(audioStream);
+ } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
+ completableFuture.completeExceptionally(e);
+ }
+ if (audioStream == null) {
+ // No need to delay the post process task
+ completableFuture.complete(null);
+ }
+ return completableFuture;
+ }
+
+ @Override
+ public void process(@Nullable AudioStream audioStream)
+ throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
+ processAsynchronously(audioStream);
+ }
+
+ /**
+ * Processes the passed {@link AudioStream} asynchronously. This method is expected to return before the stream is
+ * fully played. This is the sink responsibility to call the {@link AudioSinkAsync#playbackFinished(AudioStream)}
+ * when it is.
+ *
+ * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException}
+ * is thrown.
+ *
+ * If the passed {@link AudioStream} has an {@link AudioFormat} not supported by this instance,
+ * an {@link UnsupportedAudioFormatException} is thrown.
+ *
+ * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream.
+ *
+ * @param audioStream the audio stream to play or null to keep quiet
+ * @throws UnsupportedAudioFormatException If audioStream format is not supported
+ * @throws UnsupportedAudioStreamException If audioStream is not supported
+ */
+ protected abstract void processAsynchronously(@Nullable AudioStream audioStream)
+ throws UnsupportedAudioFormatException, UnsupportedAudioStreamException;
+
+ /**
+ * Will complete the future previously returned, allowing the core to run delayed task.
+ *
+ * @param audioStream The AudioStream is the key to find the delayed CompletableFuture in the storage.
+ */
+ protected void playbackFinished(AudioStream audioStream) {
+ CompletableFuture<@Nullable Void> completableFuture = runnableByAudioStream.remove(audioStream);
+ if (completableFuture != null) {
+ completableFuture.complete(null);
+ }
+
+ // if the stream is not needed anymore, then we should call back the AudioStream to let it a chance
+ // to auto dispose.
+ if (audioStream instanceof Disposable disposableAudioStream) {
+ try {
+ disposableAudioStream.dispose();
+ } catch (IOException e) {
+ String fileName = audioStream instanceof FileAudioStream file ? file.toString() : "unknown";
+ if (logger.isDebugEnabled()) {
+ logger.debug("Cannot dispose of stream {}", fileName, e);
+ } else {
+ logger.warn("Cannot dispose of stream {}, reason {}", fileName, e.getMessage());
+ }
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java
new file mode 100644
index 00000000000..97c888f7468
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java
@@ -0,0 +1,85 @@
+/**
+ * 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 java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.common.Disposable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Definition of an audio output like headphones, a speaker or for writing to
+ * a file / clip.
+ * Helper class for synchronous sink : when the process() method returns,
+ * the source is considered played, and could be disposed.
+ * Any delayed tasks can then be performed, such as volume restoration.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AudioSinkSync implements AudioSink {
+
+ private final Logger logger = LoggerFactory.getLogger(AudioSinkSync.class);
+
+ @Override
+ public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
+ try {
+ processSynchronously(audioStream);
+ return CompletableFuture.completedFuture(null);
+ } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
+ return CompletableFuture.failedFuture(e);
+ } finally {
+ // as the stream is not needed anymore, we should dispose of it
+ if (audioStream instanceof Disposable disposableAudioStream) {
+ try {
+ disposableAudioStream.dispose();
+ } catch (IOException e) {
+ String fileName = audioStream instanceof FileAudioStream file ? file.toString() : "unknown";
+ if (logger.isDebugEnabled()) {
+ logger.debug("Cannot dispose of stream {}", fileName, e);
+ } else {
+ logger.warn("Cannot dispose of stream {}, reason {}", fileName, e.getMessage());
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void process(@Nullable AudioStream audioStream)
+ throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
+ processSynchronously(audioStream);
+ }
+
+ /**
+ * Processes the passed {@link AudioStream} and returns only when the playback is ended.
+ *
+ * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException}
+ * is thrown.
+ *
+ * If the passed {@link AudioStream} has an {@link AudioFormat} not supported by this instance,
+ * an {@link UnsupportedAudioFormatException} is thrown.
+ *
+ * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream.
+ *
+ * @param audioStream the audio stream to play or null to keep quiet
+ * @throws UnsupportedAudioFormatException If audioStream format is not supported
+ * @throws UnsupportedAudioStreamException If audioStream is not supported
+ */
+ protected abstract void processSynchronously(@Nullable AudioStream audioStream)
+ throws UnsupportedAudioFormatException, UnsupportedAudioStreamException;
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioStream.java
index c122aaf5fc6..560fcc21278 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioStream.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioStream.java
@@ -15,6 +15,7 @@
import java.io.InputStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
/**
* Wrapper for a source of audio data.
@@ -37,4 +38,14 @@ public abstract class AudioStream extends InputStream {
* @return The supported audio format
*/
public abstract AudioFormat getFormat();
+
+ /**
+ * Usefull for sinks playing the same stream multiple times,
+ * to avoid already done computation (like reencoding).
+ *
+ * @return A string uniquely identifying the stream.
+ */
+ public @Nullable String getId() {
+ return null;
+ }
}
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
new file mode 100644
index 00000000000..57dfcdc3c80
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/ClonableAudioStream.java
@@ -0,0 +1,35 @@
+/**
+ * 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 java.io.InputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is for an {@link AudioStream}, that can be cloned
+ *
+ * @author Gwendal Roulleau - Initial contribution, separation from {@link FixedLengthAudioStream}
+ */
+@NonNullByDefault
+public interface ClonableAudioStream {
+
+ /**
+ * Returns a new, fully independent stream instance, which can be read and closed without impacting the original
+ * instance.
+ *
+ * @return a new input stream that can be consumed by the caller
+ * @throws AudioException if stream cannot be created
+ */
+ 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 481d62353aa..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
@@ -18,10 +18,12 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.audio.utils.AudioStreamUtils;
import org.openhab.core.audio.utils.AudioWaveUtils;
+import org.openhab.core.common.Disposable;
/**
* This is an AudioStream from an audio file
@@ -31,7 +33,7 @@
* @author Christoph Weitkamp - Refactored use of filename extension
*/
@NonNullByDefault
-public class FileAudioStream extends FixedLengthAudioStream {
+public class FileAudioStream extends FixedLengthAudioStream implements Disposable {
public static final String WAV_EXTENSION = "wav";
public static final String MP3_EXTENSION = "mp3";
@@ -40,18 +42,26 @@ public class FileAudioStream extends FixedLengthAudioStream {
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));
}
public FileAudioStream(File file, AudioFormat format) throws AudioException {
+ this(file, format, false);
+ }
+
+ public FileAudioStream(File file, AudioFormat format, boolean isTemporaryFile) throws AudioException {
this.file = file;
this.inputStream = getInputStream(file);
this.audioFormat = format;
this.length = file.length();
+ this.isTemporaryFile = isTemporaryFile;
}
private static AudioFormat getAudioFormat(File file) throws AudioException {
@@ -79,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) {
@@ -94,7 +104,9 @@ public AudioFormat getFormat() {
@Override
public int read() throws IOException {
- return inputStream.read();
+ int read = inputStream.read();
+ alreadyRead++;
+ return read;
}
@Override
@@ -116,13 +128,32 @@ 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);
}
+
+ @Override
+ public void dispose() throws IOException {
+ if (isTemporaryFile) {
+ Files.delete(file.toPath());
+ }
+ }
}
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 6e47d35b054..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
@@ -12,32 +12,18 @@
*/
package org.openhab.core.audio;
-import java.io.InputStream;
-
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
- * This is an {@link AudioStream}, which can provide information about its absolute length and is able to provide
- * cloned streams.
+ * 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 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 AudioStream {
-
- /**
- * Provides the length of the stream in bytes.
- *
- * @return absolute length in bytes
- */
- public abstract long length();
+@Deprecated
+public abstract class FixedLengthAudioStream extends AudioStream implements SizeableAudioStream, ClonableAudioStream {
- /**
- * Returns a new, fully independent stream instance, which can be read and closed without impacting the original
- * instance.
- *
- * @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;
}
diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Car.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/SizeableAudioStream.java
similarity index 55%
rename from bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Car.java
rename to bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/SizeableAudioStream.java
index 2875a98b5dd..6074f2424c2 100644
--- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Car.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/SizeableAudioStream.java
@@ -10,18 +10,22 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.core.semantics.model.equipment;
+package org.openhab.core.audio;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.semantics.Equipment;
-import org.openhab.core.semantics.TagInfo;
/**
- * This class defines a Car.
+ * This is for an {@link AudioStream}, which size is known
*
- * @author Generated from generateTagClasses.groovy - Initial contribution
+ * @author Gwendal Roulleau - Initial contribution, separation from {@link FixedLengthAudioStream}
*/
@NonNullByDefault
-@TagInfo(id = "Equipment_Car", label = "Car", synonyms = "Cars", description = "")
-public interface Car extends Equipment {
+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/StreamServed.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/StreamServed.java
new file mode 100644
index 00000000000..35bf5946957
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/StreamServed.java
@@ -0,0 +1,30 @@
+/**
+ * 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 java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Streams served by the AudioHTTPServer.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public record StreamServed(String url, AudioStream audioStream, AtomicInteger currentlyServedStream, AtomicLong timeout,
+ boolean multiTimeStream, CompletableFuture<@Nullable Void> playEnd) {
+}
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 fef7e76f579..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 AudioStream {
+public class URLAudioStream extends AudioStream implements ClonableAudioStream {
private static final Pattern PLS_STREAM_PATTERN = Pattern.compile("^File[0-9]=(.+)$");
@@ -154,4 +154,9 @@ public void close() throws IOException {
public String toString() {
return url;
}
+
+ @Override
+ public InputStream getClonedStream() throws AudioException {
+ return new URLAudioStream(url);
+ }
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java
index 914bedf9115..eb79b9344f5 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java
@@ -38,8 +38,6 @@
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream;
import org.openhab.core.audio.URLAudioStream;
-import org.openhab.core.audio.UnsupportedAudioFormatException;
-import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.audio.utils.ToneSynthesizer;
import org.openhab.core.config.core.ConfigOptionProvider;
import org.openhab.core.config.core.ConfigurableService;
@@ -122,39 +120,11 @@ public void play(@Nullable AudioStream audioStream, @Nullable String sinkId) {
public void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume) {
AudioSink sink = getSink(sinkId);
if (sink != null) {
- PercentType oldVolume = null;
- // set notification sound volume
- if (volume != null) {
- try {
- // get current volume
- oldVolume = sink.getVolume();
- } catch (IOException e) {
- logger.debug("An exception occurred while getting the volume of sink '{}' : {}", sink.getId(),
- e.getMessage(), e);
- }
-
- try {
- sink.setVolume(volume);
- } catch (IOException e) {
- logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(),
- e.getMessage(), e);
- }
- }
- try {
- sink.process(audioStream);
- } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
- logger.warn("Error playing '{}': {}", audioStream, e.getMessage(), e);
- } finally {
- if (volume != null && oldVolume != null) {
- // restore volume only if it was set before
- try {
- sink.setVolume(oldVolume);
- } catch (IOException e) {
- logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(),
- e.getMessage(), e);
- }
- }
- }
+ Runnable restoreVolume = handleVolumeCommand(volume, sink);
+ sink.processAndComplete(audioStream).exceptionally((exception) -> {
+ logger.warn("Error playing '{}': {}", audioStream, exception.getMessage(), exception);
+ return null;
+ }).thenRun(restoreVolume);
} else {
logger.warn("Failed playing audio stream '{}' as no audio sink was found.", audioStream);
}
@@ -351,6 +321,53 @@ public Set getSinkIds(String pattern) {
return null;
}
+ @Override
+ public Runnable handleVolumeCommand(@Nullable PercentType volume, AudioSink sink) {
+ boolean volumeChanged = false;
+ PercentType oldVolume = null;
+
+ Runnable toRunWhenProcessFinished = () -> {
+ };
+
+ if (volume == null) {
+ return toRunWhenProcessFinished;
+ }
+
+ // set notification sound volume
+ try {
+ // get current volume
+ oldVolume = sink.getVolume();
+ } catch (IOException | UnsupportedOperationException e) {
+ logger.debug("An exception occurred while getting the volume of sink '{}' : {}", sink.getId(),
+ e.getMessage(), e);
+ }
+
+ if (!volume.equals(oldVolume) || oldVolume == null) {
+ try {
+ sink.setVolume(volume);
+ volumeChanged = true;
+ } catch (IOException | UnsupportedOperationException e) {
+ logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(),
+ e.getMessage(), e);
+ }
+ }
+
+ final PercentType oldVolumeFinal = oldVolume;
+ // restore volume only if it was set before
+ if (volumeChanged && oldVolumeFinal != null) {
+ toRunWhenProcessFinished = () -> {
+ try {
+ sink.setVolume(oldVolumeFinal);
+ } catch (IOException | UnsupportedOperationException e) {
+ logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(),
+ e.getMessage(), e);
+ }
+ };
+ }
+
+ return toRunWhenProcessFinished;
+ }
+
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addAudioSource(AudioSource audioSource) {
this.audioSources.put(audioSource.getId(), audioSource);
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 8a704810bbd..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
@@ -12,16 +12,24 @@
*/
package org.openhab.core.audio.internal;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -37,9 +45,17 @@
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.ByteArrayAudioStream;
+import org.openhab.core.audio.ClonableAudioStream;
+import org.openhab.core.audio.FileAudioStream;
+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;
+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.http.whiteboard.propertytypes.HttpWhiteboardServletName;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern;
import org.slf4j.Logger;
@@ -60,23 +76,34 @@ public class AudioServlet extends HttpServlet implements AudioHTTPServer {
private static final List WAV_MIME_TYPES = List.of("audio/wav", "audio/x-wav", "audio/vnd.wave");
+ // A 1MB in memory buffer will help playing multiple times an AudioStream, if the sink cannot do otherwise
+ private static final int ONETIME_STREAM_BUFFER_MAX_SIZE = 1048576;
+ // 5MB max for a file buffer
+ private static final int ONETIME_STREAM_FILE_MAX_SIZE = 5242880;
+
static final String SERVLET_PATH = "/audio";
private final Logger logger = LoggerFactory.getLogger(AudioServlet.class);
- private final Map oneTimeStreams = new ConcurrentHashMap<>();
- private final Map multiTimeStreams = new ConcurrentHashMap<>();
+ private final Map servedStreams = new ConcurrentHashMap<>();
+
+ private final ScheduledExecutorService threadPool = ThreadPoolManager
+ .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
+ @Nullable
+ ScheduledFuture> periodicCleaner;
- private final Map streamTimeouts = new ConcurrentHashMap<>();
+ private AudioSinkUtils audioSinkUtils;
+
+ @Activate
+ public AudioServlet(@Reference AudioSinkUtils audioSinkUtils) {
+ super();
+ this.audioSinkUtils = audioSinkUtils;
+ }
@Deactivate
protected synchronized void deactivate() {
- multiTimeStreams.values().forEach(this::tryClose);
- multiTimeStreams.clear();
- streamTimeouts.clear();
-
- oneTimeStreams.values().forEach(this::tryClose);
- oneTimeStreams.clear();
+ servedStreams.values().stream().map(streamServed -> streamServed.audioStream()).forEach(this::tryClose);
+ servedStreams.clear();
}
private void tryClose(@Nullable AudioStream stream) {
@@ -88,29 +115,17 @@ private void tryClose(@Nullable AudioStream stream) {
}
}
- private @Nullable InputStream prepareInputStream(final String streamId, final HttpServletResponse resp,
+ private InputStream prepareInputStream(final StreamServed streamServed, final HttpServletResponse resp,
List acceptedMimeTypes) throws AudioException {
- final AudioStream stream;
- final boolean multiAccess;
- if (oneTimeStreams.containsKey(streamId)) {
- stream = oneTimeStreams.remove(streamId);
- multiAccess = false;
- } else if (multiTimeStreams.containsKey(streamId)) {
- stream = multiTimeStreams.get(streamId);
- multiAccess = true;
- } else {
- return null;
- }
-
- logger.debug("Stream to serve is {}", streamId);
+ logger.debug("Stream to serve is {}", streamServed.url());
// try to set the content-type, if possible
final String mimeType;
- if (AudioFormat.CODEC_MP3.equals(stream.getFormat().getCodec())) {
+ if (AudioFormat.CODEC_MP3.equals(streamServed.audioStream().getFormat().getCodec())) {
mimeType = "audio/mpeg";
- } else if (AudioFormat.CONTAINER_WAVE.equals(stream.getFormat().getContainer())) {
+ } else if (AudioFormat.CONTAINER_WAVE.equals(streamServed.audioStream().getFormat().getContainer())) {
mimeType = WAV_MIME_TYPES.stream().filter(acceptedMimeTypes::contains).findFirst().orElse("audio/wav");
- } else if (AudioFormat.CONTAINER_OGG.equals(stream.getFormat().getContainer())) {
+ } else if (AudioFormat.CONTAINER_OGG.equals(streamServed.audioStream().getFormat().getContainer())) {
mimeType = "audio/ogg";
} else {
mimeType = null;
@@ -120,16 +135,17 @@ private void tryClose(@Nullable AudioStream stream) {
}
// try to set the content-length, if possible
- if (stream instanceof FixedLengthAudioStream) {
- final long size = ((FixedLengthAudioStream) stream).length();
+ if (streamServed.audioStream() instanceof SizeableAudioStream sizeableServedStream) {
+ final long size = sizeableServedStream.length();
resp.setContentLength((int) size);
}
- if (multiAccess) {
+ if (streamServed.multiTimeStream()
+ && streamServed.audioStream() instanceof ClonableAudioStream clonableAudioStream) {
// we need to care about concurrent access and have a separate stream for each thread
- return ((FixedLengthAudioStream) stream).getClonedStream();
+ return clonableAudioStream.getClonedStream();
} else {
- return stream;
+ return streamServed.audioStream();
}
}
@@ -146,8 +162,6 @@ private String substringBefore(String str, String separator) {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- removeTimedOutStreams();
-
String requestURI = req.getRequestURI();
if (requestURI == null) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "requestURI is null");
@@ -159,55 +173,154 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
List acceptedMimeTypes = Stream.of(Objects.requireNonNullElse(req.getHeader("Accept"), "").split(","))
.map(String::trim).collect(Collectors.toList());
- try (final InputStream stream = prepareInputStream(streamId, resp, acceptedMimeTypes)) {
- if (stream == null) {
- logger.debug("Received request for invalid stream id at {}", requestURI);
- resp.sendError(HttpServletResponse.SC_NOT_FOUND);
- } else {
- stream.transferTo(resp.getOutputStream());
+ StreamServed servedStream = servedStreams.get(streamId);
+ if (servedStream == null) {
+ logger.debug("Received request for invalid stream id at {}", requestURI);
+ resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // we count the number of active process using the input stream
+ AtomicInteger currentlyServedStream = servedStream.currentlyServedStream();
+ if (currentlyServedStream.incrementAndGet() == 1 || servedStream.multiTimeStream()) {
+ try (final InputStream stream = prepareInputStream(servedStream, resp, acceptedMimeTypes)) {
+ Long endOfPlayTimestamp = audioSinkUtils.transferAndAnalyzeLength(stream, resp.getOutputStream(),
+ servedStream.audioStream().getFormat());
+ // update timeout with the sound duration :
+ if (endOfPlayTimestamp != null) {
+ servedStream.timeout().set(Math.max(servedStream.timeout().get(), endOfPlayTimestamp));
+ }
resp.flushBuffer();
+ } catch (final AudioException ex) {
+ resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
+ } finally {
+ currentlyServedStream.decrementAndGet();
}
- } catch (final AudioException ex) {
- resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
+ } else {
+ logger.debug("Received request for already consumed stream id at {}", requestURI);
+ resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // we can immediately dispose and remove, if it is a one time stream
+ if (!servedStream.multiTimeStream()) {
+ servedStreams.remove(streamId);
+ servedStream.playEnd().complete(null);
+ logger.debug("Removed timed out stream {}", streamId);
}
}
private synchronized void removeTimedOutStreams() {
// Build list of expired streams.
long now = System.nanoTime();
- final List toRemove = streamTimeouts.entrySet().stream().filter(e -> e.getValue() < now)
+ final List toRemove = servedStreams.entrySet().stream()
+ .filter(e -> e.getValue().timeout().get() < now && e.getValue().currentlyServedStream().get() <= 0)
.map(Entry::getKey).collect(Collectors.toList());
toRemove.forEach(streamId -> {
- // the stream has expired, we need to remove it!
- final FixedLengthAudioStream stream = multiTimeStreams.remove(streamId);
- streamTimeouts.remove(streamId);
- tryClose(stream);
- logger.debug("Removed timed out stream {}", streamId);
+ // the stream has expired and no one is using it, we need to remove it!
+ StreamServed streamServed = servedStreams.remove(streamId);
+ if (streamServed != null) {
+ tryClose(streamServed.audioStream());
+ // we can notify the caller of the stream consumption
+ streamServed.playEnd().complete(null);
+ logger.debug("Removed timed out stream {}", streamId);
+ }
});
+
+ // Because the callback should be executed as soon as possible,
+ // we cannot wait for the next doGet to perform a clean. So we have to schedule a periodic cleaner.
+ ScheduledFuture> periodicCleanerLocal = periodicCleaner;
+ if (!servedStreams.isEmpty()) {
+ if (periodicCleanerLocal == null || periodicCleanerLocal.isDone()) {
+ // reschedule a clean
+ periodicCleaner = threadPool.scheduleWithFixedDelay(this::removeTimedOutStreams, 5, 5,
+ TimeUnit.SECONDS);
+ }
+ } else if (periodicCleanerLocal != null) { // no more stream to serve, shut the periodic cleaning thread:
+ periodicCleanerLocal.cancel(true);
+ periodicCleaner = null;
+ }
}
@Override
public String serve(AudioStream stream) {
- String streamId = UUID.randomUUID().toString();
- oneTimeStreams.put(streamId, stream);
- return getRelativeURL(streamId);
+ try {
+ // In case the stream is never played, we cannot wait indefinitely before executing the callback.
+ // so we set a timeout (even if this is a one time stream).
+ return serve(stream, 10, false).url();
+ } catch (IOException e) {
+ logger.warn("Cannot precache the audio stream to serve it", e);
+ return getRelativeURL("error");
+ }
+ }
+
+ @Override
+ public String serve(AudioStream stream, int seconds) {
+ try {
+ return serve(stream, seconds, true).url();
+ } catch (IOException e) {
+ logger.warn("Cannot precache the audio stream to serve it", e);
+ return getRelativeURL("error");
+ }
}
@Override
- public String serve(FixedLengthAudioStream stream, int seconds) {
+ public StreamServed serve(AudioStream originalStream, int seconds, boolean multiTimeStream) throws IOException {
String streamId = UUID.randomUUID().toString();
- multiTimeStreams.put(streamId, stream);
- streamTimeouts.put(streamId, System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds));
- return getRelativeURL(streamId);
+ AudioStream audioStream = originalStream;
+ if (!(originalStream instanceof ClonableAudioStream) && multiTimeStream) {
+ // we we can try to make a Cloneable stream as it is needed
+ audioStream = createClonableInputStream(originalStream, streamId);
+ }
+ long timeOut = System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds);
+ CompletableFuture<@Nullable Void> playEnd = new CompletableFuture<@Nullable Void>();
+ StreamServed streamToServe = new StreamServed(getRelativeURL(streamId), audioStream, new AtomicInteger(),
+ new AtomicLong(timeOut), multiTimeStream, playEnd);
+ servedStreams.put(streamId, streamToServe);
+
+ // try to clean, or a least launch the periodic cleanse:
+ removeTimedOutStreams();
+
+ return streamToServe;
}
- Map getMultiTimeStreams() {
- return Collections.unmodifiableMap(multiTimeStreams);
+ private AudioStream createClonableInputStream(AudioStream stream, String streamId) throws IOException {
+ byte[] dataBytes = stream.readNBytes(ONETIME_STREAM_BUFFER_MAX_SIZE + 1);
+ 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());
+ } else {
+ // in memory max size exceeded, sound is too long, we will use a file
+ File tempFile = File.createTempFile(streamId, ".snd");
+ tempFile.deleteOnExit();
+ try (OutputStream outputStream = new FileOutputStream(tempFile)) {
+ // copy already read data to file :
+ outputStream.write(dataBytes);
+ // copy the remaining stream data to a file.
+ byte[] buf = new byte[8192];
+ int length;
+ // but with a limit
+ int fileSize = ONETIME_STREAM_BUFFER_MAX_SIZE + 1;
+ while ((length = stream.read(buf)) != -1 && fileSize < ONETIME_STREAM_FILE_MAX_SIZE) {
+ int lengthToWrite = Math.min(length, ONETIME_STREAM_FILE_MAX_SIZE - fileSize);
+ outputStream.write(buf, 0, lengthToWrite);
+ fileSize += lengthToWrite;
+ }
+ }
+ try {
+ clonableAudioStreamResult = new FileAudioStream(tempFile, stream.getFormat(), true);
+ } catch (AudioException e) { // this is in fact a FileNotFoundException and should not happen
+ throw new IOException("Cannot find the cache file we just created.", e);
+ }
+ }
+ tryClose(stream);
+ return clonableAudioStreamResult;
}
- Map getOneTimeStreams() {
- return Collections.unmodifiableMap(oneTimeStreams);
+ Map getServedStreams() {
+ return Collections.unmodifiableMap(servedStreams);
}
private String getRelativeURL(String streamId) {
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java
index 606b20e0195..41208b514f6 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java
@@ -13,7 +13,6 @@
package org.openhab.core.audio.internal.javasound;
import java.io.IOException;
-import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Scanner;
@@ -32,6 +31,7 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioSinkAsync;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
@@ -55,7 +55,7 @@
*/
@NonNullByDefault
@Component(service = AudioSink.class, immediate = true)
-public class JavaSoundAudioSink implements AudioSink {
+public class JavaSoundAudioSink extends AudioSinkAsync {
private static final Logger LOGGER = LoggerFactory.getLogger(JavaSoundAudioSink.class);
@@ -79,13 +79,14 @@ protected void activate(BundleContext context) {
}
@Override
- public synchronized void process(final @Nullable AudioStream audioStream)
+ public synchronized void processAsynchronously(final @Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
if (audioStream != null && !AudioFormat.CODEC_MP3.equals(audioStream.getFormat().getCodec())) {
AudioPlayer audioPlayer = new AudioPlayer(audioStream);
audioPlayer.start();
try {
audioPlayer.join();
+ playbackFinished(audioStream);
} catch (InterruptedException e) {
LOGGER.error("Playing audio has been interrupted.");
}
@@ -103,8 +104,7 @@ public synchronized void process(final @Nullable AudioStream audioStream)
} else {
try {
// we start a new continuous stream and store its handle
- streamPlayer = new Player(audioStream);
- playInThread(streamPlayer);
+ playInThread(audioStream, true);
} catch (JavaLayerException e) {
LOGGER.error("An exception occurred while playing url audio stream : '{}'", e.getMessage());
}
@@ -113,7 +113,7 @@ public synchronized void process(final @Nullable AudioStream audioStream)
} else {
// we are playing some normal file (no url stream)
try {
- playInThread(new Player(audioStream));
+ playInThread(audioStream, false);
} catch (JavaLayerException e) {
LOGGER.error("An exception occurred while playing audio : '{}'", e.getMessage());
}
@@ -121,17 +121,20 @@ public synchronized void process(final @Nullable AudioStream audioStream)
}
}
- private void playInThread(final @Nullable Player player) {
+ private void playInThread(final AudioStream audioStream, boolean store) throws JavaLayerException {
// run in new thread
+ Player streamPlayerFinal = new Player(audioStream);
+ if (store) { // we store its handle in case we want to interrupt it.
+ streamPlayer = streamPlayerFinal;
+ }
threadFactory.newThread(() -> {
- if (player != null) {
- try {
- player.play();
- } catch (Exception e) {
- LOGGER.error("An exception occurred while playing audio : '{}'", e.getMessage());
- } finally {
- player.close();
- }
+ try {
+ streamPlayerFinal.play();
+ } catch (Exception e) {
+ LOGGER.error("An exception occurred while playing audio : '{}'", e.getMessage());
+ } finally {
+ streamPlayerFinal.close();
+ playbackFinished(audioStream);
}
}).start();
}
@@ -174,7 +177,7 @@ public PercentType getVolume() throws IOException {
return true;
});
if (volumes[0] != null) {
- return new PercentType(new BigDecimal(volumes[0] * 100f));
+ return new PercentType(Math.round(volumes[0] * 100f));
} else {
LOGGER.warn("Cannot determine master volume level - assuming 100%");
return PercentType.HUNDRED;
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java
index 8227f10de77..36e492ee772 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java
@@ -21,8 +21,9 @@
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioSinkAsync;
import org.openhab.core.audio.AudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.StreamServed;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
@@ -44,13 +45,12 @@
*/
@NonNullByDefault
@Component(service = AudioSink.class, immediate = true)
-public class WebAudioAudioSink implements AudioSink {
+public class WebAudioAudioSink extends AudioSinkAsync {
private final Logger logger = LoggerFactory.getLogger(WebAudioAudioSink.class);
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(AudioStream.class);
private AudioHTTPServer audioHTTPServer;
private EventPublisher eventPublisher;
@@ -62,7 +62,7 @@ public WebAudioAudioSink(@Reference AudioHTTPServer audioHTTPServer, @Reference
}
@Override
- public void process(@Nullable AudioStream audioStream)
+ public void processAsynchronously(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
if (audioStream == null) {
// in case the audioStream is null, this should be interpreted as a request to end any currently playing
@@ -70,23 +70,26 @@ public void process(@Nullable AudioStream audioStream)
logger.debug("Web Audio sink does not support stopping the currently playing stream.");
return;
}
- try (AudioStream stream = audioStream) {
- logger.debug("Received audio stream of format {}", audioStream.getFormat());
- if (audioStream instanceof URLAudioStream) {
+ logger.debug("Received audio stream of format {}", audioStream.getFormat());
+ if (audioStream instanceof URLAudioStream urlAudioStream) {
+ try (AudioStream stream = urlAudioStream) {
+ // in this case only, we need to close the stream by ourself in a try with block,
+ // because nothing will consume it
// it is an external URL, so we can directly pass this on.
- URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
sendEvent(urlAudioStream.getURL());
- } else if (audioStream instanceof FixedLengthAudioStream) {
- // we need to serve it for a while and make it available to multiple clients, hence only
- // FixedLengthAudioStreams are supported.
- sendEvent(audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 10).toString());
- } else {
- throw new UnsupportedAudioStreamException(
- "Web audio sink can only handle FixedLengthAudioStreams and URLAudioStreams.",
- audioStream.getClass());
+ } catch (IOException e) {
+ logger.debug("Error while closing the audio stream: {}", e.getMessage(), e);
+ }
+ } else {
+ // we need to serve it for a while and make it available to multiple clients
+ try {
+ StreamServed servedStream = audioHTTPServer.serve(audioStream, 10, true);
+ // we will let the HTTP servlet run the delayed task when finished with the stream
+ servedStream.playEnd().thenRun(() -> this.playbackFinished(audioStream));
+ sendEvent(servedStream.url());
+ } catch (IOException e) {
+ logger.warn("Cannot precache the audio stream to serve it", e);
}
- } catch (IOException e) {
- logger.debug("Error while closing the audio stream: {}", e.getMessage(), e);
}
}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.java
new file mode 100644
index 00000000000..b5a96e10811
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.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.audio.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.audio.AudioFormat;
+
+/**
+ * Some utility methods for sink
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+@NonNullByDefault
+public interface AudioSinkUtils {
+
+ /**
+ * Transfers data from an input stream to an output stream and computes on the fly its duration
+ *
+ * @param in the input stream giving audio data ta play
+ * @param out the output stream receiving data to play
+ * @return the timestamp (from System.nanoTime) when the sound should be fully played. Returns null if computing
+ * time fails.
+ * @throws IOException if reading from the stream or writing to the stream failed
+ */
+ @Nullable
+ Long transferAndAnalyzeLength(InputStream in, OutputStream out, AudioFormat audioFormat) throws IOException;
+}
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
new file mode 100644
index 00000000000..069096baee8
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.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.audio.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import javazoom.jl.decoder.Bitstream;
+import javazoom.jl.decoder.BitstreamException;
+import javazoom.jl.decoder.Header;
+
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.audio.AudioFormat;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Some utility methods for sink
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component
+public class AudioSinkUtilsImpl implements AudioSinkUtils {
+
+ private final Logger logger = LoggerFactory.getLogger(AudioSinkUtilsImpl.class);
+
+ @Override
+ public @Nullable Long transferAndAnalyzeLength(InputStream in, OutputStream out, AudioFormat audioFormat)
+ throws IOException {
+ // take some data from the stream beginning
+ byte[] dataBytes = in.readNBytes(8192);
+
+ // beginning sound timestamp :
+ long startTime = System.nanoTime();
+ // copy already read data to the output stream :
+ out.write(dataBytes);
+ // transfer everything else
+ Long dataTransferedLength = dataBytes.length + in.transferTo(out);
+
+ if (dataTransferedLength > 0) {
+ if (AudioFormat.CODEC_PCM_SIGNED.equals(audioFormat.getCodec())) {
+ try (AudioInputStream audioInputStream = AudioSystem
+ .getAudioInputStream(new ByteArrayInputStream(dataBytes))) {
+ int frameSize = audioInputStream.getFormat().getFrameSize();
+ float frameRate = audioInputStream.getFormat().getFrameRate();
+ long computedDuration = Float.valueOf((dataTransferedLength / (frameSize * frameRate)) * 1000000000)
+ .longValue();
+ return startTime + computedDuration;
+ } catch (IOException | UnsupportedAudioFileException 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 accurate, no VBR support, but better than nothing
+ Bitstream bitstream = new Bitstream(new ByteArrayInputStream(dataBytes));
+ try {
+ Header h = bitstream.readFrame();
+ if (h != null) {
+ long computedDuration = Float.valueOf(h.total_ms(dataTransferedLength.intValue()) * 1000000)
+ .longValue();
+ return startTime + computedDuration;
+ }
+ } catch (BitstreamException ex) {
+ logger.debug("Cannot compute the duration of input stream", ex);
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/ToneSynthesizer.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/ToneSynthesizer.java
index ac47ca828df..c6c7f613289 100644
--- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/ToneSynthesizer.java
+++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/ToneSynthesizer.java
@@ -82,7 +82,7 @@ public static List parseMelody(String melody) throws ParseException {
case 1:
var note = noteTextParts[0];
int octaves = (int) note.chars().filter(ch -> ch == '\'').count();
- note = note.replaceAll("'", "");
+ note = note.replace("'", "");
var noteObj = Note.fromString(note);
if (noteObj.isPresent()) {
melodySounds.add(noteTone(noteObj.get(), soundMillis, octaves));
diff --git a/bundles/org.openhab.core.audio/src/main/resources/OH-INF/i18n/audio_ro.properties b/bundles/org.openhab.core.audio/src/main/resources/OH-INF/i18n/audio_ro.properties
new file mode 100644
index 00000000000..c3a9819d97f
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/resources/OH-INF/i18n/audio_ro.properties
@@ -0,0 +1,6 @@
+system.config.audio.defaultSource.label = Sursa Implicită
+system.config.audio.defaultSource.description = Sursa audio implicită pentru utilizare dacă nu este specificată o altă sursă.
+system.config.audio.defaultSink.label = Sink implicit
+system.config.audio.defaultSink.description = Rezerva audio implicită pentru utilizare în cazul în care nu este specificat niciun altul.
+
+service.system.audio.label = Audio
diff --git a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java
index 50b387df286..c70545adc4e 100644
--- a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java
+++ b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java
@@ -33,7 +33,8 @@
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.ByteArrayAudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.utils.AudioSinkUtils;
+import org.openhab.core.audio.utils.AudioSinkUtilsImpl;
import org.openhab.core.test.TestPortUtil;
import org.openhab.core.test.TestServer;
import org.openhab.core.test.java.JavaTest;
@@ -60,12 +61,13 @@ public abstract class AbstractAudioServletTest extends JavaTest {
private @NonNullByDefault({}) HttpClient httpClient;
private @NonNullByDefault({}) CompletableFuture serverStarted;
- private @Mock @NonNullByDefault({}) HttpService httpServiceMock;
- private @Mock @NonNullByDefault({}) HttpContext httpContextMock;
+ public @Mock @NonNullByDefault({}) HttpService httpServiceMock;
+ public @Mock @NonNullByDefault({}) HttpContext httpContextMock;
+ public AudioSinkUtils audioSinkUtils = new AudioSinkUtilsImpl();
@BeforeEach
public void setupServerAndClient() {
- audioServlet = new AudioServlet();
+ audioServlet = new AudioServlet(audioSinkUtils);
ServletHolder servletHolder = new ServletHolder(audioServlet);
@@ -126,7 +128,7 @@ protected String serveStream(AudioStream stream, @Nullable Integer timeInterval)
String path;
if (timeInterval != null) {
- path = audioServlet.serve((FixedLengthAudioStream) stream, timeInterval);
+ path = audioServlet.serve(stream, timeInterval);
} else {
path = audioServlet.serve(stream);
}
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 13e4957b5a6..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
@@ -14,10 +14,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.*;
import java.io.File;
import java.util.concurrent.TimeUnit;
@@ -29,8 +27,9 @@
import org.junit.jupiter.api.Test;
import org.openhab.core.audio.AudioFormat;
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;
/**
@@ -128,7 +127,7 @@ public void onlyOneRequestToOneTimeStreamsCanBeMade() throws Exception {
}
@Test
- public void requestToMultitimeStreamCannotBeDoneAfterTheTimeoutOfTheStreamHasExipred() throws Exception {
+ public void requestToMultitimeStreamCannotBeDoneAfterTheTimeoutOfTheStreamHasExpired() throws Exception {
final int streamTimeout = 3;
AudioStream audioStream = getByteArrayAudioStream(testByteArray, AudioFormat.CONTAINER_NONE,
@@ -151,8 +150,8 @@ public void requestToMultitimeStreamCannotBeDoneAfterTheTimeoutOfTheStreamHasExi
assertThat("The response media type was not as expected", response.getMediaType(),
is(MEDIA_TYPE_AUDIO_MPEG));
- assertThat("The audio stream was not added to the multitime streams",
- audioServlet.getMultiTimeStreams().containsValue(audioStream), is(true));
+ assertThat("The audio stream was not added to the multitime streams", audioServlet.getServedStreams()
+ .values().stream().map(StreamServed::audioStream).toList().contains(audioStream), is(true));
}
waitForAssert(() -> {
@@ -161,33 +160,60 @@ public void requestToMultitimeStreamCannotBeDoneAfterTheTimeoutOfTheStreamHasExi
} catch (Exception e) {
throw new IllegalStateException(e);
}
- assertThat("The audio stream was not removed from multitime streams",
- audioServlet.getMultiTimeStreams().containsValue(audioStream), is(false));
+ assertThat("The audio stream was not removed from multitime streams", audioServlet.getServedStreams()
+ .values().stream().map(StreamServed::audioStream).toList().contains(audioStream), is(false));
});
response = getHttpRequest(url).send();
assertThat("The response status was not as expected", response.getStatus(), is(HttpStatus.NOT_FOUND_404));
}
+ @Test
+ public void oneTimeStreamIsRecreatedAsAClonable() throws Exception {
+ AudioStream audioStream = mock(AudioStream.class);
+ AudioFormat audioFormat = mock(AudioFormat.class);
+ when(audioStream.getFormat()).thenReturn(audioFormat);
+ when(audioFormat.getCodec()).thenReturn(AudioFormat.CODEC_MP3);
+ when(audioStream.readNBytes(anyInt())).thenReturn(testByteArray);
+
+ String url = serveStream(audioStream, 10);
+ String uuid = url.substring(url.lastIndexOf("/") + 1);
+ StreamServed servedStream = audioServlet.getServedStreams().get(uuid);
+
+ // does not contain directly the stream because it is now a new stream wrapper
+ assertThat(servedStream.audioStream(), not(audioStream));
+ // it is now a ByteArrayAudioStream wrapper :
+ assertThat(servedStream.audioStream(), instanceOf(ByteArrayAudioStream.class));
+
+ ContentResponse response = getHttpRequest(url).send();
+ assertThat("The response content was not as expected", response.getContent(), is(testByteArray));
+
+ verify(audioStream).close();
+ }
+
@Test
public void oneTimeStreamIsClosedAndRemovedAfterServed() throws Exception {
AudioStream audioStream = mock(AudioStream.class);
AudioFormat audioFormat = mock(AudioFormat.class);
when(audioStream.getFormat()).thenReturn(audioFormat);
when(audioFormat.getCodec()).thenReturn(AudioFormat.CODEC_MP3);
+ when(audioStream.readNBytes(anyInt())).thenReturn(new byte[] { 1, 2, 3 });
String url = serveStream(audioStream);
+ assertThat(audioServlet.getServedStreams().values().stream().map(StreamServed::audioStream).toList(),
+ contains(audioStream));
getHttpRequest(url).send();
verify(audioStream).close();
- assertThat(audioServlet.getOneTimeStreams().values(), not(contains(audioStream)));
+ assertThat(audioServlet.getServedStreams().values().stream().map(StreamServed::audioStream).toList(),
+ not(contains(audioStream)));
}
@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);
@@ -195,9 +221,13 @@ public void multiTimeStreamIsClosedAfterExpired() throws Exception {
cloneCounter.getAndIncrement();
return clonedStream;
});
+ when(audioStream.readNBytes(anyInt())).thenReturn(new byte[] { 1, 2, 3 });
+ when(clonedStream.readNBytes(anyInt())).thenReturn(new byte[] { 1, 2, 3 });
when(audioFormat.getCodec()).thenReturn(AudioFormat.CODEC_MP3);
String url = serveStream(audioStream, 2);
+ assertThat(audioServlet.getServedStreams().values().stream().map(StreamServed::audioStream).toList(),
+ contains(audioStream));
waitForAssert(() -> {
try {
@@ -210,7 +240,8 @@ public void multiTimeStreamIsClosedAfterExpired() throws Exception {
});
verify(audioStream).close();
- assertThat(audioServlet.getMultiTimeStreams().values(), not(contains(audioStream)));
+ assertThat(audioServlet.getServedStreams().values().stream().map(StreamServed::audioStream).toList(),
+ not(contains(audioStream)));
verify(clonedStream, times(cloneCounter.get())).close();
}
@@ -218,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.audio/src/test/java/org/openhab/core/audio/internal/utils/BundledSoundFileHandler.java b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/utils/BundledSoundFileHandler.java
index 7adfb3a0ffe..05de1268042 100644
--- a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/utils/BundledSoundFileHandler.java
+++ b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/utils/BundledSoundFileHandler.java
@@ -21,6 +21,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
+import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.OpenHAB;
@@ -71,12 +72,10 @@ public BundledSoundFileHandler() throws IOException {
public void close() {
System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, OpenHAB.DEFAULT_CONFIG_FOLDER);
- if (tmpdir != null) {
- try {
- Files.walk(tmpdir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
- } catch (IOException ex) {
- logger.error("Exception while deleting files", ex);
- }
+ try (Stream files = Files.walk(tmpdir)) {
+ files.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+ } catch (IOException ex) {
+ logger.error("Exception while deleting files", ex);
}
}
diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java
index 42735ddce03..2249031139b 100644
--- a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java
+++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java
@@ -81,10 +81,10 @@ public Authentication authenticate(final Credentials credentials) throws Authent
public void handle(@NonNullByDefault({}) Callback[] callbacks)
throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
- if (callback instanceof PasswordCallback) {
- ((PasswordCallback) callback).setPassword(password);
- } else if (callback instanceof NameCallback) {
- ((NameCallback) callback).setName(name);
+ if (callback instanceof PasswordCallback passwordCallback) {
+ passwordCallback.setPassword(password);
+ } else if (callback instanceof NameCallback nameCallback) {
+ nameCallback.setName(name);
} else {
throw new UnsupportedCallbackException(callback);
}
@@ -133,8 +133,8 @@ protected void modified(Map properties) {
Object propertyValue = properties.get("realmName");
if (propertyValue != null) {
- if (propertyValue instanceof String) {
- realmName = (String) propertyValue;
+ if (propertyValue instanceof String string) {
+ realmName = string;
} else {
realmName = propertyValue.toString();
}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthClientServiceImpl.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthClientServiceImpl.java
index fceb10b0856..1d8ce4cb125 100644
--- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthClientServiceImpl.java
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthClientServiceImpl.java
@@ -35,7 +35,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.gson.JsonDeserializer;
+import com.google.gson.GsonBuilder;
/**
* Implementation of OAuthClientService.
@@ -68,16 +68,19 @@ public class OAuthClientServiceImpl implements OAuthClientService {
private final String handle;
private final int tokenExpiresInSeconds;
private final HttpClientFactory httpClientFactory;
+ private final @Nullable GsonBuilder gsonBuilder;
private final List accessTokenRefreshListeners = new ArrayList<>();
private PersistedParams persistedParams = new PersistedParams();
private volatile boolean closed = false;
- private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpClientFactory httpClientFactory) {
+ private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpClientFactory httpClientFactory,
+ @Nullable GsonBuilder gsonBuilder) {
this.handle = handle;
this.tokenExpiresInSeconds = tokenExpiresInSeconds;
this.httpClientFactory = httpClientFactory;
+ this.gsonBuilder = gsonBuilder;
}
/**
@@ -103,7 +106,7 @@ private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpCli
return null;
}
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, tokenExpiresInSeconds,
- httpClientFactory);
+ httpClientFactory, null);
clientService.storeHandler = storeHandler;
clientService.persistedParams = persistedParamsFromStore;
@@ -118,13 +121,13 @@ private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpCli
* {@link org.openhab.core.auth.client.oauth2.OAuthFactory#createOAuthClientService}*
* @param storeHandler Storage handler
* @param httpClientFactory Http client factory
- * @param persistedParams These parameters are static with respect to the oauth provider and thus can be persisted.
+ * @param persistedParams These parameters are static with respect to the OAuth provider and thus can be persisted.
* @return OAuthClientServiceImpl an instance
*/
static OAuthClientServiceImpl createInstance(String handle, OAuthStoreHandler storeHandler,
HttpClientFactory httpClientFactory, PersistedParams params) {
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, params.tokenExpiresInSeconds,
- httpClientFactory);
+ httpClientFactory, null);
clientService.storeHandler = storeHandler;
clientService.persistedParams = params;
@@ -153,7 +156,9 @@ public String getAuthorizationUrl(@Nullable String redirectURI, @Nullable String
throw new OAuthException("Missing client ID");
}
- OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName);
+ GsonBuilder gsonBuilder = this.gsonBuilder;
+ OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
+ : new OAuthConnector(httpClientFactory, gsonBuilder);
return connector.getAuthorizationUrl(authorizationUrl, clientId, redirectURI, persistedParams.state,
scopeToUse);
}
@@ -207,7 +212,9 @@ public AccessTokenResponse getAccessTokenResponseByAuthorizationCode(String auth
throw new OAuthException("Missing client ID");
}
- OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName);
+ GsonBuilder gsonBuilder = this.gsonBuilder;
+ OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
+ : new OAuthConnector(httpClientFactory, gsonBuilder);
AccessTokenResponse accessTokenResponse = connector.grantTypeAuthorizationCode(tokenUrl, authorizationCode,
clientId, persistedParams.clientSecret, redirectURI,
Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -239,7 +246,9 @@ public AccessTokenResponse getAccessTokenByResourceOwnerPasswordCredentials(Stri
throw new OAuthException("Missing token url");
}
- OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName);
+ GsonBuilder gsonBuilder = this.gsonBuilder;
+ OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
+ : new OAuthConnector(httpClientFactory, gsonBuilder);
AccessTokenResponse accessTokenResponse = connector.grantTypePassword(tokenUrl, username, password,
persistedParams.clientId, persistedParams.clientSecret, scope,
Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -264,7 +273,9 @@ public AccessTokenResponse getAccessTokenByClientCredentials(@Nullable String sc
throw new OAuthException("Missing client ID");
}
- OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName);
+ GsonBuilder gsonBuilder = this.gsonBuilder;
+ OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
+ : new OAuthConnector(httpClientFactory, gsonBuilder);
// depending on usage, cannot guarantee every parameter is not null at the beginning
AccessTokenResponse accessTokenResponse = connector.grantTypeClientCredentials(tokenUrl, clientId,
persistedParams.clientSecret, scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -298,7 +309,9 @@ public AccessTokenResponse refreshToken() throws OAuthException, IOException, OA
throw new OAuthException("tokenUrl is required but null");
}
- OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName);
+ GsonBuilder gsonBuilder = this.gsonBuilder;
+ OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
+ : new OAuthConnector(httpClientFactory, gsonBuilder);
AccessTokenResponse accessTokenResponse = connector.grantTypeRefreshToken(tokenUrl,
lastAccessToken.getRefreshToken(), persistedParams.clientId, persistedParams.clientSecret,
persistedParams.scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -400,10 +413,9 @@ private String createNewState() {
}
@Override
- public > OAuthClientService withDeserializer(Class deserializerClass) {
+ public OAuthClientService withGsonBuilder(GsonBuilder gsonBuilder) {
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, persistedParams.tokenExpiresInSeconds,
- httpClientFactory);
- persistedParams.deserializerClassName = deserializerClass.getName();
+ httpClientFactory, gsonBuilder);
clientService.persistedParams = persistedParams;
clientService.storeHandler = storeHandler;
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java
index 9bda7aeeb06..00d5a28d879 100644
--- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java
@@ -15,7 +15,6 @@
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@@ -67,29 +66,20 @@ public class OAuthConnector {
private final Logger logger = LoggerFactory.getLogger(OAuthConnector.class);
private final Gson gson;
- public OAuthConnector(HttpClientFactory httpClientFactory, @Nullable String deserializerClassName) {
+ public OAuthConnector(HttpClientFactory httpClientFactory) {
+ this(httpClientFactory, new GsonBuilder());
+ }
+
+ public OAuthConnector(HttpClientFactory httpClientFactory, GsonBuilder gsonBuilder) {
this.httpClientFactory = httpClientFactory;
- GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ gson = gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant.class, (JsonDeserializer) (json, typeOfT, context) -> {
try {
return Instant.parse(json.getAsString());
} catch (DateTimeParseException e) {
return LocalDateTime.parse(json.getAsString()).atZone(ZoneId.systemDefault()).toInstant();
}
- });
-
- if (deserializerClassName != null) {
- try {
- Class> deserializerClass = Class.forName(deserializerClassName);
- gsonBuilder = gsonBuilder.registerTypeAdapter(AccessTokenResponse.class,
- deserializerClass.getConstructor().newInstance());
- } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
- | InvocationTargetException | NoSuchMethodException | SecurityException
- | ClassNotFoundException e) {
- logger.error("Unable to construct custom deserializer '{}'", deserializerClassName, e);
- }
- }
- gson = gsonBuilder.create();
+ }).create();
}
/**
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java
index 565cf21702c..9fb9a9eae25 100644
--- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java
@@ -127,9 +127,7 @@ public void deactivate() {
// token does not exist
return null;
}
-
- AccessTokenResponse decryptedAccessToken = decryptToken(accessTokenResponseFromStore);
- return decryptedAccessToken;
+ return decryptToken(accessTokenResponseFromStore);
}
@Override
@@ -167,8 +165,7 @@ public void savePersistedParams(String handle, @Nullable PersistedParams persist
@Override
public @Nullable PersistedParams loadPersistedParams(String handle) {
- PersistedParams persistedParams = (PersistedParams) storageFacade.get(handle, SERVICE_CONFIGURATION);
- return persistedParams;
+ return (PersistedParams) storageFacade.get(handle, SERVICE_CONFIGURATION);
}
private AccessTokenResponse encryptToken(AccessTokenResponse accessTokenResponse) throws GeneralSecurityException {
@@ -185,7 +182,7 @@ private AccessTokenResponse encryptToken(AccessTokenResponse accessTokenResponse
private AccessTokenResponse decryptToken(AccessTokenResponse accessTokenResponse) throws GeneralSecurityException {
AccessTokenResponse decryptedToken = (AccessTokenResponse) accessTokenResponse.clone();
- if (!storageCipher.isPresent()) {
+ if (storageCipher.isEmpty()) {
return decryptedToken; // do nothing if no cipher
}
logger.debug("Decrypting token: {}", accessTokenResponse);
@@ -195,7 +192,7 @@ private AccessTokenResponse decryptToken(AccessTokenResponse accessTokenResponse
}
private @Nullable String encrypt(String token) throws GeneralSecurityException {
- if (!storageCipher.isPresent()) {
+ if (storageCipher.isEmpty()) {
return token; // do nothing if no cipher
} else {
StorageCipher cipher = storageCipher.get();
@@ -288,8 +285,7 @@ public Set getAllHandlesFromIndex() {
// update last used when it is an access token
if (ACCESS_TOKEN_RESPONSE.equals(recordType)) {
try {
- AccessTokenResponse accessTokenResponse = gson.fromJson(value, AccessTokenResponse.class);
- return accessTokenResponse;
+ return gson.fromJson(value, AccessTokenResponse.class);
} catch (Exception e) {
logger.error(
"Unable to deserialize json, discarding AccessTokenResponse. "
@@ -299,16 +295,14 @@ public Set getAllHandlesFromIndex() {
}
} else if (SERVICE_CONFIGURATION.equals(recordType)) {
try {
- PersistedParams params = gson.fromJson(value, PersistedParams.class);
- return params;
+ return gson.fromJson(value, PersistedParams.class);
} catch (Exception e) {
logger.error("Unable to deserialize json, discarding PersistedParams. json:\n{}", value, e);
return null;
}
} else if (LAST_USED.equals(recordType)) {
try {
- Instant lastUsedDate = gson.fromJson(value, Instant.class);
- return lastUsedDate;
+ return gson.fromJson(value, Instant.class);
} catch (Exception e) {
logger.info("Unable to deserialize json, reset LAST_USED to now. json:\n{}", value);
return Instant.now();
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/PersistedParams.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/PersistedParams.java
index e1705d9e801..861c3aed9cf 100644
--- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/PersistedParams.java
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/PersistedParams.java
@@ -33,8 +33,6 @@ class PersistedParams {
String state;
String redirectUri;
int tokenExpiresInSeconds = 60;
- @Nullable
- String deserializerClassName;
/**
* Default constructor needed for json serialization.
@@ -59,7 +57,6 @@ public PersistedParams() {
* of the access tokens. This allows the access token to expire earlier than the
* official stated expiry time; thus prevents the caller obtaining a valid token at the time of invoke,
* only to find the token immediately expired.
- * @param deserializerClass (optional) if a specific deserializer is needed
*/
public PersistedParams(String handle, String tokenUrl, String authorizationUrl, String clientId,
String clientSecret, String scope, Boolean supportsBasicAuth, int tokenExpiresInSeconds,
@@ -72,7 +69,6 @@ public PersistedParams(String handle, String tokenUrl, String authorizationUrl,
this.scope = scope;
this.supportsBasicAuth = supportsBasicAuth;
this.tokenExpiresInSeconds = tokenExpiresInSeconds;
- this.deserializerClassName = deserializerClassName;
}
@Override
@@ -89,7 +85,6 @@ public int hashCode() {
result = prime * result + ((supportsBasicAuth == null) ? 0 : supportsBasicAuth.hashCode());
result = prime * result + tokenExpiresInSeconds;
result = prime * result + ((tokenUrl == null) ? 0 : tokenUrl.hashCode());
- result = prime * result + ((deserializerClassName != null) ? deserializerClassName.hashCode() : 0);
return result;
}
@@ -168,13 +163,6 @@ public boolean equals(@Nullable Object obj) {
} else if (!tokenUrl.equals(other.tokenUrl)) {
return false;
}
- if (deserializerClassName == null) {
- if (other.deserializerClassName != null) {
- return false;
- }
- } else if (deserializerClassName != null && !deserializerClassName.equals(other.deserializerClassName)) {
- return false;
- }
return true;
}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java
index 8192857cb2d..060bb8b7b2a 100644
--- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java
@@ -89,7 +89,7 @@ public String getUniqueCipherId() {
}
// Generate IV
- byte iv[] = new byte[IV_BYTE_SIZE];
+ byte[] iv = new byte[IV_BYTE_SIZE];
random.nextBytes(iv);
Cipher cipherEnc = Cipher.getInstance(ENCRYPTION_ALGO_MODE_WITH_PADDING);
cipherEnc.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv));
@@ -100,9 +100,8 @@ public String getUniqueCipherId() {
System.arraycopy(iv, 0, encryptedBytesWithIV, 0, IV_BYTE_SIZE);
// append encrypted text to tail
System.arraycopy(encryptedBytes, 0, encryptedBytesWithIV, IV_BYTE_SIZE, encryptedBytes.length);
- String encryptedBase64String = Base64.getEncoder().encodeToString(encryptedBytesWithIV);
- return encryptedBase64String;
+ return Base64.getEncoder().encodeToString(encryptedBytesWithIV);
}
@Override
@@ -128,8 +127,7 @@ public String getUniqueCipherId() {
private static SecretKey generateEncryptionKey() throws NoSuchAlgorithmException {
KeyGenerator keygen = KeyGenerator.getInstance(ENCRYPTION_ALGO);
keygen.init(ENCRYPTION_KEY_SIZE_BITS);
- SecretKey secretKey = keygen.generateKey();
- return secretKey;
+ return keygen.generateKey();
}
private SecretKey getOrGenerateEncryptionKey() throws NoSuchAlgorithmException, IOException {
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java
index df13630417c..7a52a6602c6 100644
--- a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java
@@ -120,19 +120,15 @@ private List getConfigSynthesizeDesc(@Nullable Local
}
private ConfigDescriptionParameter getAudioSinkConfigDescParam(@Nullable Locale locale) {
- ConfigDescriptionParameter param2 = ConfigDescriptionParameterBuilder
- .create(SayActionHandler.PARAM_SINK, Type.TEXT).withRequired(false).withLabel("Sink")
- .withDescription("the audio sink id").withOptions(getSinkOptions(locale)).withLimitToOptions(true)
- .build();
- return param2;
+ return ConfigDescriptionParameterBuilder.create(SayActionHandler.PARAM_SINK, Type.TEXT).withRequired(false)
+ .withLabel("Sink").withDescription("the audio sink id").withOptions(getSinkOptions(locale))
+ .withLimitToOptions(true).build();
}
private ConfigDescriptionParameter getVolumeConfigDescParam(@Nullable Locale locale) {
- ConfigDescriptionParameter param3 = ConfigDescriptionParameterBuilder
- .create(SayActionHandler.PARAM_VOLUME, Type.INTEGER).withLabel("Volume")
+ return ConfigDescriptionParameterBuilder.create(SayActionHandler.PARAM_VOLUME, Type.INTEGER).withLabel("Volume")
.withDescription("the volume to use").withMinimum(BigDecimal.ZERO).withMaximum(BigDecimal.valueOf(100))
.withStepSize(BigDecimal.ONE).build();
- return param3;
}
/**
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java
index 065735f159b..954abcfb6ec 100644
--- a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java
@@ -62,14 +62,14 @@ public Collection getTypes() {
@Override
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
- if (module instanceof Action) {
+ if (module instanceof Action action) {
switch (module.getTypeUID()) {
case SayActionHandler.TYPE_ID:
- return new SayActionHandler((Action) module, voiceManager);
+ return new SayActionHandler(action, voiceManager);
case PlayActionHandler.TYPE_ID:
- return new PlayActionHandler((Action) module, audioManager);
+ return new PlayActionHandler(action, audioManager);
case SynthesizeActionHandler.TYPE_ID:
- return new SynthesizeActionHandler((Action) module, audioManager);
+ return new SynthesizeActionHandler(action, audioManager);
default:
break;
}
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java
index fadceea8b6f..027404b6d32 100644
--- a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java
@@ -57,7 +57,7 @@ public PlayActionHandler(Action module, AudioManager audioManager) {
this.sink = sinkParam != null ? sinkParam.toString() : null;
Object volumeParam = module.getConfiguration().get(PARAM_VOLUME);
- this.volume = volumeParam instanceof BigDecimal ? new PercentType((BigDecimal) volumeParam) : null;
+ this.volume = volumeParam instanceof BigDecimal bd ? new PercentType(bd) : null;
}
@Override
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java
index 856271f12cd..66757d48fb0 100644
--- a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java
@@ -52,7 +52,7 @@ public SayActionHandler(Action module, VoiceManager voiceManager) {
this.sink = sinkParam != null ? sinkParam.toString() : null;
Object volumeParam = module.getConfiguration().get(PARAM_VOLUME);
- this.volume = volumeParam instanceof BigDecimal ? new PercentType((BigDecimal) volumeParam) : null;
+ this.volume = volumeParam instanceof BigDecimal bd ? new PercentType(bd) : null;
}
@Override
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SynthesizeActionHandler.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SynthesizeActionHandler.java
index ae0e1fa674c..2dc89d27496 100644
--- a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SynthesizeActionHandler.java
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SynthesizeActionHandler.java
@@ -50,7 +50,7 @@ public SynthesizeActionHandler(Action module, AudioManager audioManager) {
this.sink = sinkParam != null ? sinkParam.toString() : null;
Object volumeParam = module.getConfiguration().get(PARAM_VOLUME);
- this.volume = volumeParam instanceof BigDecimal ? new PercentType((BigDecimal) volumeParam) : null;
+ this.volume = volumeParam instanceof BigDecimal bd ? new PercentType(bd) : null;
}
@Override
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java
index 7723ded11f5..714ff83b7fa 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java
@@ -46,20 +46,18 @@ public abstract class AbstractScriptedModuleHandlerFactory extends BaseModuleHan
ModuleHandler moduleHandler = null;
if (scriptedHandler != null) {
- if (scriptedHandler instanceof SimpleActionHandler) {
- moduleHandler = new SimpleActionHandlerDelegate((Action) module, (SimpleActionHandler) scriptedHandler);
- } else if (scriptedHandler instanceof SimpleConditionHandler) {
- moduleHandler = new SimpleConditionHandlerDelegate((Condition) module,
- (SimpleConditionHandler) scriptedHandler);
- } else if (scriptedHandler instanceof SimpleTriggerHandler) {
- moduleHandler = new SimpleTriggerHandlerDelegate((Trigger) module,
- (SimpleTriggerHandler) scriptedHandler);
- } else if (scriptedHandler instanceof ScriptedActionHandlerFactory) {
- moduleHandler = ((ScriptedActionHandlerFactory) scriptedHandler).get((Action) module);
- } else if (scriptedHandler instanceof ScriptedTriggerHandlerFactory) {
- moduleHandler = ((ScriptedTriggerHandlerFactory) scriptedHandler).get((Trigger) module);
- } else if (scriptedHandler instanceof ScriptedConditionHandlerFactory) {
- moduleHandler = ((ScriptedConditionHandlerFactory) scriptedHandler).get((Condition) module);
+ if (scriptedHandler instanceof SimpleActionHandler handler) {
+ moduleHandler = new SimpleActionHandlerDelegate((Action) module, handler);
+ } else if (scriptedHandler instanceof SimpleConditionHandler handler) {
+ moduleHandler = new SimpleConditionHandlerDelegate((Condition) module, handler);
+ } else if (scriptedHandler instanceof SimpleTriggerHandler handler) {
+ moduleHandler = new SimpleTriggerHandlerDelegate((Trigger) module, handler);
+ } else if (scriptedHandler instanceof ScriptedActionHandlerFactory factory) {
+ moduleHandler = factory.get((Action) module);
+ } else if (scriptedHandler instanceof ScriptedTriggerHandlerFactory factory) {
+ moduleHandler = factory.get((Trigger) module);
+ } else if (scriptedHandler instanceof ScriptedConditionHandlerFactory factory) {
+ moduleHandler = factory.get((Condition) module);
} else {
logger.error("Not supported moduleHandler: {}", module.getTypeUID());
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/CacheScriptExtension.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/CacheScriptExtension.java
index 9abaa9d390c..def3ae78056 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/CacheScriptExtension.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/CacheScriptExtension.java
@@ -137,12 +137,12 @@ public void unload(String scriptIdentifier) {
*/
private void asyncCancelJob(@Nullable Object o) {
Runnable cancelJob = null;
- if (o instanceof ScheduledFuture) {
- cancelJob = () -> ((ScheduledFuture>) o).cancel(true);
- } else if (o instanceof java.util.Timer) {
- cancelJob = () -> ((java.util.Timer) o).cancel();
- } else if (o instanceof org.openhab.core.automation.module.script.action.Timer) {
- cancelJob = () -> ((org.openhab.core.automation.module.script.action.Timer) o).cancel();
+ if (o instanceof ScheduledFuture future) {
+ cancelJob = () -> future.cancel(true);
+ } else if (o instanceof java.util.Timer timer) {
+ cancelJob = () -> timer.cancel();
+ } else if (o instanceof org.openhab.core.automation.module.script.action.Timer timer) {
+ cancelJob = () -> timer.cancel();
}
if (cancelJob != null) {
// not using execute so ensure this operates in another thread and we don't block here
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java
index 931ebaebe7d..52293dd137e 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java
@@ -79,9 +79,9 @@ public Path getLibraryPath() {
@Override
public void processWatchEvent(WatchService.Kind kind, Path path) {
- File file = path.toFile();
+ File file = libraryPath.resolve(path).toFile();
if (!file.isHidden() && (kind == DELETE || (file.canRead() && (kind == CREATE || kind == MODIFY)))) {
- dependencyChanged(file.getPath());
+ dependencyChanged(file.toString());
}
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java
index fb4ee3ef5d6..0c51034f125 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java
@@ -167,9 +167,8 @@ public Rule addUnmanagedRule(Rule element) {
List actions = new ArrayList<>();
actions.addAll(element.getActions());
- if (element instanceof SimpleRuleActionHandler) {
- String privId = addPrivateActionHandler(
- new SimpleRuleActionHandlerDelegate((SimpleRuleActionHandler) element));
+ if (element instanceof SimpleRuleActionHandler handler) {
+ String privId = addPrivateActionHandler(new SimpleRuleActionHandlerDelegate(handler));
Action scriptedAction = ActionBuilder.create().withId(Integer.toString(moduleIndex++))
.withTypeUID("jsr223.ScriptedAction").withConfiguration(new Configuration()).build();
@@ -180,9 +179,7 @@ public Rule addUnmanagedRule(Rule element) {
builder.withConfiguration(element.getConfiguration());
builder.withActions(actions);
- Rule rule = builder.build();
-
- return rule;
+ return builder.build();
}
public void addConditionType(ConditionType conditionType) {
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java
index 09500aca0a5..dc98181e234 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java
@@ -24,5 +24,6 @@
*/
@NonNullByDefault
public interface ScriptedActionHandlerFactory extends ScriptedHandler {
- public @Nullable ActionHandler get(Action action);
+ @Nullable
+ ActionHandler get(Action action);
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java
index bd6514227d8..e98fcd76c38 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java
@@ -24,5 +24,6 @@
*/
@NonNullByDefault
public interface ScriptedConditionHandlerFactory extends ScriptedHandler {
- public @Nullable ConditionHandler get(Condition module);
+ @Nullable
+ ConditionHandler get(Condition module);
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java
index d21bf0138e8..bfaa6484177 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java
@@ -24,5 +24,6 @@
*/
@NonNullByDefault
public interface ScriptedTriggerHandlerFactory extends ScriptedHandler {
- public @Nullable TriggerHandler get(Trigger module);
+ @Nullable
+ TriggerHandler get(Trigger module);
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java
index 9ea2aea099e..196a9271bf1 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java
@@ -23,5 +23,5 @@
*/
@NonNullByDefault
public interface SimpleTriggerHandlerCallback extends TriggerHandlerCallback {
- public void triggered(Map context);
+ void triggered(Map context);
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java
index 4c4632fb252..3b27f589964 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java
@@ -15,6 +15,8 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
+import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -22,6 +24,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
@@ -40,15 +43,27 @@
public class AbstractScriptDependencyTrackerTest {
private static final String WATCH_DIR = "test";
+ private static final Path DEPENDENCY = Path.of("depFile");
+ private static final Path DEPENDENCY2 = Path.of("depFile2");
private @NonNullByDefault({}) AbstractScriptDependencyTracker scriptDependencyTracker;
private @Mock @NonNullByDefault({}) WatchService watchServiceMock;
+ private @NonNullByDefault({}) @TempDir Path rootWatchPath;
+ private @NonNullByDefault({}) Path depPath;
+ private @NonNullByDefault({}) Path depPath2;
@BeforeEach
- public void setup() {
- when(watchServiceMock.getWatchPath()).thenReturn(Path.of(""));
+ public void setup() throws IOException {
+ when(watchServiceMock.getWatchPath()).thenReturn(rootWatchPath);
scriptDependencyTracker = new AbstractScriptDependencyTracker(watchServiceMock, WATCH_DIR) {
};
+
+ depPath = rootWatchPath.resolve(WATCH_DIR).resolve(DEPENDENCY);
+ depPath2 = rootWatchPath.resolve(WATCH_DIR).resolve(DEPENDENCY2);
+
+ Files.createFile(depPath);
+
+ Files.createFile(depPath2);
}
@AfterEach
@@ -58,7 +73,7 @@ public void tearDown() {
@Test
public void testScriptLibraryWatcherIsCreatedAndActivated() {
- verify(watchServiceMock).registerListener(eq(scriptDependencyTracker), eq(Path.of(WATCH_DIR)));
+ verify(watchServiceMock).registerListener(eq(scriptDependencyTracker), eq(rootWatchPath.resolve(WATCH_DIR)));
}
@Test
@@ -69,15 +84,15 @@ public void testScriptLibraryWatchersIsDeactivatedOnShutdown() {
}
@Test
- public void testDependencyChangeIsForwardedToMultipleListeners() {
+ public void testDependencyChangeIsForwardedToMultipleListeners() throws IOException {
ScriptDependencyTracker.Listener listener1 = mock(ScriptDependencyTracker.Listener.class);
ScriptDependencyTracker.Listener listener2 = mock(ScriptDependencyTracker.Listener.class);
scriptDependencyTracker.addChangeTracker(listener1);
scriptDependencyTracker.addChangeTracker(listener2);
- scriptDependencyTracker.startTracking("scriptId", "depPath");
- scriptDependencyTracker.dependencyChanged("depPath");
+ scriptDependencyTracker.startTracking("scriptId", depPath.toString());
+ scriptDependencyTracker.processWatchEvent(WatchService.Kind.CREATE, DEPENDENCY);
verify(listener1).onDependencyChange(eq("scriptId"));
verify(listener2).onDependencyChange(eq("scriptId"));
@@ -91,10 +106,9 @@ public void testDependencyChangeIsForwardedForMultipleScriptIds() {
scriptDependencyTracker.addChangeTracker(listener);
- scriptDependencyTracker.startTracking("scriptId1", "depPath");
- scriptDependencyTracker.startTracking("scriptId2", "depPath");
-
- scriptDependencyTracker.dependencyChanged("depPath");
+ scriptDependencyTracker.startTracking("scriptId1", depPath.toString());
+ scriptDependencyTracker.startTracking("scriptId2", depPath.toString());
+ scriptDependencyTracker.processWatchEvent(WatchService.Kind.MODIFY, DEPENDENCY);
verify(listener).onDependencyChange(eq("scriptId1"));
verify(listener).onDependencyChange(eq("scriptId2"));
@@ -107,11 +121,10 @@ public void testDependencyChangeIsForwardedForMultipleDependencies() {
scriptDependencyTracker.addChangeTracker(listener);
- scriptDependencyTracker.startTracking("scriptId", "depPath1");
- scriptDependencyTracker.startTracking("scriptId", "depPath2");
-
- scriptDependencyTracker.dependencyChanged("depPath1");
- scriptDependencyTracker.dependencyChanged("depPath2");
+ scriptDependencyTracker.startTracking("scriptId", depPath.toString());
+ scriptDependencyTracker.startTracking("scriptId", depPath2.toString());
+ scriptDependencyTracker.processWatchEvent(WatchService.Kind.MODIFY, DEPENDENCY);
+ scriptDependencyTracker.processWatchEvent(WatchService.Kind.DELETE, DEPENDENCY2);
verify(listener, times(2)).onDependencyChange(eq("scriptId"));
verifyNoMoreInteractions(listener);
@@ -123,10 +136,10 @@ public void testDependencyChangeIsForwardedForCorrectDependencies() {
scriptDependencyTracker.addChangeTracker(listener);
- scriptDependencyTracker.startTracking("scriptId1", "depPath1");
- scriptDependencyTracker.startTracking("scriptId2", "depPath2");
+ scriptDependencyTracker.startTracking("scriptId1", depPath.toString());
+ scriptDependencyTracker.startTracking("scriptId2", depPath2.toString());
- scriptDependencyTracker.dependencyChanged("depPath1");
+ scriptDependencyTracker.processWatchEvent(WatchService.Kind.CREATE, DEPENDENCY);
verify(listener).onDependencyChange(eq("scriptId1"));
verifyNoMoreInteractions(listener);
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java
index 0ac610a4024..dedebcbc6ad 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java
@@ -24,5 +24,6 @@
@NonNullByDefault
@FunctionalInterface
public interface ScriptDependencyListener extends Consumer {
+ @Override
void accept(String dependency);
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationService.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationService.java
index 38b474ab431..c5f22eb8d81 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationService.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationService.java
@@ -12,8 +12,13 @@
*/
package org.openhab.core.automation.module.script;
+import static org.openhab.core.automation.module.script.profile.ScriptProfileFactory.PROFILE_CONFIG_URI_PREFIX;
+
import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -34,11 +39,15 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
import org.openhab.core.automation.module.script.profile.ScriptProfile;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.common.registry.RegistryChangeListener;
+import org.openhab.core.config.core.ConfigDescription;
+import org.openhab.core.config.core.ConfigDescriptionBuilder;
+import org.openhab.core.config.core.ConfigDescriptionProvider;
+import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigOptionProvider;
+import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.transform.Transformation;
import org.openhab.core.transform.TransformationException;
@@ -48,8 +57,6 @@
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;
@@ -59,35 +66,50 @@
*
* @author Jan N. Klug - Initial contribution
*/
-@Component(service = { TransformationService.class, ScriptTransformationService.class,
- ConfigOptionProvider.class }, property = { "openhab.transform=SCRIPT" })
@NonNullByDefault
-public class ScriptTransformationService
- implements TransformationService, RegistryChangeListener, ConfigOptionProvider {
+@Component(factory = "org.openhab.core.automation.module.script.transformation.factory", service = {
+ TransformationService.class, ScriptTransformationService.class, ConfigOptionProvider.class,
+ ConfigDescriptionProvider.class })
+public class ScriptTransformationService implements TransformationService, ConfigOptionProvider,
+ ConfigDescriptionProvider, RegistryChangeListener {
+ public static final String SCRIPT_TYPE_PROPERTY_NAME = "openhab.transform.script.scriptType";
public static final String OPENHAB_TRANSFORMATION_SCRIPT = "openhab-transformation-script-";
- private static final String PROFILE_CONFIG_URI = "profile:transform:SCRIPT";
- public static final String SUPPORTED_CONFIGURATION_TYPE = "script";
- private static final Pattern SCRIPT_CONFIG_PATTERN = Pattern
- .compile("(?.*?):(?.*?)(\\?(?.*?))?");
+ private static final URI CONFIG_DESCRIPTION_TEMPLATE_URI = URI.create(PROFILE_CONFIG_URI_PREFIX + "SCRIPT");
+
+ private static final Pattern INLINE_SCRIPT_CONFIG_PATTERN = Pattern.compile("\\|(?.+)");
+
+ private static final Pattern SCRIPT_CONFIG_PATTERN = Pattern.compile("(?.+?)(\\?(?.*?))?");
private final Logger logger = LoggerFactory.getLogger(ScriptTransformationService.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
+ private final String scriptType;
+ private final URI profileConfigUri;
+
private final Map scriptCache = new ConcurrentHashMap<>();
private final TransformationRegistry transformationRegistry;
- private final Map supportedScriptTypes = new ConcurrentHashMap<>();
-
private final ScriptEngineManager scriptEngineManager;
+ private final ConfigDescriptionRegistry configDescRegistry;
@Activate
public ScriptTransformationService(@Reference TransformationRegistry transformationRegistry,
- @Reference ScriptEngineManager scriptEngineManager) {
+ @Reference ConfigDescriptionRegistry configDescRegistry, @Reference ScriptEngineManager scriptEngineManager,
+ Map config) {
+ String scriptType = ConfigParser.valueAs(config.get(SCRIPT_TYPE_PROPERTY_NAME), String.class);
+ if (scriptType == null) {
+ throw new IllegalStateException(
+ "'" + SCRIPT_TYPE_PROPERTY_NAME + "' must not be null in service configuration");
+ }
+
this.transformationRegistry = transformationRegistry;
+ this.configDescRegistry = configDescRegistry;
this.scriptEngineManager = scriptEngineManager;
+ this.scriptType = scriptType;
+ this.profileConfigUri = URI.create(PROFILE_CONFIG_URI_PREFIX + scriptType.toUpperCase());
transformationRegistry.addRegistryChangeListener(this);
}
@@ -101,28 +123,34 @@ public void deactivate() {
@Override
public @Nullable String transform(String function, String source) throws TransformationException {
- Matcher configMatcher = SCRIPT_CONFIG_PATTERN.matcher(function);
- if (!configMatcher.matches()) {
- throw new TransformationException("Script Type must be prepended to transformation UID.");
+ String scriptUid;
+ String inlineScript = null;
+ String params = null;
+
+ Matcher configMatcher = INLINE_SCRIPT_CONFIG_PATTERN.matcher(function);
+ if (configMatcher.matches()) {
+ inlineScript = configMatcher.group("inlineScript");
+ // prefix with | to avoid clashing with a real filename
+ scriptUid = "|" + Integer.toString(inlineScript.hashCode());
+ } else {
+ configMatcher = SCRIPT_CONFIG_PATTERN.matcher(function);
+ if (!configMatcher.matches()) {
+ throw new TransformationException("Invalid syntax for the script transformation: '" + function + "'");
+ }
+ scriptUid = configMatcher.group("scriptUid");
+ params = configMatcher.group("params");
}
- String scriptType = configMatcher.group("scriptType");
- String scriptUid = configMatcher.group("scriptUid");
ScriptRecord scriptRecord = scriptCache.computeIfAbsent(scriptUid, k -> new ScriptRecord());
scriptRecord.lock.lock();
try {
if (scriptRecord.script.isBlank()) {
- if (scriptUid.startsWith("|")) {
- // inline script -> strip inline-identifier
- scriptRecord.script = scriptUid.substring(1);
+ if (inlineScript != null) {
+ scriptRecord.script = inlineScript;
} else {
// get script from transformation registry
Transformation transformation = transformationRegistry.get(scriptUid);
if (transformation != null) {
- if (!SUPPORTED_CONFIGURATION_TYPE.equals(transformation.getType())) {
- throw new TransformationException("Configuration does not have correct type 'script' but '"
- + transformation.getType() + "'.");
- }
scriptRecord.script = transformation.getConfiguration().getOrDefault(Transformation.FUNCTION,
"");
}
@@ -160,7 +188,6 @@ public void deactivate() {
ScriptContext executionContext = engine.getContext();
executionContext.setAttribute("input", source, ScriptContext.ENGINE_SCOPE);
- String params = configMatcher.group("params");
if (params != null) {
for (String param : params.split("&")) {
String[] splitString = param.split("=");
@@ -169,7 +196,9 @@ public void deactivate() {
"Parameter '{}' does not consist of two parts for configuration UID {}, skipping.",
param, scriptUid);
} else {
- executionContext.setAttribute(splitString[0], splitString[1], ScriptContext.ENGINE_SCOPE);
+ param = URLDecoder.decode(splitString[0], StandardCharsets.UTF_8);
+ String value = URLDecoder.decode(splitString[1], StandardCharsets.UTF_8);
+ executionContext.setAttribute(param, value, ScriptContext.ENGINE_SCOPE);
}
}
}
@@ -208,6 +237,44 @@ public void updated(Transformation oldElement, Transformation element) {
clearCache(element.getUID());
}
+ @Override
+ public @Nullable Collection getParameterOptions(URI uri, String param, @Nullable String context,
+ @Nullable Locale locale) {
+ if (!uri.equals(profileConfigUri)) {
+ return null;
+ }
+
+ if (ScriptProfile.CONFIG_TO_HANDLER_SCRIPT.equals(param) || ScriptProfile.CONFIG_TO_ITEM_SCRIPT.equals(param)) {
+ return transformationRegistry.getTransformations(List.of(scriptType.toLowerCase())).stream()
+ .map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
+ }
+ return null;
+ }
+
+ @Override
+ public Collection getConfigDescriptions(@Nullable Locale locale) {
+ ConfigDescription configDescription = getConfigDescription(profileConfigUri, locale);
+ if (configDescription != null) {
+ return List.of(configDescription);
+ }
+
+ return Collections.emptyList();
+ }
+
+ @Override
+ public @Nullable ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale) {
+ if (!uri.equals(profileConfigUri)) {
+ return null;
+ }
+
+ ConfigDescription template = configDescRegistry.getConfigDescription(CONFIG_DESCRIPTION_TEMPLATE_URI, locale);
+ if (template == null) {
+ return null;
+ }
+ return ConfigDescriptionBuilder.create(uri).withParameters(template.getParameters())
+ .withParameterGroups(template.getParameterGroups()).build();
+ }
+
private void clearCache(String uid) {
ScriptRecord scriptRecord = scriptCache.remove(uid);
if (scriptRecord != null) {
@@ -243,38 +310,6 @@ private void disposeScriptEngine(ScriptEngine scriptEngine) {
}
}
- @Override
- public @Nullable Collection getParameterOptions(URI uri, String param, @Nullable String context,
- @Nullable Locale locale) {
- if (PROFILE_CONFIG_URI.equals(uri.toString())) {
- if (ScriptProfile.CONFIG_TO_HANDLER_SCRIPT.equals(param)
- || ScriptProfile.CONFIG_TO_ITEM_SCRIPT.equals(param)) {
- return transformationRegistry.getTransformations(List.of(SUPPORTED_CONFIGURATION_TYPE)).stream()
- .map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
- }
- if (ScriptProfile.CONFIG_SCRIPT_LANGUAGE.equals(param)) {
- return supportedScriptTypes.entrySet().stream().map(e -> new ParameterOption(e.getKey(), e.getValue()))
- .collect(Collectors.toList());
- }
- }
- return null;
- }
-
- /**
- * As {@link ScriptEngineFactory}s are added/removed, this method will cache all available script types
- */
- @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
- public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
- Map.Entry parameterOption = ScriptEngineFactoryHelper.getParameterOption(engineFactory);
- if (parameterOption != null) {
- supportedScriptTypes.put(parameterOption.getKey(), parameterOption.getValue());
- }
- }
-
- public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
- supportedScriptTypes.remove(ScriptEngineFactoryHelper.getPreferredMimeType(engineFactory));
- }
-
private static class ScriptRecord {
public String script = "";
public @Nullable ScriptEngineContainer scriptEngineContainer;
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationServiceFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationServiceFactory.java
new file mode 100644
index 00000000000..fdcd21adc52
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptTransformationServiceFactory.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.automation.module.script;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.script.ScriptEngine;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
+import org.openhab.core.transform.TransformationService;
+import org.osgi.service.component.ComponentFactory;
+import org.osgi.service.component.ComponentInstance;
+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;
+
+/**
+ * The {@link ScriptTransformationServiceFactory} registers a {@link ScriptTransformationService}
+ * for each newly added script engine.
+ *
+ * @author Jimmy Tanagra - Initial contribution
+ */
+@Component(immediate = true, service = { ScriptTransformationServiceFactory.class })
+@NonNullByDefault
+public class ScriptTransformationServiceFactory {
+
+ private final ComponentFactory scriptTransformationFactory;
+
+ private final Map> scriptTransformations = new ConcurrentHashMap<>();
+
+ @Activate
+ public ScriptTransformationServiceFactory(
+ @Reference(target = "(component.factory=org.openhab.core.automation.module.script.transformation.factory)") ComponentFactory factory) {
+ this.scriptTransformationFactory = factory;
+ }
+
+ @Deactivate
+ public void deactivate() {
+ scriptTransformations.values().forEach(this::unregisterService);
+ scriptTransformations.clear();
+ }
+
+ /**
+ * As {@link ScriptEngineFactory}s are added/removed, this method will cache all available script types
+ * and registers a transformation service for the script engine.
+ */
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
+ Optional scriptType = ScriptEngineFactoryHelper.getPreferredExtension(engineFactory);
+ if (scriptType.isEmpty()) {
+ return;
+ }
+
+ scriptTransformations.computeIfAbsent(engineFactory, factory -> {
+ ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptType.get());
+ if (scriptEngine == null) {
+ return null;
+ }
+ String languageName = ScriptEngineFactoryHelper.getLanguageName(scriptEngine.getFactory());
+ Dictionary properties = new Hashtable<>();
+ properties.put(TransformationService.SERVICE_PROPERTY_NAME, scriptType.get().toUpperCase());
+ properties.put(TransformationService.SERVICE_PROPERTY_LABEL, "SCRIPT " + languageName);
+ properties.put(ScriptTransformationService.SCRIPT_TYPE_PROPERTY_NAME, scriptType.get());
+ return scriptTransformationFactory.newInstance(properties);
+ });
+ }
+
+ public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
+ ComponentInstance toBeUnregistered = scriptTransformations.remove(engineFactory);
+ if (toBeUnregistered != null) {
+ unregisterService(toBeUnregistered);
+ }
+ }
+
+ private void unregisterService(ComponentInstance instance) {
+ instance.getInstance().deactivate();
+ instance.dispose();
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/GenericScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/GenericScriptEngineFactory.java
deleted file mode 100644
index e6fe0c49fd6..00000000000
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/GenericScriptEngineFactory.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * 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.automation.module.script.internal;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.automation.module.script.AbstractScriptEngineFactory;
-import org.openhab.core.automation.module.script.ScriptEngineFactory;
-import org.osgi.service.component.annotations.Component;
-
-/**
- * An implementation of {@link ScriptEngineFactory} for ScriptEngines that do not require customizations.
- *
- * @author Simon Merschjohann - Initial contribution
- * @author Scott Rushworth - added service and removed methods now inherited from AbstractScriptEngineFactory
- */
-@NonNullByDefault
-@Component(service = ScriptEngineFactory.class)
-public class GenericScriptEngineFactory extends AbstractScriptEngineFactory {
-
-}
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
new file mode 100644
index 00000000000..69c23fecc66
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryBundleTracker.java
@@ -0,0 +1,161 @@
+/**
+ * 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.automation.module.script.internal;
+
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.openhab.core.service.ReadyMarker;
+import org.openhab.core.service.ReadyMarkerFilter;
+import org.openhab.core.service.ReadyService;
+import org.openhab.core.service.StartLevelService;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+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.util.tracker.BundleTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ScriptEngineFactoryBundleTracker} tracks bundles that provide {@link ScriptEngineFactory} and sets the
+ * {@link #READY_MARKER} when all registered bundles are active
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class ScriptEngineFactoryBundleTracker extends BundleTracker implements ReadyService.ReadyTracker {
+ private static final int STATE_MASK = Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE | Bundle.STARTING
+ | Bundle.STOPPING | Bundle.UNINSTALLED;
+ public static final ReadyMarker READY_MARKER = new ReadyMarker("automation", "scriptEngineFactories");
+
+ private final Logger logger = LoggerFactory.getLogger(ScriptEngineFactoryBundleTracker.class);
+
+ private final ReadyService readyService;
+
+ private final Map bundles = new ConcurrentHashMap<>();
+ private boolean startLevel = false;
+ private boolean ready = false;
+
+ @Activate
+ public ScriptEngineFactoryBundleTracker(final @Reference ReadyService readyService, BundleContext bc) {
+ super(bc, STATE_MASK, null);
+ this.readyService = readyService;
+ this.open();
+
+ readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)
+ .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL)));
+ }
+
+ @Deactivate
+ public void deactivate() throws Exception {
+ this.close();
+ ready = false;
+ }
+
+ private boolean allBundlesActive() {
+ return bundles.values().stream().allMatch(i -> i == Bundle.ACTIVE);
+ }
+
+ @Override
+ public Bundle addingBundle(@NonNullByDefault({}) Bundle bundle, @Nullable BundleEvent event) {
+ String bsn = bundle.getSymbolicName();
+ int state = bundle.getState();
+ if (isScriptingBundle(bundle)) {
+ logger.debug("Added {}: {} ", bsn, stateToString(state));
+ bundles.put(bsn, state);
+ checkReady();
+ }
+
+ return bundle;
+ }
+
+ @Override
+ public void modifiedBundle(@NonNullByDefault({}) Bundle bundle, @Nullable BundleEvent event,
+ @NonNullByDefault({}) Bundle object) {
+ String bsn = bundle.getSymbolicName();
+ int state = bundle.getState();
+ if (isScriptingBundle(bundle)) {
+ logger.debug("Modified {}: {}", bsn, stateToString(state));
+ bundles.put(bsn, state);
+ checkReady();
+ }
+ }
+
+ @Override
+ public void removedBundle(@NonNullByDefault({}) Bundle bundle, @Nullable BundleEvent event,
+ @NonNullByDefault({}) Bundle object) {
+ String bsn = bundle.getSymbolicName();
+ if (isScriptingBundle(bundle)) {
+ logger.debug("Removed {}", bsn);
+ bundles.remove(bsn);
+ 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 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) {
+ logger.debug("Removing ready marker: Not all automation bundles ready ({})", bundles);
+ readyService.unmarkReady(READY_MARKER);
+ ready = false;
+ }
+ }
+
+ private String stateToString(int state) {
+ return switch (state) {
+ case Bundle.UNINSTALLED -> "UNINSTALLED";
+ case Bundle.INSTALLED -> "INSTALLED";
+ case Bundle.RESOLVED -> "RESOLVED";
+ case Bundle.STARTING -> "STARTING";
+ case Bundle.STOPPING -> "STOPPING";
+ case Bundle.ACTIVE -> "ACTIVE";
+ default -> "UNKNOWN";
+ };
+ }
+
+ private boolean isScriptingBundle(Bundle bundle) {
+ Dictionary headers = bundle.getHeaders();
+ String provideCapability = headers.get("Provide-Capability");
+ return provideCapability != null && provideCapability.contains(ScriptEngineFactory.class.getName());
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryHelper.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryHelper.java
index 12ffaeb4c93..aa9935baba1 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryHelper.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineFactoryHelper.java
@@ -13,8 +13,10 @@
package org.openhab.core.automation.module.script.internal;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import javax.script.ScriptEngine;
@@ -57,9 +59,14 @@ private ScriptEngineFactoryHelper() {
}
public static String getPreferredMimeType(ScriptEngineFactory factory) {
- List mimeTypes = new ArrayList<>(factory.getScriptTypes());
+ List scriptTypes = factory.getScriptTypes();
+ if (scriptTypes.isEmpty()) {
+ throw new IllegalStateException(
+ factory.getClass().getName() + " does not support any scriptTypes. Please report it as a bug.");
+ }
+ List mimeTypes = new ArrayList<>(scriptTypes);
mimeTypes.removeIf(mimeType -> !mimeType.contains("application") || "application/python".equals(mimeType));
- return mimeTypes.isEmpty() ? factory.getScriptTypes().get(0) : mimeTypes.get(0);
+ return mimeTypes.isEmpty() ? scriptTypes.get(0) : mimeTypes.get(0);
}
public static String getLanguageName(javax.script.ScriptEngineFactory factory) {
@@ -67,4 +74,9 @@ public static String getLanguageName(javax.script.ScriptEngineFactory factory) {
factory.getLanguageName().substring(0, 1).toUpperCase() + factory.getLanguageName().substring(1),
factory.getLanguageVersion());
}
+
+ public static Optional getPreferredExtension(ScriptEngineFactory factory) {
+ return factory.getScriptTypes().stream().filter(type -> !type.contains("/"))
+ .min(Comparator.comparing(String::length));
+ }
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
index 9d8d7e80140..bdab13e386f 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
@@ -60,8 +60,7 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
private final Logger logger = LoggerFactory.getLogger(ScriptEngineManagerImpl.class);
private final Map loadedScriptEngineInstances = new HashMap<>();
- private final Map customSupport = new HashMap<>();
- private final Map genericSupport = new HashMap<>();
+ private final Map factories = new HashMap<>();
private final ScriptExtensionManager scriptExtensionManager;
private final Set listeners = new HashSet<>();
@@ -75,11 +74,7 @@ public void addScriptEngineFactory(ScriptEngineFactory engineFactory) {
List scriptTypes = engineFactory.getScriptTypes();
logger.trace("{}.getScriptTypes(): {}", engineFactory.getClass().getSimpleName(), scriptTypes);
for (String scriptType : scriptTypes) {
- if (isCustomFactory(engineFactory)) {
- this.customSupport.put(scriptType, engineFactory);
- } else {
- this.genericSupport.put(scriptType, engineFactory);
- }
+ factories.put(scriptType, engineFactory);
listeners.forEach(listener -> listener.factoryAdded(scriptType));
}
if (logger.isDebugEnabled()) {
@@ -88,10 +83,10 @@ public void addScriptEngineFactory(ScriptEngineFactory engineFactory) {
if (scriptEngine != null) {
javax.script.ScriptEngineFactory factory = scriptEngine.getFactory();
logger.debug(
- "Initialized a {} ScriptEngineFactory for {} ({}): supports {} ({}) with file extensions {}, names {}, and mimetypes {}",
- (isCustomFactory(engineFactory)) ? "custom" : "generic", factory.getEngineName(),
- factory.getEngineVersion(), factory.getLanguageName(), factory.getLanguageVersion(),
- factory.getExtensions(), factory.getNames(), factory.getMimeTypes());
+ "Initialized a ScriptEngineFactory for {} ({}): supports {} ({}) with file extensions {}, names {}, and mimetypes {}",
+ factory.getEngineName(), factory.getEngineVersion(), factory.getLanguageName(),
+ factory.getLanguageVersion(), factory.getExtensions(), factory.getNames(),
+ factory.getMimeTypes());
} else {
logger.trace("addScriptEngineFactory: engine was null");
}
@@ -105,26 +100,12 @@ public void removeScriptEngineFactory(ScriptEngineFactory engineFactory) {
List scriptTypes = engineFactory.getScriptTypes();
logger.trace("{}.getScriptTypes(): {}", engineFactory.getClass().getSimpleName(), scriptTypes);
for (String scriptType : scriptTypes) {
- if (isCustomFactory(engineFactory)) {
- this.customSupport.remove(scriptType, engineFactory);
- } else {
- this.genericSupport.remove(scriptType, engineFactory);
- }
+ factories.remove(scriptType, engineFactory);
listeners.forEach(listener -> listener.factoryRemoved(scriptType));
}
logger.debug("Removed {}", engineFactory.getClass().getSimpleName());
}
- /**
- * This method is used to determine if a given {@link ScriptEngineFactory} is generic or customized.
- *
- * @param engineFactory {@link ScriptEngineFactory}
- * @return true, if the {@link ScriptEngineFactory} is custom, otherwise false
- */
- private boolean isCustomFactory(ScriptEngineFactory engineFactory) {
- return !(engineFactory instanceof GenericScriptEngineFactory);
- }
-
@Override
public @Nullable ScriptEngineContainer createScriptEngine(String scriptType, String engineIdentifier) {
ScriptEngineContainer result = null;
@@ -182,8 +163,7 @@ public boolean loadScript(String engineIdentifier, InputStreamReader scriptData)
ScriptEngine engine = container.getScriptEngine();
try {
engine.eval(scriptData);
- if (engine instanceof Invocable) {
- Invocable inv = (Invocable) engine;
+ if (engine instanceof Invocable inv) {
try {
inv.invokeFunction("scriptLoaded", engineIdentifier);
} catch (NoSuchMethodException e) {
@@ -210,8 +190,7 @@ public void removeEngine(String engineIdentifier) {
tracker.removeTracking(engineIdentifier);
}
ScriptEngine scriptEngine = container.getScriptEngine();
- if (scriptEngine instanceof Invocable) {
- Invocable inv = (Invocable) scriptEngine;
+ if (scriptEngine instanceof Invocable inv) {
try {
inv.invokeFunction("scriptUnloaded");
} catch (NoSuchMethodException e) {
@@ -223,12 +202,11 @@ public void removeEngine(String engineIdentifier) {
logger.trace("ScriptEngine does not support Invocable interface");
}
- if (scriptEngine instanceof AutoCloseable) {
+ if (scriptEngine instanceof AutoCloseable closeable) {
// we cannot not use ScheduledExecutorService.execute here as it might execute the task in the calling
// thread (calling ScriptEngine.close in the same thread may result in a deadlock if the ScriptEngine
// tries to Thread.join)
scheduler.schedule(() -> {
- AutoCloseable closeable = (AutoCloseable) scriptEngine;
try {
closeable.close();
} catch (Exception e) {
@@ -259,15 +237,7 @@ private void removeScriptExtensions(String pathIdentifier) {
* @return {@link ScriptEngineFactory} or null
*/
private @Nullable ScriptEngineFactory findEngineFactory(String scriptType) {
- ScriptEngineFactory customFactory = customSupport.get(scriptType);
- if (customFactory != null) {
- return customFactory;
- }
- ScriptEngineFactory genericFactory = genericSupport.get(scriptType);
- if (genericFactory != null) {
- return genericFactory;
- }
- return null;
+ return factories.get(scriptType);
}
@Override
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java
index 1acc2e5fe59..f49d0d3c0bb 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java
@@ -50,9 +50,9 @@ public boolean isEmpty() {
@Override
public boolean containsKey(@Nullable Object key) {
- if (key instanceof String) {
+ if (key instanceof String string) {
try {
- itemRegistry.getItem((String) key);
+ itemRegistry.getItem(string);
return true;
} catch (ItemNotFoundException e) {
return false;
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEventImpl.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEventImpl.java
index d344e99c487..72490aff17b 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEventImpl.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEventImpl.java
@@ -158,8 +158,7 @@ public Map- storeStates(Item @Nullable... items) {
Map
- statesMap = new HashMap<>();
if (items != null) {
for (Item item : items) {
- if (item instanceof GroupItem) {
- GroupItem groupItem = (GroupItem) item;
+ if (item instanceof GroupItem groupItem) {
for (Item member : groupItem.getAllMembers()) {
statesMap.put(member, member.getState());
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java
index 2762be49dcd..b1524466349 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java
@@ -68,12 +68,10 @@ public Collection getTypes() {
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
logger.trace("create {} -> {}", module.getId(), module.getTypeUID());
String moduleTypeUID = module.getTypeUID();
- if (ScriptConditionHandler.TYPE_ID.equals(moduleTypeUID) && module instanceof Condition) {
- ScriptConditionHandler handler = new ScriptConditionHandler((Condition) module, ruleUID,
- scriptEngineManager);
- return handler;
- } else if (ScriptActionHandler.TYPE_ID.equals(moduleTypeUID) && module instanceof Action) {
- ScriptActionHandler handler = new ScriptActionHandler((Action) module, ruleUID, scriptEngineManager,
+ if (ScriptConditionHandler.TYPE_ID.equals(moduleTypeUID) && module instanceof Condition condition) {
+ return new ScriptConditionHandler(condition, ruleUID, scriptEngineManager);
+ } else if (ScriptActionHandler.TYPE_ID.equals(moduleTypeUID) && module instanceof Action action) {
+ ScriptActionHandler handler = new ScriptActionHandler(action, ruleUID, scriptEngineManager,
this::onHandlerRemoval);
trackedHandlers.put(handler.getEngineIdentifier(), handler);
return handler;
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java
index f087f5c2942..71f6ae2b76e 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java
@@ -71,8 +71,8 @@ public AbstractScriptModuleHandler(T module, String ruleUID, ScriptEngineManager
private static String getValidConfigParameter(String parameter, Configuration config, String moduleId) {
Object value = config.get(parameter);
- if (value != null && value instanceof String && !((String) value).trim().isEmpty()) {
- return (String) value;
+ if (value != null && value instanceof String string && !string.trim().isEmpty()) {
+ return string;
} else {
throw new IllegalStateException(String.format(
"Config parameter '%s' is missing in the configuration of module '%s'.", parameter, moduleId));
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java
index bea05798b28..4721be85910 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java
@@ -52,8 +52,8 @@ public boolean isSatisfied(final Map context) {
setExecutionContext(scriptEngine, context);
try {
Object returnVal = scriptEngine.eval(script);
- if (returnVal instanceof Boolean) {
- result = (boolean) returnVal;
+ if (returnVal instanceof Boolean boolean1) {
+ result = boolean1;
} else {
logger.error("Script did not return a boolean value, but '{}'", returnVal);
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfile.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfile.java
index 68d405b021e..e9e720bcb5f 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfile.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfile.java
@@ -41,7 +41,6 @@
@NonNullByDefault
public class ScriptProfile implements StateProfile {
- public static final String CONFIG_SCRIPT_LANGUAGE = "scriptLanguage";
public static final String CONFIG_TO_ITEM_SCRIPT = "toItemScript";
public static final String CONFIG_TO_HANDLER_SCRIPT = "toHandlerScript";
@@ -54,14 +53,15 @@ public class ScriptProfile implements StateProfile {
private final List> acceptedCommandTypes;
private final List> handlerAcceptedCommandTypes;
- private final String scriptLanguage;
private final String toItemScript;
private final String toHandlerScript;
+ private final ProfileTypeUID profileTypeUID;
private final boolean isConfigured;
- public ScriptProfile(ProfileCallback callback, ProfileContext profileContext,
+ public ScriptProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, ProfileContext profileContext,
TransformationService transformationService) {
+ this.profileTypeUID = profileTypeUID;
this.callback = callback;
this.transformationService = transformationService;
@@ -69,22 +69,15 @@ public ScriptProfile(ProfileCallback callback, ProfileContext profileContext,
this.acceptedDataTypes = profileContext.getAcceptedDataTypes();
this.handlerAcceptedCommandTypes = profileContext.getHandlerAcceptedCommandTypes();
- this.scriptLanguage = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_SCRIPT_LANGUAGE),
- String.class, "");
this.toItemScript = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_ITEM_SCRIPT),
String.class, "");
this.toHandlerScript = ConfigParser
.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_HANDLER_SCRIPT), String.class, "");
- if (scriptLanguage.isBlank()) {
- logger.error("Script language is not defined. Profile will discard all states and commands.");
- isConfigured = false;
- return;
- }
-
if (toItemScript.isBlank() && toHandlerScript.isBlank()) {
logger.error(
- "Neither 'toItem' nor 'toHandler' script defined. Profile will discard all states and commands.");
+ "Neither 'toItemScript' nor 'toHandlerScript' defined in link '{}'. Profile will discard all states and commands.",
+ callback.getItemChannelLink());
isConfigured = false;
return;
}
@@ -94,7 +87,7 @@ public ScriptProfile(ProfileCallback callback, ProfileContext profileContext,
@Override
public ProfileTypeUID getProfileTypeUID() {
- return ScriptProfileFactory.SCRIPT_PROFILE_UID;
+ return profileTypeUID;
}
@Override
@@ -149,7 +142,7 @@ public void onStateUpdateFromHandler(State state) {
private @Nullable String executeScript(String script, Type input) {
if (!script.isBlank()) {
try {
- return transformationService.transform(scriptLanguage + ":" + script, input.toFullString());
+ return transformationService.transform(script, input.toFullString());
} catch (TransformationException e) {
if (e.getCause() instanceof ScriptException) {
logger.error("Failed to process script '{}': {}", script, e.getCause().getMessage());
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfileFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfileFactory.java
index adbf5c9710f..6e46afa5f3a 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfileFactory.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/profile/ScriptProfileFactory.java
@@ -14,7 +14,9 @@
import java.util.Collection;
import java.util.Locale;
-import java.util.Set;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -28,48 +30,58 @@
import org.openhab.core.thing.profiles.ProfileTypeProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.transform.TransformationService;
-import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
/**
* The {@link ScriptProfileFactory} creates {@link ScriptProfile} instances
*
* @author Jan N. Klug - Initial contribution
*/
-@Component(service = { ScriptProfileFactory.class, ProfileFactory.class, ProfileTypeProvider.class })
@NonNullByDefault
+@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
public class ScriptProfileFactory implements ProfileFactory, ProfileTypeProvider {
+ public static final String PROFILE_CONFIG_URI_PREFIX = "profile:transform:";
- public static final ProfileTypeUID SCRIPT_PROFILE_UID = new ProfileTypeUID(
- TransformationService.TRANSFORM_PROFILE_SCOPE, "SCRIPT");
-
- private static final ProfileType PROFILE_TYPE_SCRIPT = ProfileTypeBuilder.newState(SCRIPT_PROFILE_UID, "Script")
- .build();
-
- private final ScriptTransformationService transformationService;
-
- @Activate
- public ScriptProfileFactory(final @Reference ScriptTransformationService transformationService) {
- this.transformationService = transformationService;
- }
+ private final Map services = new ConcurrentHashMap<>();
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext profileContext) {
- if (SCRIPT_PROFILE_UID.equals(profileTypeUID)) {
- return new ScriptProfile(callback, profileContext, transformationService);
- }
- return null;
+ String serviceId = profileTypeUID.getId();
+ ScriptTransformationService transformationService = services.get(serviceId).service();
+ return new ScriptProfile(profileTypeUID, callback, profileContext, transformationService);
}
@Override
public Collection getSupportedProfileTypeUIDs() {
- return Set.of(SCRIPT_PROFILE_UID);
+ return services.keySet().stream()
+ .map(id -> new ProfileTypeUID(TransformationService.TRANSFORM_PROFILE_SCOPE, id)).toList();
}
@Override
public Collection getProfileTypes(@Nullable Locale locale) {
- return Set.of(PROFILE_TYPE_SCRIPT);
+ return getSupportedProfileTypeUIDs().stream().map(uid -> {
+ String id = uid.getId();
+ String label = services.get(id).serviceLabel();
+ return ProfileTypeBuilder.newState(uid, label).build();
+ }).collect(Collectors.toList());
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ public void bindScriptTransformationService(ScriptTransformationService service, Map properties) {
+ String serviceId = (String) properties.get(TransformationService.SERVICE_PROPERTY_NAME);
+ String serviceLabel = (String) properties.get(TransformationService.SERVICE_PROPERTY_LABEL);
+ services.put(serviceId, new ServiceRecord(service, serviceLabel));
+ }
+
+ public void unbindScriptTransformationService(ScriptTransformationService service, Map properties) {
+ String serviceId = (String) properties.get(TransformationService.SERVICE_PROPERTY_NAME);
+ services.remove(serviceId);
+ }
+
+ private record ServiceRecord(ScriptTransformationService service, String serviceLabel) {
}
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/config/script-profile.xml b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/config/script-profile.xml
index 3fd037165e6..6e1670c5295 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/config/script-profile.xml
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/config/script-profile.xml
@@ -5,17 +5,15 @@
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
-
-
- MIME-type ("application/vnd.openhab.dsl.rule") of the scripting language
-
-
- The Script for transforming states and commands from handler to item.
+
+ The Script for transforming state updates and commands from the Thing handler to the item. The script
+ may return null to discard the updates/commands and not pass them through.
-
- The Script for transforming states and commands from item to handler.
+
+ The Script for transforming commands from the item to the Thing handler. The script may return null to
+ discard the commands and not pass them through.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile.properties
index 021f5331014..8c0323b52b6 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile.properties
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile.properties
@@ -1,6 +1,4 @@
-profile.system.script.scriptLanguage.label = Script Language
-profile.system.script.scriptLanguage.description = MIME-type ("application/vnd.openhab.dsl.rule") of the scripting language
-profile.system.script.toItemScript.label = To Item Script
-profile.system.script.toItemScript.description = The Script for transforming states and commands from handler to item.
-profile.system.script.toHandlerScript.label = To Handler Script
-profile.system.script.toHandlerScript.description = The Script for transforming states and commands from item to handler.
+profile.system.script.toItemScript.label = Thing To Item Transformation
+profile.system.script.toItemScript.description = The Script for transforming state updates and commands from the Thing handler to the item. The script may return null to discard the updates/commands and not pass them through.
+profile.system.script.toHandlerScript.label = Item To Thing Transformation
+profile.system.script.toHandlerScript.description = The Script for transforming commands from the item to the Thing handler. The script may return null to discard the commands and not pass them through.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_da.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_da.properties
index 6e1e3e64cc6..b484a6da08d 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_da.properties
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_da.properties
@@ -1,5 +1,3 @@
-profile.system.script.scriptLanguage.label = Script-sprog
-profile.system.script.scriptLanguage.description = MIME-type ("application/vnd.openhab.dsl.rule") for script-sproget
profile.system.script.toItemScript.label = Til item-script
profile.system.script.toItemScript.description = Scriptet til at transformere tilstande og kommandoer fra handler til item.
profile.system.script.toHandlerScript.label = Til handler-script
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_de.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_de.properties
new file mode 100644
index 00000000000..fdd90001389
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_de.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = Transformation Thing -> Item
+profile.system.script.toItemScript.description = Das Skript für die Transformtion von States und Commands vom Thing zum Item.
+profile.system.script.toHandlerScript.label = Transformation Item -> Thing
+profile.system.script.toHandlerScript.description = Das Skript für die Transformation von States und Commands vom Item zum Thing.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_el.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_el.properties
new file mode 100644
index 00000000000..b8b6b6a13c9
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_el.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = Μετασχηματισμός Thing σε Item
+profile.system.script.toItemScript.description = Το Σενάριο ενεργειών για τη μετατροπή ενημερώσεων/εντολών κατάστασης από το Thing handler προς το Item. Το σενάριο μπορεί να επιστρέψει με απροσδιόριστη έξοδο (null) για να απορρίψει τις ενημερώσεις/εντολές και να μην τις περάσει.
+profile.system.script.toHandlerScript.label = Μετασχηματισμός Item σε Thing
+profile.system.script.toHandlerScript.description = Το Σενάριο ενεργειών για τη μετατροπή εντολών κατάστασης από το Item προς το Thing handler. Το σενάριο μπορεί να επιστρέψει με απροσδιόριστη έξοδο (null) για να απορρίψει τις εντολές και να μην τις περάσει.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_fi.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_fi.properties
new file mode 100644
index 00000000000..454984afb8d
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_fi.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = Item-skriptiksi
+profile.system.script.toItemScript.description = Skripti, jota käytetään tilojen ja komentojen muuntoon käsittelijästä itemiksi.
+profile.system.script.toHandlerScript.label = Käsittelijäskriptiksi
+profile.system.script.toHandlerScript.description = Skripti, jota käytetään tilojen ja komentojen muuntoon itemistä käsittelijäksi.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_fr.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_fr.properties
new file mode 100644
index 00000000000..d10b5ff8630
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_fr.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = Transformation Thing vers Item
+profile.system.script.toItemScript.description = Le script pour transformer les mises à jour d'état et les commandes du Thing vers un Item. Le script peut retourner null pour ignorer les mises à jour/commandes et ne pas les transférer.
+profile.system.script.toHandlerScript.label = Transformation Item vers Thing
+profile.system.script.toHandlerScript.description = Le script pour transformer les mises à jour d'état et les commandes de l'Item vers un Thing. Le script peut retourner null pour ignorer les mises à jour/commandes et ne pas les transférer.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_he.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_he.properties
new file mode 100644
index 00000000000..79b93afbb94
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_he.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = המרת Thing לפריט
+profile.system.script.toItemScript.description = הסקריפט להפיכת עדכוני מצב ופקודות מהמטפל ב-Thing לפריט. הסקריפט עשוי לחזור null כדי למחוק את העדכונים/פקודות ולא להעביר אותם.
+profile.system.script.toHandlerScript.label = שינוי פריט ל-thing
+profile.system.script.toHandlerScript.description = הסקריפט להמרת פקודות מהפריט ל-Thing handler. הסקריפט עשוי להחזיר null כדי למחוק את הפקודות ולא להעביר אותן.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_hu.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_hu.properties
new file mode 100644
index 00000000000..1e4bfb988fa
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_hu.properties
@@ -0,0 +1,6 @@
+profile.system.script.scriptLanguage.label = Szkript nyelve
+profile.system.script.scriptLanguage.description = A szkript nyelv MIME-típusa ("application/vnd.openhab.dsl.rule")
+profile.system.script.toItemScript.label = Elemhez irányuló szkript
+profile.system.script.toItemScript.description = A kezelőtől az elemhez kerülő állapotok és parancsok átalakítását végző szkript.
+profile.system.script.toHandlerScript.label = Kezelőhöz irányuló szkript
+profile.system.script.toHandlerScript.description = Az elemtől a kezelőhöz kerülő állapotok és parancsok átalakítását végző szkript.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_it.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_it.properties
index 4040707691d..810fcea2836 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_it.properties
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_it.properties
@@ -1,6 +1,4 @@
-profile.system.script.scriptLanguage.label = Linguaggio Script
-profile.system.script.scriptLanguage.description = Tipo MIME ("application/vnd.openhab.dsl.rule") del linguaggio di scripting
-profile.system.script.toItemScript.label = Allo Script dell'Item
-profile.system.script.toItemScript.description = Lo script per trasformare stati e comandi dal gestore all'Item.
-profile.system.script.toHandlerScript.label = Allo script dell'Handler
-profile.system.script.toHandlerScript.description = Lo script per trasformare stati e comandi dal'Item al gestore.
+profile.system.script.toItemScript.label = Trasformazione da Thing a Item
+profile.system.script.toItemScript.description = Lo script per trasformare gli aggiornamenti dello stato e i comandi dal gestore Thing all'Item. Lo script può restituire null per scartare gli aggiornamenti/comandi e non passarli.
+profile.system.script.toHandlerScript.label = Trasformazione da Item a Thing
+profile.system.script.toHandlerScript.description = Lo script per trasformare i comandi dall'Item al gestore di Thing. Lo script può restituire null per scartare i comandi e non passarli.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_pl.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_pl.properties
new file mode 100644
index 00000000000..7320cb082ce
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_pl.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = Skrypt transformacji z kanału do elementu
+profile.system.script.toItemScript.description = Skrypt do transformacji stanu lub wartości z kanału do elementu. Skrypt może zwrócić stan lub wartość *null* aby pominąć transformację i nie przekazać jej do elementu.
+profile.system.script.toHandlerScript.label = Skrypt transformacji z elementu do kanału
+profile.system.script.toHandlerScript.description = Skrypt do transformacji stanu lub wartości z elementu do kanału. Skrypt może zwrócić stan lub wartość *null* aby pominąć transformację i nie przekazać jej do kanału.
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_sl.properties b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_sl.properties
new file mode 100644
index 00000000000..673061ef5a2
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/OH-INF/i18n/scriptprofile_sl.properties
@@ -0,0 +1,4 @@
+profile.system.script.toItemScript.label = Pretvorba stvari v predmet
+profile.system.script.toItemScript.description = Skript za pretvorbo stanja posodobitev in ukazov iz rokovalca s stvarmi v predmet. Skript lahko vrne nič, da zavrže posodobitve/ukaze in jih ne spusti skozi.
+profile.system.script.toHandlerScript.label = Pretvorba predmeta v stvar
+profile.system.script.toHandlerScript.description = Skript za pretvorbo ukazov predmeta v rokovalca s stvarmi. Skript lahko vrne nič, da zavrže ukaze in jih ne spusti skozi.
diff --git a/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/ScriptTransformationServiceTest.java b/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/ScriptTransformationServiceTest.java
index 832b444dc27..b73010e6aa8 100644
--- a/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/ScriptTransformationServiceTest.java
+++ b/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/ScriptTransformationServiceTest.java
@@ -19,6 +19,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
+import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@@ -36,6 +37,7 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
+import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.transform.Transformation;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationRegistry;
@@ -50,7 +52,7 @@
@MockitoSettings(strictness = Strictness.LENIENT)
public class ScriptTransformationServiceTest {
private static final String SCRIPT_LANGUAGE = "customDsl";
- private static final String SCRIPT_UID = "scriptUid";
+ private static final String SCRIPT_UID = "scriptUid." + SCRIPT_LANGUAGE;
private static final String INVALID_SCRIPT_UID = "invalidScriptUid";
private static final String INLINE_SCRIPT = "|inlineScript";
@@ -59,7 +61,7 @@ public class ScriptTransformationServiceTest {
private static final String SCRIPT_OUTPUT = "output";
private static final Transformation TRANSFORMATION_CONFIGURATION = new Transformation(SCRIPT_UID, "label",
- ScriptTransformationService.SUPPORTED_CONFIGURATION_TYPE, Map.of(Transformation.FUNCTION, SCRIPT));
+ SCRIPT_LANGUAGE, Map.of(Transformation.FUNCTION, SCRIPT));
private static final Transformation INVALID_TRANSFORMATION_CONFIGURATION = new Transformation(INVALID_SCRIPT_UID,
"label", "invalid", Map.of(Transformation.FUNCTION, SCRIPT));
@@ -73,7 +75,10 @@ public class ScriptTransformationServiceTest {
@BeforeEach
public void setUp() throws ScriptException {
- service = new ScriptTransformationService(transformationRegistry, scriptEngineManager);
+ Map properties = new HashMap<>();
+ properties.put(ScriptTransformationService.SCRIPT_TYPE_PROPERTY_NAME, SCRIPT_LANGUAGE);
+ service = new ScriptTransformationService(transformationRegistry, mock(ConfigDescriptionRegistry.class),
+ scriptEngineManager, properties);
when(scriptEngineManager.createScriptEngine(eq(SCRIPT_LANGUAGE), any())).thenReturn(scriptEngineContainer);
when(scriptEngineManager.isSupported(anyString()))
@@ -96,14 +101,14 @@ public void setUp() throws ScriptException {
@Test
public void success() throws TransformationException {
- String returnValue = Objects.requireNonNull(service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input"));
+ String returnValue = Objects.requireNonNull(service.transform(SCRIPT_UID, "input"));
assertThat(returnValue, is(SCRIPT_OUTPUT));
}
@Test
public void scriptExecutionParametersAreInjectedIntoEngineContext() throws TransformationException {
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID + "?param1=value1¶m2=value2", "input");
+ service.transform(SCRIPT_UID + "?param1=value1¶m2=value2", "input");
verify(scriptContext).setAttribute(eq("input"), eq("input"), eq(ScriptContext.ENGINE_SCOPE));
verify(scriptContext).setAttribute(eq("param1"), eq("value1"), eq(ScriptContext.ENGINE_SCOPE));
@@ -111,6 +116,16 @@ public void scriptExecutionParametersAreInjectedIntoEngineContext() throws Trans
verifyNoMoreInteractions(scriptContext);
}
+ @Test
+ public void scriptExecutionParametersAreDecoded() throws TransformationException {
+ service.transform(SCRIPT_UID + "?param1=%26amp;¶m2=%3dvalue", "input");
+
+ verify(scriptContext).setAttribute(eq("input"), eq("input"), eq(ScriptContext.ENGINE_SCOPE));
+ verify(scriptContext).setAttribute(eq("param1"), eq("&"), eq(ScriptContext.ENGINE_SCOPE));
+ verify(scriptContext).setAttribute(eq("param2"), eq("=value"), eq(ScriptContext.ENGINE_SCOPE));
+ verifyNoMoreInteractions(scriptContext);
+ }
+
@Test
public void scriptSetAttributesBeforeCompiling() throws TransformationException, ScriptException {
abstract class CompilableScriptEngine implements ScriptEngine, Compilable {
@@ -122,7 +137,7 @@ abstract class CompilableScriptEngine implements ScriptEngine, Compilable {
InOrder inOrder = inOrder(scriptContext, scriptEngine);
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID + "?param1=value1", "input");
+ service.transform(SCRIPT_UID + "?param1=value1", "input");
inOrder.verify(scriptContext, times(2)).setAttribute(anyString(), anyString(), eq(ScriptContext.ENGINE_SCOPE));
inOrder.verify((Compilable) scriptEngine).compile(SCRIPT);
@@ -132,7 +147,7 @@ abstract class CompilableScriptEngine implements ScriptEngine, Compilable {
@Test
public void invalidScriptExecutionParametersAreDiscarded() throws TransformationException {
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID + "?param1=value1&invalid", "input");
+ service.transform(SCRIPT_UID + "?param1=value1&invalid", "input");
verify(scriptContext).setAttribute(eq("input"), eq("input"), eq(ScriptContext.ENGINE_SCOPE));
verify(scriptContext).setAttribute(eq("param1"), eq("value1"), eq(ScriptContext.ENGINE_SCOPE));
@@ -141,41 +156,25 @@ public void invalidScriptExecutionParametersAreDiscarded() throws Transformation
@Test
public void scriptsAreCached() throws TransformationException {
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
+ service.transform(SCRIPT_UID, "input");
+ service.transform(SCRIPT_UID, "input");
verify(transformationRegistry).get(SCRIPT_UID);
}
@Test
public void scriptCacheInvalidatedAfterChange() throws TransformationException {
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
+ service.transform(SCRIPT_UID, "input");
service.updated(TRANSFORMATION_CONFIGURATION, TRANSFORMATION_CONFIGURATION);
- service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
+ service.transform(SCRIPT_UID, "input");
verify(transformationRegistry, times(2)).get(SCRIPT_UID);
}
- @Test
- public void noScriptTypeThrowsException() {
- TransformationException e = assertThrows(TransformationException.class,
- () -> service.transform(SCRIPT_UID, "input"));
-
- assertThat(e.getMessage(), is("Script Type must be prepended to transformation UID."));
- }
-
- @Test
- public void unknownScriptTypeThrowsException() {
- TransformationException e = assertThrows(TransformationException.class,
- () -> service.transform("foo" + ":" + SCRIPT_UID, "input"));
-
- assertThat(e.getMessage(), is("Script type 'foo' is not supported by any available script engine."));
- }
-
@Test
public void unknownScriptUidThrowsException() {
TransformationException e = assertThrows(TransformationException.class,
- () -> service.transform(SCRIPT_LANGUAGE + ":" + "foo", "input"));
+ () -> service.transform("foo", "input"));
assertThat(e.getMessage(), is("Could not get script for UID 'foo'."));
}
@@ -185,24 +184,16 @@ public void scriptExceptionResultsInTransformationException() throws ScriptExcep
when(scriptEngine.eval(SCRIPT)).thenThrow(new ScriptException("exception"));
TransformationException e = assertThrows(TransformationException.class,
- () -> service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input"));
+ () -> service.transform(SCRIPT_UID, "input"));
assertThat(e.getMessage(), is("Failed to execute script."));
assertThat(e.getCause(), instanceOf(ScriptException.class));
assertThat(e.getCause().getMessage(), is("exception"));
}
- @Test
- public void invalidConfigurationTypeThrowsTransformationException() {
- TransformationException e = assertThrows(TransformationException.class,
- () -> service.transform(SCRIPT_LANGUAGE + ":" + INVALID_SCRIPT_UID, "input"));
-
- assertThat(e.getMessage(), is("Configuration does not have correct type 'script' but 'invalid'."));
- }
-
@Test
public void inlineScriptProperlyProcessed() throws TransformationException, ScriptException {
- service.transform(SCRIPT_LANGUAGE + ":" + INLINE_SCRIPT, "input");
+ service.transform(INLINE_SCRIPT, "input");
verify(scriptEngine).eval(INLINE_SCRIPT.substring(1));
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/profile/ScriptProfileTest.java b/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/profile/ScriptProfileTest.java
index 8b26a0c0ed2..d4268e65fe8 100644
--- a/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/profile/ScriptProfileTest.java
+++ b/bundles/org.openhab.core.automation.module.script/src/test/java/org/openhab/core/automation/module/script/profile/ScriptProfileTest.java
@@ -13,11 +13,11 @@
package org.openhab.core.automation.module.script.profile;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_SCRIPT_LANGUAGE;
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_TO_HANDLER_SCRIPT;
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_TO_ITEM_SCRIPT;
@@ -40,8 +40,11 @@
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.test.java.JavaTest;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
+import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
@@ -67,36 +70,15 @@ public void setUp() throws TransformationException {
@Test
public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptDefined() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL").build();
+ ProfileContext profileContext = ProfileContextBuilder.create().build();
- setupInterceptedLogger(ScriptProfile.class, LogLevel.ERROR);
-
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
-
- scriptProfile.onCommandFromHandler(OnOffType.ON);
- scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
- scriptProfile.onCommandFromItem(OnOffType.ON);
-
- verify(transformationServiceMock, never()).transform(any(), any());
- verify(profileCallback, never()).handleCommand(any());
- verify(profileCallback, never()).sendUpdate(any());
- verify(profileCallback, never()).sendCommand(any());
-
- assertLogMessage(ScriptProfile.class, LogLevel.ERROR,
- "Neither 'toItem' nor 'toHandler' script defined. Profile will discard all states and commands.");
- }
-
- @Test
- public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptLanguageDefined()
- throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
- .withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
- .withAcceptedDataTypes(List.of(PercentType.class))
- .withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
+ ItemChannelLink link = new ItemChannelLink("DummyItem", new ChannelUID("foo:bar:baz:qux"));
+ when(profileCallback.getItemChannelLink()).thenReturn(link);
setupInterceptedLogger(ScriptProfile.class, LogLevel.ERROR);
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
@@ -108,18 +90,20 @@ public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptLanguage
verify(profileCallback, never()).sendCommand(any());
assertLogMessage(ScriptProfile.class, LogLevel.ERROR,
- "Script language is not defined. Profile will discard all states and commands.");
+ "Neither 'toItemScript' nor 'toHandlerScript' defined in link '" + link.toString()
+ + "'. Profile will discard all states and commands.");
}
@Test
public void scriptExecutionErrorForwardsNoValueToCallback() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
- .withToItemScript("inScript").withToHandlerScript("outScript").build();
+ ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
+ .withToHandlerScript("outScript").build();
when(transformationServiceMock.transform(any(), any()))
.thenThrow(new TransformationException("intentional failure"));
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
@@ -133,12 +117,13 @@ public void scriptExecutionErrorForwardsNoValueToCallback() throws Transformatio
@Test
public void scriptExecutionResultNullForwardsNoValueToCallback() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
- .withToItemScript("inScript").withToHandlerScript("outScript").build();
+ ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
+ .withToHandlerScript("outScript").build();
when(transformationServiceMock.transform(any(), any())).thenReturn(null);
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
@@ -152,14 +137,15 @@ public void scriptExecutionResultNullForwardsNoValueToCallback() throws Transfor
@Test
public void scriptExecutionResultForwardsTransformedValueToCallback() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
- .withToItemScript("inScript").withToHandlerScript("outScript")
- .withAcceptedCommandTypes(List.of(OnOffType.class)).withAcceptedDataTypes(List.of(OnOffType.class))
+ ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
+ .withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(OnOffType.class))
+ .withAcceptedDataTypes(List.of(OnOffType.class))
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@@ -173,14 +159,14 @@ public void scriptExecutionResultForwardsTransformedValueToCallback() throws Tra
@Test
public void onlyToItemScriptDoesNotForwardOutboundCommands() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
- .withToItemScript("inScript").withAcceptedCommandTypes(List.of(OnOffType.class))
- .withAcceptedDataTypes(List.of(OnOffType.class))
+ ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
+ .withAcceptedCommandTypes(List.of(OnOffType.class)).withAcceptedDataTypes(List.of(OnOffType.class))
.withHandlerAcceptedCommandTypes(List.of(DecimalType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@@ -194,14 +180,14 @@ public void onlyToItemScriptDoesNotForwardOutboundCommands() throws Transformati
@Test
public void onlyToHandlerScriptDoesNotForwardInboundCommands() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
- .withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
- .withAcceptedDataTypes(List.of(DecimalType.class))
+ ProfileContext profileContext = ProfileContextBuilder.create().withToHandlerScript("outScript")
+ .withAcceptedCommandTypes(List.of(DecimalType.class)).withAcceptedDataTypes(List.of(DecimalType.class))
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@@ -215,14 +201,15 @@ public void onlyToHandlerScriptDoesNotForwardInboundCommands() throws Transforma
@Test
public void incompatibleStateOrCommandNotForwardedToCallback() throws TransformationException {
- ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
- .withToItemScript("inScript").withToHandlerScript("outScript")
- .withAcceptedCommandTypes(List.of(DecimalType.class)).withAcceptedDataTypes(List.of(PercentType.class))
+ ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
+ .withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
+ .withAcceptedDataTypes(List.of(PercentType.class))
.withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
- ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
+ ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
+ transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@@ -244,11 +231,6 @@ public static ProfileContextBuilder create() {
return new ProfileContextBuilder();
}
- public ProfileContextBuilder withScriptLanguage(String scriptLanguage) {
- configuration.put(CONFIG_SCRIPT_LANGUAGE, scriptLanguage);
- return this;
- }
-
public ProfileContextBuilder withToItemScript(String toItem) {
configuration.put(CONFIG_TO_ITEM_SCRIPT, toItem);
return this;
diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java
index 412f1428b72..7c1a5c7c043 100644
--- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java
+++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java
@@ -136,21 +136,21 @@ public Response getByUID(
}
private ModuleTypeDTO getModuleTypeDTO(final ModuleType moduleType) {
- if (moduleType instanceof ActionType) {
- if (moduleType instanceof CompositeActionType) {
- return ActionTypeDTOMapper.map((CompositeActionType) moduleType);
+ if (moduleType instanceof ActionType actionType) {
+ if (moduleType instanceof CompositeActionType compositeActionType) {
+ return ActionTypeDTOMapper.map(compositeActionType);
}
- return ActionTypeDTOMapper.map((ActionType) moduleType);
- } else if (moduleType instanceof ConditionType) {
- if (moduleType instanceof CompositeConditionType) {
- return ConditionTypeDTOMapper.map((CompositeConditionType) moduleType);
+ return ActionTypeDTOMapper.map(actionType);
+ } else if (moduleType instanceof ConditionType conditionType) {
+ if (moduleType instanceof CompositeConditionType compositeConditionType) {
+ return ConditionTypeDTOMapper.map(compositeConditionType);
}
- return ConditionTypeDTOMapper.map((ConditionType) moduleType);
- } else if (moduleType instanceof TriggerType) {
- if (moduleType instanceof CompositeTriggerType) {
- return TriggerTypeDTOMapper.map((CompositeTriggerType) moduleType);
+ return ConditionTypeDTOMapper.map(conditionType);
+ } else if (moduleType instanceof TriggerType triggerType) {
+ if (moduleType instanceof CompositeTriggerType compositeTriggerType) {
+ return TriggerTypeDTOMapper.map(compositeTriggerType);
}
- return TriggerTypeDTOMapper.map((TriggerType) moduleType);
+ return TriggerTypeDTOMapper.map(triggerType);
} else {
throw new IllegalArgumentException(
String.format("Cannot handle given module type class (%s)", moduleType.getClass()));
diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java
index 27b46dfc0c6..2ffb7adb75e 100644
--- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java
+++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java
@@ -17,9 +17,11 @@
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
+import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@@ -29,6 +31,7 @@
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
@@ -36,8 +39,10 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
@@ -64,12 +69,15 @@
import org.openhab.core.automation.dto.RuleDTOMapper;
import org.openhab.core.automation.dto.TriggerDTO;
import org.openhab.core.automation.dto.TriggerDTOMapper;
+import org.openhab.core.automation.events.AutomationEventFactory;
import org.openhab.core.automation.rest.internal.dto.EnrichedRuleDTO;
import org.openhab.core.automation.rest.internal.dto.EnrichedRuleDTOMapper;
import org.openhab.core.automation.util.ModuleBuilder;
import org.openhab.core.automation.util.RuleBuilder;
+import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
+import org.openhab.core.events.Event;
import org.openhab.core.io.rest.DTOMapper;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.RESTConstants;
@@ -78,6 +86,7 @@
import org.openhab.core.library.types.DateTimeType;
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.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
@@ -126,8 +135,10 @@ public class RuleResource implements RESTResource {
private final RuleManager ruleManager;
private final RuleRegistry ruleRegistry;
private final ManagedRuleProvider managedRuleProvider;
+ private final ResetLastModifiedChangeListener resetLastModifiedChangeListener = new ResetLastModifiedChangeListener();
private @Context @NonNullByDefault({}) UriInfo uriInfo;
+ private @Nullable Date cacheableListLastModified = null;
@Activate
public RuleResource( //
@@ -139,6 +150,13 @@ public RuleResource( //
this.ruleManager = ruleManager;
this.ruleRegistry = ruleRegistry;
this.managedRuleProvider = managedRuleProvider;
+
+ this.ruleRegistry.addRegistryChangeListener(resetLastModifiedChangeListener);
+ }
+
+ @Deactivate
+ void deactivate() {
+ this.ruleRegistry.removeRegistryChangeListener(resetLastModifiedChangeListener);
}
@GET
@@ -146,13 +164,38 @@ public RuleResource( //
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getRules", summary = "Get available rules, optionally filtered by tags and/or prefix.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedRuleDTO.class)))) })
- public Response get(@Context SecurityContext securityContext, @QueryParam("prefix") final @Nullable String prefix,
- @QueryParam("tags") final @Nullable List tags,
- @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) {
+ public Response get(@Context SecurityContext securityContext, @Context Request request,
+ @QueryParam("prefix") final @Nullable String prefix, @QueryParam("tags") final @Nullable List tags,
+ @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary,
+ @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and honors the If-Modified-Since header, all other parameters are ignored") boolean staticDataOnly) {
+
if ((summary == null || !summary) && !securityContext.isUserInRole(Role.ADMIN)) {
// users may only access the summary
return JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Authentication required");
}
+
+ if (staticDataOnly) {
+ if (cacheableListLastModified != null) {
+ Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(cacheableListLastModified);
+ if (responseBuilder != null) {
+ // send 304 Not Modified
+ return responseBuilder.build();
+ }
+ } else {
+ cacheableListLastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
+ }
+
+ Stream rules = ruleRegistry.stream()
+ .map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider));
+
+ CacheControl cc = new CacheControl();
+ cc.setMustRevalidate(true);
+ cc.setPrivate(true);
+ rules = dtoMapper.limitToFields(rules, "uid,templateUID,name,visibility,description,tags,editable");
+ return Response.ok(new Stream2JSONInputStream(rules)).lastModified(cacheableListLastModified)
+ .cacheControl(cc).build();
+ }
+
// match all
Predicate p = r -> true;
@@ -331,7 +374,14 @@ public Response runNow(@PathParam("ruleUID") @Parameter(description = "ruleUID")
ruleUID);
return Response.status(Status.NOT_FOUND).build();
} else {
- ruleManager.runNow(ruleUID, false, context);
+ if (context == null || context.isEmpty()) {
+ // only add event to context if no context given, otherwise it might interfere with the intention of the
+ // provided context
+ Event event = AutomationEventFactory.createExecutionEvent(ruleUID, null, "manual");
+ ruleManager.runNow(ruleUID, false, Map.of("event", event));
+ } else {
+ ruleManager.runNow(ruleUID, false, context);
+ }
return Response.ok().build();
}
}
@@ -558,4 +608,26 @@ public Response setModuleConfigParam(@PathParam("ruleUID") @Parameter(descriptio
return null;
}
}
+
+ private void resetStaticListLastModified() {
+ cacheableListLastModified = null;
+ }
+
+ private class ResetLastModifiedChangeListener implements RegistryChangeListener {
+
+ @Override
+ public void added(Rule element) {
+ resetStaticListLastModified();
+ }
+
+ @Override
+ public void removed(Rule element) {
+ resetStaticListLastModified();
+ }
+
+ @Override
+ public void updated(Rule oldElement, Rule element) {
+ resetStaticListLastModified();
+ }
+ }
}
diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java
index 5bf9e44f67e..3a46a40c1e3 100644
--- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java
+++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java
@@ -44,7 +44,7 @@
import org.openhab.core.automation.type.Input;
import org.openhab.core.automation.type.ModuleTypeRegistry;
import org.openhab.core.automation.type.Output;
-import org.openhab.core.automation.util.ActionBuilder;
+import org.openhab.core.automation.util.ModuleBuilder;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
@@ -64,8 +64,6 @@
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -91,7 +89,6 @@
public class ThingActionsResource implements RESTResource {
public static final String PATH_THINGS = "actions";
- private final Logger logger = LoggerFactory.getLogger(ThingActionsResource.class);
private final LocaleService localeService;
private final ModuleTypeRegistry moduleTypeRegistry;
@@ -209,7 +206,7 @@ public Response executeThingAction(@PathParam("thingUID") @Parameter(description
Configuration configuration = new Configuration();
configuration.put("config", thingUID);
- Action action = ActionBuilder.createAction().withConfiguration(configuration)
+ Action action = ModuleBuilder.createAction().withConfiguration(configuration)
.withId(UUID.randomUUID().toString()).withTypeUID(actionTypeUid).build();
ModuleHandlerFactory moduleHandlerFactory = moduleHandlerFactories.stream()
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java
index 6382bec9a8f..6b1dde5aba7 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java
@@ -68,7 +68,7 @@ public interface RuleRegistry extends Registry {
* @throws IllegalStateException when the RuleManagedProvider is unavailable.
*/
@Override
- public Rule add(Rule rule);
+ Rule add(Rule rule);
/**
* Gets a collection of {@link Rule}s which shares same tag.
@@ -76,7 +76,7 @@ public interface RuleRegistry extends Registry {
* @param tag specifies a tag that will filter the rules.
* @return collection of {@link Rule}s having specified tag.
*/
- public Collection getByTag(@Nullable String tag);
+ Collection getByTag(@Nullable String tag);
/**
* Gets a collection of {@link Rule}s which has specified tags.
@@ -84,5 +84,5 @@ public interface RuleRegistry extends Registry {
* @param tags specifies tags that will filter the rules.
* @return collection of {@link Rule}s having specified tags.
*/
- public Collection getByTags(String... tags);
+ Collection getByTags(String... tags);
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java
index 45705704c74..c9f8c714e06 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java
@@ -60,8 +60,8 @@ public static List map(final @Nullable Collection typ
}
final List dtos = new ArrayList<>(types.size());
for (final ActionType type : types) {
- if (type instanceof CompositeActionType) {
- dtos.add(map((CompositeActionType) type));
+ if (type instanceof CompositeActionType actionType) {
+ dtos.add(map(actionType));
} else {
dtos.add(map(type));
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java
index f91c3112c02..df4a84e8a36 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java
@@ -61,8 +61,8 @@ public static List map(final @Nullable Collection dtos = new ArrayList<>(types.size());
for (final ConditionType type : types) {
- if (type instanceof CompositeConditionType) {
- dtos.add(map((CompositeConditionType) type));
+ if (type instanceof CompositeConditionType conditionType) {
+ dtos.add(map(conditionType));
} else {
dtos.add(map(type));
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.java
index 17095391c55..ea520d745ba 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.java
@@ -60,8 +60,8 @@ public static List map(final @Nullable Collection t
}
final List dtos = new ArrayList<>(types.size());
for (final TriggerType type : types) {
- if (type instanceof CompositeTriggerType) {
- dtos.add(map((CompositeTriggerType) type));
+ if (type instanceof CompositeTriggerType triggerType) {
+ dtos.add(map(triggerType));
} else {
dtos.add(map(type));
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/AutomationEventFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/AutomationEventFactory.java
new file mode 100644
index 00000000000..fe85cf3794f
--- /dev/null
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/AutomationEventFactory.java
@@ -0,0 +1,101 @@
+/**
+ * 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.automation.events;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.events.AbstractEventFactory;
+import org.openhab.core.events.Event;
+import org.openhab.core.events.EventFactory;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class is a factory that creates Timer and Execution Events.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = EventFactory.class, immediate = true)
+public class AutomationEventFactory extends AbstractEventFactory {
+ private static final String MODULE_IDENTIFIER = "{moduleId}";
+ private static final String TIMER_EVENT_TOPIC = "openhab/timer/" + MODULE_IDENTIFIER + "/triggered";
+ private static final String EXECUTION_EVENT_TOPIC = "openhab/execution/" + MODULE_IDENTIFIER + "/triggered";
+
+ private final Logger logger = LoggerFactory.getLogger(AutomationEventFactory.class);
+
+ private static final Set SUPPORTED_TYPES = Set.of(TimerEvent.TYPE, ExecutionEvent.TYPE);
+
+ public AutomationEventFactory() {
+ super(SUPPORTED_TYPES);
+ }
+
+ @Override
+ protected Event createEventByType(String eventType, String topic, String payload, @Nullable String source)
+ throws Exception {
+ logger.trace("creating ruleEvent of type: {}", eventType);
+ if (TimerEvent.TYPE.equals(eventType)) {
+ return createTimerEvent(topic, payload, Objects.requireNonNullElse(source, ""));
+ } else if (ExecutionEvent.TYPE.equals(eventType)) {
+ if (source == null) {
+ throw new IllegalArgumentException("'source' must not be null for execution events");
+ }
+ return createExecutionEvent(topic, payload, source);
+ }
+ throw new IllegalArgumentException("The event type '" + eventType + "' is not supported by this factory.");
+ }
+
+ private Event createTimerEvent(String topic, String payload, String source) {
+ return new TimerEvent(topic, payload, source);
+ }
+
+ private Event createExecutionEvent(String topic, String payload, String source) {
+ return new ExecutionEvent(topic, payload, source);
+ }
+
+ /**
+ * Creates a {@link TimerEvent}
+ *
+ * @param moduleId the module type id of this event
+ * @param label The label (or id) of this object
+ * @param configuration the configuration of the trigger
+ * @return the created event
+ */
+ public static TimerEvent createTimerEvent(String moduleId, @Nullable String label,
+ Map configuration) {
+ String topic = TIMER_EVENT_TOPIC.replace(MODULE_IDENTIFIER, moduleId);
+ String payload = serializePayload(configuration);
+ return new TimerEvent(topic, payload, label);
+ }
+
+ /**
+ * Creates an {@link ExecutionEvent}
+ *
+ * @param moduleId the module type id of this event
+ * @param payload A map with additional information like preceding events when rules are called from other rules
+ * (optional)
+ * @param source The source of this event (e.g. "script" or "manual")
+ * @return the created event
+ */
+ public static ExecutionEvent createExecutionEvent(String moduleId, @Nullable Map payload,
+ String source) {
+ String topic = EXECUTION_EVENT_TOPIC.replace(MODULE_IDENTIFIER, moduleId);
+ String serializedPayload = serializePayload(Objects.requireNonNullElse(payload, Map.of()));
+ return new ExecutionEvent(topic, serializedPayload, source);
+ }
+}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/ExecutionEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/ExecutionEvent.java
new file mode 100644
index 00000000000..20177bdec23
--- /dev/null
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/ExecutionEvent.java
@@ -0,0 +1,48 @@
+/**
+ * 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.automation.events;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.events.AbstractEvent;
+
+/**
+ * An {@link ExecutionEvent} is only used to notify rules when a script or the REST API trigger the run.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ExecutionEvent extends AbstractEvent {
+
+ public static final String TYPE = ExecutionEvent.class.getSimpleName();
+
+ /**
+ * Constructs a new rule execution event
+ *
+ * @param topic the topic of the event
+ * @param payload the payload of the event
+ * @param source the source of the event
+ */
+ public ExecutionEvent(String topic, String payload, String source) {
+ super(topic, payload, source);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public String toString() {
+ return "Execution triggered by " + getSource();
+ }
+}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/TimerEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/TimerEvent.java
new file mode 100644
index 00000000000..98db2e0bf7d
--- /dev/null
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/TimerEvent.java
@@ -0,0 +1,49 @@
+/**
+ * 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.automation.events;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.events.AbstractEvent;
+
+/**
+ * An {@link TimerEvent} is only used to notify rules when timer triggers fire.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class TimerEvent extends AbstractEvent {
+
+ public static final String TYPE = TimerEvent.class.getSimpleName();
+
+ /**
+ * Constructs a new timer event
+ *
+ * @param topic the topic of the event
+ * @param payload the payload of the event (contains trigger configuration)
+ * @param source the source of the event
+ */
+ public TimerEvent(String topic, String payload, @Nullable String source) {
+ super(topic, payload, source);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public String toString() {
+ return "Timer " + getSource() + " triggered.";
+ }
+}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java
index acd496dc71a..cde2d8a830b 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java
@@ -60,7 +60,7 @@ protected Map getHandlers() {
@Override
@SuppressWarnings("null")
public @Nullable ModuleHandler getHandler(Module module, String ruleUID) {
- String id = ruleUID + module.getId();
+ String id = getModuleIdentifier(ruleUID, module.getId());
ModuleHandler handler = handlers.get(id);
handler = handler == null ? internalCreate(module, ruleUID) : handler;
if (handler != null) {
@@ -80,8 +80,12 @@ protected Map getHandlers() {
@Override
public void ungetHandler(Module module, String ruleUID, ModuleHandler handler) {
- if (handlers.remove(ruleUID + module.getId(), handler)) {
+ if (handlers.remove(getModuleIdentifier(ruleUID, module.getId()), handler)) {
handler.dispose();
}
}
+
+ protected String getModuleIdentifier(String ruleUid, String moduleId) {
+ return ruleUid + "$" + moduleId;
+ }
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java
index 41047cf32e3..1bf0363151c 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java
@@ -37,5 +37,5 @@ public interface ConditionHandler extends ModuleHandler {
* and the inputs of the {@link Condition}.
* @return {@code true} if {@link Condition} is satisfied, {@code false} otherwise.
*/
- public boolean isSatisfied(Map context);
+ boolean isSatisfied(Map context);
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java
index 574e878c6a3..81a1ccc0a03 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java
@@ -28,7 +28,7 @@ public interface ModuleHandler {
/**
* The method is called by RuleManager to free resources when {@link ModuleHandler} is released.
*/
- public void dispose();
+ void dispose();
/**
* The callback is injected to the handler through this method.
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java
index 0320848237c..fc6b9199bf3 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java
@@ -44,7 +44,7 @@ public interface ModuleHandlerFactory {
*
* @return collection of module type UIDs supported by this factory.
*/
- public Collection getTypes();
+ Collection getTypes();
/**
* Creates a {@link ModuleHandler} instance needed for the operation of the {@link Module}s
@@ -55,7 +55,8 @@ public interface ModuleHandlerFactory {
* @return a new {@link ModuleHandler} instance, or {@code null} if the type of the
* {@code module} parameter is not supported by this factory.
*/
- public @Nullable ModuleHandler getHandler(Module module, String ruleUID);
+ @Nullable
+ ModuleHandler getHandler(Module module, String ruleUID);
/**
* Releases the {@link ModuleHandler} instance when it is not needed anymore
@@ -66,5 +67,5 @@ public interface ModuleHandlerFactory {
* @param ruleUID the identifier of the {@link Rule} that the given module belongs to.
* @param handler the {@link ModuleHandler} instance that is no longer needed.
*/
- public void ungetHandler(Module module, String ruleUID, ModuleHandler handler);
+ void ungetHandler(Module module, String ruleUID, ModuleHandler handler);
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TimeBasedConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TimeBasedConditionHandler.java
index 5f65b7914bc..6e571faae3f 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TimeBasedConditionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TimeBasedConditionHandler.java
@@ -30,5 +30,5 @@ public interface TimeBasedConditionHandler extends ConditionHandler {
* @param time The time to check.
* @return
true
if and only if the given time satisfies this condition.
*/
- public abstract boolean isSatisfiedAt(ZonedDateTime time);
+ boolean isSatisfiedAt(ZonedDateTime time);
}
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 269ec3588c0..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);
}
}
}
@@ -555,12 +550,12 @@ protected void postRuleStatusInfoEvent(String ruleUID, RuleStatusInfo statusInfo
try {
ModuleHandler moduleHandler = getModuleHandler(m, rUID);
if (moduleHandler != null) {
- if (mm instanceof WrappedAction) {
- ((WrappedAction) mm).setModuleHandler((ActionHandler) moduleHandler);
- } else if (mm instanceof WrappedCondition) {
- ((WrappedCondition) mm).setModuleHandler((ConditionHandler) moduleHandler);
- } else if (mm instanceof WrappedTrigger) {
- ((WrappedTrigger) mm).setModuleHandler((TriggerHandler) moduleHandler);
+ if (mm instanceof WrappedAction action) {
+ action.setModuleHandler((ActionHandler) moduleHandler);
+ } else if (mm instanceof WrappedCondition condition) {
+ condition.setModuleHandler((ConditionHandler) moduleHandler);
+ } else if (mm instanceof WrappedTrigger trigger) {
+ trigger.setModuleHandler((TriggerHandler) moduleHandler);
}
} else {
if (sb == null) {
@@ -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) {
@@ -1183,10 +1178,10 @@ private void executeActions(WrappedRule rule, boolean stopOnFirstFail) {
updateContext(ruleUID, action.getId(), outputs);
}
} catch (Throwable t) {
- String errMessage = "Fail to execute action: " + action.getId();
+ String errMessage = "Failed to execute action: " + action.getId() + "(" + t.getMessage() + ")";
if (stopOnFirstFail) {
- RuntimeException re = new RuntimeException(errMessage, t);
- throw re;
+ logger.debug("Action {}-{} threw an exception: ", ruleUID, action.getId(), t);
+ throw new RuntimeException(errMessage, t);
} else {
logger.warn(errMessage, t);
}
@@ -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/RuleExecutionSimulator.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleExecutionSimulator.java
index 4875664bced..b3847fd470f 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleExecutionSimulator.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleExecutionSimulator.java
@@ -93,9 +93,8 @@ private List simulateExecutionsForRule(Rule rule, ZonedDateTime f
TriggerHandler triggerHandler = (TriggerHandler) this.ruleEngine.getModuleHandler(trigger, rule.getUID());
// Only triggers that are time-based will be considered within the simulation
- if (triggerHandler instanceof TimeBasedTriggerHandler) {
- SchedulerTemporalAdjuster temporalAdjuster = ((TimeBasedTriggerHandler) triggerHandler)
- .getTemporalAdjuster();
+ if (triggerHandler instanceof TimeBasedTriggerHandler handler) {
+ SchedulerTemporalAdjuster temporalAdjuster = handler.getTemporalAdjuster();
if (temporalAdjuster != null) {
executions.addAll(simulateExecutionsForCronBasedRule(rule, from, until, temporalAdjuster));
}
@@ -143,8 +142,7 @@ private boolean checkConditions(Rule rule, ZonedDateTime current) {
rule.getUID());
// Only conditions that are time based are checked
- if (conditionHandler instanceof TimeBasedConditionHandler
- && !((TimeBasedConditionHandler) conditionHandler).isSatisfiedAt(current)) {
+ if (conditionHandler instanceof TimeBasedConditionHandler handler && !handler.isSatisfiedAt(current)) {
return false;
}
}
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 584c4da295b..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
@@ -201,8 +201,8 @@ protected void unsetModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) {
*/
@Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC)
protected void setTemplateRegistry(TemplateRegistry templateRegistry) {
- if (templateRegistry instanceof RuleTemplateRegistry) {
- this.templateRegistry = (RuleTemplateRegistry) templateRegistry;
+ if (templateRegistry instanceof RuleTemplateRegistry registry) {
+ this.templateRegistry = registry;
templateRegistry.addRegistryChangeListener(this);
}
}
@@ -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));
@@ -557,9 +557,7 @@ private void processValue(@Nullable Object configValue, ConfigDescriptionParamet
if (configValue != null) {
Type type = configParameter.getType();
if (configParameter.isMultiple()) {
- if (configValue instanceof List) {
- @SuppressWarnings("rawtypes")
- List lConfigValues = (List) configValue;
+ if (configValue instanceof List lConfigValues) {
for (Object value : lConfigValues) {
if (!checkType(type, value)) {
throw new IllegalArgumentException("Unexpected value for configuration property \""
@@ -612,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.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java
index 02c694d6edf..12762f0de99 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java
@@ -71,7 +71,7 @@ public AutomationCommandList(String command, String[] params, int adminType,
*/
@Override
public String execute() {
- if (parsingResult != SUCCESS) {
+ if (!parsingResult.equals(SUCCESS)) {
return parsingResult;
}
if (providerType == AutomationCommands.MODULE_TYPE_PROVIDER) {
@@ -293,7 +293,7 @@ private Collection getRuleByFilter(Map list) {
return rules;
} else {
for (String ruleUID : list.values()) {
- if (ruleUID.indexOf(id) > -1) {
+ if (ruleUID.contains(id)) {
rules.add(autoCommands.getRule(ruleUID));
}
}
@@ -388,14 +388,14 @@ private void addCollection(Collection collection, Map list) {
Iterator i = collection.iterator();
while (i.hasNext()) {
Object element = i.next();
- if (element instanceof ModuleType) {
- list.put(((ModuleType) element).getUID(), element);
+ if (element instanceof ModuleType type) {
+ list.put(type.getUID(), element);
}
- if (element instanceof RuleTemplate) {
- list.put(((RuleTemplate) element).getUID(), element);
+ if (element instanceof RuleTemplate template) {
+ list.put(template.getUID(), element);
}
- if (element instanceof Rule) {
- list.put(((Rule) element).getUID(), element);
+ if (element instanceof Rule rule) {
+ list.put(rule.getUID(), element);
}
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java
index aafc3da3272..ceb9de0c07f 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java
@@ -231,12 +231,12 @@ static String printTemplate(Template template) {
templateProperty.set(0, TAGS);
templateProperty.set(1, getTagsRecord(template.getTags()));
templateContent.add(Utils.getRow(columnWidths, templateProperty));
- if (template instanceof RuleTemplate) {
+ if (template instanceof RuleTemplate ruleTemplate) {
templateContent.addAll(collectRecords(columnWidths, CONFIGURATION_DESCRIPTIONS,
- getConfigurationDescriptionRecords(((RuleTemplate) template).getConfigurationDescriptions())));
- templateContent.addAll(collectRecords(columnWidths, TRIGGERS, ((RuleTemplate) template).getTriggers()));
- templateContent.addAll(collectRecords(columnWidths, CONDITIONS, ((RuleTemplate) template).getConditions()));
- templateContent.addAll(collectRecords(columnWidths, ACTIONS, ((RuleTemplate) template).getActions()));
+ getConfigurationDescriptionRecords(ruleTemplate.getConfigurationDescriptions())));
+ templateContent.addAll(collectRecords(columnWidths, TRIGGERS, ruleTemplate.getTriggers()));
+ templateContent.addAll(collectRecords(columnWidths, CONDITIONS, ruleTemplate.getConditions()));
+ templateContent.addAll(collectRecords(columnWidths, ACTIONS, ruleTemplate.getActions()));
}
return Utils.getTableContent(TABLE_WIDTH, columnWidths, templateContent, titleRow);
}
@@ -278,27 +278,24 @@ static String printModuleType(ModuleType moduleType) {
moduleTypeContent.addAll(collectRecords(columnWidths, CONFIGURATION_DESCRIPTIONS,
getConfigurationDescriptionRecords(moduleType.getConfigurationDescriptions())));
- if (moduleType instanceof TriggerType) {
- moduleTypeContent.addAll(collectRecords(columnWidths, OUTPUTS, ((TriggerType) moduleType).getOutputs()));
+ if (moduleType instanceof TriggerType type) {
+ moduleTypeContent.addAll(collectRecords(columnWidths, OUTPUTS, type.getOutputs()));
}
- if (moduleType instanceof ConditionType) {
- moduleTypeContent.addAll(collectRecords(columnWidths, INPUTS, ((ConditionType) moduleType).getInputs()));
+ if (moduleType instanceof ConditionType type) {
+ moduleTypeContent.addAll(collectRecords(columnWidths, INPUTS, type.getInputs()));
}
- if (moduleType instanceof ActionType) {
- moduleTypeContent.addAll(collectRecords(columnWidths, INPUTS, ((ActionType) moduleType).getInputs()));
- moduleTypeContent.addAll(collectRecords(columnWidths, OUTPUTS, ((ActionType) moduleType).getOutputs()));
+ if (moduleType instanceof ActionType type) {
+ moduleTypeContent.addAll(collectRecords(columnWidths, INPUTS, type.getInputs()));
+ moduleTypeContent.addAll(collectRecords(columnWidths, OUTPUTS, type.getOutputs()));
}
- if (moduleType instanceof CompositeTriggerType) {
- moduleTypeContent
- .addAll(collectRecords(columnWidths, CHILDREN, ((CompositeTriggerType) moduleType).getChildren()));
+ if (moduleType instanceof CompositeTriggerType type) {
+ moduleTypeContent.addAll(collectRecords(columnWidths, CHILDREN, type.getChildren()));
}
- if (moduleType instanceof CompositeConditionType) {
- moduleTypeContent.addAll(
- collectRecords(columnWidths, CHILDREN, ((CompositeConditionType) moduleType).getChildren()));
+ if (moduleType instanceof CompositeConditionType type) {
+ moduleTypeContent.addAll(collectRecords(columnWidths, CHILDREN, type.getChildren()));
}
- if (moduleType instanceof CompositeActionType) {
- moduleTypeContent
- .addAll(collectRecords(columnWidths, CHILDREN, ((CompositeActionType) moduleType).getChildren()));
+ if (moduleType instanceof CompositeActionType type) {
+ moduleTypeContent.addAll(collectRecords(columnWidths, CHILDREN, type.getChildren()));
}
return Utils.getTableContent(TABLE_WIDTH, columnWidths, moduleTypeContent, titleRow);
}
@@ -342,14 +339,14 @@ private static List collectRecords(int[] columnWidths, String prop, Coll
values.add("");
if (list != null && !list.isEmpty()) {
for (Object element : list) {
- if (element instanceof String) {
- res.add(Utils.getColumn(columnWidths[0], values.get(0)) + (String) element);
+ if (element instanceof String string) {
+ res.add(Utils.getColumn(columnWidths[0], values.get(0)) + string);
if (isFirst) {
isFirst = false;
values.set(0, "");
}
- } else if (element instanceof Module) {
- List moduleRecords = getModuleRecords((Module) element);
+ } else if (element instanceof Module module) {
+ List moduleRecords = getModuleRecords(module);
for (String elementRecord : moduleRecords) {
res.add(Utils.getColumn(columnWidths[0], values.get(0)) + elementRecord);
if (isFirst) {
@@ -365,14 +362,14 @@ private static List collectRecords(int[] columnWidths, String prop, Coll
isFirst = false;
}
values.set(0, "");
- if (element instanceof FilterCriteria) {
- values.set(1, getFilterCriteriaRecord((FilterCriteria) element));
- } else if (element instanceof ParameterOption) {
- values.set(1, getParameterOptionRecord((ParameterOption) element));
- } else if (element instanceof Input) {
- values.set(1, getInputRecord((Input) element));
- } else if (element instanceof Output) {
- values.set(1, getOutputRecord((Output) element));
+ if (element instanceof FilterCriteria criteria) {
+ values.set(1, getFilterCriteriaRecord(criteria));
+ } else if (element instanceof ParameterOption option) {
+ values.set(1, getParameterOptionRecord(option));
+ } else if (element instanceof Input input) {
+ values.set(1, getInputRecord(input));
+ } else if (element instanceof Output output) {
+ values.set(1, getOutputRecord(output));
} else if (element instanceof Entry) {
values.set(1, " " + ((Entry) element).getKey() + " = \""
+ ((Entry) element).getValue().toString() + "\"");
@@ -425,11 +422,11 @@ private static List getModuleRecords(Module module) {
moduleContent.addAll(
collectRecords(columnWidths, CONFIGURATION, module.getConfiguration().getProperties().entrySet()));
Map inputs = null;
- if (module instanceof Condition) {
- inputs = ((Condition) module).getInputs();
+ if (module instanceof Condition condition) {
+ inputs = condition.getInputs();
}
- if (module instanceof Action) {
- inputs = ((Action) module).getInputs();
+ if (module instanceof Action action) {
+ inputs = action.getInputs();
}
if (inputs != null && !inputs.isEmpty()) {
moduleContent.addAll(collectRecords(columnWidths, INPUTS, new ArrayList<>(inputs.entrySet())));
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java
index 0c5d730f5ad..1f9e7a97220 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java
@@ -92,7 +92,7 @@ public Collection getTypes() {
@SuppressWarnings({ "unchecked" })
@Override
public void ungetHandler(Module module, String childModulePrefix, ModuleHandler handler) {
- ModuleHandler handlerOfModule = getHandlers().get(childModulePrefix + module.getId());
+ ModuleHandler handlerOfModule = getHandlers().get(getModuleIdentifier(childModulePrefix, module.getId()));
if (handlerOfModule instanceof AbstractCompositeModuleHandler) {
AbstractCompositeModuleHandler h = (AbstractCompositeModuleHandler) handlerOfModule;
Set modules = h.moduleHandlerMap.keySet();
@@ -114,8 +114,7 @@ public void ungetHandler(Module module, String childModulePrefix, ModuleHandler
private String getRuleId(String childModulePrefix) {
int i = childModulePrefix.indexOf(':');
- String ruleId = i != -1 ? childModulePrefix.substring(0, i) : childModulePrefix;
- return ruleId;
+ return i != -1 ? childModulePrefix.substring(0, i) : childModulePrefix;
}
@Override
@@ -123,29 +122,26 @@ private String getRuleId(String childModulePrefix) {
ModuleHandler handler = null;
String moduleType = module.getTypeUID();
ModuleType mt = mtRegistry.get(moduleType);
- if (mt instanceof CompositeTriggerType) {
- List childModules = ((CompositeTriggerType) mt).getChildren();
+ if (mt instanceof CompositeTriggerType type) {
+ List childModules = type.getChildren();
LinkedHashMap mapModuleToHandler = getChildHandlers(module.getId(),
module.getConfiguration(), childModules, ruleUID);
if (mapModuleToHandler != null) {
- handler = new CompositeTriggerHandler((Trigger) module, (CompositeTriggerType) mt, mapModuleToHandler,
- ruleUID);
+ handler = new CompositeTriggerHandler((Trigger) module, type, mapModuleToHandler, ruleUID);
}
- } else if (mt instanceof CompositeConditionType) {
- List childModules = ((CompositeConditionType) mt).getChildren();
+ } else if (mt instanceof CompositeConditionType type) {
+ List childModules = type.getChildren();
LinkedHashMap mapModuleToHandler = getChildHandlers(module.getId(),
module.getConfiguration(), childModules, ruleUID);
if (mapModuleToHandler != null) {
- handler = new CompositeConditionHandler((Condition) module, (CompositeConditionType) mt,
- mapModuleToHandler, ruleUID);
+ handler = new CompositeConditionHandler((Condition) module, type, mapModuleToHandler, ruleUID);
}
- } else if (mt instanceof CompositeActionType) {
- List childModules = ((CompositeActionType) mt).getChildren();
+ } else if (mt instanceof CompositeActionType type) {
+ List childModules = type.getChildren();
LinkedHashMap mapModuleToHandler = getChildHandlers(module.getId(),
module.getConfiguration(), childModules, ruleUID);
if (mapModuleToHandler != null) {
- handler = new CompositeActionHandler((Action) module, (CompositeActionType) mt, mapModuleToHandler,
- ruleUID);
+ handler = new CompositeActionHandler((Action) module, type, mapModuleToHandler, ruleUID);
}
}
if (handler != null) {
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java
index 84f01b9bab7..f69cb05cba7 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java
@@ -42,6 +42,7 @@
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.items.ItemRegistry;
+import org.openhab.core.service.StartLevelService;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@@ -78,14 +79,17 @@ public class CoreModuleHandlerFactory extends BaseModuleHandlerFactory implement
private final TimeZoneProvider timeZoneProvider;
private final EventPublisher eventPublisher;
private final BundleContext bundleContext;
+ private final StartLevelService startLevelService;
@Activate
public CoreModuleHandlerFactory(BundleContext bundleContext, final @Reference EventPublisher eventPublisher,
- final @Reference ItemRegistry itemRegistry, final @Reference TimeZoneProvider timeZoneProvider) {
+ final @Reference ItemRegistry itemRegistry, final @Reference TimeZoneProvider timeZoneProvider,
+ final @Reference StartLevelService startLevelService) {
this.bundleContext = bundleContext;
this.eventPublisher = eventPublisher;
this.itemRegistry = itemRegistry;
this.timeZoneProvider = timeZoneProvider;
+ this.startLevelService = startLevelService;
}
@Override
@@ -103,48 +107,47 @@ public Collection getTypes() {
protected synchronized @Nullable ModuleHandler internalCreate(final Module module, final String ruleUID) {
logger.trace("create {} -> {} : {}", module.getId(), module.getTypeUID(), ruleUID);
final String moduleTypeUID = module.getTypeUID();
- if (module instanceof Trigger) {
+ if (module instanceof Trigger trigger) {
// Handle triggers
if (GenericEventTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new GenericEventTriggerHandler((Trigger) module, bundleContext);
+ return new GenericEventTriggerHandler(trigger, bundleContext);
} else if (ChannelEventTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new ChannelEventTriggerHandler((Trigger) module, bundleContext);
+ return new ChannelEventTriggerHandler(trigger, bundleContext);
} else if (ItemCommandTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new ItemCommandTriggerHandler((Trigger) module, ruleUID, bundleContext, itemRegistry);
+ return new ItemCommandTriggerHandler(trigger, ruleUID, bundleContext, itemRegistry);
} else if (SystemTriggerHandler.STARTLEVEL_MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new SystemTriggerHandler((Trigger) module, bundleContext);
+ return new SystemTriggerHandler(trigger, bundleContext, startLevelService);
} else if (ThingStatusTriggerHandler.CHANGE_MODULE_TYPE_ID.equals(moduleTypeUID)
|| ThingStatusTriggerHandler.UPDATE_MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new ThingStatusTriggerHandler((Trigger) module, bundleContext);
+ return new ThingStatusTriggerHandler(trigger, bundleContext);
} else if (ItemStateTriggerHandler.CHANGE_MODULE_TYPE_ID.equals(moduleTypeUID)
|| ItemStateTriggerHandler.UPDATE_MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new ItemStateTriggerHandler((Trigger) module, ruleUID, bundleContext, itemRegistry);
+ return new ItemStateTriggerHandler(trigger, ruleUID, bundleContext, itemRegistry);
} else if (GroupCommandTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new GroupCommandTriggerHandler((Trigger) module, ruleUID, bundleContext, itemRegistry);
+ return new GroupCommandTriggerHandler(trigger, ruleUID, bundleContext, itemRegistry);
} else if (GroupStateTriggerHandler.CHANGE_MODULE_TYPE_ID.equals(moduleTypeUID)
|| GroupStateTriggerHandler.UPDATE_MODULE_TYPE_ID.equals(moduleTypeUID)) {
- return new GroupStateTriggerHandler((Trigger) module, ruleUID, bundleContext, itemRegistry);
+ return new GroupStateTriggerHandler(trigger, ruleUID, bundleContext, itemRegistry);
}
- } else if (module instanceof Condition) {
+ } else if (module instanceof Condition condition) {
// Handle conditions
if (ItemStateConditionHandler.ITEM_STATE_CONDITION.equals(moduleTypeUID)) {
- return new ItemStateConditionHandler((Condition) module, ruleUID, bundleContext, itemRegistry,
- timeZoneProvider);
+ return new ItemStateConditionHandler(condition, ruleUID, bundleContext, itemRegistry, timeZoneProvider);
} else if (GenericEventConditionHandler.MODULETYPE_ID.equals(moduleTypeUID)) {
- return new GenericEventConditionHandler((Condition) module);
+ return new GenericEventConditionHandler(condition);
} else if (CompareConditionHandler.MODULE_TYPE.equals(moduleTypeUID)) {
- return new CompareConditionHandler((Condition) module);
+ return new CompareConditionHandler(condition);
}
- } else if (module instanceof Action) {
+ } else if (module instanceof Action action) {
// Handle actions
if (ItemCommandActionHandler.ITEM_COMMAND_ACTION.equals(moduleTypeUID)) {
- return new ItemCommandActionHandler((Action) module, eventPublisher, itemRegistry);
+ return new ItemCommandActionHandler(action, eventPublisher, itemRegistry);
} else if (ItemStateUpdateActionHandler.ITEM_STATE_UPDATE_ACTION.equals(moduleTypeUID)) {
- return new ItemStateUpdateActionHandler((Action) module, eventPublisher, itemRegistry);
+ return new ItemStateUpdateActionHandler(action, eventPublisher, itemRegistry);
} else if (RuleEnablementActionHandler.UID.equals(moduleTypeUID)) {
- return new RuleEnablementActionHandler((Action) module);
+ return new RuleEnablementActionHandler(action);
} else if (RunRuleActionHandler.UID.equals(moduleTypeUID)) {
- return new RunRuleActionHandler((Action) module);
+ return new RunRuleActionHandler(action);
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/EphemerisModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/EphemerisModuleHandlerFactory.java
index 4ec5c9491aa..b2f0335fa1d 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/EphemerisModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/EphemerisModuleHandlerFactory.java
@@ -70,14 +70,14 @@ public Collection getTypes() {
final String moduleTypeUID = module.getTypeUID();
logger.trace("create {} -> {} : {}", module.getId(), moduleTypeUID, ruleUID);
- if (module instanceof Condition) {
+ if (module instanceof Condition condition) {
switch (moduleTypeUID) {
case EphemerisConditionHandler.HOLIDAY_MODULE_TYPE_ID:
case EphemerisConditionHandler.NOT_HOLIDAY_MODULE_TYPE_ID:
case EphemerisConditionHandler.WEEKEND_MODULE_TYPE_ID:
case EphemerisConditionHandler.WEEKDAY_MODULE_TYPE_ID:
case EphemerisConditionHandler.DAYSET_MODULE_TYPE_ID:
- return new EphemerisConditionHandler((Condition) module, ephemerisManager);
+ return new EphemerisConditionHandler(condition, ephemerisManager);
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java
index 5e2c7b7c59d..0d5fb51db2f 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java
@@ -66,8 +66,7 @@ public AnnotationActionHandler(Action module, ActionType mt, Method method, Obje
for (int i = 0; i < annotations.length; i++) {
Annotation[] annotationsOnParam = annotations[i];
if (annotationsOnParam != null && annotationsOnParam.length == 1) {
- if (annotationsOnParam[0] instanceof ActionInput) {
- ActionInput inputAnnotation = (ActionInput) annotationsOnParam[0];
+ if (annotationsOnParam[0] instanceof ActionInput inputAnnotation) {
// check if the moduleType has a configdescription with this input
if (hasInput(moduleType, inputAnnotation.name())) {
args.add(i, context.get(inputAnnotation.name()));
@@ -106,8 +105,8 @@ public AnnotationActionHandler(Action module, ActionType mt, Method method, Obje
method, moduleType.getUID(), ex.getMessage());
}
// we allow simple data types as return values and put them under the context key "result".
- } else if (result instanceof Boolean) {
- output.put(MODULE_RESULT, (boolean) result);
+ } else if (result instanceof Boolean boolean1) {
+ output.put(MODULE_RESULT, boolean1);
} else if (result instanceof String) {
output.put(MODULE_RESULT, result);
} else if (result instanceof Integer) {
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java
index b45d75f8e68..1d727083b50 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java
@@ -77,8 +77,7 @@ public void receive(Event event) {
@Override
public boolean apply(Event event) {
boolean eventMatches = false;
- if (event instanceof ChannelTriggeredEvent) {
- ChannelTriggeredEvent cte = (ChannelTriggeredEvent) event;
+ if (event instanceof ChannelTriggeredEvent cte) {
if (channelUID.equals(cte.getChannel())) {
String eventOnChannel = this.eventOnChannel;
logger.trace("->FILTER: {}:{}", cte.getEvent(), eventOnChannel);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java
index d1a1f403831..bfcc52c47ed 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java
@@ -51,12 +51,11 @@ public CompareConditionHandler(Condition module) {
@Override
public boolean isSatisfied(Map context) {
Object operatorObj = this.module.getConfiguration().get(OPERATOR);
- String operator = (operatorObj != null && operatorObj instanceof String) ? (String) operatorObj : null;
+ String operator = (operatorObj != null && operatorObj instanceof String s) ? s : null;
Object rightObj = this.module.getConfiguration().get(RIGHT_OP);
- String rightOperandString = (rightObj != null && rightObj instanceof String) ? (String) rightObj : null;
+ String rightOperandString = (rightObj != null && rightObj instanceof String s) ? s : null;
Object leftObjFieldNameObj = this.module.getConfiguration().get(INPUT_LEFT_FIELD);
- String leftObjectFieldName = (leftObjFieldNameObj != null && leftObjFieldNameObj instanceof String)
- ? (String) leftObjFieldNameObj
+ String leftObjectFieldName = (leftObjFieldNameObj != null && leftObjFieldNameObj instanceof String s) ? s
: null;
if (rightOperandString == null || operator == null) {
return false;
@@ -125,8 +124,8 @@ public boolean isSatisfied(Map context) {
}
case "matches":
// Matcher...
- if (toCompare instanceof String && rightValue instanceof String) {
- return ((String) toCompare).matches((String) rightValue);
+ if (toCompare instanceof String string1 && rightValue instanceof String string2) {
+ return string1.matches(string2);
}
default:
break;
@@ -157,8 +156,8 @@ private int compare(Object a, Object b) throws UncomparableException {
if ("null".equals(rightOperandString2)) {
return rightOperandString2;
}
- if (toCompare instanceof State) {
- return TypeParser.parseState(List.of(((State) toCompare).getClass()), rightOperandString2);
+ if (toCompare instanceof State state) {
+ return TypeParser.parseState(List.of(state.getClass()), rightOperandString2);
} else if (toCompare instanceof Integer) {
return Integer.parseInt(rightOperandString2);
} else if (toCompare instanceof String) {
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java
index 756a09c600b..de01e4dabff 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java
@@ -14,12 +14,16 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.ModuleHandlerCallback;
import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.events.AutomationEventFactory;
+import org.openhab.core.automation.events.TimerEvent;
import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
import org.openhab.core.automation.handler.TimeBasedTriggerHandler;
import org.openhab.core.automation.handler.TriggerHandlerCallback;
@@ -114,7 +118,9 @@ public synchronized void setCallback(ModuleHandlerCallback callback) {
public void run() {
ModuleHandlerCallback callback = this.callback;
if (callback instanceof TriggerHandlerCallback triggerHandlerCallback) {
- triggerHandlerCallback.triggered(module);
+ TimerEvent event = AutomationEventFactory.createTimerEvent(module.getTypeUID(),
+ Objects.requireNonNullElse(module.getLabel(), module.getId()), Map.of(CONFIG_ITEM_NAME, itemName));
+ triggerHandlerCallback.triggered(module, Map.of("event", event));
} else {
logger.debug("Tried to trigger, but callback isn't available!");
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java
index cb12a8bdbac..9bcfab5acf4 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java
@@ -12,10 +12,15 @@
*/
package org.openhab.core.automation.internal.module.handler;
+import java.util.Map;
+import java.util.Objects;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.ModuleHandlerCallback;
import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.events.AutomationEventFactory;
+import org.openhab.core.automation.events.TimerEvent;
import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
import org.openhab.core.automation.handler.TimeBasedTriggerHandler;
import org.openhab.core.automation.handler.TriggerHandlerCallback;
@@ -80,7 +85,10 @@ public synchronized void dispose() {
@Override
public void run() {
if (callback != null) {
- ((TriggerHandlerCallback) callback).triggered(module);
+ TimerEvent event = AutomationEventFactory.createTimerEvent(module.getTypeUID(),
+ Objects.requireNonNullElse(module.getLabel(), module.getId()),
+ Map.of(CFG_CRON_EXPRESSION, expression));
+ ((TriggerHandlerCallback) callback).triggered(module, Map.of("event", event));
} else {
logger.debug("Tried to trigger, but callback isn't available!");
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupCommandTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupCommandTriggerHandler.java
index efced669185..b9d4a781ce5 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupCommandTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupCommandTriggerHandler.java
@@ -85,33 +85,35 @@ public Set getSubscribedEventTypes() {
@Override
public void receive(Event event) {
- if (event instanceof ItemAddedEvent) {
- if (groupName.equals(((ItemAddedEvent) event).getItem().name)) {
+ if (event instanceof ItemAddedEvent addedEvent) {
+ if (groupName.equals(addedEvent.getItem().name)) {
logger.info("Group '{}' needed for rule '{}' added. Trigger '{}' will now work.", groupName, ruleUID,
module.getId());
return;
}
- } else if (event instanceof ItemRemovedEvent) {
- if (groupName.equals(((ItemRemovedEvent) event).getItem().name)) {
+ } else if (event instanceof ItemRemovedEvent removedEvent) {
+ if (groupName.equals(removedEvent.getItem().name)) {
logger.warn("Group '{}' needed for rule '{}' removed. Trigger '{}' will no longer work.", groupName,
ruleUID, module.getId());
return;
}
}
- if (callback instanceof TriggerHandlerCallback) {
- TriggerHandlerCallback cb = (TriggerHandlerCallback) callback;
+ if (callback instanceof TriggerHandlerCallback cb) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(),
event.getTopic(), event.getType(), event.getPayload());
Map values = new HashMap<>();
- if (event instanceof ItemCommandEvent) {
- ItemCommandEvent icEvent = (ItemCommandEvent) event;
+ if (event instanceof ItemCommandEvent icEvent) {
String itemName = icEvent.getItemName();
Item item = itemRegistry.get(itemName);
+ Item group = itemRegistry.get(groupName);
if (item != null && item.getGroupNames().contains(groupName)) {
String command = this.command;
Command itemCommand = icEvent.getItemCommand();
if (command == null || command.equals(itemCommand.toFullString())) {
+ if (group != null) {
+ values.put("triggeringGroup", group);
+ }
values.put("triggeringItem", item);
values.put("command", itemCommand);
values.put("event", event);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java
index 8507ad5f4fa..5a069b87550 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GroupStateTriggerHandler.java
@@ -29,7 +29,7 @@
import org.openhab.core.items.events.ItemAddedEvent;
import org.openhab.core.items.events.ItemRemovedEvent;
import org.openhab.core.items.events.ItemStateChangedEvent;
-import org.openhab.core.items.events.ItemStateEvent;
+import org.openhab.core.items.events.ItemStateUpdatedEvent;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
@@ -72,7 +72,7 @@ public GroupStateTriggerHandler(Trigger module, String ruleUID, BundleContext bu
this.state = (String) module.getConfiguration().get(CFG_STATE);
this.previousState = (String) module.getConfiguration().get(CFG_PREVIOUS_STATE);
if (UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- this.types = Set.of(ItemStateEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE);
+ this.types = Set.of(ItemStateUpdatedEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE);
} else {
this.types = Set.of(ItemStateChangedEvent.TYPE, GroupItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE,
ItemRemovedEvent.TYPE);
@@ -95,48 +95,54 @@ public Set getSubscribedEventTypes() {
@Override
public void receive(Event event) {
- if (event instanceof ItemAddedEvent) {
- if (groupName.equals(((ItemAddedEvent) event).getItem().name)) {
+ if (event instanceof ItemAddedEvent addedEvent) {
+ if (groupName.equals(addedEvent.getItem().name)) {
logger.info("Group '{}' needed for rule '{}' added. Trigger '{}' will now work.", groupName, ruleUID,
module.getId());
return;
}
- } else if (event instanceof ItemRemovedEvent) {
- if (groupName.equals(((ItemRemovedEvent) event).getItem().name)) {
+ } else if (event instanceof ItemRemovedEvent removedEvent) {
+ if (groupName.equals(removedEvent.getItem().name)) {
logger.warn("Group '{}' needed for rule '{}' removed. Trigger '{}' will no longer work.", groupName,
ruleUID, module.getId());
return;
}
}
- if (callback instanceof TriggerHandlerCallback) {
- TriggerHandlerCallback cb = (TriggerHandlerCallback) callback;
+ if (callback instanceof TriggerHandlerCallback cb) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(),
event.getTopic(), event.getType(), event.getPayload());
- if (event instanceof ItemStateEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- ItemStateEvent isEvent = (ItemStateEvent) event;
+ if (event instanceof ItemStateUpdatedEvent isEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
String itemName = isEvent.getItemName();
Item item = itemRegistry.get(itemName);
+ Item group = itemRegistry.get(groupName);
if (item != null && item.getGroupNames().contains(groupName)) {
State state = isEvent.getItemState();
if ((this.state == null || state.toFullString().equals(this.state))) {
Map values = new HashMap<>();
+ if (group != null) {
+ values.put("triggeringGroup", group);
+ }
values.put("triggeringItem", item);
values.put("state", state);
values.put("event", event);
cb.triggered(this.module, values);
}
}
- } else if (event instanceof ItemStateChangedEvent && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- ItemStateChangedEvent iscEvent = (ItemStateChangedEvent) event;
+ } else if (event instanceof ItemStateChangedEvent iscEvent
+ && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
String itemName = iscEvent.getItemName();
Item item = itemRegistry.get(itemName);
+ Item group = itemRegistry.get(groupName);
if (item != null && item.getGroupNames().contains(groupName)) {
State state = iscEvent.getItemState();
State oldState = iscEvent.getOldItemState();
if (stateMatches(this.state, state) && stateMatches(this.previousState, oldState)) {
Map values = new HashMap<>();
+ if (group != null) {
+ values.put("triggeringGroup", group);
+ }
values.put("triggeringItem", item);
values.put("oldState", oldState);
values.put("newState", state);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java
index 0c51d2f93f2..e3224ea921e 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java
@@ -75,9 +75,9 @@ public ItemCommandActionHandler(Action module, EventPublisher eventPublisher, It
} else {
Object cmd = inputs.get(COMMAND);
- if (cmd instanceof Command) {
+ if (cmd instanceof Command command1) {
if (item.getAcceptedCommandTypes().contains(cmd.getClass())) {
- commandObj = (Command) cmd;
+ commandObj = command1;
}
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java
index 5283e2eda1b..ac9cb07ca63 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java
@@ -12,9 +12,7 @@
*/
package org.openhab.core.automation.internal.module.handler;
-import java.util.Dictionary;
import java.util.HashMap;
-import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
@@ -74,7 +72,6 @@ public ItemCommandTriggerHandler(Trigger module, String ruleUID, BundleContext b
this.bundleContext = bundleContext;
this.ruleUID = ruleUID;
this.types = Set.of(ItemCommandEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE);
- Dictionary properties = new Hashtable<>();
eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, null);
if (itemRegistry.get(itemName) == null) {
logger.warn("Item '{}' needed for rule '{}' is missing. Trigger '{}' will not work.", itemName, ruleUID,
@@ -94,14 +91,14 @@ public Set getSubscribedEventTypes() {
@Override
public void receive(Event event) {
- if (event instanceof ItemAddedEvent) {
- if (itemName.equals(((ItemAddedEvent) event).getItem().name)) {
+ if (event instanceof ItemAddedEvent addedEvent) {
+ if (itemName.equals(addedEvent.getItem().name)) {
logger.info("Item '{}' needed for rule '{}' added. Trigger '{}' will now work.", itemName, ruleUID,
module.getId());
return;
}
- } else if (event instanceof ItemRemovedEvent) {
- if (itemName.equals(((ItemRemovedEvent) event).getItem().name)) {
+ } else if (event instanceof ItemRemovedEvent removedEvent) {
+ if (itemName.equals(removedEvent.getItem().name)) {
logger.warn("Item '{}' needed for rule '{}' removed. Trigger '{}' will no longer work.", itemName,
ruleUID, module.getId());
return;
@@ -113,9 +110,9 @@ public void receive(Event event) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(),
event.getTopic(), event.getType(), event.getPayload());
Map values = new HashMap<>();
- if (event instanceof ItemCommandEvent) {
+ if (event instanceof ItemCommandEvent commandEvent) {
String command = this.command;
- Command itemCommand = ((ItemCommandEvent) event).getItemCommand();
+ Command itemCommand = commandEvent.getItemCommand();
if (command == null || command.equals(itemCommand.toFullString())) {
values.put("command", itemCommand);
values.put("event", event);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java
index fcbbb366c64..e55a91e6118 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java
@@ -106,14 +106,14 @@ public Set getSubscribedEventTypes() {
@Override
public void receive(Event event) {
- if (event instanceof ItemAddedEvent) {
- if (itemName.equals(((ItemAddedEvent) event).getItem().name)) {
+ if (event instanceof ItemAddedEvent addedEvent) {
+ if (itemName.equals(addedEvent.getItem().name)) {
logger.info("Item '{}' needed for rule '{}' added. Condition '{}' will now work.", itemName, ruleUID,
module.getId());
return;
}
- } else if (event instanceof ItemRemovedEvent) {
- if (itemName.equals(((ItemRemovedEvent) event).getItem().name)) {
+ } else if (event instanceof ItemRemovedEvent removedEvent) {
+ if (itemName.equals(removedEvent.getItem().name)) {
logger.warn("Item '{}' needed for rule '{}' removed. Condition '{}' will no longer work.", itemName,
ruleUID, module.getId());
return;
@@ -126,12 +126,13 @@ public boolean isSatisfied(Map inputs) {
String state = (String) module.getConfiguration().get(STATE);
String operator = (String) module.getConfiguration().get(OPERATOR);
if (operator == null || state == null || itemName == null) {
- logger.error("Module is not well configured: itemName={} operator={} state = {}", itemName, operator,
- state);
+ logger.error("Module is not well configured: itemName={} operator={} state = {} for rule {}", itemName,
+ operator, state, ruleUID);
return false;
}
try {
- logger.debug("ItemStateCondition '{}' checking if {} {} {}", module.getId(), itemName, operator, state);
+ logger.debug("ItemStateCondition '{}' checking if {} {} {} for rule {}", module.getId(), itemName, operator,
+ state, ruleUID);
switch (operator) {
case "=":
return equalsToItemState(itemName, state);
@@ -149,7 +150,7 @@ public boolean isSatisfied(Map inputs) {
return greaterThanOrEqualsToItemState(itemName, state);
}
} catch (ItemNotFoundException e) {
- logger.error("Item with name {} not found in ItemRegistry.", itemName);
+ logger.error("Item with name {} not found in ItemRegistry for condition of rule {}.", itemName, ruleUID);
}
return false;
}
@@ -159,35 +160,33 @@ private boolean lessThanOrEqualsToItemState(String itemName, String state) throw
Item item = itemRegistry.getItem(itemName);
State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state);
State itemState = item.getState();
- if (itemState instanceof DateTimeType) {
- ZonedDateTime itemTime = ((DateTimeType) itemState).getZonedDateTime();
+ if (itemState instanceof DateTimeType type) {
+ ZonedDateTime itemTime = type.getZonedDateTime();
ZonedDateTime compareTime = getCompareTime(state);
return itemTime.compareTo(compareTime) <= 0;
- } else if (itemState instanceof QuantityType) {
- QuantityType qtState = (QuantityType) itemState;
- if (compareState instanceof DecimalType) {
+ } else if (itemState instanceof QuantityType qtState) {
+ if (compareState instanceof DecimalType type) {
// allow compareState without unit -> implicitly assume its the same as the one from the
// state, but warn the user
if (!Units.ONE.equals(qtState.getUnit())) {
logger.warn(
- "Received a QuantityType state '{}' with unit for item {}, but the condition is defined as a plain number without unit ({}), please consider adding a unit to the condition.",
- qtState, itemName, state);
+ "Received a QuantityType state '{}' with unit for item {}, but the condition is defined as a plain number without unit ({}), please consider adding a unit to the condition for rule {}.",
+ qtState, itemName, state, ruleUID);
}
- return qtState.compareTo(
- new QuantityType<>(((DecimalType) compareState).toBigDecimal(), qtState.getUnit())) <= 0;
- } else if (compareState instanceof QuantityType) {
- return qtState.compareTo((QuantityType) compareState) <= 0;
+ return qtState.compareTo(new QuantityType<>(type.toBigDecimal(), qtState.getUnit())) <= 0;
+ } else if (compareState instanceof QuantityType type) {
+ return qtState.compareTo(type) <= 0;
}
- } else if (itemState instanceof PercentType && null != compareState) {
+ } else if (itemState instanceof PercentType type && null != compareState) {
// we need to handle PercentType first, otherwise the comparison will fail
PercentType percentState = compareState.as(PercentType.class);
if (null != percentState) {
- return ((PercentType) itemState).compareTo(percentState) <= 0;
+ return type.compareTo(percentState) <= 0;
}
- } else if (itemState instanceof DecimalType && null != compareState) {
+ } else if (itemState instanceof DecimalType type && null != compareState) {
DecimalType decimalState = compareState.as(DecimalType.class);
if (null != decimalState) {
- return ((DecimalType) itemState).compareTo(decimalState) <= 0;
+ return type.compareTo(decimalState) <= 0;
}
}
return false;
@@ -198,35 +197,33 @@ private boolean greaterThanOrEqualsToItemState(String itemName, String state) th
Item item = itemRegistry.getItem(itemName);
State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state);
State itemState = item.getState();
- if (itemState instanceof DateTimeType) {
- ZonedDateTime itemTime = ((DateTimeType) itemState).getZonedDateTime();
+ if (itemState instanceof DateTimeType type) {
+ ZonedDateTime itemTime = type.getZonedDateTime();
ZonedDateTime compareTime = getCompareTime(state);
return itemTime.compareTo(compareTime) >= 0;
- } else if (itemState instanceof QuantityType) {
- QuantityType qtState = (QuantityType) itemState;
- if (compareState instanceof DecimalType) {
+ } else if (itemState instanceof QuantityType qtState) {
+ if (compareState instanceof DecimalType type) {
// allow compareState without unit -> implicitly assume its the same as the one from the
// state, but warn the user
if (!Units.ONE.equals(qtState.getUnit())) {
logger.warn(
- "Received a QuantityType state '{}' with unit for item {}, but the condition is defined as a plain number without unit ({}), please consider adding a unit to the condition.",
- qtState, itemName, state);
+ "Received a QuantityType state '{}' with unit for item {}, but the condition is defined as a plain number without unit ({}), please consider adding a unit to the condition for rule {}.",
+ qtState, itemName, state, ruleUID);
}
- return qtState.compareTo(
- new QuantityType<>(((DecimalType) compareState).toBigDecimal(), qtState.getUnit())) >= 0;
- } else if (compareState instanceof QuantityType) {
- return qtState.compareTo((QuantityType) compareState) >= 0;
+ return qtState.compareTo(new QuantityType<>(type.toBigDecimal(), qtState.getUnit())) >= 0;
+ } else if (compareState instanceof QuantityType type) {
+ return qtState.compareTo(type) >= 0;
}
- } else if (itemState instanceof PercentType && null != compareState) {
+ } else if (itemState instanceof PercentType type && null != compareState) {
// we need to handle PercentType first, otherwise the comparison will fail
PercentType percentState = compareState.as(PercentType.class);
if (null != percentState) {
- return ((PercentType) itemState).compareTo(percentState) >= 0;
+ return type.compareTo(percentState) >= 0;
}
- } else if (itemState instanceof DecimalType && null != compareState) {
+ } else if (itemState instanceof DecimalType type && null != compareState) {
DecimalType decimalState = compareState.as(DecimalType.class);
if (null != decimalState) {
- return ((DecimalType) itemState).compareTo(decimalState) >= 0;
+ return type.compareTo(decimalState) >= 0;
}
}
return false;
@@ -236,17 +233,15 @@ private boolean equalsToItemState(String itemName, String state) throws ItemNotF
Item item = itemRegistry.getItem(itemName);
State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state);
State itemState = item.getState();
- if (itemState instanceof QuantityType && compareState instanceof DecimalType) {
- QuantityType> qtState = (QuantityType>) itemState;
+ if (itemState instanceof QuantityType qtState && compareState instanceof DecimalType type) {
if (Units.ONE.equals(qtState.getUnit())) {
// allow compareStates without unit if the unit of the state equals to ONE
- return itemState
- .equals(new QuantityType<>(((DecimalType) compareState).toBigDecimal(), qtState.getUnit()));
+ return itemState.equals(new QuantityType<>(type.toBigDecimal(), qtState.getUnit()));
} else {
// log a warning if the unit of the state differs from ONE
logger.warn(
- "Received a QuantityType state '{}' with unit for item {}, but the condition is defined as a plain number without unit ({}), comparison will fail unless a unit is added to the condition.",
- itemState, itemName, state);
+ "Received a QuantityType state '{}' with unit for item {}, but the condition is defined as a plain number without unit ({}), comparison will fail unless a unit is added to the condition for rule {}.",
+ itemState, itemName, state, ruleUID);
return false;
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java
index 702e1977731..820c641c262 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java
@@ -28,10 +28,11 @@
import org.openhab.core.events.TopicPrefixEventFilter;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.events.GroupItemStateChangedEvent;
+import org.openhab.core.items.events.GroupStateUpdatedEvent;
import org.openhab.core.items.events.ItemAddedEvent;
import org.openhab.core.items.events.ItemRemovedEvent;
import org.openhab.core.items.events.ItemStateChangedEvent;
-import org.openhab.core.items.events.ItemStateEvent;
+import org.openhab.core.items.events.ItemStateUpdatedEvent;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
@@ -77,7 +78,8 @@ public ItemStateTriggerHandler(Trigger module, String ruleUID, BundleContext bun
this.previousState = (String) module.getConfiguration().get(CFG_PREVIOUS_STATE);
this.ruleUID = ruleUID;
if (UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- this.types = Set.of(ItemStateEvent.TYPE, ItemAddedEvent.TYPE, ItemRemovedEvent.TYPE);
+ this.types = Set.of(ItemStateUpdatedEvent.TYPE, GroupStateUpdatedEvent.TYPE, ItemAddedEvent.TYPE,
+ ItemRemovedEvent.TYPE);
} else {
this.types = Set.of(ItemStateChangedEvent.TYPE, GroupItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE,
ItemRemovedEvent.TYPE);
@@ -103,14 +105,14 @@ public Set getSubscribedEventTypes() {
@Override
public void receive(Event event) {
- if (event instanceof ItemAddedEvent) {
- if (itemName.equals(((ItemAddedEvent) event).getItem().name)) {
+ if (event instanceof ItemAddedEvent addedEvent) {
+ if (itemName.equals(addedEvent.getItem().name)) {
logger.info("Item '{}' needed for rule '{}' added. Trigger '{}' will now work.", itemName, ruleUID,
module.getId());
return;
}
- } else if (event instanceof ItemRemovedEvent) {
- if (itemName.equals(((ItemRemovedEvent) event).getItem().name)) {
+ } else if (event instanceof ItemRemovedEvent removedEvent) {
+ if (itemName.equals(removedEvent.getItem().name)) {
logger.warn("Item '{}' needed for rule '{}' removed. Trigger '{}' will no longer work.", itemName,
ruleUID, module.getId());
return;
@@ -122,15 +124,17 @@ public void receive(Event event) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(),
event.getTopic(), event.getType(), event.getPayload());
Map values = new HashMap<>();
- if (event instanceof ItemStateEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
+ if (event instanceof ItemStateUpdatedEvent updatedEvent
+ && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
String state = this.state;
- State itemState = ((ItemStateEvent) event).getItemState();
+ State itemState = updatedEvent.getItemState();
if ((state == null || state.equals(itemState.toFullString()))) {
values.put("state", itemState);
}
- } else if (event instanceof ItemStateChangedEvent && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- State itemState = ((ItemStateChangedEvent) event).getItemState();
- State oldItemState = ((ItemStateChangedEvent) event).getOldItemState();
+ } else if (event instanceof ItemStateChangedEvent changedEvent
+ && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
+ State itemState = changedEvent.getItemState();
+ State oldItemState = changedEvent.getOldItemState();
if (stateMatches(this.state, itemState) && stateMatches(this.previousState, oldItemState)) {
values.put("oldState", oldItemState);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateUpdateActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateUpdateActionHandler.java
index 2bff826e79f..4a515b0d57c 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateUpdateActionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateUpdateActionHandler.java
@@ -71,9 +71,9 @@ public ItemStateUpdateActionHandler(Action module, EventPublisher eventPublisher
} else {
final Object st = inputs.get(STATE);
- if (st instanceof State) {
+ if (st instanceof State state1) {
if (item.getAcceptedDataTypes().contains(st.getClass())) {
- stateObj = (State) st;
+ stateObj = state1;
}
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java
index 6755dbe9952..9a4c329d1ff 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java
@@ -12,14 +12,17 @@
*/
package org.openhab.core.automation.internal.module.handler;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.Action;
+import org.openhab.core.automation.events.AutomationEventFactory;
import org.openhab.core.automation.handler.BaseActionModuleHandler;
import org.openhab.core.config.core.Configuration;
+import org.openhab.core.events.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,6 +67,7 @@ public class RunRuleActionHandler extends BaseActionModuleHandler {
* the UIDs of the rules to be executed.
*/
private final List ruleUIDs;
+ private final String moduleId;
/**
* boolean to express if the conditions should be considered, defaults to
@@ -84,16 +88,22 @@ public RunRuleActionHandler(final Action module) {
throw new IllegalArgumentException("'ruleUIDs' property must not be null.");
}
if (config.get(CONSIDER_CONDITIONS_KEY) != null && config.get(CONSIDER_CONDITIONS_KEY) instanceof Boolean) {
- this.considerConditions = ((Boolean) config.get(CONSIDER_CONDITIONS_KEY)).booleanValue();
+ this.considerConditions = (Boolean) config.get(CONSIDER_CONDITIONS_KEY);
}
+ this.moduleId = module.getId();
}
@Override
public @Nullable Map execute(Map context) {
// execute each rule after the other; at the moment synchronously
+ Object previousEvent = context.get("event");
+ Event event = AutomationEventFactory.createExecutionEvent(moduleId,
+ previousEvent instanceof Event ? Map.of("previous", previousEvent) : null, "runRuleAction");
+ Map newContext = new HashMap<>(context);
+ newContext.put("event", event);
for (String uid : ruleUIDs) {
if (callback != null) {
- callback.runNow(uid, considerConditions, context);
+ callback.runNow(uid, considerConditions, newContext);
} else {
logger.warn("Action is not applied to {} because rule engine is not available.", uid);
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandler.java
index 58c7b5ca961..9310be832a6 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandler.java
@@ -47,14 +47,15 @@ public class SystemTriggerHandler extends BaseTriggerModuleHandler implements Ev
private final Integer startlevel;
private final Set types;
- private final BundleContext bundleContext;
+ private final StartLevelService startLevelService;
private boolean triggered = false;
- private ServiceRegistration> eventSubscriberRegistration;
+ private final ServiceRegistration> eventSubscriberRegistration;
- public SystemTriggerHandler(Trigger module, BundleContext bundleContext) {
+ public SystemTriggerHandler(Trigger module, BundleContext bundleContext, StartLevelService startLevelService) {
super(module);
+ this.startLevelService = startLevelService;
this.startlevel = ((BigDecimal) module.getConfiguration().get(CFG_STARTLEVEL)).intValue();
if (STARTLEVEL_MODULE_TYPE_ID.equals(module.getTypeUID())) {
this.types = Set.of(StartlevelEvent.TYPE);
@@ -62,8 +63,18 @@ public SystemTriggerHandler(Trigger module, BundleContext bundleContext) {
logger.warn("Module type '{}' is not (yet) handled by this class.", module.getTypeUID());
throw new IllegalArgumentException(module.getTypeUID() + " is no valid module type.");
}
- this.bundleContext = bundleContext;
- eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, null);
+ eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
+ }
+
+ @Override
+ public void setCallback(ModuleHandlerCallback callback) {
+ super.setCallback(callback);
+
+ // trigger immediately when start level is already reached
+ int currentStartLevel = startLevelService.getStartLevel();
+ if (currentStartLevel > StartLevelService.STARTLEVEL_RULEENGINE && currentStartLevel >= startlevel) {
+ trigger();
+ }
}
@Override
@@ -79,8 +90,8 @@ public void receive(Event event) {
}
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), event.getTopic(),
event.getType(), event.getPayload());
- if (event instanceof StartlevelEvent && STARTLEVEL_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- Integer sl = ((StartlevelEvent) event).getStartlevel();
+ if (event instanceof StartlevelEvent startlevelEvent && STARTLEVEL_MODULE_TYPE_ID.equals(module.getTypeUID())) {
+ Integer sl = startlevelEvent.getStartlevel();
if (startlevel <= sl && startlevel > StartLevelService.STARTLEVEL_RULEENGINE) {
// only execute rules if their start level is higher than the rule engine activation level, since
// otherwise the rule engine takes care of the execution already
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ThingStatusTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ThingStatusTriggerHandler.java
index 87996ec1826..c185cb4b82b 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ThingStatusTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ThingStatusTriggerHandler.java
@@ -104,14 +104,15 @@ public void receive(Event event) {
logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), event.getTopic(),
event.getType(), event.getPayload());
Map values = new HashMap<>();
- if (event instanceof ThingStatusInfoEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- ThingStatus status = ((ThingStatusInfoEvent) event).getStatusInfo().getStatus();
+ if (event instanceof ThingStatusInfoEvent infoEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
+ ThingStatus status = infoEvent.getStatusInfo().getStatus();
if (statusMatches(this.status, status)) {
values.put(OUT_STATUS, status);
}
- } else if (event instanceof ThingStatusInfoChangedEvent && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
- ThingStatus newStatus = ((ThingStatusInfoChangedEvent) event).getStatusInfo().getStatus();
- ThingStatus oldStatus = ((ThingStatusInfoChangedEvent) event).getOldStatusInfo().getStatus();
+ } else if (event instanceof ThingStatusInfoChangedEvent changedEvent
+ && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) {
+ ThingStatus newStatus = changedEvent.getStatusInfo().getStatus();
+ ThingStatus oldStatus = changedEvent.getOldStatusInfo().getStatus();
if (statusMatches(this.status, newStatus) && statusMatches(this.previousStatus, oldStatus)) {
values.put(OUT_NEW_STATUS, newStatus);
values.put(OUT_OLD_STATUS, oldStatus);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.java
index e3729c297a7..a1a2b2d1db2 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.java
@@ -13,11 +13,15 @@
package org.openhab.core.automation.internal.module.handler;
import java.text.MessageFormat;
+import java.util.Map;
+import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.ModuleHandlerCallback;
import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.events.AutomationEventFactory;
+import org.openhab.core.automation.events.TimerEvent;
import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
import org.openhab.core.automation.handler.TimeBasedTriggerHandler;
import org.openhab.core.automation.handler.TriggerHandlerCallback;
@@ -46,13 +50,14 @@ public class TimeOfDayTriggerHandler extends BaseTriggerModuleHandler
public static final String CFG_TIME = "time";
private final CronScheduler scheduler;
+ private final String time;
private final String expression;
private @Nullable ScheduledCompletableFuture> schedule;
public TimeOfDayTriggerHandler(Trigger module, CronScheduler scheduler) {
super(module);
this.scheduler = scheduler;
- String time = module.getConfiguration().get(CFG_TIME).toString();
+ this.time = module.getConfiguration().get(CFG_TIME).toString();
this.expression = buildExpressionFromConfigurationTime(time);
}
@@ -83,7 +88,9 @@ private void scheduleJob() {
@Override
public void run() {
if (callback != null) {
- ((TriggerHandlerCallback) callback).triggered(module);
+ TimerEvent event = AutomationEventFactory.createTimerEvent(module.getTypeUID(),
+ Objects.requireNonNullElse(module.getLabel(), module.getId()), Map.of(CFG_TIME, time));
+ ((TriggerHandlerCallback) callback).triggered(module, Map.of("event", event));
} else {
logger.debug("Tried to trigger, but callback isn't available!");
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java
index 829db351cc3..2c87ce5ea21 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java
@@ -78,16 +78,18 @@ public Collection getTypes() {
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
logger.trace("create {} -> {}", module.getId(), module.getTypeUID());
String moduleTypeUID = module.getTypeUID();
- if (GenericCronTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) {
- return new GenericCronTriggerHandler((Trigger) module, scheduler);
- } else if (TimeOfDayTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) {
- return new TimeOfDayTriggerHandler((Trigger) module, scheduler);
- } else if (DateTimeTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) {
- return new DateTimeTriggerHandler((Trigger) module, scheduler, itemRegistry, bundleContext);
- } else if (TimeOfDayConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Condition) {
- return new TimeOfDayConditionHandler((Condition) module);
- } else if (DayOfWeekConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Condition) {
- return new DayOfWeekConditionHandler((Condition) module);
+ if (GenericCronTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger trigger) {
+ return new GenericCronTriggerHandler(trigger, scheduler);
+ } else if (TimeOfDayTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger trigger) {
+ return new TimeOfDayTriggerHandler(trigger, scheduler);
+ } else if (DateTimeTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger trigger) {
+ return new DateTimeTriggerHandler(trigger, scheduler, itemRegistry, bundleContext);
+ } else if (TimeOfDayConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID)
+ && module instanceof Condition condition) {
+ return new TimeOfDayConditionHandler(condition);
+ } else if (DayOfWeekConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID)
+ && module instanceof Condition condition) {
+ return new DayOfWeekConditionHandler(condition);
} else {
logger.error("The module handler type '{}' is not supported.", moduleTypeUID);
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java
index d6aaac127d5..e2e8555824c 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java
@@ -122,9 +122,7 @@ public Collection getModuleTypes(@Nullable Locale loca
Bundle bundle = FrameworkUtil.getBundle(mi.getActionProvider().getClass());
ModuleType mt = helper.buildModuleType(uid, moduleInformation);
-
- ModuleType localizedModuleType = moduleTypeI18nService.getModuleTypePerLocale(mt, locale, bundle);
- return localizedModuleType;
+ return moduleTypeI18nService.getModuleTypePerLocale(mt, locale, bundle);
}
return null;
}
@@ -198,8 +196,8 @@ public void removeActionProvider(AnnotatedActions actionProvider, Map properties) {
Object o = properties.get(OpenHAB.SERVICE_CONTEXT);
String configName = null;
- if (o instanceof String) {
- configName = (String) o;
+ if (o instanceof String string) {
+ configName = string;
}
return configName;
}
@@ -211,8 +209,7 @@ public Collection getTypes() {
@Override
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
- if (module instanceof Action) {
- Action actionModule = (Action) module;
+ if (module instanceof Action actionModule) {
if (moduleInformation.containsKey(actionModule.getTypeUID())) {
ModuleInformation finalMI = helper.getModuleInformationForIdentifier(actionModule, moduleInformation,
false);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java
index 8abeec89c16..1a995ec9ede 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java
@@ -74,12 +74,12 @@ public void serialize(Set dataObjects, OutputStreamWriter writer) th
private void addAll(Set result, @Nullable List extends ModuleTypeDTO> moduleTypes) {
if (moduleTypes != null) {
for (ModuleTypeDTO mt : moduleTypes) {
- if (mt instanceof CompositeTriggerTypeDTO) {
- result.add(TriggerTypeDTOMapper.map((CompositeTriggerTypeDTO) mt));
- } else if (mt instanceof CompositeConditionTypeDTO) {
- result.add(ConditionTypeDTOMapper.map((CompositeConditionTypeDTO) mt));
- } else if (mt instanceof CompositeActionTypeDTO) {
- result.add(ActionTypeDTOMapper.map((CompositeActionTypeDTO) mt));
+ if (mt instanceof CompositeTriggerTypeDTO tO) {
+ result.add(TriggerTypeDTOMapper.map(tO));
+ } else if (mt instanceof CompositeConditionTypeDTO tO) {
+ result.add(ConditionTypeDTOMapper.map(tO));
+ } else if (mt instanceof CompositeActionTypeDTO tO) {
+ result.add(ActionTypeDTOMapper.map(tO));
}
}
}
@@ -92,12 +92,12 @@ private Map> createMapByType(Set
List conditions = new ArrayList<>();
List actions = new ArrayList<>();
for (ModuleType moduleType : dataObjects) {
- if (moduleType instanceof TriggerType) {
- triggers.add((TriggerType) moduleType);
- } else if (moduleType instanceof ConditionType) {
- conditions.add((ConditionType) moduleType);
- } else if (moduleType instanceof ActionType) {
- actions.add((ActionType) moduleType);
+ if (moduleType instanceof TriggerType type) {
+ triggers.add(type);
+ } else if (moduleType instanceof ConditionType type) {
+ conditions.add(type);
+ } else if (moduleType instanceof ActionType type) {
+ actions.add(type);
}
}
map.put("triggers", triggers);
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/AutomationResourceBundlesTracker.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/AutomationResourceBundlesTracker.java
index 546bd418901..b60262c6bb3 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/AutomationResourceBundlesTracker.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/AutomationResourceBundlesTracker.java
@@ -94,8 +94,8 @@ protected void deactivate(BundleContext bc) {
@SuppressWarnings({ "rawtypes" })
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, target = "(provider.type=bundle)")
protected void addProvider(Provider provider) {
- if (provider instanceof AbstractResourceBundleProvider) {
- addAbstractResourceBundleProvider((AbstractResourceBundleProvider) provider);
+ if (provider instanceof AbstractResourceBundleProvider bundleProvider) {
+ addAbstractResourceBundleProvider(bundleProvider);
}
}
@@ -110,8 +110,8 @@ protected void addAbstractResourceBundleProvider(AbstractResourceBundleProvider
@SuppressWarnings({ "rawtypes" })
protected void removeProvider(Provider provider) {
- if (provider instanceof AbstractResourceBundleProvider) {
- removeAbstractResourceBundleProvider((AbstractResourceBundleProvider) provider);
+ if (provider instanceof AbstractResourceBundleProvider bundleProvider) {
+ removeAbstractResourceBundleProvider(bundleProvider);
}
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/Vendor.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/Vendor.java
index 42d1b7e9e54..e3136d74594 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/Vendor.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/Vendor.java
@@ -120,8 +120,7 @@ public int count() {
*/
@Override
public boolean equals(@Nullable Object obj) {
- if (obj instanceof Vendor) {
- Vendor other = (Vendor) obj;
+ if (obj instanceof Vendor other) {
return vendorSymbolicName.equals(other.vendorSymbolicName) && vendorVersion.equals(other.vendorVersion);
}
return false;
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleI18nUtil.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleI18nUtil.java
index 763c5367c6d..1ba63685015 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleI18nUtil.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleI18nUtil.java
@@ -59,14 +59,14 @@ public List getLocalizedModules(List modules, Bundle bu
@SuppressWarnings("unchecked")
private @Nullable T createLocalizedModule(T module, @Nullable String label,
@Nullable String description) {
- if (module instanceof Action) {
- return (T) createLocalizedAction((Action) module, label, description);
+ if (module instanceof Action action) {
+ return (T) createLocalizedAction(action, label, description);
}
- if (module instanceof Condition) {
- return (T) createLocalizedCondition((Condition) module, label, description);
+ if (module instanceof Condition condition) {
+ return (T) createLocalizedCondition(condition, label, description);
}
- if (module instanceof Trigger) {
- return (T) createLocalizedTrigger((Trigger) module, label, description);
+ if (module instanceof Trigger trigger) {
+ return (T) createLocalizedTrigger(trigger, label, description);
}
return null;
}
diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleTypeI18nServiceImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleTypeI18nServiceImpl.java
index 36647875bd6..346685f7f86 100644
--- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleTypeI18nServiceImpl.java
+++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/i18n/ModuleTypeI18nServiceImpl.java
@@ -86,19 +86,19 @@ public ModuleTypeI18nServiceImpl(final @Reference ConfigI18nLocalizationService
List lconfigDescriptionParameters = getLocalizedConfigDescriptionParameters(
defModuleType.getConfigurationDescriptions(), ModuleTypeI18nUtil.MODULE_TYPE, uid, bundle, locale);
- if (defModuleType instanceof ActionType) {
- return createLocalizedActionType((ActionType) defModuleType, bundle, uid, locale,
- lconfigDescriptionParameters, llabel == null ? defModuleType.getLabel() : llabel,
+ if (defModuleType instanceof ActionType type) {
+ return createLocalizedActionType(type, bundle, uid, locale, lconfigDescriptionParameters,
+ llabel == null ? defModuleType.getLabel() : llabel,
ldescription == null ? defModuleType.getDescription() : ldescription);
}
- if (defModuleType instanceof ConditionType) {
- return createLocalizedConditionType((ConditionType) defModuleType, bundle, uid, locale,
- lconfigDescriptionParameters, llabel == null ? defModuleType.getLabel() : llabel,
+ if (defModuleType instanceof ConditionType type) {
+ return createLocalizedConditionType(type, bundle, uid, locale, lconfigDescriptionParameters,
+ llabel == null ? defModuleType.getLabel() : llabel,
ldescription == null ? defModuleType.getDescription() : ldescription);
}
- if (defModuleType instanceof TriggerType) {
- return createLocalizedTriggerType((TriggerType) defModuleType, bundle, uid, locale,
- lconfigDescriptionParameters, llabel != null ? llabel : defModuleType.getLabel(),
+ if (defModuleType instanceof TriggerType type) {
+ return createLocalizedTriggerType(type, bundle, uid, locale, lconfigDescriptionParameters,
+ llabel != null ? llabel : defModuleType.getLabel(),
ldescription == null ? defModuleType.getDescription() : ldescription);
}
return null;
@@ -136,9 +136,9 @@ public ModuleTypeI18nServiceImpl(final @Reference ConfigI18nLocalizationService
List inputs = moduleTypeI18nUtil.getLocalizedInputs(at.getInputs(), bundle, moduleTypeUID, locale);
List