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 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 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 outputs = moduleTypeI18nUtil.getLocalizedOutputs(at.getOutputs(), bundle, moduleTypeUID, locale); ActionType lat = null; - if (at instanceof CompositeActionType) { - List modules = moduleI18nUtil.getLocalizedModules(((CompositeActionType) at).getChildren(), bundle, - moduleTypeUID, ModuleTypeI18nUtil.MODULE_TYPE, locale); + if (at instanceof CompositeActionType type) { + List modules = moduleI18nUtil.getLocalizedModules(type.getChildren(), bundle, moduleTypeUID, + ModuleTypeI18nUtil.MODULE_TYPE, locale); lat = new CompositeActionType(moduleTypeUID, lconfigDescriptions, llabel, ldescription, at.getTags(), at.getVisibility(), inputs, outputs, modules); } else { @@ -165,9 +165,9 @@ public ModuleTypeI18nServiceImpl(final @Reference ConfigI18nLocalizationService @Nullable String llabel, @Nullable String ldescription) { List inputs = moduleTypeI18nUtil.getLocalizedInputs(ct.getInputs(), bundle, moduleTypeUID, locale); ConditionType lct = null; - if (ct instanceof CompositeConditionType) { - List modules = moduleI18nUtil.getLocalizedModules(((CompositeConditionType) ct).getChildren(), - bundle, moduleTypeUID, ModuleTypeI18nUtil.MODULE_TYPE, locale); + if (ct instanceof CompositeConditionType type) { + List modules = moduleI18nUtil.getLocalizedModules(type.getChildren(), bundle, moduleTypeUID, + ModuleTypeI18nUtil.MODULE_TYPE, locale); lct = new CompositeConditionType(moduleTypeUID, lconfigDescriptions, llabel, ldescription, ct.getTags(), ct.getVisibility(), inputs, modules); } else { @@ -194,9 +194,9 @@ public ModuleTypeI18nServiceImpl(final @Reference ConfigI18nLocalizationService @Nullable String llabel, @Nullable String ldescription) { List outputs = moduleTypeI18nUtil.getLocalizedOutputs(tt.getOutputs(), bundle, moduleTypeUID, locale); TriggerType ltt = null; - if (tt instanceof CompositeTriggerType) { - List modules = moduleI18nUtil.getLocalizedModules(((CompositeTriggerType) tt).getChildren(), - bundle, moduleTypeUID, ModuleTypeI18nUtil.MODULE_TYPE, locale); + if (tt instanceof CompositeTriggerType type) { + List modules = moduleI18nUtil.getLocalizedModules(type.getChildren(), bundle, moduleTypeUID, + ModuleTypeI18nUtil.MODULE_TYPE, locale); ltt = new CompositeTriggerType(moduleTypeUID, lconfigDescriptions, llabel, ldescription, tt.getTags(), tt.getVisibility(), outputs, modules); } else { diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/type/ModuleTypeRegistryImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/type/ModuleTypeRegistryImpl.java index 16ecad9ded3..4444d18d756 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/type/ModuleTypeRegistryImpl.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/type/ModuleTypeRegistryImpl.java @@ -127,8 +127,8 @@ public Collection getTriggers(@Nullable Locale locale, String... ta Collection moduleTypes = getByTags(locale, tags); Collection triggerTypes = new ArrayList<>(); for (ModuleType mt : moduleTypes) { - if (mt instanceof TriggerType) { - triggerTypes.add((TriggerType) mt); + if (mt instanceof TriggerType type) { + triggerTypes.add(type); } } return triggerTypes; @@ -139,8 +139,8 @@ public Collection getTriggers(String... tags) { Collection moduleTypes = getByTags(tags); Collection triggerTypes = new ArrayList<>(); for (ModuleType mt : moduleTypes) { - if (mt instanceof TriggerType) { - triggerTypes.add((TriggerType) mt); + if (mt instanceof TriggerType type) { + triggerTypes.add(type); } } return triggerTypes; @@ -151,8 +151,8 @@ public Collection getConditions(String... tags) { Collection moduleTypes = getByTags(tags); Collection conditionTypes = new ArrayList<>(); for (ModuleType mt : moduleTypes) { - if (mt instanceof ConditionType) { - conditionTypes.add((ConditionType) mt); + if (mt instanceof ConditionType type) { + conditionTypes.add(type); } } return conditionTypes; @@ -163,8 +163,8 @@ public Collection getConditions(@Nullable Locale locale, String.. Collection moduleTypes = getByTags(locale, tags); Collection conditionTypes = new ArrayList<>(); for (ModuleType mt : moduleTypes) { - if (mt instanceof ConditionType) { - conditionTypes.add((ConditionType) mt); + if (mt instanceof ConditionType type) { + conditionTypes.add(type); } } return conditionTypes; @@ -175,8 +175,8 @@ public Collection getActions(String... tags) { Collection moduleTypes = getByTags(tags); Collection actionTypes = new ArrayList<>(); for (ModuleType mt : moduleTypes) { - if (mt instanceof ActionType) { - actionTypes.add((ActionType) mt); + if (mt instanceof ActionType type) { + actionTypes.add(type); } } return actionTypes; @@ -187,8 +187,8 @@ public Collection getActions(@Nullable Locale locale, String... tags Collection moduleTypes = getByTags(locale, tags); Collection actionTypes = new ArrayList<>(); for (ModuleType mt : moduleTypes) { - if (mt instanceof ActionType) { - actionTypes.add((ActionType) mt); + if (mt instanceof ActionType type) { + actionTypes.add(type); } } return actionTypes; @@ -199,36 +199,30 @@ public Collection getActions(@Nullable Locale locale, String... tags return null; } ModuleType result; - if (mType instanceof CompositeTriggerType) { - CompositeTriggerType m = (CompositeTriggerType) mType; + if (mType instanceof CompositeTriggerType m) { result = new CompositeTriggerType(mType.getUID(), mType.getConfigurationDescriptions(), mType.getLabel(), mType.getDescription(), mType.getTags(), mType.getVisibility(), m.getOutputs(), new ArrayList<>(m.getChildren())); - } else if (mType instanceof TriggerType) { - TriggerType m = (TriggerType) mType; + } else if (mType instanceof TriggerType m) { result = new TriggerType(mType.getUID(), mType.getConfigurationDescriptions(), mType.getLabel(), mType.getDescription(), mType.getTags(), mType.getVisibility(), m.getOutputs()); - } else if (mType instanceof CompositeConditionType) { - CompositeConditionType m = (CompositeConditionType) mType; + } else if (mType instanceof CompositeConditionType m) { result = new CompositeConditionType(mType.getUID(), mType.getConfigurationDescriptions(), mType.getLabel(), mType.getDescription(), mType.getTags(), mType.getVisibility(), m.getInputs(), new ArrayList<>(m.getChildren())); - } else if (mType instanceof ConditionType) { - ConditionType m = (ConditionType) mType; + } else if (mType instanceof ConditionType m) { result = new ConditionType(mType.getUID(), mType.getConfigurationDescriptions(), mType.getLabel(), mType.getDescription(), mType.getTags(), mType.getVisibility(), m.getInputs()); - } else if (mType instanceof CompositeActionType) { - CompositeActionType m = (CompositeActionType) mType; + } else if (mType instanceof CompositeActionType m) { result = new CompositeActionType(mType.getUID(), mType.getConfigurationDescriptions(), mType.getLabel(), mType.getDescription(), mType.getTags(), mType.getVisibility(), m.getInputs(), m.getOutputs(), new ArrayList<>(m.getChildren())); - } else if (mType instanceof ActionType) { - ActionType m = (ActionType) mType; + } else if (mType instanceof ActionType m) { result = new ActionType(mType.getUID(), mType.getConfigurationDescriptions(), mType.getLabel(), mType.getDescription(), mType.getTags(), mType.getVisibility(), m.getInputs(), m.getOutputs()); } else { diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java index 26f3758725f..75a651f318d 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java @@ -116,9 +116,7 @@ private List getInputsFromAction(Method method) { "", "")); } else if (paramAnnotations.length == 1) { Annotation a = paramAnnotations[0]; - if (a instanceof ActionInput) { - ActionInput inp = (ActionInput) a; - + if (a instanceof ActionInput inp) { // check if a type is given, otherwise use the java type specified on the parameter String type; if (!"".equals(inp.type())) { @@ -174,10 +172,8 @@ private List getOutputsFromMethod(Method method) { if (configParam != null) { configDescriptions.add(configParam); } - - ActionType at = new ActionType(UID, configDescriptions, mi.getLabel(), mi.getDescription(), mi.getTags(), + return new ActionType(UID, configDescriptions, mi.getLabel(), mi.getDescription(), mi.getTags(), mi.getVisibility(), mi.getInputs(), mi.getOutputs()); - return at; } return null; } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/parser/Parser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/parser/Parser.java index 3ad62dbc864..5c398991dae 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/parser/Parser.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/parser/Parser.java @@ -32,22 +32,22 @@ public interface Parser { * Example : "parser.type" = "parser.module.type"; * It is used as registration property of the corresponding service. */ - public static String PARSER_TYPE = "parser.type"; + static String PARSER_TYPE = "parser.type"; /** * Defines one of the possible values of property {@link #PARSER_TYPE}. */ - public static String PARSER_MODULE_TYPE = "parser.module.type"; + static String PARSER_MODULE_TYPE = "parser.module.type"; /** * Defines one of the possible values of property {@link #PARSER_TYPE}. */ - public static String PARSER_TEMPLATE = "parser.template"; + static String PARSER_TEMPLATE = "parser.template"; /** * Defines one of the possible values of property {@link #PARSER_TYPE}. */ - public static String PARSER_RULE = "parser.rule"; + static String PARSER_RULE = "parser.rule"; /** * Defines a service registration property used for recognition of which file format is supported by the parser. @@ -55,12 +55,12 @@ public interface Parser { * Example : "format" = "json"; * It is used as registration property of the corresponding service. */ - public static String FORMAT = "format"; + static String FORMAT = "format"; /** * Defines the possible value of property {@link #FORMAT}. It means that the parser supports json format. */ - public static String FORMAT_JSON = "json"; + static String FORMAT_JSON = "json"; /** * Loads a file with some particular format and parse it to the corresponding automation objects. @@ -70,7 +70,7 @@ public interface Parser { * @throws ParsingException is thrown when json format is wrong or there is a semantic error in description of * the automation objects. */ - public Set parse(InputStreamReader reader) throws ParsingException; + Set parse(InputStreamReader reader) throws ParsingException; /** * Records the automation objects in a file with some particular format. @@ -80,5 +80,5 @@ public interface Parser { * @throws Exception is thrown when I/O operation has failed or has been interrupted or generating of the text fails * for some reasons. */ - public void serialize(Set dataObjects, OutputStreamWriter writer) throws Exception; + void serialize(Set dataObjects, OutputStreamWriter writer) throws Exception; } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/Template.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/Template.java index 8d366fee646..f814a246dfa 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/Template.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/Template.java @@ -40,7 +40,7 @@ public interface Template extends Identifiable { * @return the identifier of the Template. */ @Override - public String getUID(); + String getUID(); /** * Gets the assigned tags to a Template. The templates can have {@code tags} - non-hierarchical keywords or terms @@ -48,7 +48,7 @@ public interface Template extends Identifiable { * * @return the tags assigned to the template. */ - public Set getTags(); + Set getTags(); /** * Gets the label of a Template. The label is a short, human-readable description of the Template defined by its @@ -56,7 +56,8 @@ public interface Template extends Identifiable { * * @return the label of the Template. */ - public @Nullable String getLabel(); + @Nullable + String getLabel(); /** * Gets the description of a Template. The description is a detailed, human-understandable description of the @@ -64,12 +65,13 @@ public interface Template extends Identifiable { * * @return the description of the Template. */ - public @Nullable String getDescription(); + @Nullable + String getDescription(); /** * Shows the visibility of a Template. * * @return the visibility of the Template. */ - public Visibility getVisibility(); + Visibility getVisibility(); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/TemplateRegistry.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/TemplateRegistry.java index 09d332bb1a8..32a474c284e 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/TemplateRegistry.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/template/TemplateRegistry.java @@ -41,7 +41,8 @@ public interface TemplateRegistry extends Registry extends Registry getByTag(@Nullable String tag); + Collection getByTag(@Nullable String tag); /** * Gets the templates filtered by tag. @@ -62,7 +63,7 @@ public interface TemplateRegistry extends Registry getByTag(@Nullable String tag, @Nullable Locale locale); + Collection getByTag(@Nullable String tag, @Nullable Locale locale); /** * Gets the templates filtered by tags. @@ -71,7 +72,7 @@ public interface TemplateRegistry extends Registry getByTags(String... tags); + Collection getByTags(String... tags); /** * Gets the templates filtered by tags. @@ -83,7 +84,7 @@ public interface TemplateRegistry extends Registry getByTags(@Nullable Locale locale, String... tags); + Collection getByTags(@Nullable Locale locale, String... tags); /** * Gets all available templates, localized by specified locale. @@ -93,5 +94,5 @@ public interface TemplateRegistry extends Registry getAll(@Nullable Locale locale); + Collection getAll(@Nullable Locale locale); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java index ce8fded8214..8151450db4d 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java @@ -126,9 +126,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; } @@ -228,8 +226,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, true); diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/type/ModuleTypeRegistry.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/type/ModuleTypeRegistry.java index a97bcfec75f..f1b69f8c359 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/type/ModuleTypeRegistry.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/type/ModuleTypeRegistry.java @@ -40,7 +40,7 @@ public interface ModuleTypeRegistry extends Registry { * @return the desired {@link ModuleType} instance or {@code null} if a module type with such UID does not exist or * the passed UID is {@code null}. */ - public @Nullable T get(String moduleTypeUID, @Nullable Locale locale); + @Nullable T get(String moduleTypeUID, @Nullable Locale locale); /** * Gets the {@link ModuleType}s filtered by tag. @@ -50,7 +50,7 @@ public interface ModuleTypeRegistry extends Registry { * @param the type of the required object. * @return the {@link ModuleType}s, which correspond to the specified filter. */ - public Collection getByTag(@Nullable String moduleTypeTag); + Collection getByTag(@Nullable String moduleTypeTag); /** * This method is used for getting the {@link ModuleType}s filtered by tag. @@ -61,7 +61,7 @@ public interface ModuleTypeRegistry extends Registry { * @param the type of the required object. * @return the {@link ModuleType}s, which correspond to the specified filter. */ - public Collection getByTag(@Nullable String moduleTypeTag, @Nullable Locale locale); + Collection getByTag(@Nullable String moduleTypeTag, @Nullable Locale locale); /** * This method is used for getting the {@link ModuleType}s filtered by tags. @@ -71,7 +71,7 @@ public interface ModuleTypeRegistry extends Registry { * @param the type of the required object. * @return the {@link ModuleType}s, which correspond to the filter. */ - public Collection getByTags(String... tags); + Collection getByTags(String... tags); /** * This method is used for getting the {@link ModuleType}s filtered by tags. @@ -82,7 +82,7 @@ public interface ModuleTypeRegistry extends Registry { * @param the type of the required object. * @return the {@link ModuleType}s, which correspond to the filter. */ - public Collection getByTags(@Nullable Locale locale, String... tags); + Collection getByTags(@Nullable Locale locale, String... tags); /** * This method is used for getting the {@link TriggerType}s. The returned {@link TriggerType}s are @@ -92,7 +92,7 @@ public interface ModuleTypeRegistry extends Registry { * {@link TriggerType}s. * @return collection of all available {@link TriggerType}s, localized by default locale. */ - public Collection getTriggers(String... tags); + Collection getTriggers(String... tags); /** * This method is used for getting the {@link TriggerType}s, localized depending on passed locale parameter. @@ -105,7 +105,7 @@ public interface ModuleTypeRegistry extends Registry { * @return a collection of all available {@link TriggerType}s, localized by default locale or the passed locale * parameter. */ - public Collection getTriggers(@Nullable Locale locale, String... tags); + Collection getTriggers(@Nullable Locale locale, String... tags); /** * This method is used for getting the {@link ConditionType}s. The returned {@link ConditionType}s are @@ -115,7 +115,7 @@ public interface ModuleTypeRegistry extends Registry { * {@link ConditionType}s. * @return collection of all available {@link ConditionType}s, localized by default locale. */ - public Collection getConditions(String... tags); + Collection getConditions(String... tags); /** * This method is used for getting the {@link ConditionType}s, localized depending on passed locale parameter. @@ -128,7 +128,7 @@ public interface ModuleTypeRegistry extends Registry { * @return a collection of all available {@link ConditionType}s, localized by default locale or the passed locale * parameter. */ - public Collection getConditions(@Nullable Locale locale, String... tags); + Collection getConditions(@Nullable Locale locale, String... tags); /** * This method is used for getting the {@link ActionType}s. The returned {@link ActionType}s are @@ -138,7 +138,7 @@ public interface ModuleTypeRegistry extends Registry { * {@link ActionType}s. * @return collection of all available {@link ActionType}s, localized by default locale. */ - public Collection getActions(String... tags); + Collection getActions(String... tags); /** * This method is used for getting the {@link ActionType}s, localized depending on passed locale parameter. @@ -151,5 +151,5 @@ public interface ModuleTypeRegistry extends Registry { * @return a collection of all available {@link ActionType}s, localized by default locale or the passed locale * parameter. */ - public Collection getActions(@Nullable Locale locale, String... tags); + Collection getActions(@Nullable Locale locale, String... tags); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ConfigurationNormalizer.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ConfigurationNormalizer.java index 8067a043967..b548573b86e 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ConfigurationNormalizer.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ConfigurationNormalizer.java @@ -83,7 +83,7 @@ public static void normalizeConfiguration(Configuration configuration, if (parameter != null) { String parameterName = entry.getKey(); final Object value = configuration.get(parameterName); - if (value instanceof String && ((String) value).contains("${")) { + if (value instanceof String string && string.contains("${")) { continue; // It is a reference } if (value == null) { diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ModuleBuilder.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ModuleBuilder.java index 6e5a1ccae60..41b264aedd3 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ModuleBuilder.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ModuleBuilder.java @@ -55,12 +55,12 @@ public static TriggerBuilder createTrigger(final Trigger trigger) { @SuppressWarnings("unchecked") public static , T extends Module> ModuleBuilder create(Module module) { - if (module instanceof Action) { - return (ModuleBuilder) createAction((Action) module); - } else if (module instanceof Condition) { - return (ModuleBuilder) createCondition((Condition) module); - } else if (module instanceof Trigger) { - return (ModuleBuilder) createTrigger((Trigger) module); + if (module instanceof Action action) { + return (ModuleBuilder) createAction(action); + } else if (module instanceof Condition condition) { + return (ModuleBuilder) createCondition(condition); + } else if (module instanceof Trigger trigger) { + return (ModuleBuilder) createTrigger(trigger); } else { throw new IllegalArgumentException("Parameter must be an instance of Action, Condition or Trigger."); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ReferenceResolver.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ReferenceResolver.java index e0456892c1c..7c4c365ec5b 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ReferenceResolver.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ReferenceResolver.java @@ -81,15 +81,14 @@ public class ReferenceResolver { public static void updateConfiguration(Configuration config, Map context, Logger logger) { for (String configKey : config.keySet()) { Object o = config.get(configKey); - if (o instanceof String) { - Object result = resolveProperty(config, context, logger, configKey, (String) o); + if (o instanceof String string) { + Object result = resolveProperty(config, context, logger, configKey, string); config.put(configKey, result); - } else if (o instanceof List) { + } else if (o instanceof List list) { List resultList = new ArrayList<>(); - List list = (List) o; for (Object obj : list) { - if (obj instanceof String) { - resultList.add(resolveProperty(config, context, logger, configKey, (String) obj)); + if (obj instanceof String string) { + resultList.add(resolveProperty(config, context, logger, configKey, string)); } } config.put(configKey, resultList); @@ -124,10 +123,10 @@ public static Map getCompositeChildContext(Module module, Map resultContext = new HashMap<>(); Map inputs = null; - if (module instanceof Condition) { - inputs = ((Condition) module).getInputs(); - } else if (module instanceof Action) { - inputs = ((Action) module).getInputs(); + if (module instanceof Condition condition) { + inputs = condition.getInputs(); + } else if (module instanceof Action action) { + inputs = action.getInputs(); } if (inputs != null) { @@ -340,10 +339,10 @@ public static Object resolveComplexDataReference(Object object, String... tokens try { Object obj = object; for (String token : tokens) { - if (obj instanceof Map) { - obj = getValueFromMap(((Map) obj), token); - } else if (obj instanceof List) { - obj = getValueFromList(((List) obj), Integer.parseInt(token)); + if (obj instanceof Map map) { + obj = getValueFromMap(map, token); + } else if (obj instanceof List list) { + obj = getValueFromList(list, Integer.parseInt(token)); } else { final Class objClass = obj.getClass(); obj = getValueFromBean(objClass, obj, token); diff --git a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json index ee1bdaf7294..7e2a500ef46 100644 --- a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json +++ b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/automation/moduletypes/ItemTriggers.json @@ -290,6 +290,15 @@ } ], "outputs": [ + { + "name": "triggeringGroup", + "type": "org.openhab.core.items.Item", + "description": "the group that the item belongs to", + "label": "Triggering Group", + "tags": [ + "item" + ] + }, { "name": "triggeringItem", "type": "org.openhab.core.items.Item", @@ -366,6 +375,15 @@ } ], "outputs": [ + { + "name": "triggeringGroup", + "type": "org.openhab.core.items.Item", + "description": "the group that the item belongs to", + "label": "Triggering Group", + "tags": [ + "item" + ] + }, { "name": "triggeringItem", "type": "org.openhab.core.items.Item", diff --git a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation.properties b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation.properties index f319b9e88cd..23059910c9e 100644 --- a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation.properties +++ b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation.properties @@ -66,6 +66,8 @@ module-type.core.GroupCommandTrigger.config.command.option.OPEN = OPEN module-type.core.GroupCommandTrigger.config.command.option.CLOSED = CLOSED module-type.core.GroupCommandTrigger.config.command.option.UP = UP module-type.core.GroupCommandTrigger.config.command.option.DOWN = DOWN +module-type.core.GroupCommandTrigger.output.triggeringGroup.label = Triggering Group +module-type.core.GroupCommandTrigger.output.triggeringGroup.description = the group that the item belongs to module-type.core.GroupCommandTrigger.output.triggeringItem.label = Triggering Item module-type.core.GroupCommandTrigger.output.triggeringItem.description = the member of the group that received the command module-type.core.GroupCommandTrigger.output.command.label = Command @@ -118,6 +120,8 @@ module-type.core.GroupStateUpdateTrigger.config.state.option.OPEN = OPEN module-type.core.GroupStateUpdateTrigger.config.state.option.CLOSED = CLOSED module-type.core.GroupStateUpdateTrigger.config.state.option.UP = UP module-type.core.GroupStateUpdateTrigger.config.state.option.DOWN = DOWN +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.label = Triggering Group +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.description = the group that the item belongs to module-type.core.GroupStateUpdateTrigger.output.triggeringItem.label = Triggering Item module-type.core.GroupStateUpdateTrigger.output.triggeringItem.description = the member of the group that updated its state module-type.core.GroupStateUpdateTrigger.output.state.label = State diff --git a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_he.properties b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_he.properties index 7a170475e7f..4867b9cef56 100644 --- a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_he.properties +++ b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_he.properties @@ -66,6 +66,8 @@ module-type.core.GroupCommandTrigger.config.command.option.OPEN = פתוח module-type.core.GroupCommandTrigger.config.command.option.CLOSED = סגור module-type.core.GroupCommandTrigger.config.command.option.UP = למעלה module-type.core.GroupCommandTrigger.config.command.option.DOWN = למטה +module-type.core.GroupCommandTrigger.output.triggeringGroup.label = קבוצה מפעילה +module-type.core.GroupCommandTrigger.output.triggeringGroup.description = הקבוצה שאליה שייך הפריט module-type.core.GroupCommandTrigger.output.triggeringItem.label = פריט מפעיל module-type.core.GroupCommandTrigger.output.triggeringItem.description = חבר הקבוצה שקיבל את הפקודה module-type.core.GroupCommandTrigger.output.command.label = פקודה @@ -118,6 +120,8 @@ module-type.core.GroupStateUpdateTrigger.config.state.option.OPEN = פתוח module-type.core.GroupStateUpdateTrigger.config.state.option.CLOSED = סגור module-type.core.GroupStateUpdateTrigger.config.state.option.UP = למעלה module-type.core.GroupStateUpdateTrigger.config.state.option.DOWN = למטה +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.label = קבוצה מפעילה +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.description = הקבוצה שאליה שייך הפריט module-type.core.GroupStateUpdateTrigger.output.triggeringItem.label = פריט מפעיל module-type.core.GroupStateUpdateTrigger.output.triggeringItem.description = החבר בקבוצה שעדכן את מצבה module-type.core.GroupStateUpdateTrigger.output.state.label = מצב @@ -251,7 +255,7 @@ module-type.core.ItemStateUpdateTrigger.output.event.description = האירוע # core.RuleEnablementAction -module-type.core.RuleEnablementAction.label = הפעל או השבת חוקים +module-type.core.RuleEnablementAction.label = מאפשר או משבית כללים module-type.core.RuleEnablementAction.description = מפעיל או משבית כלל או קבוצת כללים שצוינו במזהי ה-UID שלהם. module-type.core.RuleEnablementAction.config.enable.label = אפשר חוקים module-type.core.RuleEnablementAction.config.enable.description = 'true' מאפשר את כל החוקים שצוינו, 'false' משבית אותם. diff --git a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_it.properties b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_it.properties index ba888e231c0..be0e180238f 100644 --- a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_it.properties +++ b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_it.properties @@ -66,6 +66,8 @@ module-type.core.GroupCommandTrigger.config.command.option.OPEN = APERTO module-type.core.GroupCommandTrigger.config.command.option.CLOSED = CHIUSO module-type.core.GroupCommandTrigger.config.command.option.UP = SU module-type.core.GroupCommandTrigger.config.command.option.DOWN = GIÙ +module-type.core.GroupCommandTrigger.output.triggeringGroup.label = Gruppo Di Attivazione +module-type.core.GroupCommandTrigger.output.triggeringGroup.description = il gruppo a cui appartiene l'item module-type.core.GroupCommandTrigger.output.triggeringItem.label = Elemento innescante module-type.core.GroupCommandTrigger.output.triggeringItem.description = Il membro del gruppo che ha ricevuto il comando module-type.core.GroupCommandTrigger.output.command.label = Comando @@ -118,6 +120,8 @@ module-type.core.GroupStateUpdateTrigger.config.state.option.OPEN = APERTO module-type.core.GroupStateUpdateTrigger.config.state.option.CLOSED = CHIUSO module-type.core.GroupStateUpdateTrigger.config.state.option.UP = SU module-type.core.GroupStateUpdateTrigger.config.state.option.DOWN = GIÙ +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.label = Gruppo Di Attivazione +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.description = il gruppo a cui appartiene l'item module-type.core.GroupStateUpdateTrigger.output.triggeringItem.label = Elemento innescante module-type.core.GroupStateUpdateTrigger.output.triggeringItem.description = Il membro del gruppo che ha aggiornato il suo stato module-type.core.GroupStateUpdateTrigger.output.state.label = Stato diff --git a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_pl.properties b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_pl.properties index 411dc199435..148f3b3099e 100644 --- a/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_pl.properties +++ b/bundles/org.openhab.core.automation/src/main/resources/OH-INF/i18n/automation_pl.properties @@ -66,6 +66,8 @@ module-type.core.GroupCommandTrigger.config.command.option.OPEN = OTWÓRZ module-type.core.GroupCommandTrigger.config.command.option.CLOSED = ZAMKNIĘTE module-type.core.GroupCommandTrigger.config.command.option.UP = W GÓRĘ module-type.core.GroupCommandTrigger.config.command.option.DOWN = W DUŁ +module-type.core.GroupCommandTrigger.output.triggeringGroup.label = Grupa wyzwalająca +module-type.core.GroupCommandTrigger.output.triggeringGroup.description = grupa, do której należy element module-type.core.GroupCommandTrigger.output.triggeringItem.label = Wlement wyzwalający module-type.core.GroupCommandTrigger.output.triggeringItem.description = członek grupy, który otrzymał polecenie module-type.core.GroupCommandTrigger.output.command.label = Komenda @@ -118,6 +120,8 @@ module-type.core.GroupStateUpdateTrigger.config.state.option.OPEN = OTWÓRZ module-type.core.GroupStateUpdateTrigger.config.state.option.CLOSED = ZAMKNIĘTY module-type.core.GroupStateUpdateTrigger.config.state.option.UP = W GÓRĘ module-type.core.GroupStateUpdateTrigger.config.state.option.DOWN = W DUŁ +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.label = Grupa wyzwalająca +module-type.core.GroupStateUpdateTrigger.output.triggeringGroup.description = grupa, do której należy ten element module-type.core.GroupStateUpdateTrigger.output.triggeringItem.label = Element wyzwalający module-type.core.GroupStateUpdateTrigger.output.triggeringItem.description = członek grupy, który zaktualizował swój stan module-type.core.GroupStateUpdateTrigger.output.state.label = Stan diff --git a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandlerTest.java b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandlerTest.java index 969e57c1b54..1b2638718d2 100644 --- a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandlerTest.java +++ b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandlerTest.java @@ -13,6 +13,7 @@ package org.openhab.core.automation.internal.module.handler; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.time.ZoneId; @@ -20,6 +21,8 @@ import java.util.Collection; import java.util.Map; +import javax.measure.quantity.Temperature; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +37,7 @@ import org.openhab.core.automation.util.ConditionBuilder; import org.openhab.core.config.core.Configuration; import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; @@ -77,7 +81,9 @@ public ParameterSet(String itemType, String comparisonState, State itemState, bo ((NumberItem) item).setState(itemState); break; case "Number:Temperature": - item = new NumberItem("Number:Temperature", ITEM_NAME); + UnitProvider unitProviderMock = mock(UnitProvider.class); + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); + item = new NumberItem("Number:Temperature", ITEM_NAME, unitProviderMock); ((NumberItem) item).setState(itemState); break; case "Dimmer": @@ -102,8 +108,8 @@ public static Collection equalsParameters() { { new ParameterSet("Number", "5", new DecimalType(23), false) }, // { new ParameterSet("Number", "5", new DecimalType(5), true) }, // { new ParameterSet("Number:Temperature", "5 °C", new DecimalType(23), false) }, // - { new ParameterSet("Number:Temperature", "5 °C", new DecimalType(5), false) }, // - { new ParameterSet("Number:Temperature", "0", new QuantityType<>(), true) }, // + { new ParameterSet("Number:Temperature", "5 °C", new DecimalType(5), true) }, // + { new ParameterSet("Number:Temperature", "0", new DecimalType(), false) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), false) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), false) }, // { new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), false) }, // @@ -119,7 +125,7 @@ public static Collection greaterThanParameters() { { new ParameterSet("Number", "5", new DecimalType(5), false) }, // { new ParameterSet("Number", "5 °C", new DecimalType(23), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(5), false) }, // - { new ParameterSet("Number:Temperature", "0", new QuantityType<>(), false) }, // + { new ParameterSet("Number:Temperature", "0", new DecimalType(), false) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), false) }, // { new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), true) }, // @@ -138,7 +144,7 @@ public static Collection greaterThanOrEqualsParameters() { { new ParameterSet("Number", "5 °C", new DecimalType(23), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(4), false) }, // - { new ParameterSet("Number:Temperature", "0", new QuantityType<>(), true) }, // + { new ParameterSet("Number:Temperature", "0", new DecimalType(), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), false) }, // @@ -159,7 +165,7 @@ public static Collection lessThanParameters() { { new ParameterSet("Number", "5", new DecimalType(4), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(23), false) }, // { new ParameterSet("Number", "5 °C", new DecimalType(4), true) }, // - { new ParameterSet("Number:Temperature", "0", new QuantityType<>(), false) }, // + { new ParameterSet("Number:Temperature", "0", new DecimalType(), false) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), false) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), false) }, // @@ -179,7 +185,7 @@ public static Collection lessThanOrEqualsParameters() { { new ParameterSet("Number", "5 °C", new DecimalType(23), false) }, // { new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(4), true) }, // - { new ParameterSet("Number:Temperature", "0", new QuantityType<>(), true) }, // + { new ParameterSet("Number:Temperature", "0", new DecimalType(), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), false) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), true) }, // @@ -220,9 +226,11 @@ public void testEqualsCondition(ParameterSet parameterSet) { ItemStateConditionHandler handler = initItemStateConditionHandler("=", parameterSet.comparisonState); if (parameterSet.expectedResult) { - assertTrue(handler.isSatisfied(Map.of())); + assertTrue(handler.isSatisfied(Map.of()), + parameterSet.item + ", comparisonState=" + parameterSet.comparisonState); } else { - assertFalse(handler.isSatisfied(Map.of())); + assertFalse(handler.isSatisfied(Map.of()), + parameterSet.item + ", comparisonState=" + parameterSet.comparisonState); } } diff --git a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandlerTest.java b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandlerTest.java new file mode 100644 index 00000000000..4d25df52dec --- /dev/null +++ b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/handler/SystemTriggerHandlerTest.java @@ -0,0 +1,165 @@ +/** + * 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.internal.module.handler; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.events.Event; +import org.openhab.core.events.system.SystemEventFactory; +import org.openhab.core.service.StartLevelService; +import org.osgi.framework.BundleContext; + +/** + * The {@link SystemTriggerHandlerTest} contains tests for the {@link SystemTriggerHandler} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class SystemTriggerHandlerTest { + private static final int CFG_STARTLEVEL = 80; + + private @Mock @NonNullByDefault({}) BundleContext bundleContextMock; + private @Mock @NonNullByDefault({}) StartLevelService startLevelServiceMock; + private @Mock @NonNullByDefault({}) TriggerHandlerCallback callbackMock; + + private @Mock @NonNullByDefault({}) Trigger triggerMock; + + @BeforeEach + public void setup() { + when(triggerMock.getConfiguration()) + .thenReturn(new Configuration(Map.of(SystemTriggerHandler.CFG_STARTLEVEL, CFG_STARTLEVEL))); + when(triggerMock.getTypeUID()).thenReturn(SystemTriggerHandler.STARTLEVEL_MODULE_TYPE_ID); + } + + @Test + public void testDoesNotTriggerIfStartLevelTooLow() { + when(startLevelServiceMock.getStartLevel()).thenReturn(0); + + SystemTriggerHandler triggerHandler = new SystemTriggerHandler(triggerMock, bundleContextMock, + startLevelServiceMock); + triggerHandler.setCallback(callbackMock); + + verifyNoInteractions(callbackMock); + } + + @Test + public void testDoesTriggerImmediatelyIfStartLevelHigherOnInit() { + when(startLevelServiceMock.getStartLevel()).thenReturn(100); + + SystemTriggerHandler triggerHandler = new SystemTriggerHandler(triggerMock, bundleContextMock, + startLevelServiceMock); + triggerHandler.setCallback(callbackMock); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Map.class); + verify(callbackMock).triggered(eq(triggerMock), captor.capture()); + + Map configuration = (Map) captor.getValue(); + assertThat(configuration.get(SystemTriggerHandler.OUT_STARTLEVEL), is(CFG_STARTLEVEL)); + } + + @Test + public void testDoesNotTriggerIfStartLevelEventLower() { + when(startLevelServiceMock.getStartLevel()).thenReturn(0); + + SystemTriggerHandler triggerHandler = new SystemTriggerHandler(triggerMock, bundleContextMock, + startLevelServiceMock); + triggerHandler.setCallback(callbackMock); + + Event event = SystemEventFactory.createStartlevelEvent(70); + triggerHandler.receive(event); + + verifyNoInteractions(callbackMock); + } + + @Test + public void testDoesTriggerIfStartLevelEventHigher() { + when(startLevelServiceMock.getStartLevel()).thenReturn(0); + + SystemTriggerHandler triggerHandler = new SystemTriggerHandler(triggerMock, bundleContextMock, + startLevelServiceMock); + triggerHandler.setCallback(callbackMock); + + Event event = SystemEventFactory.createStartlevelEvent(100); + triggerHandler.receive(event); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Map.class); + verify(callbackMock).triggered(eq(triggerMock), captor.capture()); + + Map configuration = (Map) captor.getValue(); + assertThat(configuration.get(SystemTriggerHandler.OUT_STARTLEVEL), is(CFG_STARTLEVEL)); + } + + @Test + public void testDoesNotTriggerAfterInitialTrigger() { + when(startLevelServiceMock.getStartLevel()).thenReturn(100); + + SystemTriggerHandler triggerHandler = new SystemTriggerHandler(triggerMock, bundleContextMock, + startLevelServiceMock); + triggerHandler.setCallback(callbackMock); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Map.class); + verify(callbackMock).triggered(eq(triggerMock), captor.capture()); + + Map configuration = (Map) captor.getValue(); + assertThat(configuration.get(SystemTriggerHandler.OUT_STARTLEVEL), is(CFG_STARTLEVEL)); + + Event event = SystemEventFactory.createStartlevelEvent(100); + triggerHandler.receive(event); + + verifyNoMoreInteractions(callbackMock); + } + + @Test + public void testDoesNotTriggerAfterEventTrigger() { + when(startLevelServiceMock.getStartLevel()).thenReturn(0); + + SystemTriggerHandler triggerHandler = new SystemTriggerHandler(triggerMock, bundleContextMock, + startLevelServiceMock); + triggerHandler.setCallback(callbackMock); + + Event event = SystemEventFactory.createStartlevelEvent(100); + triggerHandler.receive(event); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Map.class); + verify(callbackMock).triggered(eq(triggerMock), captor.capture()); + + Map configuration = (Map) captor.getValue(); + assertThat(configuration.get(SystemTriggerHandler.OUT_STARTLEVEL), is(CFG_STARTLEVEL)); + + triggerHandler.receive(event); + + verifyNoMoreInteractions(callbackMock); + } +} diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java index 338a5944f49..98212328d12 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java @@ -69,7 +69,7 @@ public enum Type { /** * The data type for a boolean ({@code true} or {@code false}). */ - BOOLEAN; + BOOLEAN } diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigParser.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigParser.java index faf94df0268..b59980bf193 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigParser.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigParser.java @@ -102,7 +102,7 @@ private ConfigParser() { } // Allows to have List, List, List etc (and the corresponding Set) - if (value instanceof Collection) { + if (value instanceof Collection collection1) { Class innerClass = (Class) ((ParameterizedType) field.getGenericType()) .getActualTypeArguments()[0]; Collection collection; @@ -114,7 +114,7 @@ private ConfigParser() { LOGGER.warn("Skipping field '{}', only List and Set is supported as target Collection", fieldName); continue; } - for (final Object it : (Collection) value) { + for (final Object it : collection1) { final Object normalized = valueAs(it, innerClass); if (normalized == null) { continue; @@ -185,8 +185,7 @@ public static T valueAsOrElse(@Nullable Object value, Class type, T defau Object result = value; // Handle the conversion case of Number to Float,Double,Long,Integer,Short,Byte,BigDecimal - if (value instanceof Number) { - Number number = (Number) value; + if (value instanceof Number number) { if (Float.class.equals(typeClass)) { result = number.floatValue(); } else if (Double.class.equals(typeClass)) { @@ -202,9 +201,8 @@ public static T valueAsOrElse(@Nullable Object value, Class type, T defau } else if (BigDecimal.class.equals(typeClass)) { result = new BigDecimal(number.toString()); } - } else if (value instanceof String && !String.class.equals(typeClass)) { + } else if (value instanceof String strValue && !String.class.equals(typeClass)) { // Handle the conversion case of String to Float,Double,Long,Integer,BigDecimal,Boolean - String strValue = (String) value; if (Float.class.equals(typeClass)) { result = Float.valueOf(strValue); } else if (Double.class.equals(typeClass)) { diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java index f662f6d3167..88e92affcca 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java @@ -184,8 +184,8 @@ public static Object normalizeType(Object value, @Nullable ConfigDescriptionPara return value; } else if (value instanceof Number) { return new BigDecimal(value.toString()); - } else if (value instanceof Collection) { - return normalizeCollection((Collection) value); + } else if (value instanceof Collection collection) { + return normalizeCollection(collection); } throw new IllegalArgumentException( "Invalid type '{" + value.getClass().getCanonicalName() + "}' of configuration value!"); diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/Configuration.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/Configuration.java index 9691601cec1..50d15021043 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/Configuration.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/Configuration.java @@ -135,7 +135,7 @@ public int hashCode() { @Override public boolean equals(@Nullable Object obj) { - return (obj instanceof Configuration) && this.properties.equals(((Configuration) obj).properties); + return (obj instanceof Configuration c) && this.properties.equals(c.properties); } @Override diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigurationSerializer.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigurationSerializer.java index 2c4658ddb46..53069f26eca 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigurationSerializer.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigurationSerializer.java @@ -38,9 +38,9 @@ public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializatio JsonObject result = new JsonObject(); src.keySet().stream().sorted().forEachOrdered((String propName) -> { Object value = src.get(propName); - if (value instanceof List) { + if (value instanceof List list) { JsonArray array = new JsonArray(); - for (Object element : (List) value) { + for (Object element : list) { array.add(serializePrimitive(element)); } result.add(propName, array); @@ -52,12 +52,12 @@ public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializatio } private JsonPrimitive serializePrimitive(Object primitive) { - if (primitive instanceof String) { - return new JsonPrimitive((String) primitive); - } else if (primitive instanceof Number) { - return new JsonPrimitive((Number) primitive); - } else if (primitive instanceof Boolean) { - return new JsonPrimitive((Boolean) primitive); + if (primitive instanceof String string) { + return new JsonPrimitive(string); + } else if (primitive instanceof Number number) { + return new JsonPrimitive(number); + } else if (primitive instanceof Boolean boolean1) { + return new JsonPrimitive(boolean1); } return null; } diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/OrderingMapSerializer.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/OrderingMapSerializer.java index 0b51e6e09ce..c6a74a4d5dd 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/OrderingMapSerializer.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/OrderingMapSerializer.java @@ -48,8 +48,8 @@ public JsonElement serialize(Map<@Nullable Object, @Nullable Object> src, Type t } possiblySortedStream.forEachOrdered(entry -> { Object key = entry.getKey(); - if (key instanceof String) { - ordered.add((String) key, context.serialize(entry.getValue())); + if (key instanceof String string) { + ordered.add(string, context.serialize(entry.getValue())); } else { JsonElement serialized = context.serialize(key); ordered.add(serialized.isJsonPrimitive() ? serialized.getAsString() : serialized.toString(), diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java index 97a8845101d..c5613ca8cc0 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java @@ -79,16 +79,10 @@ public class I18nConfigOptionsProvider implements ConfigOptionProvider { } private Collection processTimeZoneParam() { - Comparator byOffset = (t1, t2) -> { - return t1.getRawOffset() - t2.getRawOffset(); - }; - Comparator byID = (t1, t2) -> { - return t1.getID().compareTo(t2.getID()); - }; + Comparator byOffset = (t1, t2) -> t1.getRawOffset() - t2.getRawOffset(); + Comparator byID = (t1, t2) -> t1.getID().compareTo(t2.getID()); return ZoneId.getAvailableZoneIds().stream().map(TimeZone::getTimeZone).sorted(byOffset.thenComparing(byID)) - .map(tz -> { - return new ParameterOption(tz.getID(), getTimeZoneRepresentation(tz)); - }).collect(Collectors.toList()); + .map(tz -> new ParameterOption(tz.getID(), getTimeZoneRepresentation(tz))).collect(Collectors.toList()); } private static String getTimeZoneRepresentation(TimeZone tz) { diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/ListNormalizer.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/ListNormalizer.java index 8569e9459fb..ce19336fb32 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/ListNormalizer.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/ListNormalizer.java @@ -45,16 +45,16 @@ public Object doNormalize(Object value) { } return ret; } - if (value instanceof List) { - List ret = new ArrayList(((List) value).size()); - for (Object object : (List) value) { + if (value instanceof List list) { + List ret = new ArrayList(list.size()); + for (Object object : list) { ret.add(delegate.normalize(object)); } return ret; } - if (value instanceof Iterable) { + if (value instanceof Iterable iterable) { List ret = new ArrayList(); - for (Object object : (Iterable) value) { + for (Object object : iterable) { ret.add(delegate.normalize(object)); } return ret; diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/Normalizer.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/Normalizer.java index 2b2c577677b..593b8a96e6b 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/Normalizer.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/normalization/Normalizer.java @@ -31,5 +31,5 @@ public interface Normalizer { * @param value the object to be normalized * @return the well-defined type or the given object, if it was not possible to convert it */ - public Object normalize(Object value); + Object normalize(Object value); } diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionConverter.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionConverter.java index 6d7d50a2fa8..a3a30be56f9 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionConverter.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionConverter.java @@ -89,11 +89,11 @@ public ConfigDescriptionConverter() { // respective arrays while (nodeIterator.hasNext()) { Object node = nodeIterator.next(); - if (node instanceof ConfigDescriptionParameter) { - configDescriptionParams.add((ConfigDescriptionParameter) node); + if (node instanceof ConfigDescriptionParameter parameter) { + configDescriptionParams.add(parameter); } - if (node instanceof ConfigDescriptionParameterGroup) { - configDescriptionGroups.add((ConfigDescriptionParameterGroup) node); + if (node instanceof ConfigDescriptionParameterGroup group) { + configDescriptionGroups.add(group); } } diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionParameterConverter.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionParameterConverter.java index 6dcf86bcd15..7574db2d7f2 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionParameterConverter.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionParameterConverter.java @@ -146,12 +146,10 @@ private Boolean falseIfNull(@Nullable Boolean b) { } private @Nullable List readParameterOptions(@Nullable Object rawNodeValueList) { - if (rawNodeValueList instanceof List) { - List list = (List) rawNodeValueList; + if (rawNodeValueList instanceof List list) { List result = new ArrayList<>(); for (Object object : list) { - if (object instanceof NodeValue) { - NodeValue nodeValue = (NodeValue) object; + if (object instanceof NodeValue nodeValue) { String value = nodeValue.getAttributes().get("value"); String label = nodeValue.getValue().toString(); result.add(new ParameterOption(value, label)); diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/ConverterValueMap.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/ConverterValueMap.java index dd8e12d1fa4..d67175c8bad 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/ConverterValueMap.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/ConverterValueMap.java @@ -154,9 +154,9 @@ public static Map readValueMap(HierarchicalStreamReader reader, public @Nullable String getString(String nodeName, @Nullable String defaultValue) { Object value = this.valueMap.get(nodeName); - if (value instanceof String) { + if (value instanceof String string) { // fixes a formatting problem with line breaks in text - return ((String) value).replaceAll("\\n\\s*", " ").trim(); + return string.replaceAll("\\n\\s*", " ").trim(); } return defaultValue; diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeIterator.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeIterator.java index 16a215207f4..fdee71def99 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeIterator.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeIterator.java @@ -109,8 +109,8 @@ public void assertEndOfType() throws ConversionException { if (hasNext()) { Object nextNode = next(); - if (nextNode instanceof NodeName) { - if (nodeName.equals(((NodeName) nextNode).getNodeName())) { + if (nextNode instanceof NodeName name) { + if (nodeName.equals(name.getNodeName())) { return nextNode; } } @@ -147,9 +147,9 @@ public void assertEndOfType() throws ConversionException { if (hasNext()) { Object nextNode = next(); - if (nextNode instanceof NodeAttributes) { - if (nodeName.equals(((NodeName) nextNode).getNodeName())) { - return ((NodeAttributes) nextNode).getAttribute(attributeName); + if (nextNode instanceof NodeAttributes attributes) { + if (nodeName.equals(attributes.getNodeName())) { + return attributes.getAttribute(attributeName); } } @@ -180,8 +180,8 @@ public void assertEndOfType() throws ConversionException { public @Nullable Object nextValue(String nodeName, boolean required) throws ConversionException { Object value = next(nodeName, required); - if (value instanceof NodeValue) { - return ((NodeValue) value).getValue(); + if (value instanceof NodeValue nodeValue) { + return nodeValue.getValue(); } return null; @@ -203,8 +203,8 @@ public void assertEndOfType() throws ConversionException { public @Nullable List<@NonNull ?> nextList(String nodeName, boolean required) throws ConversionException { Object list = next(nodeName, required); - if (list instanceof NodeList) { - return ((NodeList) list).getList(); + if (list instanceof NodeList nodeList) { + return nodeList.getList(); } return null; diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeValue.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeValue.java index 55710a7059b..042254d6f82 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeValue.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/NodeValue.java @@ -55,8 +55,8 @@ public NodeValue(String nodeName, @Nullable Map attributes, @Nul private @Nullable Object formatText(@Nullable Object object) { // fixes a formatting problem with line breaks in text - if (object instanceof String) { - return ((String) object).replaceAll("\\n\\s*", " ").trim(); + if (object instanceof String string) { + return string.replaceAll("\\n\\s*", " ").trim(); } return object; diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties index aff65cbe021..07d06aa9e2f 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties @@ -13,6 +13,5 @@ options_violated=The value {0} does not match allowed parameter options. Allowed multiple_limit_violated=Only {0} elements are allowed but {1} are provided. bridge_not_configured=Configuring a bridge is mandatory. -type_description_missing=Type description for '{0}' not found also we checked the presence before. -config_description_missing=Config description for '{0}' not found also we checked the presence before. - +type_description_missing=Type description {0} for {1} not found, although we checked the presence before. +config_description_missing=Config description {0} for {1} not found, although we checked the presence before. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_cs.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_cs.properties index 7b9fb8c12fc..7306d12294b 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_cs.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_cs.properties @@ -13,6 +13,3 @@ options_violated=Hodnota {0} neodpovídá parametru povolených možností. Povo multiple_limit_violated=Pouze {0} prvků je povoleno, ale {1} je k dispozici. bridge_not_configured=Konfigurace mostu (bridge) je povinná. -type_description_missing=Popis typu pro ''{0}'' nebyl nalezen, také jsme zkontrolovali předchozí. -config_description_missing=Popis konfigurace pro ''{0}'' nebyl nalezen, také jsme zkontrolovali předchozí. - diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_de.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_de.properties index 65640512249..0321c872143 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_de.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_de.properties @@ -11,3 +11,5 @@ min_value_numeric_violated=Der Wert muss mindestens {0} sein. pattern_violated=Der Wert {0} entspricht nicht dem Muster {1}. options_violated=Der Wert {0} ist nicht in den erlaubten Optionen enthalten. Erlaubte Optionen sind\: {1} multiple_limit_violated=Nur {0} Optionen sind zulässig, aber {1} wurden übergeben. + +bridge_not_configured=Es wird eine Bridge benötigt. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fi.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fi.properties index cbfd0e36f99..fc0af12bce7 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fi.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fi.properties @@ -11,3 +11,7 @@ min_value_numeric_violated=Arvo ei saa olla pienempi kuin {0}. pattern_violated=Arvo {0} ei vastaa malli-lauseketta {1}. options_violated=Arvo {0} ei vastaa sallittuja parametrivaihtoehtoja. Sallitut vaihtoehdot ovat\: {1} multiple_limit_violated=Vain {0} elementtiä on sallittu, mutta {1} on annettu. + +bridge_not_configured=Silta on määritettävä. +type_description_missing=Tyypin kuvausta {0} tiedolle {1} ei löydy, vaikka olemassaolo tarkastettiin aikaisemmin. +config_description_missing=Asetuksen kuvausta {0} tiedolle {1} ei löydy, vaikka olemassaolo tarkastettiin aikaisemmin. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fr.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fr.properties index 456b27dca4e..4ff7be44ec1 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fr.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_fr.properties @@ -11,3 +11,7 @@ min_value_numeric_violated=La valeur ne doit pas être inférieure à {0}. pattern_violated=La valeur {0} ne correspond pas au modèle {1}. options_violated=La valeur {0} ne correspond pas aux options de paramètre autorisées. Les options autorisées sont \: {1} multiple_limit_violated=Seuls {0} éléments sont autorisés alors que {1} sont fournis. + +bridge_not_configured=La configuration d'un pont de connexion est obligatoire. +type_description_missing=La description de type {0} pour {1} n''a pas été trouvée, bien que nous ayons vérifié sa présence avant. +config_description_missing=La description de configuration {0} pour {1} n''a pas été trouvée, bien que nous ayons vérifié sa présence avant. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_he.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_he.properties index 4a9e1670427..ac08733c15f 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_he.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_he.properties @@ -11,3 +11,7 @@ min_value_numeric_violated=הערך איננו יכול להיות קטן מ- {0 pattern_violated=הערך {0} לא מתאים לתבנית {1}. options_violated=הערך {0} אינו תואם לאפשרויות הפרמטרים המותרות. האפשרויות המותרות הן\: {1} multiple_limit_violated=רק {0} אלמנטים מותרים אך הוזנו {1}. + +bridge_not_configured=הגדרת מגשר היא חובה. +type_description_missing=סוג תיאור {0} עבור {1} לא נמצא, למרות שבדקנו את הנוכחות בעבר. +config_description_missing=תיאור התצורה {0} עבור {1} לא נמצא, למרות שבדקנו את הנוכחות בעבר. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_hu.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_hu.properties index 13308502ae6..3d2bdf1cc5c 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_hu.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_hu.properties @@ -13,6 +13,3 @@ options_violated=A(z) {0} érték nem egyezik a megengedett paraméter-beállít multiple_limit_violated=Csak {0} elem engedélyezett, de {1} szerepel. bridge_not_configured=A híd beállítása kötelező. -type_description_missing=A {0} típus leírása nem található még az előzményekben sem. -config_description_missing=A {0} beállítás leírása nem található még az előzményekben sem. - diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_it.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_it.properties index cbd65218bd9..1c331c64f92 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_it.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_it.properties @@ -13,6 +13,5 @@ options_violated=Il valore {0} non è tra le opzioni consentite per il parametro multiple_limit_violated=Sono consentiti {0} elementi, ma ne sono stati forniti {1}. bridge_not_configured=Configurare un bridge è obbligatorio. -type_description_missing=Descrizione del tipo per ''{0}'' non trovato anche se abbiamo prima controllato la presenza. -config_description_missing=Descrizione della configurazione per ''{0}'' non trovato anche se abbiamo prima controllato la presenza. - +type_description_missing=Descrizione del tipo {0} per {1} non trovata, anche se prima abbiamo controllato la presenza. +config_description_missing=Descrizione di configurazione {0} per {1} non trovata, anche se abbiamo prima controllato la presenza. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_no.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_no.properties index 03cb5ef14d0..0d20d68d8b7 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_no.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_no.properties @@ -13,6 +13,3 @@ options_violated=Verdien {0} samsvarer ikke med tillatte parameteralternativer. multiple_limit_violated=Kun {0} elementer er tillatt mens {1} er angitt. bridge_not_configured=Konfigurasjon av en bro er obligatorisk. -type_description_missing=Typebeskivelsen for ''{0}'' ble ikke funnet, vi sjekket også tilstedeværelsen tidligere. -config_description_missing=Konfigurasjonsbeskrivelsen for ''{0}'' ble ikke funnet, vi sjekket også tilstedeværelsen tidligere. - diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_pl.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_pl.properties index 077b74545bd..c5aef7d487b 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_pl.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_pl.properties @@ -13,6 +13,5 @@ options_violated=Wartość {0} nie pasuje do dozwolonych opcji parametrów. Dozw multiple_limit_violated=Tylko {0} elementy są dozwolone, ale {1} jest zapewniony. bridge_not_configured=Konfiguracja mostka jest obowiązkowa. -type_description_missing=Nie znaleziono opisu dla ''{0}'' również sprawdzono wcześniejsze opisy. -config_description_missing=Nie znaleziono opisu konfiguracji dla ''{0}'' również sprawdzono wcześniejsze opisy. - +type_description_missing=Nie znaleziono opisu typu {0} dla {1} pomimo iż sprawdzaliśmy poprzednie wyniki. +config_description_missing=Nie znaleziono opisu typu {0} dla {1} pomimo iż sprawdzaliśmy poprzednie wyniki. diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_sl.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_sl.properties index 6e81dbf2794..0ba3b7b4159 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_sl.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation_sl.properties @@ -13,6 +13,5 @@ options_violated=Vrednost {0} se ne ujema z dovoljenimim izborom parametrov. Dov multiple_limit_violated=Dovoljenih je le {0} elementov, podanih pa je {1}. bridge_not_configured=Konfiguriranje mostu je obvezno. -type_description_missing=Opis vrste za ''{0}'' ni bil najden. Obstoj smo preverili tudi že prej. -config_description_missing=Opis konfiguracije za ''{0}'' ni bil najden. Obstoj smo preverili tudi že prej. - +type_description_missing=Opis vrste {0} za {1} ni bil najden, čeprav je obstoj že bil preverjen. +config_description_missing=Opis konfiguracije {0} za {1} ni bil najden, čeprav je obstoj že bil preverjen. diff --git a/bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java b/bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java index 2096ddd0e02..3efdb8389ed 100644 --- a/bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java +++ b/bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java @@ -154,16 +154,18 @@ private Set getSerialPortInfos() throws IOException { Path devSerialDir = Path.of(DEV_SERIAL_BY_ID_DIRECTORY); if (exists(devSerialDir) && isDirectory(devSerialDir) && isReadable(devSerialDir)) { // browse serial/by-id directory : - for (Path devLinkPath : newDirectoryStream(devSerialDir)) { - if (Files.isSymbolicLink(devLinkPath)) { - Path devicePath = getRealDevicePath(devLinkPath); - if (devicePath != null) { - String serialPortName = devicePath.getFileName().toString(); - // get the corresponding real sysinfo special dir : - Path sysfsDevicePath = getRealDevicePath( - Paths.get(sysfsTtyDevicesDirectory).resolve(serialPortName)); - if (sysfsDevicePath != null && isReadable(devicePath) && isWritable(devicePath)) { - result.add(new SerialPortInfo(devLinkPath, sysfsDevicePath)); + try (DirectoryStream directoryStream = newDirectoryStream(devSerialDir)) { + for (Path devLinkPath : directoryStream) { + if (Files.isSymbolicLink(devLinkPath)) { + Path devicePath = getRealDevicePath(devLinkPath); + if (devicePath != null) { + String serialPortName = devicePath.getFileName().toString(); + // get the corresponding real sysinfo special dir : + Path sysfsDevicePath = getRealDevicePath( + Paths.get(sysfsTtyDevicesDirectory).resolve(serialPortName)); + if (sysfsDevicePath != null && isReadable(devicePath) && isWritable(devicePath)) { + result.add(new SerialPortInfo(devLinkPath, sysfsDevicePath)); + } } } } diff --git a/bundles/org.openhab.core.config.discovery.usbserial/src/main/java/org/openhab/core/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java b/bundles/org.openhab.core.config.discovery.usbserial/src/main/java/org/openhab/core/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java index 44aba1654d2..d9c68ee69f5 100644 --- a/bundles/org.openhab.core.config.discovery.usbserial/src/main/java/org/openhab/core/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java +++ b/bundles/org.openhab.core.config.discovery.usbserial/src/main/java/org/openhab/core/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java @@ -36,7 +36,7 @@ public interface UsbSerialDiscoveryParticipant { * * @return a set of thing type UIDs for which results can be created */ - public Set getSupportedThingTypeUIDs(); + Set getSupportedThingTypeUIDs(); /** * Creates a discovery result for a USB device with corresponding serial port. @@ -45,7 +45,8 @@ public interface UsbSerialDiscoveryParticipant { * @return the according discovery result or null if the device is not * supported by this participant */ - public @Nullable DiscoveryResult createResult(UsbSerialDeviceInformation deviceInformation); + @Nullable + DiscoveryResult createResult(UsbSerialDeviceInformation deviceInformation); /** * Returns the thing UID for a USB device with corresponding serial port. @@ -54,5 +55,6 @@ public interface UsbSerialDiscoveryParticipant { * @return a thing UID or null if the device is not supported * by this participant */ - public @Nullable ThingUID getThingUID(UsbSerialDeviceInformation deviceInformation); + @Nullable + ThingUID getThingUID(UsbSerialDeviceInformation deviceInformation); } diff --git a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java index acfaf9192a4..094ab1f484b 100644 --- a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java +++ b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java @@ -429,15 +429,15 @@ protected long getTimestampOfLastScan() { } private boolean getAutoDiscoveryEnabled(Object autoDiscoveryEnabled) { - if (autoDiscoveryEnabled instanceof String) { - return Boolean.valueOf((String) autoDiscoveryEnabled); + if (autoDiscoveryEnabled instanceof String string) { + return Boolean.valueOf(string); } else { return Boolean.TRUE.equals(autoDiscoveryEnabled); } } private String inferKey(DiscoveryResult discoveryResult, String lastSegment) { - return "discovery." + discoveryResult.getThingUID().getAsString().replaceAll(":", ".") + "." + lastSegment; + return "discovery." + discoveryResult.getThingUID().getAsString().replace(":", ".") + "." + lastSegment; } protected DiscoveryResult getLocalizedDiscoveryResult(final DiscoveryResult discoveryResult, diff --git a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/DiscoveryResultFlag.java b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/DiscoveryResultFlag.java index b668a5fe18e..524c5ef400a 100644 --- a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/DiscoveryResultFlag.java +++ b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/DiscoveryResultFlag.java @@ -35,6 +35,6 @@ public enum DiscoveryResultFlag { * The flag {@code IGNORED} to signal that the result object should be regarded * as known by the system so that a further processing should be skipped. */ - IGNORED; + IGNORED } diff --git a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/internal/PersistentInbox.java b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/internal/PersistentInbox.java index d55ee3045fa..c56a661426d 100644 --- a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/internal/PersistentInbox.java +++ b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/internal/PersistentInbox.java @@ -284,8 +284,7 @@ private void internalAdd(DiscoveryResultWrapper discoveryResultWrapper) { logger.info("Added new thing '{}' to inbox.", thingUID); discoveryResultWrapper.future.complete(true); } else { - if (inboxResult instanceof DiscoveryResultImpl) { - DiscoveryResultImpl resultImpl = (DiscoveryResultImpl) inboxResult; + if (inboxResult instanceof DiscoveryResultImpl resultImpl) { resultImpl.synchronize(discoveryResult); discoveryResultStorage.put(discoveryResult.getThingUID().toString(), resultImpl); notifyListeners(resultImpl, EventType.UPDATED); @@ -478,8 +477,7 @@ public void updated(Thing oldThing, Thing thing) { @Override public void setFlag(ThingUID thingUID, @Nullable DiscoveryResultFlag flag) { DiscoveryResult result = get(thingUID); - if (result instanceof DiscoveryResultImpl) { - DiscoveryResultImpl resultImpl = (DiscoveryResultImpl) result; + if (result instanceof DiscoveryResultImpl resultImpl) { resultImpl.setFlag((flag == null) ? DiscoveryResultFlag.NEW : flag); discoveryResultStorage.put(resultImpl.getThingUID().toString(), resultImpl); notifyListeners(resultImpl, EventType.UPDATED); diff --git a/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java b/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java index 2842a08d851..f0e31c81b19 100644 --- a/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java +++ b/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java @@ -51,10 +51,10 @@ public class AbstractDiscoveryServiceTest implements DiscoveryListener { private static final String VALUE1 = "value1"; private static final String VALUE2 = "value2"; private final Map properties = Map.of(KEY1, VALUE1, KEY2, VALUE2); - private static final String DISCOVERY_THING2_INFERED_KEY = "discovery." - + THING_UID2.getAsString().replaceAll(":", ".") + ".label"; - private static final String DISCOVERY_THING4_INFERED_KEY = "discovery." - + THING_UID4.getAsString().replaceAll(":", ".") + ".label"; + private static final String DISCOVERY_THING2_INFERED_KEY = "discovery." + THING_UID2.getAsString().replace(":", ".") + + ".label"; + private static final String DISCOVERY_THING4_INFERED_KEY = "discovery." + THING_UID4.getAsString().replace(":", ".") + + ".label"; private static final String DISCOVERY_LABEL = "Result Test"; private static final String DISCOVERY_LABEL_KEY1 = "@text/test"; private static final String DISCOVERY_LABEL_KEY2 = "@text/test2 [ \"50\", \"number\" ]"; diff --git a/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcher.java b/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcher.java index 382b97ecdd0..3246d06dcb1 100644 --- a/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcher.java +++ b/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcher.java @@ -233,7 +233,7 @@ public void processConfigFile(File dir) { Arrays.sort(files, new Comparator() { @Override public int compare(File left, File right) { - return Long.valueOf(left.lastModified()).compareTo(right.lastModified()); + return Long.compare(left.lastModified(), right.lastModified()); } }); for (File file : files) { diff --git a/bundles/org.openhab.core.ephemeris/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImpl.java b/bundles/org.openhab.core.ephemeris/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImpl.java index 8b68ab80b59..65b9b8bf9d0 100644 --- a/bundles/org.openhab.core.ephemeris/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImpl.java +++ b/bundles/org.openhab.core.ephemeris/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImpl.java @@ -46,8 +46,8 @@ import org.openhab.core.ephemeris.EphemerisManager; import org.openhab.core.i18n.LocaleProvider; import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; -import org.osgi.framework.FrameworkUtil; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Modified; @@ -59,6 +59,7 @@ import de.jollyday.HolidayManager; import de.jollyday.ManagerParameter; import de.jollyday.ManagerParameters; +import de.jollyday.parameter.CalendarPartManagerParameter; import de.jollyday.util.ResourceUtil; /** @@ -82,7 +83,9 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi public static final String CONFIG_REGION = "region"; public static final String CONFIG_CITY = "city"; - private static final String JOLLYDAY_COUNTRY_DESCRIPTIONS = "jollyday/descriptions/country_descriptions.properties"; + private static final String RESOURCES_ROOT = "jollyday/"; + private static final String JOLLYDAY_COUNTRY_DESCRIPTIONS = RESOURCES_ROOT + + "descriptions/country_descriptions.properties"; private static final String PROPERTY_COUNTRY_DESCRIPTION_PREFIX = "country.description."; private static final String PROPERTY_COUNTRY_DESCRIPTION_DELIMITER = "\\."; @@ -99,15 +102,16 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi private final ResourceUtil resourceUtil = new ResourceUtil(); private final LocaleProvider localeProvider; + private final Bundle bundle; private @NonNullByDefault({}) String country; private @Nullable String region; @Activate - public EphemerisManagerImpl(final @Reference LocaleProvider localeProvider) { + public EphemerisManagerImpl(final @Reference LocaleProvider localeProvider, final BundleContext bundleContext) { this.localeProvider = localeProvider; + bundle = bundleContext.getBundle(); - final Bundle bundle = FrameworkUtil.getBundle(getClass()); try (InputStream stream = bundle.getResource(JOLLYDAY_COUNTRY_DESCRIPTIONS).openStream()) { final Properties properties = new Properties(); properties.load(stream); @@ -153,8 +157,8 @@ protected void modified(Map config) { } else { logger.warn("Erroneous day set definition {} : {}", e.getKey(), entry); } - } else if (entry instanceof Iterable) { - addDayset(setName, (Iterable) entry); + } else if (entry instanceof Iterable iterable) { + addDayset(setName, iterable); } } else { logger.warn("Erroneous day set definition {}", e.getKey()); @@ -164,23 +168,26 @@ protected void modified(Map config) { } }); - if (config.containsKey(CONFIG_COUNTRY)) { - country = config.get(CONFIG_COUNTRY).toString().toLowerCase(); + Object configValue = config.get(CONFIG_COUNTRY); + if (configValue != null) { + country = configValue.toString().toLowerCase(); } else { country = localeProvider.getLocale().getCountry().toLowerCase(); logger.debug("Using system default country '{}' ", country); } - if (config.containsKey(CONFIG_REGION)) { - String region = config.get(CONFIG_REGION).toString().toLowerCase(); + configValue = config.get(CONFIG_REGION); + if (configValue != null) { + String region = configValue.toString().toLowerCase(); countryParameters.add(region); this.region = region; } else { this.region = null; } - if (config.containsKey(CONFIG_CITY)) { - countryParameters.add(config.get(CONFIG_CITY).toString()); + configValue = config.get(CONFIG_CITY); + if (configValue != null) { + countryParameters.add(configValue.toString()); } } @@ -231,9 +238,16 @@ private URL getUrl(String filename) throws FileNotFoundException { private HolidayManager getHolidayManager(Object managerKey) { HolidayManager holidayManager = holidayManagers.get(managerKey); if (holidayManager == null) { - final ManagerParameter parameters = managerKey.getClass() == String.class - ? ManagerParameters.create((String) managerKey) - : ManagerParameters.create((URL) managerKey); + final ManagerParameter parameters; + if (managerKey instanceof String stringKey) { + URL urlOverride = bundle + .getResource(RESOURCES_ROOT + CalendarPartManagerParameter.getConfigurationFileName(stringKey)); + parameters = urlOverride != null // + ? ManagerParameters.create(urlOverride) + : ManagerParameters.create(stringKey); + } else { + parameters = ManagerParameters.create((URL) managerKey); + } holidayManager = HolidayManager.getInstance(parameters); holidayManagers.put(managerKey, holidayManager); } @@ -297,9 +311,10 @@ public boolean isWeekend(ZonedDateTime date) { @Override public boolean isInDayset(String daysetName, ZonedDateTime date) { - if (daysets.containsKey(daysetName)) { + Set dayset = daysets.get(daysetName); + if (dayset != null) { DayOfWeek dow = date.getDayOfWeek(); - return daysets.get(daysetName).contains(dow); + return dayset.contains(dow); } else { logger.warn("This dayset is not configured : {}", daysetName); return false; @@ -381,8 +396,9 @@ void parseProperty(Object key, Object value) throws IllegalArgumentException { case 2: part = getValidPart(parts[0]); option = new ParameterOption(getValidPart(parts[1]), name); - if (regions.containsKey(part)) { - regions.get(part).add(option); + List regionsPart = regions.get(part); + if (regionsPart != null) { + regionsPart.add(option); } else { final List options = new ArrayList<>(); options.add(option); @@ -392,8 +408,9 @@ void parseProperty(Object key, Object value) throws IllegalArgumentException { case 3: part = getValidPart(parts[1]); option = new ParameterOption(getValidPart(parts[2]), name); - if (cities.containsKey(part)) { - cities.get(part).add(option); + List citiesPart = cities.get(part); + if (citiesPart != null) { + citiesPart.add(option); } else { final List options = new ArrayList<>(); options.add(option); diff --git a/bundles/org.openhab.core.ephemeris/src/main/resources/jollyday/holidays/Holidays_dk.xml b/bundles/org.openhab.core.ephemeris/src/main/resources/jollyday/holidays/Holidays_dk.xml new file mode 100644 index 00000000000..e7510def3c3 --- /dev/null +++ b/bundles/org.openhab.core.ephemeris/src/main/resources/jollyday/holidays/Holidays_dk.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.io.bin2json/src/main/java/org/openhab/core/io/bin2json/Bin2Json.java b/bundles/org.openhab.core.io.bin2json/src/main/java/org/openhab/core/io/bin2json/Bin2Json.java index 203c3f644ec..f268a9b4316 100644 --- a/bundles/org.openhab.core.io.bin2json/src/main/java/org/openhab/core/io/bin2json/Bin2Json.java +++ b/bundles/org.openhab.core.io.bin2json/src/main/java/org/openhab/core/io/bin2json/Bin2Json.java @@ -166,41 +166,40 @@ private JsonObject convertToJSon(@Nullable final JsonObject json, final JBBPAbst final String fieldName = field.getFieldName() == null ? "nonamed" : field.getFieldName(); if (field instanceof JBBPAbstractArrayField) { final JsonArray jsonArray = new JsonArray(); - if (field instanceof JBBPFieldArrayBit) { - for (final byte b : ((JBBPFieldArrayBit) field).getArray()) { + if (field instanceof JBBPFieldArrayBit bit) { + for (final byte b : bit.getArray()) { jsonArray.add(new JsonPrimitive(b)); } - } else if (field instanceof JBBPFieldArrayBoolean) { - for (final boolean b : ((JBBPFieldArrayBoolean) field).getArray()) { + } else if (field instanceof JBBPFieldArrayBoolean boolean1) { + for (final boolean b : boolean1.getArray()) { jsonArray.add(new JsonPrimitive(b)); } - } else if (field instanceof JBBPFieldArrayByte) { - for (final byte b : ((JBBPFieldArrayByte) field).getArray()) { + } else if (field instanceof JBBPFieldArrayByte byte1) { + for (final byte b : byte1.getArray()) { jsonArray.add(new JsonPrimitive(b)); } - } else if (field instanceof JBBPFieldArrayInt) { - for (final int b : ((JBBPFieldArrayInt) field).getArray()) { + } else if (field instanceof JBBPFieldArrayInt int1) { + for (final int b : int1.getArray()) { jsonArray.add(new JsonPrimitive(b)); } - } else if (field instanceof JBBPFieldArrayLong) { - for (final long b : ((JBBPFieldArrayLong) field).getArray()) { + } else if (field instanceof JBBPFieldArrayLong long1) { + for (final long b : long1.getArray()) { jsonArray.add(new JsonPrimitive(b)); } - } else if (field instanceof JBBPFieldArrayShort) { - for (final short b : ((JBBPFieldArrayShort) field).getArray()) { + } else if (field instanceof JBBPFieldArrayShort short1) { + for (final short b : short1.getArray()) { jsonArray.add(new JsonPrimitive(b)); } - } else if (field instanceof JBBPFieldArrayStruct) { - final JBBPFieldArrayStruct array = (JBBPFieldArrayStruct) field; + } else if (field instanceof JBBPFieldArrayStruct array) { for (int i = 0; i < array.size(); i++) { jsonArray.add(convertToJSon(new JsonObject(), array.getElementAt(i))); } - } else if (field instanceof JBBPFieldArrayUByte) { - for (final byte b : ((JBBPFieldArrayUByte) field).getArray()) { + } else if (field instanceof JBBPFieldArrayUByte byte1) { + for (final byte b : byte1.getArray()) { jsonArray.add(new JsonPrimitive(b & 0xFF)); } - } else if (field instanceof JBBPFieldArrayUShort) { - for (final short b : ((JBBPFieldArrayUShort) field).getArray()) { + } else if (field instanceof JBBPFieldArrayUShort short1) { + for (final short b : short1.getArray()) { jsonArray.add(new JsonPrimitive(b & 0xFFFF)); } } else { @@ -208,20 +207,19 @@ private JsonObject convertToJSon(@Nullable final JsonObject json, final JBBPAbst } jsn.add(fieldName, jsonArray); } else { - if (field instanceof JBBPFieldBit) { - jsn.addProperty(fieldName, ((JBBPFieldBit) field).getAsInt()); - } else if (field instanceof JBBPFieldBoolean) { - jsn.addProperty(fieldName, ((JBBPFieldBoolean) field).getAsBool()); - } else if (field instanceof JBBPFieldByte) { - jsn.addProperty(fieldName, ((JBBPFieldByte) field).getAsInt()); - } else if (field instanceof JBBPFieldInt) { - jsn.addProperty(fieldName, ((JBBPFieldInt) field).getAsInt()); - } else if (field instanceof JBBPFieldLong) { - jsn.addProperty(fieldName, ((JBBPFieldLong) field).getAsLong()); - } else if (field instanceof JBBPFieldShort) { - jsn.addProperty(fieldName, ((JBBPFieldShort) field).getAsInt()); - } else if (field instanceof JBBPFieldStruct) { - final JBBPFieldStruct struct = (JBBPFieldStruct) field; + if (field instanceof JBBPFieldBit bit) { + jsn.addProperty(fieldName, bit.getAsInt()); + } else if (field instanceof JBBPFieldBoolean boolean1) { + jsn.addProperty(fieldName, boolean1.getAsBool()); + } else if (field instanceof JBBPFieldByte byte1) { + jsn.addProperty(fieldName, byte1.getAsInt()); + } else if (field instanceof JBBPFieldInt int1) { + jsn.addProperty(fieldName, int1.getAsInt()); + } else if (field instanceof JBBPFieldLong long1) { + jsn.addProperty(fieldName, long1.getAsLong()); + } else if (field instanceof JBBPFieldShort short1) { + jsn.addProperty(fieldName, short1.getAsInt()); + } else if (field instanceof JBBPFieldStruct struct) { final JsonObject obj = new JsonObject(); for (final JBBPAbstractField f : struct.getArray()) { convertToJSon(obj, f); @@ -231,10 +229,10 @@ private JsonObject convertToJSon(@Nullable final JsonObject json, final JBBPAbst } else { jsn.add(fieldName, obj); } - } else if (field instanceof JBBPFieldUByte) { - jsn.addProperty(fieldName, ((JBBPFieldUByte) field).getAsInt()); - } else if (field instanceof JBBPFieldUShort) { - jsn.addProperty(fieldName, ((JBBPFieldUShort) field).getAsInt()); + } else if (field instanceof JBBPFieldUByte byte1) { + jsn.addProperty(fieldName, byte1.getAsInt()); + } else if (field instanceof JBBPFieldUShort short1) { + jsn.addProperty(fieldName, short1.getAsInt()); } else { throw new ConversionException(String.format("Unexpected field '%s'", field)); } diff --git a/bundles/org.openhab.core.io.console.rfc147/src/main/java/org/openhab/core/io/console/rfc147/internal/ConsoleCommandsContainer.java b/bundles/org.openhab.core.io.console.rfc147/src/main/java/org/openhab/core/io/console/rfc147/internal/ConsoleCommandsContainer.java index 2b6affbac80..c988051b774 100644 --- a/bundles/org.openhab.core.io.console.rfc147/src/main/java/org/openhab/core/io/console/rfc147/internal/ConsoleCommandsContainer.java +++ b/bundles/org.openhab.core.io.console.rfc147/src/main/java/org/openhab/core/io/console/rfc147/internal/ConsoleCommandsContainer.java @@ -24,5 +24,5 @@ @NonNullByDefault public interface ConsoleCommandsContainer { - public Collection getConsoleCommandExtensions(); + Collection getConsoleCommandExtensions(); } diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java index d4f94db622a..01c8d076eb4 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleInterpreter.java @@ -33,7 +33,7 @@ public class ConsoleInterpreter { public static String getHelp(final String base, final String separator, Collection extensions) { final List usages = ConsoleInterpreter.getUsages(extensions); - final StringBuffer buffer = new StringBuffer(); + final StringBuilder buffer = new StringBuilder(); buffer.append("---openHAB commands---\n\t"); for (int i = 0; i < usages.size(); i++) { diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java index 02d6a5aecd6..4cd4d45dc97 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java @@ -138,8 +138,7 @@ public void execute(String[] args, Console console) { case SUBCMD_ADDTAG: if (args.length > 2) { Item item = itemRegistry.get(args[1]); - if (item instanceof GenericItem) { - GenericItem gItem = (GenericItem) item; + if (item instanceof GenericItem gItem) { handleTags(gItem::addTag, args[2], gItem, console); } } else { @@ -150,8 +149,7 @@ public void execute(String[] args, Console console) { case SUBCMD_RMTAG: if (args.length > 2) { Item item = itemRegistry.get(args[1]); - if (item instanceof GenericItem) { - GenericItem gItem = (GenericItem) item; + if (item instanceof GenericItem gItem) { handleTags(gItem::removeTag, args[2], gItem, console); } } else { diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/MetadataConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/MetadataConsoleCommandExtension.java index 1096e29c4e9..b6914215f4b 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/MetadataConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/MetadataConsoleCommandExtension.java @@ -97,7 +97,7 @@ public void execute(String[] args, Console console) { } break; case SUBCMD_ORPHAN: - if (args.length == 2 && (args[1].equals("list") || args[1].equals("purge"))) { + if (args.length == 2 && ("list".equals(args[1]) || "purge".equals(args[1]))) { orphan(console, args[1], metadataRegistry.getAll(), itemRegistry.getAll()); } else { console.println("Specify action 'list' or 'purge' to be executed: orphan "); diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java index d08b6cf18bf..ff3e18aa610 100644 --- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java +++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java @@ -51,7 +51,7 @@ @NonNullByDefault public abstract class AbstractAuthPageServlet extends HttpServlet { - protected static final long serialVersionUID = 5340598701104679840L; + private static final long serialVersionUID = 5340598701104679840L; private static final String MESSAGES_BUNDLE_NAME = "messages"; diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthenticationHandler.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthenticationHandler.java index 7e054f7d494..0fd2fd60b60 100644 --- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthenticationHandler.java +++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthenticationHandler.java @@ -149,12 +149,12 @@ void activate(Map properties) { void modified(Map properties) { Object authenticationEnabled = properties.get(AUTHENTICATION_ENABLED); if (authenticationEnabled != null) { - this.enabled = Boolean.valueOf(authenticationEnabled.toString()); + this.enabled = Boolean.parseBoolean(authenticationEnabled.toString()); } Object loginUri = properties.get(AUTHENTICATION_ENDPOINT); - if (loginUri != null && loginUri instanceof String) { - this.loginUri = (String) loginUri; + if (loginUri instanceof String string) { + this.loginUri = string; } } diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java index 64a27c7bbcc..b116d3051e5 100644 --- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java +++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java @@ -166,12 +166,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S String authorizationCode = UUID.randomUUID().toString().replace("-", ""); - if (user instanceof ManagedUser) { + if (user instanceof ManagedUser managedUser) { String codeChallenge = params.containsKey("code_challenge") ? params.get("code_challenge")[0] : null; String codeChallengeMethod = params.containsKey("code_challenge_method") ? params.get("code_challenge_method")[0] : null; - ManagedUser managedUser = (ManagedUser) user; PendingToken pendingToken = new PendingToken(authorizationCode, clientId, baseRedirectUri, scope, codeChallenge, codeChallengeMethod); managedUser.setPendingToken(pendingToken); diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java index 10a7fcc387d..f3c21a728aa 100644 --- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java +++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java @@ -99,9 +99,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S User user = login(username, password); String newApiToken; - if (user instanceof ManagedUser) { - if (((ManagedUser) user).getApiTokens().stream() - .anyMatch(apiToken -> apiToken.getName().equals(tokenName))) { + if (user instanceof ManagedUser managedUser) { + if (managedUser.getApiTokens().stream().anyMatch(apiToken -> apiToken.getName().equals(tokenName))) { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().append( getPageBody(params, getLocalizedMessage("auth.createapitoken.name.unique.fail"), false)); diff --git a/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_de.properties b/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_de.properties index 99902399d3b..345d3c58412 100644 --- a/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_de.properties +++ b/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_de.properties @@ -1,4 +1,4 @@ -auth.login.prompt = Anmelden, um %s Zugriff auf %s zu gewähren\: +auth.login.prompt = Anmelden, um %s-Zugriff auf %s zu gewähren\: auth.login.fail = Bitte erneut versuchen. auth.createaccount.prompt = Erstelle ein Administrator-Konto, um fortzufahren. @@ -8,7 +8,7 @@ auth.createapitoken.prompt = Erstelle einen neuen Token, um externe Dienste zu a auth.createapitoken.name.unique.fail = Ein Token mit dem selben Namen existiert bereits. Bitte erneut versuchen. auth.createapitoken.name.format.fail = Ungültiger Token Name. Bitte nutzen Sie nur alphanumerische Zeichen. auth.createapitoken.success = Neuer Token erstellt\: -auth.createapitoken.success.footer = Bitte kopieren Sie es jetzt. Es wird anschließend nicht mehr angezeigt. +auth.createapitoken.success.footer = Bitte kopieren Sie ihn jetzt, da er anschließend nicht mehr angezeigt wird. auth.password.confirm.fail = Passwörter stimmen nicht überein. Bitte erneut versuchen. diff --git a/bundles/org.openhab.core.io.jetty.certificate/src/main/java/org/openhab/core/io/jetty/certificate/internal/CertificateGenerator.java b/bundles/org.openhab.core.io.jetty.certificate/src/main/java/org/openhab/core/io/jetty/certificate/internal/CertificateGenerator.java index 640bb22057f..7ed9834565e 100644 --- a/bundles/org.openhab.core.io.jetty.certificate/src/main/java/org/openhab/core/io/jetty/certificate/internal/CertificateGenerator.java +++ b/bundles/org.openhab.core.io.jetty.certificate/src/main/java/org/openhab/core/io/jetty/certificate/internal/CertificateGenerator.java @@ -125,7 +125,7 @@ private KeyStore ensureKeystore() throws KeyStoreException { throw new KeyStoreException("Failed to create the keystore " + keystoreFile.getAbsolutePath(), e); } } else { - try (InputStream keystoreStream = new FileInputStream(keystoreFile);) { + try (InputStream keystoreStream = new FileInputStream(keystoreFile)) { logger.debug("Keystore found. Trying to load {}", keystoreFile.getAbsolutePath()); keyStore.load(keystoreStream, KEYSTORE_PASSWORD.toCharArray()); } catch (NoSuchAlgorithmException | CertificateException | IOException e) { diff --git a/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/EventLogger.java b/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/EventLogger.java index a60a2b07bc8..2f857b7ebc2 100644 --- a/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/EventLogger.java +++ b/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/EventLogger.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -73,13 +74,8 @@ public void receive(Event event) { } private Logger getLogger(String eventType) { - String loggerName = "openhab.event." + eventType; - Logger logger = eventLoggers.get(loggerName); - if (logger == null) { - logger = LoggerFactory.getLogger(loggerName); - eventLoggers.put(loggerName, logger); - } - return logger; + return Objects.requireNonNull( + eventLoggers.computeIfAbsent(eventType, type -> LoggerFactory.getLogger("openhab.event." + eventType))); } @Override diff --git a/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java b/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java index b74cd99416a..4501bbacfed 100644 --- a/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java +++ b/bundles/org.openhab.core.io.monitor/src/main/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetric.java @@ -40,16 +40,18 @@ import io.micrometer.core.instrument.Tags; /** - * The {@link ThingStateMetric} class implements a metric for the openHAB things states. + * The {@link ThingStateMetric} class implements a metric for the openHAB things + * states. * * @author Robert Bach - Initial contribution + * @author Scott Hraban - Create Meter using thingUid instead of thingId during + * bind phase */ @NonNullByDefault public class ThingStateMetric implements OpenhabCoreMeterBinder, EventSubscriber { private final Logger logger = LoggerFactory.getLogger(ThingStateMetric.class); public static final String METRIC_NAME = "openhab.thing.state"; private static final String THING_TAG_NAME = "thing"; - private static final String THINGSTATUS_TOPIC_PREFIX = "openhab/things/"; private final ThingRegistry thingRegistry; private final Meter.Id commonMeterId; private final Map registeredMeters = new HashMap<>(); @@ -70,7 +72,7 @@ public void bindTo(@NonNullByDefault({}) MeterRegistry meterRegistry) { logger.debug("ThingStateMetric is being bound..."); this.meterRegistry = meterRegistry; thingRegistry.getAll().forEach( - thing -> createOrUpdateMetricForBundleState(thing.getUID().getId(), thing.getStatus().ordinal())); + thing -> createOrUpdateMetricForBundleState(thing.getUID().getAsString(), thing.getStatus().ordinal())); eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, null); } @@ -111,10 +113,13 @@ public Set getSubscribedEventTypes() { @Override public void receive(Event event) { - logger.trace("Received ThingStatusInfo(Changed)Event..."); - String thingId = event.getTopic().substring(THINGSTATUS_TOPIC_PREFIX.length(), - event.getTopic().lastIndexOf('/')); - ThingStatus status = gson.fromJson(event.getPayload(), ThingStatusInfo.class).getStatus(); - createOrUpdateMetricForBundleState(thingId, status.ordinal()); + if (event instanceof ThingStatusInfoEvent thingEvent) { + logger.trace("Received ThingStatusInfo(Changed)Event..."); + String thingUid = thingEvent.getThingUID().getAsString(); + ThingStatus status = gson.fromJson(event.getPayload(), ThingStatusInfo.class).getStatus(); + createOrUpdateMetricForBundleState(thingUid, status.ordinal()); + } else { + logger.trace("Received unsubscribed for event type {}", event.getClass().getSimpleName()); + } } } diff --git a/bundles/org.openhab.core.io.monitor/src/test/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetricTest.java b/bundles/org.openhab.core.io.monitor/src/test/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetricTest.java new file mode 100644 index 00000000000..61118050546 --- /dev/null +++ b/bundles/org.openhab.core.io.monitor/src/test/java/org/openhab/core/io/monitor/internal/metrics/ThingStateMetricTest.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.monitor.internal.metrics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.events.ThingEventFactory; +import org.openhab.core.thing.internal.ThingImpl; +import org.osgi.framework.BundleContext; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +/** + * Tests for ThingStateMetric class + * + * @author Scott Hraban - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class ThingStateMetricTest { + + @Test + public void testThingUidAlwaysUsedToCreateMeter() { + final String strThingTypeUid = "sonos:Amp"; + + final String strThingUid = strThingTypeUid + ":RINCON_347E5C0D150501400"; + ThingUID thingUid = new ThingUID(strThingUid); + Thing thing = new ThingImpl(new ThingTypeUID(strThingTypeUid), thingUid); + + final String strThingUid2 = strThingTypeUid + ":foo"; + ThingUID thingUid2 = new ThingUID(strThingUid2); + + ThingRegistry thingRegistry = mock(ThingRegistry.class); + + SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + + ThingStateMetric thingStateMetric = new ThingStateMetric(mock(BundleContext.class), thingRegistry, + new HashSet()); + + // Only one meter registered at bind time + doReturn(Collections.singleton(thing)).when(thingRegistry).getAll(); + thingStateMetric.bindTo(meterRegistry); + + List meters = meterRegistry.getMeters(); + assertEquals(1, meters.size()); + assertEquals(strThingUid, meters.get(0).getId().getTag("thing")); + + // Still only one meter registered after receiving an event + ThingStatusInfo thingStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + thingStateMetric.receive(ThingEventFactory.createStatusInfoEvent(thingUid, thingStatusInfo)); + + meters = meterRegistry.getMeters(); + assertEquals(1, meters.size()); + assertEquals(strThingUid, meters.get(0).getId().getTag("thing")); + + // Now another one is added + thingStateMetric.receive(ThingEventFactory.createStatusInfoEvent(thingUid2, thingStatusInfo)); + + meters = meterRegistry.getMeters(); + assertEquals(2, meters.size()); + } +} diff --git a/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/HttpUtil.java b/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/HttpUtil.java index 9cfe66ca3ff..76deaab77a1 100644 --- a/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/HttpUtil.java +++ b/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/HttpUtil.java @@ -154,7 +154,7 @@ public static String executeUrl(String httpMethod, String url, Properties httpHe String proxyPassword, String nonProxyHosts) throws IOException { ContentResponse response = executeUrlAndGetReponse(httpMethod, url, httpHeaders, content, contentType, timeout, proxyHost, proxyPort, proxyUser, proxyPassword, nonProxyHosts); - String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim() + String encoding = response.getEncoding() != null ? response.getEncoding().replace("\"", "").trim() : StandardCharsets.UTF_8.name(); String responseBody; try { @@ -319,7 +319,7 @@ private static boolean shouldUseProxy(String urlString, String nonProxyHosts) { if (host.contains("*")) { // the nonProxyHots-pattern allows wildcards '*' which must // be masked to be used with regular expressions - String hostRegexp = host.replaceAll("\\.", "\\\\."); + String hostRegexp = host.replace(".", "\\."); hostRegexp = hostRegexp.replaceAll("\\*", ".*"); if (givenHost.matches(hostRegexp)) { return false; diff --git a/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/TrustManagerUtil.java b/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/TrustManagerUtil.java index 19fb96a90db..cb9dbb900bc 100644 --- a/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/TrustManagerUtil.java +++ b/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/TrustManagerUtil.java @@ -36,8 +36,8 @@ static X509ExtendedTrustManager keyStoreToTrustManager(@Nullable KeyStore keySto tmf.init(keyStore); // Get hold of the X509ExtendedTrustManager for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509ExtendedTrustManager) { - return (X509ExtendedTrustManager) tm; + if (tm instanceof X509ExtendedTrustManager manager) { + return manager; } } } catch (NoSuchAlgorithmException e) { diff --git a/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/WebClientFactoryImpl.java b/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/WebClientFactoryImpl.java index 46fba9b0027..21b8ab051fd 100644 --- a/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/WebClientFactoryImpl.java +++ b/bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/WebClientFactoryImpl.java @@ -189,9 +189,9 @@ private int getConfigParameter(Map parameters, String parameter, if (value instanceof Integer) { return (Integer) value; } - if (value instanceof String) { + if (value instanceof String string) { try { - return Integer.parseInt((String) value); + return Integer.parseInt(string); } catch (NumberFormatException e) { logger.warn("ignoring invalid value {} for parameter {}", value, parameter); return defaultValue; @@ -322,7 +322,6 @@ private WebSocketClient createWebSocketClientInternal(String consumerName, } return webSocketClient; - } catch (RuntimeException e) { throw e; } catch (Exception e) { diff --git a/bundles/org.openhab.core.io.net/src/test/java/org/openhab/core/io/net/exec/ExecUtilTest.java b/bundles/org.openhab.core.io.net/src/test/java/org/openhab/core/io/net/exec/ExecUtilTest.java index 678c1787612..cfcf9ac25bc 100644 --- a/bundles/org.openhab.core.io.net/src/test/java/org/openhab/core/io/net/exec/ExecUtilTest.java +++ b/bundles/org.openhab.core.io.net/src/test/java/org/openhab/core/io/net/exec/ExecUtilTest.java @@ -64,7 +64,7 @@ public void testExecuteCommandLineAndWaitResponseWithArguments() { private boolean isWindowsSystem() { String osName = System.getProperty("os.name").toLowerCase(); - return osName.indexOf("windows") >= 0; + return osName.contains("windows"); } @Test diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AnonymousUserSecurityContext.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AnonymousUserSecurityContext.java similarity index 96% rename from bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AnonymousUserSecurityContext.java rename to bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AnonymousUserSecurityContext.java index aa3bbae99e4..ec4a0fa4427 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AnonymousUserSecurityContext.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AnonymousUserSecurityContext.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.rest.auth.internal; +package org.openhab.core.io.rest.auth; import java.security.Principal; diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java similarity index 78% rename from bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java rename to bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java index 2b159e8ddfe..543f3688d33 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthFilter.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.rest.auth.internal; +package org.openhab.core.io.rest.auth; import java.io.IOException; import java.net.InetAddress; @@ -53,6 +53,7 @@ import org.openhab.core.config.core.ConfigurableService; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.RESTConstants; +import org.openhab.core.io.rest.auth.internal.*; import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -77,7 +78,8 @@ * @author Miguel Álvarez - Add trusted networks for implicit user role */ @PreMatching -@Component(configurationPid = "org.openhab.restauth", property = Constants.SERVICE_PID + "=org.openhab.restauth") +@Component(configurationPid = "org.openhab.restauth", property = Constants.SERVICE_PID + + "=org.openhab.restauth", service = { ContainerRequestFilter.class, AuthFilter.class }) @ConfigurableService(category = "system", label = "API Security", description_uri = AuthFilter.CONFIG_URI) @JaxrsExtension @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")") @@ -115,7 +117,6 @@ public class AuthFilter implements ContainerRequestFilter { @Override public void added(User element) { - return; } @Override @@ -233,56 +234,80 @@ private SecurityContext authenticateBasicAuth(String credentialString) throws Au public void filter(@Nullable ContainerRequestContext requestContext) throws IOException { if (requestContext != null) { try { - String altTokenHeader = requestContext.getHeaderString(ALT_AUTH_HEADER); - if (altTokenHeader != null) { - requestContext.setSecurityContext(authenticateBearerToken(altTokenHeader)); - return; + SecurityContext sc = getSecurityContext(servletRequest, false); + if (sc != null) { + requestContext.setSecurityContext(sc); } + } catch (AuthenticationException e) { + logger.warn("Unauthorized API request from {}: {}", getClientIp(servletRequest), e.getMessage()); + requestContext.abortWith(JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Invalid credentials")); + } + } + } - String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - if (authHeader != null) { - String[] authParts = authHeader.split(" "); - if (authParts.length == 2) { - String authType = authParts[0]; - String authValue = authParts[1]; - if ("Bearer".equalsIgnoreCase(authType)) { - requestContext.setSecurityContext(authenticateBearerToken(authValue)); - return; - } else if ("Basic".equalsIgnoreCase(authType)) { - String[] decodedCredentials = new String(Base64.getDecoder().decode(authValue), "UTF-8") - .split(":"); - if (decodedCredentials.length > 2) { - throw new AuthenticationException("Invalid Basic authentication credential format"); - } - switch (decodedCredentials.length) { - case 1: - requestContext.setSecurityContext(authenticateBearerToken(decodedCredentials[0])); - break; - case 2: - if (!allowBasicAuth) { - throw new AuthenticationException( - "Basic authentication with username/password is not allowed"); - } - requestContext.setSecurityContext(authenticateBasicAuth(authValue)); - } - } + public @Nullable SecurityContext getSecurityContext(HttpServletRequest request, boolean allowQueryToken) + throws AuthenticationException, IOException { + String altTokenHeader = request.getHeader(ALT_AUTH_HEADER); + if (altTokenHeader != null) { + return authenticateBearerToken(altTokenHeader); + } + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + String authType = null; + String authValue = null; + boolean authFromQuery = false; + if (authHeader != null) { + String[] authParts = authHeader.split(" "); + if (authParts.length == 2) { + authType = authParts[0]; + authValue = authParts[1]; + } + } else if (allowQueryToken) { + Map parameterMap = request.getParameterMap(); + String[] accessToken = parameterMap.get("accessToken"); + if (accessToken != null && accessToken.length > 0) { + authValue = accessToken[0]; + authFromQuery = true; + } + } + if (authValue != null) { + if (authFromQuery) { + try { + return authenticateBearerToken(authValue); + } catch (AuthenticationException e) { + if (allowBasicAuth) { + return authenticateBasicAuth(authValue); } - } else if (isImplicitUserRole(requestContext)) { - requestContext.setSecurityContext(new AnonymousUserSecurityContext()); } - } catch (AuthenticationException e) { - logger.warn("Unauthorized API request from {}: {}", getClientIp(requestContext), e.getMessage()); - requestContext.abortWith(JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Invalid credentials")); + } else if ("Bearer".equalsIgnoreCase(authType)) { + return authenticateBearerToken(authValue); + } else if ("Basic".equalsIgnoreCase(authType)) { + String[] decodedCredentials = new String(Base64.getDecoder().decode(authValue), "UTF-8").split(":"); + if (decodedCredentials.length > 2) { + throw new AuthenticationException("Invalid Basic authentication credential format"); + } + switch (decodedCredentials.length) { + case 1: + return authenticateBearerToken(decodedCredentials[0]); + case 2: + if (!allowBasicAuth) { + throw new AuthenticationException( + "Basic authentication with username/password is not allowed"); + } + return authenticateBasicAuth(authValue); + } } + } else if (isImplicitUserRole(servletRequest)) { + return new AnonymousUserSecurityContext(); } + return null; } - private boolean isImplicitUserRole(ContainerRequestContext requestContext) { + private boolean isImplicitUserRole(HttpServletRequest request) { if (implicitUserRole) { return true; } try { - byte[] clientAddress = InetAddress.getByName(getClientIp(requestContext)).getAddress(); + byte[] clientAddress = InetAddress.getByName(getClientIp(request)).getAddress(); return trustedNetworks.stream().anyMatch(networkCIDR -> networkCIDR.isInRange(clientAddress)); } catch (IOException e) { logger.debug("Error validating trusted networks: {}", e.getMessage()); @@ -304,8 +329,8 @@ private List parseTrustedNetworks(String value) { return cidrList; } - private String getClientIp(ContainerRequestContext requestContext) throws UnknownHostException { - String ipForwarded = Objects.requireNonNullElse(requestContext.getHeaderString("x-forwarded-for"), ""); + private String getClientIp(HttpServletRequest request) throws UnknownHostException { + String ipForwarded = Objects.requireNonNullElse(request.getHeader("x-forwarded-for"), ""); String clientIp = ipForwarded.split(",")[0]; return clientIp.isBlank() ? servletRequest.getRemoteAddr() : clientIp; } diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthenticationSecurityContext.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthenticationSecurityContext.java index a0272e2bde1..045c38a4515 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthenticationSecurityContext.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthenticationSecurityContext.java @@ -27,5 +27,5 @@ public interface AuthenticationSecurityContext extends SecurityContext { * * @return the authentication instance */ - public Authentication getAuthentication(); + Authentication getAuthentication(); } diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/ExpiringUserSecurityContextCache.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/ExpiringUserSecurityContextCache.java index a5dab8e6499..5112d10e682 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/ExpiringUserSecurityContextCache.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/ExpiringUserSecurityContextCache.java @@ -37,7 +37,7 @@ public class ExpiringUserSecurityContextCache { private int calls = 0; - ExpiringUserSecurityContextCache(long expirationTime) { + public ExpiringUserSecurityContextCache(long expirationTime) { this.keepPeriod = expirationTime; entryMap = new LinkedHashMap<>() { private static final long serialVersionUID = -1220310861591070462L; @@ -48,7 +48,7 @@ protected boolean removeEldestEntry(Map.@Nullable Entry eldest) { }; } - synchronized @Nullable UserSecurityContext get(String key) { + public synchronized @Nullable UserSecurityContext get(String key) { calls++; if (calls >= CLEANUP_FREQUENCY) { new HashSet<>(entryMap.keySet()).forEach(k -> getEntry(k)); @@ -61,11 +61,11 @@ protected boolean removeEldestEntry(Map.@Nullable Entry eldest) { return null; } - synchronized void put(String key, UserSecurityContext value) { + public synchronized void put(String key, UserSecurityContext value) { entryMap.put(key, new Entry(System.currentTimeMillis(), value)); } - synchronized void clear() { + public synchronized void clear() { entryMap.clear(); } diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/JwtHelper.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/JwtHelper.java index 0b35b839c9d..69ad5f14cb7 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/JwtHelper.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/JwtHelper.java @@ -124,9 +124,8 @@ public String getJwtAccessToken(User user, String clientId, String scope, int to jws.setKey(jwtWebKey.getPrivateKey()); jws.setKeyIdHeaderValue(jwtWebKey.getKeyId()); jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); - String jwt = jws.getCompactSerialization(); - return jwt; + return jws.getCompactSerialization(); } catch (JoseException e) { throw new IllegalStateException("Error while writing JWT token", e); } diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/TokenResource.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/TokenResource.java index 105afb2f710..51f24f0672e 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/TokenResource.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/TokenResource.java @@ -277,7 +277,7 @@ private Response processAuthorizationCodeGrant(String code, String redirectUri, return (pendingToken != null && pendingToken.getAuthorizationCode().equals(code)); }).findAny(); - if (!user.isPresent()) { + if (user.isEmpty()) { logger.warn("Couldn't find a user with the provided authentication code pending"); throw new TokenEndpointException(ErrorType.INVALID_GRANT); } @@ -382,7 +382,7 @@ private Response processRefreshTokenGrant(String clientId, @Nullable String refr u -> ((ManagedUser) u).getSessions().stream().anyMatch(s -> refreshToken.equals(s.getRefreshToken()))) .findAny(); - if (!refreshTokenUser.isPresent()) { + if (refreshTokenUser.isEmpty()) { logger.warn("Couldn't find a user with a session matching the provided refresh_token"); throw new TokenEndpointException(ErrorType.INVALID_GRANT); } diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth_el.properties b/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth_el.properties index 87026d71635..1ae87dde61d 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth_el.properties +++ b/bundles/org.openhab.core.io.rest.auth/src/main/resources/OH-INF/i18n/restauth_el.properties @@ -4,5 +4,7 @@ system.config.restauth.cacheExpiration.label = Χρόνος λήξης της π system.config.restauth.cacheExpiration.description = Όταν είναι ενεργοποιημένος ο βασικός έλεγχος ταυτότητας, τα διαπιστευτήρια τοποθετούνται σε μια προσωρινή μνήμη για να επιταχυνθεί η εξουσιοδότηση αιτήματος. Οι καταχωρήσεις στην προσωρινή μνήμη λήγουν μετά από λίγο, προκειμένου να μην διατηρούνται τα διαπιστευτήρια στη μνήμη επ 'αόριστον. Αυτή η τιμή καθορίζει τον χρόνο λήξης σε ώρες. Ορίστε το στο 0 για απενεργοποίηση της προσωρινής μνήμης. system.config.restauth.implicitUserRole.label = Έμμεσος ρόλος χρήστη system.config.restauth.implicitUserRole.description = Από προεπιλογή, οι λειτουργίες που απαιτούν τον ρόλο "χρήστη" είναι διαθέσιμες και χωρίς πιστοποίηση. Η απενεργοποίηση αυτής της επιλογής θα επιβάλει εξουσιοδότηση για αυτές τις λειτουργίες. Προειδοποίηση\: Αυτό προκαλεί διακοπή των πελατών που δεν υποστηρίζουν έλεγχο ταυτότητας. +system.config.restauth.trustedNetworks.label = Αξιόπιστα Δίκτυα +system.config.restauth.trustedNetworks.description = Έμμεση εκχώρηση πρόσβασης χρήστη σε αιτήματα που προέρχονται από αυτά τα δίκτυα. Συμπληρώστε λίστα CIDR διαχωρισμένη με κόμματα (αγνοείται αν είναι ενεργοποιημένη η επιλογή "Implicit User Role"). service.system.restauth.label = Ασφάλεια API diff --git a/bundles/org.openhab.core.io.rest.auth/src/test/java/org/openhab/core/io/rest/auth/internal/AuthFilterTest.java b/bundles/org.openhab.core.io.rest.auth/src/test/java/org/openhab/core/io/rest/auth/AuthFilterTest.java similarity index 93% rename from bundles/org.openhab.core.io.rest.auth/src/test/java/org/openhab/core/io/rest/auth/internal/AuthFilterTest.java rename to bundles/org.openhab.core.io.rest.auth/src/test/java/org/openhab/core/io/rest/auth/AuthFilterTest.java index a4d98fefa2d..2f5bca9c806 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/test/java/org/openhab/core/io/rest/auth/internal/AuthFilterTest.java +++ b/bundles/org.openhab.core.io.rest.auth/src/test/java/org/openhab/core/io/rest/auth/AuthFilterTest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.rest.auth.internal; +package org.openhab.core.io.rest.auth; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -32,6 +32,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.auth.UserRegistry; +import org.openhab.core.io.rest.auth.internal.JwtHelper; /** * The {@link AuthFilterTest} is a @@ -79,7 +80,7 @@ public void noImplicitUserRoleDeniesAccess() throws IOException { public void trustedNetworkAllowsAccessIfForwardedHeaderMatches() throws IOException { authFilter.activate(Map.of(AuthFilter.CONFIG_IMPLICIT_USER_ROLE, false, AuthFilter.CONFIG_TRUSTED_NETWORKS, "192.168.1.0/24")); - when(containerRequestContext.getHeaderString("x-forwarded-for")).thenReturn("192.168.1.100"); + when(servletRequest.getHeader("x-forwarded-for")).thenReturn("192.168.1.100"); authFilter.filter(containerRequestContext); verify(containerRequestContext).setSecurityContext(any()); @@ -89,7 +90,7 @@ public void trustedNetworkAllowsAccessIfForwardedHeaderMatches() throws IOExcept public void trustedNetworkDeniesAccessIfForwardedHeaderDoesNotMatch() throws IOException { authFilter.activate(Map.of(AuthFilter.CONFIG_IMPLICIT_USER_ROLE, false, AuthFilter.CONFIG_TRUSTED_NETWORKS, "192.168.1.0/24")); - when(containerRequestContext.getHeaderString("x-forwarded-for")).thenReturn("192.168.2.100"); + when(servletRequest.getHeader("x-forwarded-for")).thenReturn("192.168.2.100"); authFilter.filter(containerRequestContext); verify(containerRequestContext, never()).setSecurityContext(any()); diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/GsonMessageBodyWriter.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/GsonMessageBodyWriter.java index a03f906cf4e..11da99be169 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/GsonMessageBodyWriter.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/GsonMessageBodyWriter.java @@ -79,8 +79,8 @@ public void writeTo(final T object, final Class type, final Type genericType, } try { - if (object instanceof InputStream && object instanceof JSONInputStream) { - ((InputStream) object).transferTo(entityStream); + if (object instanceof InputStream stream && object instanceof JSONInputStream) { + stream.transferTo(entityStream); } else { entityStream.write(gson.toJson(object).getBytes(StandardCharsets.UTF_8)); } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/channel/ChannelTypeResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/channel/ChannelTypeResource.java index b79db676d5e..46dd33c3ac9 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/channel/ChannelTypeResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/channel/ChannelTypeResource.java @@ -173,8 +173,8 @@ public Response getLinkableItemTypes( Set result = new HashSet<>(); for (ProfileType profileType : profileTypeRegistry.getProfileTypes()) { - if (profileType instanceof TriggerProfileType) { - if (((TriggerProfileType) profileType).getSupportedChannelTypeUIDs().contains(ctUID)) { + if (profileType instanceof TriggerProfileType type) { + if (type.getSupportedChannelTypeUIDs().contains(ctUID)) { for (String itemType : profileType.getSupportedItemTypes()) { result.add(itemType); } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/config/ConfigDescriptionResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/config/ConfigDescriptionResource.java index 47107fbaf66..3f0579f1495 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/config/ConfigDescriptionResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/config/ConfigDescriptionResource.java @@ -102,9 +102,9 @@ public Response getAll( @QueryParam("scheme") @Parameter(description = "scheme filter") @Nullable String scheme) { Locale locale = localeService.getLocale(language); Collection configDescriptions = configDescriptionRegistry.getConfigDescriptions(locale); - return Response.ok(new Stream2JSONInputStream(configDescriptions.stream().filter(configDescription -> { - return scheme == null || scheme.equals(configDescription.getUID().getScheme()); - }).map(EnrichedConfigDescriptionDTOMapper::map))).build(); + return Response.ok(new Stream2JSONInputStream(configDescriptions.stream() + .filter(configDescription -> scheme == null || scheme.equals(configDescription.getUID().getScheme())) + .map(EnrichedConfigDescriptionDTOMapper::map))).build(); } @GET diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java index 21d69b05886..59f8e4c465c 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java @@ -12,9 +12,12 @@ */ package org.openhab.core.io.rest.core.internal.item; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -40,9 +43,11 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; @@ -52,6 +57,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; +import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.events.EventPublisher; import org.openhab.core.io.rest.DTOMapper; import org.openhab.core.io.rest.JSONResponse; @@ -68,6 +74,7 @@ import org.openhab.core.items.ItemBuilderFactory; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.ItemRegistryChangeListener; import org.openhab.core.items.ManagedItemProvider; import org.openhab.core.items.Metadata; import org.openhab.core.items.MetadataKey; @@ -81,13 +88,14 @@ import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.RawType; import org.openhab.core.library.types.UpDownType; -import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticsPredicates; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; 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; @@ -174,6 +182,11 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final ManagedItemProvider managedItemProvider; private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; + private final SemanticTagRegistry semanticTagRegistry; + private final ItemRegistryChangeListener resetLastModifiedItemChangeListener = new ResetLastModifiedItemChangeListener(); + private final RegistryChangeListener resetLastModifiedMetadataChangeListener = new ResetLastModifiedMetadataChangeListener(); + + private Map<@Nullable String, Date> cacheableListsLastModified = new HashMap<>(); @Activate public ItemResource(// @@ -184,7 +197,8 @@ public ItemResource(// final @Reference LocaleService localeService, // final @Reference ManagedItemProvider managedItemProvider, final @Reference MetadataRegistry metadataRegistry, - final @Reference MetadataSelectorMatcher metadataSelectorMatcher) { + final @Reference MetadataSelectorMatcher metadataSelectorMatcher, + final @Reference SemanticTagRegistry semanticTagRegistry) { this.dtoMapper = dtoMapper; this.eventPublisher = eventPublisher; this.itemBuilderFactory = itemBuilderFactory; @@ -193,6 +207,16 @@ public ItemResource(// this.managedItemProvider = managedItemProvider; this.metadataRegistry = metadataRegistry; this.metadataSelectorMatcher = metadataSelectorMatcher; + this.semanticTagRegistry = semanticTagRegistry; + + this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener); + this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener); + } + + @Deactivate + void deactivate() { + this.itemRegistry.removeRegistryChangeListener(resetLastModifiedItemChangeListener); + this.metadataRegistry.removeRegistryChangeListener(resetLastModifiedMetadataChangeListener); } private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) { @@ -207,17 +231,47 @@ private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeade @Operation(operationId = "getItems", summary = "Get all available items.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedItemDTO.class)))) }) public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders, + @Context Request request, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @QueryParam("type") @Parameter(description = "item type filter") @Nullable String type, @QueryParam("tags") @Parameter(description = "item tag filter") @Nullable String tags, - @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (suppressed if no value given)") @Nullable String namespaceSelector, + @DefaultValue(".*") @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (returns all if no value given)") @Nullable String namespaceSelector, @DefaultValue("false") @QueryParam("recursive") @Parameter(description = "get member items recursively") boolean recursive, - @QueryParam("fields") @Parameter(description = "limit output to the given fields (comma separated)") @Nullable String fields) { + @QueryParam("fields") @Parameter(description = "limit output to the given fields (comma separated)") @Nullable String fields, + @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and checks the If-Modified-Since header, all other parameters are ignored except \"metadata\"") boolean staticDataOnly) { final Locale locale = localeService.getLocale(language); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders); + if (staticDataOnly) { + Date lastModifiedDate = Date.from(Instant.now()); + if (cacheableListsLastModified.containsKey(namespaceSelector)) { + lastModifiedDate = cacheableListsLastModified.get(namespaceSelector); + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModifiedDate); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + lastModifiedDate = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + cacheableListsLastModified.put(namespaceSelector, lastModifiedDate); + } + + Stream itemStream = getItems(null, null).stream() // + .map(item -> EnrichedItemDTOMapper.map(item, false, null, uriBuilder, locale)) // + .peek(dto -> addMetadata(dto, namespaces, null)) // + .peek(dto -> dto.editable = isEditable(dto.name)); + itemStream = dtoMapper.limitToFields(itemStream, + "name,label,type,groupType,function,category,editable,groupNames,link,tags,metadata"); + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + return Response.ok(new Stream2JSONInputStream(itemStream)).lastModified(lastModifiedDate).cacheControl(cc) + .build(); + } + Stream itemStream = getItems(type, tags).stream() // .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) // .peek(dto -> addMetadata(dto, namespaces, null)) // @@ -259,7 +313,7 @@ public Response getItemNamespaces(@PathParam("itemname") @Parameter(description @ApiResponse(responseCode = "404", description = "Item not found") }) public Response getItemData(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, - @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (suppressed if no value given)") @Nullable String namespaceSelector, + @DefaultValue(".*") @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (returns all if no value given)") @Nullable String namespaceSelector, @DefaultValue("true") @QueryParam("recursive") @Parameter(description = "get member items if the item is a group item") boolean recursive, @PathParam("itemname") @Parameter(description = "item name") String itemname) { final Locale locale = localeService.getLocale(language); @@ -333,9 +387,9 @@ public Response getBinaryItemState(@HeaderParam("Accept") @Nullable String media // if it exists if (item != null) { State state = item.getState(); - if (state instanceof RawType) { - String mimeType = ((RawType) state).getMimeType(); - byte[] data = ((RawType) state).getBytes(); + if (state instanceof RawType type) { + String mimeType = type.getMimeType(); + byte[] data = type.getBytes(); if ((acceptedMediaTypes.contains("image/*") && mimeType.startsWith("image/")) || acceptedMediaTypes.contains(mimeType)) { return Response.ok(data).type(mimeType).build(); @@ -814,7 +868,8 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H @PathParam("semanticClass") @Parameter(description = "semantic class") String semanticClassName) { Locale locale = localeService.getLocale(language); - Class semanticClass = SemanticTags.getById(semanticClassName); + Class semanticClass = semanticTagRegistry + .getTagClassById(semanticClassName); if (semanticClass == null) { return Response.status(Status.NOT_FOUND).build(); } @@ -921,8 +976,8 @@ private void addMetadata(EnrichedItemDTO dto, Set namespaces, @Nullable metadata.put(namespace, mdDto); } } - if (dto instanceof EnrichedGroupItemDTO) { - for (EnrichedItemDTO member : ((EnrichedGroupItemDTO) dto).members) { + if (dto instanceof EnrichedGroupItemDTO tO) { + for (EnrichedItemDTO member : tO.members) { addMetadata(member, namespaces, filter); } } @@ -935,4 +990,48 @@ private void addMetadata(EnrichedItemDTO dto, Set namespaces, @Nullable private boolean isEditable(String itemName) { return managedItemProvider.get(itemName) != null; } + + private void resetCacheableListsLastModified() { + this.cacheableListsLastModified.clear(); + } + + private class ResetLastModifiedItemChangeListener implements ItemRegistryChangeListener { + @Override + public void added(Item element) { + resetCacheableListsLastModified(); + } + + @Override + public void allItemsChanged(Collection oldItemNames) { + resetCacheableListsLastModified(); + } + + @Override + public void removed(Item element) { + resetCacheableListsLastModified(); + } + + @Override + public void updated(Item oldElement, Item element) { + resetCacheableListsLastModified(); + } + } + + private class ResetLastModifiedMetadataChangeListener implements RegistryChangeListener { + + @Override + public void added(Metadata element) { + resetCacheableListsLastModified(); + } + + @Override + public void removed(Metadata element) { + resetCacheableListsLastModified(); + } + + @Override + public void updated(Metadata oldElement, Metadata element) { + resetCacheableListsLastModified(); + } + } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java index ef1945f8bbc..fe0f01a6af0 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java @@ -20,6 +20,7 @@ import java.util.Locale; import javax.annotation.security.RolesAllowed; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; @@ -33,6 +34,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -57,7 +59,12 @@ import org.openhab.core.persistence.PersistenceServiceRegistry; import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.dto.ItemHistoryDTO; +import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; import org.openhab.core.persistence.dto.PersistenceServiceDTO; +import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationDTOMapper; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; import org.osgi.service.component.annotations.Activate; @@ -114,6 +121,8 @@ public class PersistenceResource implements RESTResource { private final ItemRegistry itemRegistry; private final LocaleService localeService; private final PersistenceServiceRegistry persistenceServiceRegistry; + private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry; + private final ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider; private final TimeZoneProvider timeZoneProvider; @Activate @@ -121,10 +130,14 @@ public PersistenceResource( // final @Reference ItemRegistry itemRegistry, // final @Reference LocaleService localeService, final @Reference PersistenceServiceRegistry persistenceServiceRegistry, + final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry, + final @Reference ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider, final @Reference TimeZoneProvider timeZoneProvider) { this.itemRegistry = itemRegistry; this.localeService = localeService; this.persistenceServiceRegistry = persistenceServiceRegistry; + this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry; + this.managedPersistenceServiceConfigurationProvider = managedPersistenceServiceConfigurationProvider; this.timeZoneProvider = timeZoneProvider; } @@ -142,6 +155,96 @@ public Response httpGetPersistenceServices(@Context HttpHeaders headers, return Response.ok(responseObject).build(); } + @GET + @RolesAllowed({ Role.ADMIN }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{serviceId: [a-zA-Z0-9]+}") + @Operation(operationId = "getPersistenceServiceConfiguration", summary = "Gets a persistence service configuration.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = PersistenceServiceConfigurationDTO.class))), + @ApiResponse(responseCode = "404", description = "Service configuration not found.") }) + public Response httpGetPersistenceServiceConfiguration(@Context HttpHeaders headers, + @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId) { + PersistenceServiceConfiguration configuration = persistenceServiceConfigurationRegistry.get(serviceId); + + if (configuration != null) { + PersistenceServiceConfigurationDTO configurationDTO = PersistenceServiceConfigurationDTOMapper + .map(configuration); + configurationDTO.editable = managedPersistenceServiceConfigurationProvider.get(serviceId) != null; + return JSONResponse.createResponse(Status.OK, configurationDTO, null); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @PUT + @RolesAllowed({ Role.ADMIN }) + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{serviceId: [a-zA-Z0-9]+}") + @Operation(operationId = "putPersistenceServiceConfiguration", summary = "Sets a persistence service configuration.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = PersistenceServiceConfigurationDTO.class))), + @ApiResponse(responseCode = "201", description = "PersistenceServiceConfiguration created."), + @ApiResponse(responseCode = "400", description = "Payload invalid."), + @ApiResponse(responseCode = "405", description = "PersistenceServiceConfiguration not editable.") }) + public Response httpPutPersistenceServiceConfiguration(@Context UriInfo uriInfo, @Context HttpHeaders headers, + @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId, + @Parameter(description = "service configuration", required = true) @Nullable PersistenceServiceConfigurationDTO serviceConfigurationDTO) { + if (serviceConfigurationDTO == null) { + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Payload must not be null."); + } + if (!serviceId.equals(serviceConfigurationDTO.serviceId)) { + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "serviceId in payload '" + + serviceConfigurationDTO.serviceId + "' differs from serviceId in URL '" + serviceId + "'"); + } + + PersistenceServiceConfiguration persistenceServiceConfiguration; + try { + persistenceServiceConfiguration = PersistenceServiceConfigurationDTOMapper.map(serviceConfigurationDTO); + } catch (IllegalArgumentException e) { + logger.warn("Received HTTP PUT request at '{}' with an invalid payload: '{}'.", uriInfo.getPath(), + e.getMessage()); + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, e.getMessage()); + } + + if (persistenceServiceConfigurationRegistry.get(serviceId) == null) { + managedPersistenceServiceConfigurationProvider.add(persistenceServiceConfiguration); + return JSONResponse.createResponse(Status.CREATED, serviceConfigurationDTO, null); + } else if (managedPersistenceServiceConfigurationProvider.get(serviceId) != null) { + // item already exists as a managed item, update it + managedPersistenceServiceConfigurationProvider.update(persistenceServiceConfiguration); + return JSONResponse.createResponse(Status.OK, serviceConfigurationDTO, null); + } else { + // Configuration exists but cannot be updated + logger.warn("Cannot update existing persistence service configuration '{}', because is not managed.", + serviceId); + return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, + "Cannot update non-managed persistence service configuration " + serviceId); + } + } + + @DELETE + @RolesAllowed({ Role.ADMIN }) + @Path("{serviceId: [a-zA-Z0-9]+}") + @Operation(operationId = "deletePersistenceServiceConfiguration", summary = "Deletes a persistence service configuration.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Persistence service configuration not found."), + @ApiResponse(responseCode = "405", description = "Persistence service configuration not editable.") }) + public Response httpDeletePersistenceServiceConfiguration(@Context UriInfo uriInfo, @Context HttpHeaders headers, + @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId) { + if (persistenceServiceConfigurationRegistry.get(serviceId) == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + if (managedPersistenceServiceConfigurationProvider.remove(serviceId) == null) { + return Response.status(Status.METHOD_NOT_ALLOWED).build(); + } else { + return Response.ok().build(); + } + } + @GET @RolesAllowed({ Role.ADMIN }) @Path("/items") @@ -238,7 +341,7 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, protected @Nullable ItemHistoryDTO createDTO(@Nullable String serviceId, String itemName, @Nullable String timeBegin, @Nullable String timeEnd, int pageNumber, int pageLength, boolean boundary) { // If serviceId is null, then use the default service - PersistenceService service = null; + PersistenceService service; String effectiveServiceId = serviceId != null ? serviceId : persistenceServiceRegistry.getDefaultId(); service = persistenceServiceRegistry.get(effectiveServiceId); @@ -283,9 +386,9 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, } Iterable result; - State state = null; + State state; - Long quantity = 0l; + long quantity = 0L; ItemHistoryDTO dto = new ItemHistoryDTO(); dto.name = itemName; @@ -363,7 +466,7 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, /** * Gets a list of persistence services currently configured in the system * - * @return list of persistence services as {@link ServiceBean} + * @return list of persistence services */ private List getPersistenceServiceList(Locale locale) { List dtoList = new ArrayList<>(); @@ -389,7 +492,7 @@ private List getPersistenceServiceList(Locale locale) { private Response getServiceItemList(@Nullable String serviceId) { // If serviceId is null, then use the default service - PersistenceService service = null; + PersistenceService service; if (serviceId == null) { service = persistenceServiceRegistry.getDefault(); } else { diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/profile/ProfileTypeResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/profile/ProfileTypeResource.java index 20861006ccb..3e202346890 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/profile/ProfileTypeResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/profile/ProfileTypeResource.java @@ -13,6 +13,7 @@ package org.openhab.core.io.rest.core.internal.profile; import java.util.Collection; +import java.util.Comparator; import java.util.Locale; import java.util.function.Predicate; import java.util.stream.Stream; @@ -114,7 +115,8 @@ public Response getAll( protected Stream getProfileTypes(@Nullable Locale locale, @Nullable String channelTypeUID, @Nullable String itemType) { return profileTypeRegistry.getProfileTypes(locale).stream().filter(matchesChannelUID(channelTypeUID, locale)) - .filter(matchesItemType(itemType)).map(profileType -> ProfileTypeDTOMapper.map(profileType)); + .filter(matchesItemType(itemType)).sorted(Comparator.comparing(ProfileType::getLabel)) + .map(profileType -> ProfileTypeDTOMapper.map(profileType)); } private Predicate matchesChannelUID(@Nullable String channelTypeUID, @Nullable Locale locale) { @@ -150,9 +152,7 @@ private boolean profileTypeMatchesItemType(ProfileType pt, String itemType) { } private boolean triggerProfileMatchesProfileType(ProfileType profileType, ChannelType channelType) { - if (profileType instanceof TriggerProfileType) { - TriggerProfileType triggerProfileType = (TriggerProfileType) profileType; - + if (profileType instanceof TriggerProfileType triggerProfileType) { if (triggerProfileType.getSupportedChannelTypeUIDs().isEmpty()) { return true; } @@ -165,9 +165,7 @@ private boolean triggerProfileMatchesProfileType(ProfileType profileType, Channe } private boolean stateProfileMatchesProfileType(ProfileType profileType, ChannelType channelType) { - if (profileType instanceof StateProfileType) { - StateProfileType stateProfileType = (StateProfileType) profileType; - + if (profileType instanceof StateProfileType stateProfileType) { if (stateProfileType.getSupportedItemTypesOfChannel().isEmpty()) { return true; } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/service/ConfigurableServiceResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/service/ConfigurableServiceResource.java index d404a5efed1..1b5f5490cf1 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/service/ConfigurableServiceResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/service/ConfigurableServiceResource.java @@ -364,13 +364,11 @@ private String getServiceId(ServiceReference serviceReference) { } final String serviceId; - if (pid instanceof String) { - serviceId = (String) pid; - } else if (pid instanceof String[]) { - final String[] pids = (String[]) pid; + if (pid instanceof String string) { + serviceId = string; + } else if (pid instanceof String[] pids) { serviceId = getServicePID(cn, Arrays.asList(pids)); - } else if (pid instanceof Collection) { - Collection pids = (Collection) pid; + } else if (pid instanceof Collection pids) { serviceId = getServicePID(cn, pids.stream().map(entry -> entry.toString()).collect(Collectors.toList())); } else { logger.warn("The component \"{}\" is using an unhandled service PID type ({}). Use component name.", cn, @@ -410,6 +408,6 @@ private String getServicePID(final String cn, final List pids) { } private String inferKey(String uri, String lastSegment) { - return "service." + uri.replaceAll(":", ".") + "." + lastSegment; + return "service." + uri.replace(":", ".") + "." + lastSegment; } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java new file mode 100644 index 00000000000..f19d33f6802 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.core.internal.tag; + +import java.util.List; + +import org.openhab.core.semantics.SemanticTag; + +/** + * A DTO representing a {@link SemanticTag}. + * + * @author Jimmy Tanagra - initial contribution + * @author Laurent Garnier - Class renamed and members uid, description and editable added + */ +public class EnrichedSemanticTagDTO { + String uid; + String name; + String label; + String description; + List synonyms; + boolean editable; + + public EnrichedSemanticTagDTO(SemanticTag tag, boolean editable) { + this.uid = tag.getUID(); + this.name = tag.getUID().substring(tag.getUID().lastIndexOf("_") + 1); + this.label = tag.getLabel(); + this.description = tag.getDescription(); + this.synonyms = tag.getSynonyms(); + this.editable = editable; + } +} diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java new file mode 100644 index 00000000000..b3d954f3d3c --- /dev/null +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.core.internal.tag; + +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.Role; +import org.openhab.core.io.rest.JSONResponse; +import org.openhab.core.io.rest.LocaleService; +import org.openhab.core.io.rest.RESTConstants; +import org.openhab.core.io.rest.RESTResource; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagRegistry; +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.jaxrs.whiteboard.JaxrsWhiteboardConstants; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired; +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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +/** + * This class acts as a REST resource for retrieving a list of tags. + * + * @author Jimmy Tanagra - Initial contribution + * @author Laurent Garnier - Extend REST API to allow adding/updating/removing user tags + */ +@Component +@JaxrsResource +@JaxrsName(TagResource.PATH_TAGS) +@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")") +@JSONRequired +@Path(TagResource.PATH_TAGS) +@io.swagger.v3.oas.annotations.tags.Tag(name = TagResource.PATH_TAGS) +@NonNullByDefault +public class TagResource implements RESTResource { + + /** The URI path to this resource */ + public static final String PATH_TAGS = "tags"; + + private final LocaleService localeService; + private final SemanticTagRegistry semanticTagRegistry; + private final ManagedSemanticTagProvider managedSemanticTagProvider; + + // TODO pattern in @Path + + @Activate + public TagResource(final @Reference LocaleService localeService, + final @Reference SemanticTagRegistry semanticTagRegistry, + final @Reference ManagedSemanticTagProvider managedSemanticTagProvider) { + this.localeService = localeService; + this.semanticTagRegistry = semanticTagRegistry; + this.managedSemanticTagProvider = managedSemanticTagProvider; + } + + @GET + @RolesAllowed({ Role.USER, Role.ADMIN }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getSemanticTags", summary = "Get all available semantic tags.", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))) }) + public Response getTags(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders, + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language) { + final Locale locale = localeService.getLocale(language); + + List tagsDTO = semanticTagRegistry.getAll().stream() + .sorted(Comparator.comparing(SemanticTag::getUID)) + .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))).toList(); + return JSONResponse.createResponse(Status.OK, tagsDTO, null); + } + + @GET + @RolesAllowed({ Role.USER, Role.ADMIN }) + @Path("/{tagId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getSemanticTagAndSubTags", summary = "Gets a semantic tag and its sub tags.", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))), + @ApiResponse(responseCode = "404", description = "Semantic tag not found.") }) + public Response getTagAndSubTags( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("tagId") @Parameter(description = "tag id") String tagId) { + final Locale locale = localeService.getLocale(language); + String uid = tagId.trim(); + + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag != null) { + List tagsDTO = semanticTagRegistry.getSubTree(tag).stream() + .sorted(Comparator.comparing(SemanticTag::getUID)) + .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))) + .toList(); + return JSONResponse.createResponse(Status.OK, tagsDTO, null); + } else { + return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!"); + } + } + + @POST + @RolesAllowed({ Role.ADMIN }) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(operationId = "createSemanticTag", summary = "Creates a new semantic tag and adds it to the registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "201", description = "Created", content = @Content(schema = @Schema(implementation = EnrichedSemanticTagDTO.class))), + @ApiResponse(responseCode = "400", description = "The tag identifier is invalid."), + @ApiResponse(responseCode = "409", description = "A tag with the same identifier already exists.") }) + public Response create( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @Parameter(description = "tag data", required = true) EnrichedSemanticTagDTO data) { + final Locale locale = localeService.getLocale(language); + + if (data.uid == null) { + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Tag identifier is required!"); + } + + String uid = data.uid.trim(); + + // check if a tag with this UID already exists + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag != null) { + // report a conflict + return JSONResponse.createResponse(Status.CONFLICT, + new EnrichedSemanticTagDTO(tag.localized(locale), semanticTagRegistry.isEditable(tag)), + "Tag " + uid + " already exists!"); + } + + tag = new SemanticTagImpl(uid, data.label, data.description, data.synonyms); + + // Check that a tag with this uid can be added to the registry + if (!semanticTagRegistry.canBeAdded(tag)) { + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Invalid tag identifier " + uid); + } + + managedSemanticTagProvider.add(tag); + + return JSONResponse.createResponse(Status.CREATED, + new EnrichedSemanticTagDTO(tag.localized(locale), semanticTagRegistry.isEditable(tag)), null); + } + + @DELETE + @RolesAllowed({ Role.ADMIN }) + @Path("/{tagId}") + @Operation(operationId = "removeSemanticTag", summary = "Removes a semantic tag and its sub tags from the registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK, was deleted."), + @ApiResponse(responseCode = "404", description = "Semantic tag not found."), + @ApiResponse(responseCode = "405", description = "Semantic tag not removable.") }) + public Response remove( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("tagId") @Parameter(description = "tag id") String tagId) { + final Locale locale = localeService.getLocale(language); + + String uid = tagId.trim(); + + // check whether tag exists and throw 404 if not + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag == null) { + return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!"); + } + + // Check that tag is removable, 405 otherwise + if (!semanticTagRegistry.isEditable(tag)) { + return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, "Tag " + uid + " is not removable."); + } + + semanticTagRegistry.removeSubTree(tag); + + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + + @PUT + @RolesAllowed({ Role.ADMIN }) + @Path("/{tagId}") + @Consumes(MediaType.APPLICATION_JSON) + @Operation(operationId = "updateSemanticTag", summary = "Updates a semantic tag.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = EnrichedSemanticTagDTO.class))), + @ApiResponse(responseCode = "404", description = "Semantic tag not found."), + @ApiResponse(responseCode = "405", description = "Semantic tag not editable.") }) + public Response update( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("tagId") @Parameter(description = "tag id") String tagId, + @Parameter(description = "tag data", required = true) EnrichedSemanticTagDTO data) { + final Locale locale = localeService.getLocale(language); + + String uid = tagId.trim(); + + // check whether tag exists and throw 404 if not + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag == null) { + return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!"); + } + + // Check that tag is editable, 405 otherwise + if (!semanticTagRegistry.isEditable(tag)) { + return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, "Tag " + uid + " is not editable."); + } + + tag = new SemanticTagImpl(uid, data.label != null ? data.label : tag.getLabel(), + data.description != null ? data.description : tag.getDescription(), + data.synonyms != null ? data.synonyms : tag.getSynonyms()); + managedSemanticTagProvider.update(tag); + + return JSONResponse.createResponse(Status.OK, + new EnrichedSemanticTagDTO(tag.localized(locale), semanticTagRegistry.isEditable(tag)), null); + } +} diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java index 324482635ff..84816fa841a 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java @@ -15,9 +15,12 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -39,9 +42,11 @@ 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.HttpHeaders; 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.UriInfo; @@ -49,6 +54,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; +import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionRegistry; import org.openhab.core.config.core.ConfigUtil; @@ -100,6 +106,7 @@ import org.openhab.core.thing.util.ThingHelper; 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; @@ -164,8 +171,10 @@ public class ThingResource implements RESTResource { private final ThingRegistry thingRegistry; private final ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService; private final ThingTypeRegistry thingTypeRegistry; + private final ResetLastModifiedChangeListener resetLastModifiedChangeListener = new ResetLastModifiedChangeListener(); private @Context @NonNullByDefault({}) UriInfo uriInfo; + private @Nullable Date cacheableListLastModified = null; @Activate public ThingResource( // @@ -198,6 +207,13 @@ public ThingResource( // this.thingRegistry = thingRegistry; this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService; this.thingTypeRegistry = thingTypeRegistry; + + this.thingRegistry.addRegistryChangeListener(resetLastModifiedChangeListener); + } + + @Deactivate + void deactivate() { + this.thingRegistry.removeRegistryChangeListener(resetLastModifiedChangeListener); } /** @@ -291,13 +307,34 @@ public Response create( @Operation(operationId = "getThings", summary = "Get all available things.", security = { @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedThingDTO.class), uniqueItems = true))) }) - public Response getAll( + public Response getAll(@Context Request request, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, - @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) { + @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 checks the If-Modified-Since header") boolean staticDataOnly) { final Locale locale = localeService.getLocale(language); Stream thingStream = thingRegistry.stream().map(t -> convertToEnrichedThingDTO(t, locale)) .distinct(); + + 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)); + } + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + thingStream = dtoMapper.limitToFields(thingStream, "UID,label,bridgeUID,thingTypeUID,location,editable"); + return Response.ok(new Stream2JSONInputStream(thingStream)).lastModified(cacheableListLastModified) + .cacheControl(cc).build(); + } + if (summary != null && summary) { thingStream = dtoMapper.limitToFields(thingStream, "UID,label,bridgeUID,thingTypeUID,statusInfo,firmwareStatus,location,editable"); @@ -853,4 +890,26 @@ private URI getConfigDescriptionURI(ChannelUID channelUID) { throw new BadRequestException("Invalid URI syntax: " + uriString); } } + + private void resetCacheableListLastModified() { + cacheableListLastModified = null; + } + + private class ResetLastModifiedChangeListener implements RegistryChangeListener { + + @Override + public void added(Thing element) { + resetCacheableListLastModified(); + } + + @Override + public void removed(Thing element) { + resetCacheableListLastModified(); + } + + @Override + public void updated(Thing oldElement, Thing element) { + resetCacheableListLastModified(); + } + } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java index 9ce41efd977..2036e5b853f 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java @@ -93,8 +93,7 @@ private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown EnrichedItemDTO enrichedItemDTO; - if (item instanceof GroupItem) { - GroupItem groupItem = (GroupItem) item; + if (item instanceof GroupItem groupItem) { EnrichedItemDTO[] memberDTOs; if (drillDown) { Collection members = new LinkedHashSet<>(); diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java index dfa37e601a3..c17ce4df1b4 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java @@ -31,7 +31,7 @@ public class EnrichedThingDTO extends AbstractThingDTO { public List channels; public ThingStatusInfo statusInfo; - public final FirmwareStatusDTO firmwareStatus; + public FirmwareStatusDTO firmwareStatus; public boolean editable; /** diff --git a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java index 6cf040ab7f5..7ad448a9ff2 100644 --- a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java +++ b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java @@ -39,6 +39,8 @@ import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.dto.ItemHistoryDTO; import org.openhab.core.persistence.dto.ItemHistoryDTO.HistoryDataBean; +import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; import org.openhab.core.types.State; /** @@ -58,11 +60,14 @@ public class PersistenceResourceTest { private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) LocaleService localeServiceMock; private @Mock @NonNullByDefault({}) PersistenceServiceRegistry persistenceServiceRegistryMock; + private @Mock @NonNullByDefault({}) PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock; + private @Mock @NonNullByDefault({}) ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProviderMock; private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; @BeforeEach public void beforeEach() { pResource = new PersistenceResource(itemRegistryMock, localeServiceMock, persistenceServiceRegistryMock, + persistenceServiceConfigurationRegistryMock, managedPersistenceServiceConfigurationProviderMock, timeZoneProviderMock); int startValue = 2016; diff --git a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java index 0b0d0a144af..9b022f92197 100644 --- a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java +++ b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java @@ -15,10 +15,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.library.CoreItemFactory; @@ -38,7 +40,7 @@ public void setup() { @Test public void testFiltering() { - CoreItemFactory itemFactory = new CoreItemFactory(); + CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class)); GroupItem group = new GroupItem("TestGroup"); GroupItem subGroup = new GroupItem("TestSubGroup"); diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java index 94ca20138b7..d788c1762ea 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java @@ -42,6 +42,8 @@ import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.thing.events.ChannelDescriptionChangedEvent; import org.openhab.core.ui.items.ItemUIRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -72,6 +74,7 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener private static final int DEFAULT_MAX_SUBSCRIPTIONS = 50; private final Logger logger = LoggerFactory.getLogger(SitemapSubscriptionService.class); + private final BundleContext bundleContext; public interface SitemapSubscriptionCallback { @@ -94,15 +97,17 @@ public interface SitemapSubscriptionCallback { private final Map creationInstants = new ConcurrentHashMap<>(); /* sitemap+page -> listener */ - private final Map pageChangeListeners = new ConcurrentHashMap<>(); + private final Map pageChangeListeners = new ConcurrentHashMap<>(); /* Max number of subscriptions at the same time */ private int maxSubscriptions = DEFAULT_MAX_SUBSCRIPTIONS; @Activate - public SitemapSubscriptionService(Map config, final @Reference ItemUIRegistry itemUIRegistry) { - applyConfig(config); + public SitemapSubscriptionService(Map config, final @Reference ItemUIRegistry itemUIRegistry, + BundleContext bundleContext) { this.itemUIRegistry = itemUIRegistry; + this.bundleContext = bundleContext; + applyConfig(config); } @Deactivate @@ -110,9 +115,7 @@ protected void deactivate() { pageOfSubscription.clear(); callbacks.clear(); creationInstants.clear(); - for (PageChangeListener listener : pageChangeListeners.values()) { - listener.dispose(); - } + pageChangeListeners.values().forEach(l -> l.serviceRegistration.unregister()); pageChangeListeners.clear(); } @@ -150,7 +153,7 @@ protected void removeSitemapProvider(SitemapProvider provider) { * Creates a new subscription with the given id. * * @param callback an instance that should receive the events - * @returns a unique id that identifies the subscription or null if the limit of subscriptions is already reached + * @return a unique id that identifies the subscription or null if the limit of subscriptions is already reached */ public @Nullable String createSubscription(SitemapSubscriptionCallback callback) { if (maxSubscriptions >= 0 && callbacks.size() >= maxSubscriptions) { @@ -176,9 +179,9 @@ public void removeSubscription(String subscriptionId) { String sitemapPage = pageOfSubscription.remove(subscriptionId); if (sitemapPage != null && !pageOfSubscription.values().contains(sitemapPage)) { // this was the only subscription listening on this page, so we can dispose the listener - PageChangeListener listener = pageChangeListeners.remove(sitemapPage); + ListenerRecord listener = pageChangeListeners.remove(sitemapPage); if (listener != null) { - listener.dispose(); + listener.serviceRegistration().unregister(); } } logger.debug("Removed subscription with id {} ({} active subscriptions)", subscriptionId, callbacks.size()); @@ -249,13 +252,14 @@ public void setPageId(String subscriptionId, String sitemapName, String pageId) } private void addCallbackToListener(String sitemapName, String pageId, SitemapSubscriptionCallback callback) { - PageChangeListener listener = pageChangeListeners.get(getValue(sitemapName, pageId)); - if (listener == null) { - // there is no listener for this page yet, so let's try to create one - listener = new PageChangeListener(sitemapName, pageId, itemUIRegistry, collectWidgets(sitemapName, pageId)); - pageChangeListeners.put(getValue(sitemapName, pageId), listener); - } - listener.addCallback(callback); + ListenerRecord listener = pageChangeListeners.computeIfAbsent(getValue(sitemapName, pageId), v -> { + PageChangeListener newListener = new PageChangeListener(sitemapName, pageId, itemUIRegistry, + collectWidgets(sitemapName, pageId)); + ServiceRegistration registration = bundleContext.registerService(EventSubscriber.class.getName(), + newListener, null); + return new ListenerRecord(newListener, registration); + }); + listener.pageChangeListener().addCallback(callback); } private EList collectWidgets(String sitemapName, String pageId) { @@ -267,8 +271,8 @@ private EList collectWidgets(String sitemapName, String pageId) { widgets = itemUIRegistry.getChildren(sitemap); } else { Widget pageWidget = itemUIRegistry.getWidget(sitemap, pageId); - if (pageWidget instanceof LinkableWidget) { - widgets = itemUIRegistry.getChildren((LinkableWidget) pageWidget); + if (pageWidget instanceof LinkableWidget widget) { + widgets = itemUIRegistry.getChildren(widget); // We add the page widget. It will help any UI to update the page title. widgets.add(pageWidget); } @@ -278,12 +282,12 @@ private EList collectWidgets(String sitemapName, String pageId) { } private void removeCallbackFromListener(String sitemapPage, SitemapSubscriptionCallback callback) { - PageChangeListener oldListener = pageChangeListeners.get(sitemapPage); + ListenerRecord oldListener = pageChangeListeners.get(sitemapPage); if (oldListener != null) { - oldListener.removeCallback(callback); - if (!pageOfSubscription.values().contains(sitemapPage)) { + oldListener.pageChangeListener().removeCallback(callback); + if (!pageOfSubscription.containsValue(sitemapPage)) { // no other callbacks are left here, so we can safely dispose the listener - oldListener.dispose(); + oldListener.serviceRegistration().unregister(); pageChangeListeners.remove(sitemapPage); } } @@ -311,14 +315,14 @@ public void modelChanged(String modelName, EventType type) { String changedSitemapName = modelName.substring(0, modelName.length() - SITEMAP_SUFFIX.length()); - for (Entry listenerEntry : pageChangeListeners.entrySet()) { + for (Entry listenerEntry : pageChangeListeners.entrySet()) { String sitemapWithPage = listenerEntry.getKey(); String sitemapName = extractSitemapName(sitemapWithPage); String pageId = extractPageId(sitemapWithPage); if (sitemapName.equals(changedSitemapName)) { EList widgets = collectWidgets(sitemapName, pageId); - listenerEntry.getValue().sitemapContentChanged(widgets); + listenerEntry.getValue().pageChangeListener().sitemapContentChanged(widgets); } } } @@ -336,9 +340,7 @@ public void checkAliveClients() { } } // Send an ALIVE event to all subscribers to trigger an exception for dead subscribers - for (Entry listenerEntry : pageChangeListeners.entrySet()) { - listenerEntry.getValue().sendAliveEvent(); - } + pageChangeListeners.values().forEach(l -> l.pageChangeListener().sendAliveEvent()); } @Override @@ -348,28 +350,29 @@ public Set getSubscribedEventTypes() { @Override public void receive(Event event) { - if (event instanceof ItemStatePredictedEvent) { - ItemStatePredictedEvent prediction = (ItemStatePredictedEvent) event; + if (event instanceof ItemStatePredictedEvent prediction) { Item item = itemUIRegistry.get(prediction.getItemName()); if (item instanceof GroupItem) { // don't send out auto-update events for group items as those will calculate their state based on their // members and predictions aren't really possible in that case (or at least would be highly complex). return; } - for (PageChangeListener pageChangeListener : pageChangeListeners.values()) { + for (ListenerRecord listener : pageChangeListeners.values()) { if (prediction.isConfirmation()) { - pageChangeListener.keepCurrentState(item); + listener.pageChangeListener().keepCurrentState(item); } else { - pageChangeListener.changeStateTo(item, prediction.getPredictedState()); + listener.pageChangeListener().changeStateTo(item, prediction.getPredictedState()); } } - } else if (event instanceof ChannelDescriptionChangedEvent) { - ChannelDescriptionChangedEvent channelDescriptionChangedEvent = (ChannelDescriptionChangedEvent) event; + } else if (event instanceof ChannelDescriptionChangedEvent channelDescriptionChangedEvent) { channelDescriptionChangedEvent.getLinkedItemNames().forEach(itemName -> { - for (PageChangeListener pageChangeListener : pageChangeListeners.values()) { - pageChangeListener.descriptionChanged(itemName); + for (ListenerRecord listener : pageChangeListeners.values()) { + listener.pageChangeListener().descriptionChanged(itemName); } }); } } + + private record ListenerRecord(PageChangeListener pageChangeListener, ServiceRegistration serviceRegistration) { + } } diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java index 263e6821e4d..30aa6bc5e92 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java @@ -20,16 +20,19 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.eclipse.emf.common.util.EList; import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventSubscriber; import org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper; import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback; -import org.openhab.core.items.GenericItem; -import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; -import org.openhab.core.items.StateChangeListener; +import org.openhab.core.items.events.GroupStateUpdatedEvent; +import org.openhab.core.items.events.ItemEvent; +import org.openhab.core.items.events.ItemStateChangedEvent; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.model.sitemap.sitemap.Chart; import org.openhab.core.model.sitemap.sitemap.ColorArray; @@ -45,7 +48,7 @@ * @author Kai Kreuzer - Initial contribution * @author Laurent Garnier - Added support for icon color */ -public class PageChangeListener implements StateChangeListener { +public class PageChangeListener implements EventSubscriber { private static final int REVERT_INTERVAL = 300; private final ScheduledExecutorService scheduler = ThreadPoolManager @@ -55,6 +58,7 @@ public class PageChangeListener implements StateChangeListener { private final ItemUIRegistry itemUIRegistry; private EList widgets; private Set items; + private final HashSet filterItems = new HashSet<>(); private final List callbacks = Collections.synchronizedList(new ArrayList<>()); private Set distinctCallbacks = Collections.emptySet(); @@ -75,23 +79,10 @@ public PageChangeListener(String sitemapName, String pageId, ItemUIRegistry item } private void updateItemsAndWidgets(EList widgets) { - if (this.widgets != null) { - // cleanup statechange listeners in case widgets were removed - items = getAllItems(this.widgets); - for (Item item : items) { - if (item instanceof GenericItem) { - ((GenericItem) item).removeStateChangeListener(this); - } - } - } - this.widgets = widgets; items = getAllItems(widgets); - for (Item item : items) { - if (item instanceof GenericItem) { - ((GenericItem) item).addStateChangeListener(this); - } - } + filterItems.clear(); + filterItems.addAll(items.stream().map(Item::getName).collect(Collectors.toSet())); } public String getSitemapName() { @@ -113,19 +104,6 @@ public void removeCallback(SitemapSubscriptionCallback callback) { distinctCallbacks = new HashSet<>(callbacks); } - /** - * Disposes this instance and releases all resources. - */ - public void dispose() { - for (Item item : items) { - if (item instanceof GenericItem) { - ((GenericItem) item).removeStateChangeListener(this); - } else if (item instanceof GroupItem) { - ((GroupItem) item).removeStateChangeListener(this); - } - } - } - /** * Collects all items that are represented by a given list of widgets * @@ -138,8 +116,8 @@ private Set getAllItems(EList widgets) { if (itemUIRegistry != null) { for (Widget widget : widgets) { addItemWithName(items, widget.getItem()); - if (widget instanceof Frame) { - items.addAll(getAllItems(((Frame) widget).getChildren())); + if (widget instanceof Frame frame) { + items.addAll(getAllItems(frame.getChildren())); } // now scan visibility rules for (VisibilityRule rule : widget.getVisibility()) { @@ -182,26 +160,6 @@ private void constructAndSendEvents(Item item, State newState) { } } - @Override - public void stateChanged(Item item, State oldState, State newState) { - // For all items except group, send an event only when the event state is changed. - if (item instanceof GroupItem) { - return; - } - constructAndSendEvents(item, newState); - } - - @Override - public void stateUpdated(Item item, State state) { - // For group item only, send an event each time the event state is updated. - // It allows updating the group label while the group state is unchanged, - // for example the count in label for Group:Switch:OR - if (!(item instanceof GroupItem)) { - return; - } - constructAndSendEvents(item, state); - } - public void keepCurrentState(Item item) { scheduler.schedule(() -> { constructAndSendEvents(item, item.getState()); @@ -215,15 +173,14 @@ public void changeStateTo(Item item, State state) { private Set constructSitemapEvents(Item item, State state, List widgets) { Set events = new HashSet<>(); for (Widget w : widgets) { - if (w instanceof Frame) { - events.addAll(constructSitemapEvents(item, state, itemUIRegistry.getChildren((Frame) w))); + if (w instanceof Frame frame) { + events.addAll(constructSitemapEvents(item, state, itemUIRegistry.getChildren(frame))); } boolean itemBelongsToWidget = w.getItem() != null && w.getItem().equals(item.getName()); boolean skipWidget = !itemBelongsToWidget; // We skip the chart widgets having a refresh argument - if (!skipWidget && w instanceof Chart) { - Chart chartWidget = (Chart) w; + if (!skipWidget && w instanceof Chart chartWidget) { skipWidget = chartWidget.getRefresh() > 0; } if (!skipWidget || definesVisibilityOrColor(w, item.getName())) { @@ -326,8 +283,8 @@ public void descriptionChanged(String itemName) { private Set constructSitemapEventsForUpdatedDescr(Item item, List widgets) { Set events = new HashSet<>(); for (Widget w : widgets) { - if (w instanceof Frame) { - events.addAll(constructSitemapEventsForUpdatedDescr(item, itemUIRegistry.getChildren((Frame) w))); + if (w instanceof Frame frame) { + events.addAll(constructSitemapEventsForUpdatedDescr(item, itemUIRegistry.getChildren(frame))); } boolean itemBelongsToWidget = w.getItem() != null && w.getItem().equals(item.getName()); @@ -339,4 +296,24 @@ private Set constructSitemapEventsForUpdatedDescr(Item item, List< } return events; } + + @Override + public Set getSubscribedEventTypes() { + return Set.of(ItemStateChangedEvent.TYPE, GroupStateUpdatedEvent.TYPE); + } + + @Override + public void receive(Event event) { + if (event instanceof ItemEvent itemEvent && filterItems.contains(itemEvent.getItemName())) { + Item item = itemUIRegistry.get(itemEvent.getItemName()); + if (item == null) { + return; + } + if (event instanceof GroupStateUpdatedEvent groupStateUpdatedEvent) { + constructAndSendEvents(item, groupStateUpdatedEvent.getItemState()); + } else if (event instanceof ItemStateChangedEvent itemStateChangedEvent) { + constructAndSendEvents(item, itemStateChangedEvent.getItemState()); + } + } + } } diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java index a0597d54f60..2c92feed0f7 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java @@ -21,10 +21,12 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.security.RolesAllowed; import javax.servlet.http.HttpServletRequest; @@ -56,6 +58,8 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventSubscriber; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.RESTConstants; @@ -67,7 +71,8 @@ import org.openhab.core.items.GenericItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; -import org.openhab.core.items.StateChangeListener; +import org.openhab.core.items.events.ItemEvent; +import org.openhab.core.items.events.ItemStateChangedEvent; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.types.HSBType; import org.openhab.core.model.sitemap.SitemapProvider; @@ -75,6 +80,7 @@ import org.openhab.core.model.sitemap.sitemap.ColorArray; import org.openhab.core.model.sitemap.sitemap.Frame; import org.openhab.core.model.sitemap.sitemap.Image; +import org.openhab.core.model.sitemap.sitemap.Input; import org.openhab.core.model.sitemap.sitemap.LinkableWidget; import org.openhab.core.model.sitemap.sitemap.Mapping; import org.openhab.core.model.sitemap.sitemap.Mapview; @@ -124,8 +130,9 @@ * @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification * @author Wouter Born - Migrated to OpenAPI annotations * @author Laurent Garnier - Added support for icon color + * @author Mark Herwege - Added pattern and unit fields */ -@Component(service = RESTResource.class) +@Component(service = { RESTResource.class, EventSubscriber.class }) @JaxrsResource @JaxrsName(SitemapResource.PATH_SITEMAPS) @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")") @@ -135,7 +142,7 @@ @Tag(name = SitemapResource.PATH_SITEMAPS) @NonNullByDefault public class SitemapResource - implements RESTResource, SitemapSubscriptionCallback, SseBroadcaster.Listener { + implements RESTResource, SitemapSubscriptionCallback, SseBroadcaster.Listener, EventSubscriber { private final Logger logger = LoggerFactory.getLogger(SitemapResource.class); @@ -175,6 +182,7 @@ public class SitemapResource .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); private @Nullable ScheduledFuture cleanSubscriptionsJob; + private Set stateChangeListeners = new CopyOnWriteArraySet<>(); @Activate public SitemapResource( // @@ -367,8 +375,8 @@ private PageDTO getPageBean(String sitemapName, String pageId, URI uri, Locale l false, isLeaf(children), uri, locale, timeout, includeHidden); } else { Widget pageWidget = itemUIRegistry.getWidget(sitemap, pageId); - if (pageWidget instanceof LinkableWidget) { - EList children = itemUIRegistry.getChildren((LinkableWidget) pageWidget); + if (pageWidget instanceof LinkableWidget widget) { + EList children = itemUIRegistry.getChildren(widget); PageDTO pageBean = createPageBean(sitemapName, itemUIRegistry.getLabel(pageWidget), itemUIRegistry.getCategory(pageWidget), pageId, children, false, isLeaf(children), uri, locale, timeout, includeHidden); @@ -376,8 +384,8 @@ private PageDTO getPageBean(String sitemapName, String pageId, URI uri, Locale l while (parentPage instanceof Frame) { parentPage = parentPage.eContainer(); } - if (parentPage instanceof Widget) { - String parentId = itemUIRegistry.getWidgetId((Widget) parentPage); + if (parentPage instanceof Widget parentPageWidget) { + String parentId = itemUIRegistry.getWidgetId(parentPageWidget); pageBean.parent = getPageBean(sitemapName, parentId, uri, locale, timeout, includeHidden); pageBean.parent.widgets = null; pageBean.parent.parent = null; @@ -519,10 +527,11 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState); bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState); bean.label = itemUIRegistry.getLabel(widget); + bean.pattern = itemUIRegistry.getFormatPattern(widget); + bean.unit = itemUIRegistry.getUnitForWidget(widget); bean.type = widget.eClass().getName(); bean.visibility = itemUIRegistry.getVisiblity(widget); - if (widget instanceof LinkableWidget) { - LinkableWidget linkableWidget = (LinkableWidget) widget; + if (widget instanceof LinkableWidget linkableWidget) { EList children = itemUIRegistry.getChildren(linkableWidget); if (widget instanceof Frame) { for (Widget child : children) { @@ -540,8 +549,7 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null isLeaf(children), uri, locale, false, evenIfHidden); } } - if (widget instanceof Switch) { - Switch switchWidget = (Switch) widget; + if (widget instanceof Switch switchWidget) { for (Mapping mapping : switchWidget.getMappings()) { MappingDTO mappingBean = new MappingDTO(); mappingBean.command = mapping.getCmd(); @@ -549,8 +557,7 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.mappings.add(mappingBean); } } - if (widget instanceof Selection) { - Selection selectionWidget = (Selection) widget; + if (widget instanceof Selection selectionWidget) { for (Mapping mapping : selectionWidget.getMappings()) { MappingDTO mappingBean = new MappingDTO(); mappingBean.command = mapping.getCmd(); @@ -558,23 +565,23 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.mappings.add(mappingBean); } } - if (widget instanceof Slider) { - Slider sliderWidget = (Slider) widget; + if (widget instanceof Input inputWidget) { + bean.inputHint = inputWidget.getInputHint(); + } + if (widget instanceof Slider sliderWidget) { bean.sendFrequency = sliderWidget.getFrequency(); bean.switchSupport = sliderWidget.isSwitchEnabled(); bean.minValue = sliderWidget.getMinValue(); bean.maxValue = sliderWidget.getMaxValue(); bean.step = sliderWidget.getStep(); } - if (widget instanceof Image) { + if (widget instanceof Image imageWidget) { bean.url = buildProxyUrl(sitemapName, widget, uri); - Image imageWidget = (Image) widget; if (imageWidget.getRefresh() > 0) { bean.refresh = imageWidget.getRefresh(); } } - if (widget instanceof Video) { - Video videoWidget = (Video) widget; + if (widget instanceof Video videoWidget) { if (videoWidget.getEncoding() != null) { bean.encoding = videoWidget.getEncoding(); } @@ -584,17 +591,14 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.url = buildProxyUrl(sitemapName, videoWidget, uri); } } - if (widget instanceof Webview) { - Webview webViewWidget = (Webview) widget; + if (widget instanceof Webview webViewWidget) { bean.url = webViewWidget.getUrl(); bean.height = webViewWidget.getHeight(); } - if (widget instanceof Mapview) { - Mapview mapViewWidget = (Mapview) widget; + if (widget instanceof Mapview mapViewWidget) { bean.height = mapViewWidget.getHeight(); } - if (widget instanceof Chart) { - Chart chartWidget = (Chart) widget; + if (widget instanceof Chart chartWidget) { bean.service = chartWidget.getService(); bean.period = chartWidget.getPeriod(); bean.legend = chartWidget.getLegend(); @@ -604,8 +608,7 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.refresh = chartWidget.getRefresh(); } } - if (widget instanceof Setpoint) { - Setpoint setpointWidget = (Setpoint) widget; + if (widget instanceof Setpoint setpointWidget) { bean.minValue = setpointWidget.getMinValue(); bean.maxValue = setpointWidget.getMaxValue(); bean.step = setpointWidget.getStep(); @@ -636,12 +639,11 @@ private String buildProxyUrl(String sitemapName, Widget widget, URI uri) { private boolean isLeaf(EList children) { for (Widget w : children) { - if (w instanceof Frame) { - if (isLeaf(((Frame) w).getChildren())) { + if (w instanceof Frame frame) { + if (isLeaf(frame.getChildren())) { return false; } - } else if (w instanceof LinkableWidget) { - LinkableWidget linkableWidget = (LinkableWidget) w; + } else if (w instanceof LinkableWidget linkableWidget) { if (!itemUIRegistry.getChildren(linkableWidget).isEmpty()) { return false; } @@ -670,8 +672,8 @@ private boolean blockUnlessChangeOccurs(String sitemapname, String pageId) { timeout = waitForChanges(children); } else { Widget pageWidget = itemUIRegistry.getWidget(sitemap, pageId); - if (pageWidget instanceof LinkableWidget) { - EList children = itemUIRegistry.getChildren((LinkableWidget) pageWidget); + if (pageWidget instanceof LinkableWidget widget) { + EList children = itemUIRegistry.getChildren(widget); timeout = waitForChanges(children); } } @@ -690,13 +692,11 @@ private boolean blockUnlessChangeOccurs(String sitemapname, String pageId) { private boolean waitForChanges(EList widgets) { long startTime = (new Date()).getTime(); boolean timeout = false; - BlockingStateChangeListener listener = new BlockingStateChangeListener(); - // let's get all items for these widgets - Set items = getAllItems(widgets); - for (GenericItem item : items) { - item.addStateChangeListener(listener); - } - while (!listener.hasChangeOccurred() && !timeout) { + Set items = getAllItems(widgets).stream().map(Item::getName).collect(Collectors.toSet()); + BlockingStateChangeListener listener = new BlockingStateChangeListener(items); + stateChangeListeners.add(listener); + + while (!listener.hasChanged() && !timeout) { timeout = (new Date()).getTime() - startTime > TIMEOUT_IN_MS; try { Thread.sleep(300); @@ -705,9 +705,8 @@ private boolean waitForChanges(EList widgets) { break; } } - for (GenericItem item : items) { - item.removeStateChangeListener(listener); - } + + stateChangeListeners.remove(listener); return timeout; } @@ -723,24 +722,23 @@ private Set getAllItems(EList widgets) { for (Widget widget : widgets) { // We skip the chart widgets having a refresh argument boolean skipWidget = false; - if (widget instanceof Chart) { - Chart chartWidget = (Chart) widget; + if (widget instanceof Chart chartWidget) { skipWidget = chartWidget.getRefresh() > 0; } String itemName = widget.getItem(); if (!skipWidget && itemName != null) { try { Item item = itemUIRegistry.getItem(itemName); - if (item instanceof GenericItem) { - items.add((GenericItem) item); + if (item instanceof GenericItem genericItem) { + items.add(genericItem); } } catch (ItemNotFoundException e) { // ignore } } // Consider all items inside the frame - if (widget instanceof Frame) { - items.addAll(getAllItems(((Frame) widget).getChildren())); + if (widget instanceof Frame frame) { + items.addAll(getAllItems(frame.getChildren())); } // Consider items involved in any visibility, labelcolor, valuecolor and iconcolor condition items.addAll(getItemsInVisibilityCond(widget.getVisibility())); @@ -758,8 +756,8 @@ private Set getItemsInVisibilityCond(EList ruleList if (itemName != null) { try { Item item = itemUIRegistry.getItem(itemName); - if (item instanceof GenericItem) { - items.add((GenericItem) item); + if (item instanceof GenericItem genericItem) { + items.add(genericItem); } } catch (ItemNotFoundException e) { // ignore @@ -776,8 +774,8 @@ private Set getItemsInColorCond(EList colorList) { if (itemName != null) { try { Item item = itemUIRegistry.getItem(itemName); - if (item instanceof GenericItem) { - items.add((GenericItem) item); + if (item instanceof GenericItem genericItem) { + items.add(genericItem); } } catch (ItemNotFoundException e) { // ignore @@ -787,34 +785,16 @@ private Set getItemsInColorCond(EList colorList) { return items; } - /** - * This is a state change listener, which is merely used to determine, if a - * state change has occurred on one of a list of items. - * - * @author Kai Kreuzer - Initial contribution - * - */ - private static class BlockingStateChangeListener implements StateChangeListener { - - private boolean changed = false; - - @Override - public void stateChanged(Item item, State oldState, State newState) { - changed = true; - } - - /** - * determines, whether a state change has occurred since its creation - * - * @return true, if a state has changed - */ - public boolean hasChangeOccurred() { - return changed; - } + @Override + public Set getSubscribedEventTypes() { + return Set.of(ItemStateChangedEvent.TYPE); + } - @Override - public void stateUpdated(Item item, State state) { - // ignore if the state did not change + @Override + public void receive(Event event) { + if (event instanceof ItemEvent itemEvent) { + String itemName = itemEvent.getItemName(); + stateChangeListeners.forEach(l -> l.itemChanged(itemName)); } } @@ -834,9 +814,9 @@ public void onEvent(SitemapEvent event) { if (sitemapName != null && sitemapName.equals(subscriptions.getSitemapName(info.subscriptionId)) && pageId != null && pageId.equals(subscriptions.getPageId(info.subscriptionId))) { if (logger.isDebugEnabled()) { - if (event instanceof SitemapWidgetEvent) { - logger.debug("Sent sitemap event for widget {} to subscription {}.", - ((SitemapWidgetEvent) event).widgetId, info.subscriptionId); + if (event instanceof SitemapWidgetEvent widgetEvent) { + logger.debug("Sent sitemap event for widget {} to subscription {}.", widgetEvent.widgetId, + info.subscriptionId); } else if (event instanceof ServerAliveEvent) { logger.debug("Sent alive event to subscription {}.", info.subscriptionId); } @@ -860,4 +840,23 @@ public void sseEventSinkRemoved(SseEventSink sink, SseSinkInfo info) { subscriptions.removeSubscription(info.subscriptionId); knownSubscriptions.remove(info.subscriptionId); } + + private static class BlockingStateChangeListener { + private final Set items; + private boolean changed = false; + + public BlockingStateChangeListener(Set items) { + this.items = items; + } + + public void itemChanged(String item) { + if (items.contains(item)) { + changed = true; + } + } + + public boolean hasChanged() { + return changed; + } + } } diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java index 61e79b7c347..c9dc713b082 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java @@ -24,6 +24,7 @@ * @author Kai Kreuzer - Initial contribution * @author Chris Jackson - Initial contribution * @author Laurent Garnier - New field iconcolor + * @author Mark herwege - New fields pattern, unit */ public class WidgetDTO { @@ -38,6 +39,9 @@ public class WidgetDTO { public String valuecolor; public String iconcolor; + public String pattern; + public String unit; + // widget-specific attributes public final List mappings = new ArrayList<>(); public Boolean switchSupport; @@ -47,6 +51,7 @@ public class WidgetDTO { public BigDecimal minValue; public BigDecimal maxValue; public BigDecimal step; + public String inputHint; public String url; public String encoding; public String service; diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java index fe384aeabb0..b8a0b0da6ab 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java @@ -18,7 +18,6 @@ import static org.hamcrest.collection.IsEmptyCollection.empty; import static org.mockito.Mockito.*; -import java.math.BigDecimal; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -45,7 +44,7 @@ import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService; import org.openhab.core.items.GenericItem; import org.openhab.core.items.ItemNotFoundException; -import org.openhab.core.library.types.DecimalType; +import org.openhab.core.items.events.ItemEvent; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.model.sitemap.SitemapProvider; @@ -158,11 +157,13 @@ public void whenSitemapsAreProvidedShouldReturnSitemapBeans() { @Test public void whenLongPollingShouldObserveItems() { + ItemEvent itemEvent = mock(ItemEvent.class); + when(itemEvent.getItemName()).thenReturn(item.getName()); new Thread(() -> { try { Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the // item - item.setState(PercentType.ZERO); + sitemapResource.receive(itemEvent); } catch (InterruptedException e) { } }).start(); @@ -180,11 +181,13 @@ public void whenLongPollingShouldObserveItems() { @Test public void whenLongPollingShouldObserveItemsFromVisibilityRules() { + ItemEvent itemEvent = mock(ItemEvent.class); + when(itemEvent.getItemName()).thenReturn(visibilityRuleItem.getName()); new Thread(() -> { try { Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the // item - visibilityRuleItem.setState(new DecimalType(BigDecimal.ONE)); + sitemapResource.receive(itemEvent); } catch (InterruptedException e) { } }).start(); @@ -202,11 +205,13 @@ public void whenLongPollingShouldObserveItemsFromVisibilityRules() { @Test public void whenLongPollingShouldObserveItemsFromLabelColorConditions() { + ItemEvent itemEvent = mock(ItemEvent.class); + when(itemEvent.getItemName()).thenReturn(labelColorItem.getName()); new Thread(() -> { try { Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the // item - labelColorItem.setState(new DecimalType(BigDecimal.ONE)); + sitemapResource.receive(itemEvent); } catch (InterruptedException e) { } }).start(); @@ -224,11 +229,13 @@ public void whenLongPollingShouldObserveItemsFromLabelColorConditions() { @Test public void whenLongPollingShouldObserveItemsFromValueColorConditions() { + ItemEvent itemEvent = mock(ItemEvent.class); + when(itemEvent.getItemName()).thenReturn(valueColorItem.getName()); new Thread(() -> { try { Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the // item - valueColorItem.setState(new DecimalType(BigDecimal.ONE)); + sitemapResource.receive(itemEvent); } catch (InterruptedException e) { } }).start(); diff --git a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java index 778192b6337..f5a9c562973 100644 --- a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java +++ b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java @@ -150,8 +150,8 @@ public void broadcast(Event event) { executorService.execute(() -> { handleEventBroadcastTopic(event); - if (event instanceof ItemStateChangedEvent) { - handleEventBroadcastItemState((ItemStateChangedEvent) event); + if (event instanceof ItemStateChangedEvent changedEvent) { + handleEventBroadcastItemState(changedEvent); } }); } @@ -236,7 +236,7 @@ public Object updateTrackedItems(@PathParam("connectionId") @Nullable String con } Optional itemStateInfo = itemStatesBroadcaster.getInfoIf(hasConnectionId(connectionId)) .findFirst(); - if (!itemStateInfo.isPresent()) { + if (itemStateInfo.isEmpty()) { return Response.status(Status.NOT_FOUND).build(); } diff --git a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java index 6ec6ead98d6..8bb59852435 100644 --- a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java +++ b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java @@ -78,6 +78,7 @@ public SseItemStatesEventBuilder(final BundleContext bundleContext, final @Refer Item item = itemRegistry.getItem(itemName); StateDTO stateDto = new StateDTO(); stateDto.state = item.getState().toString(); + stateDto.type = getStateType(item.getState()); String displayState = getDisplayState(item, localeService.getLocale(null)); // Only include the display state if it's different than the raw state if (stateDto.state != null && !stateDto.state.equals(displayState)) { @@ -128,8 +129,7 @@ public SseItemStatesEventBuilder(final BundleContext bundleContext, final @Refer } else { // if it's not a transformation pattern, then it must be a format string - if (state instanceof QuantityType) { - QuantityType quantityState = (QuantityType) state; + if (state instanceof QuantityType quantityState) { // sanity convert current state to the item state description unit in case it was // updated in the meantime. The item state is still in the "original" unit while the // state description will display the new unit: @@ -141,10 +141,10 @@ public SseItemStatesEventBuilder(final BundleContext bundleContext, final @Refer if (quantityState != null) { state = quantityState; } - } else if (state instanceof DateTimeType) { + } else if (state instanceof DateTimeType type) { // Translate a DateTimeType state to the local time zone try { - state = ((DateTimeType) state).toLocaleZone(); + state = type.toLocaleZone(); } catch (DateTimeException e) { } } @@ -169,4 +169,10 @@ public SseItemStatesEventBuilder(final BundleContext bundleContext, final @Refer return displayState; } + + // Taken from org.openhab.core.items.events.ItemEventFactory + private static String getStateType(State state) { + String stateClassName = state.getClass().getSimpleName(); + return stateClassName.substring(0, stateClassName.length() - "Type".length()); + } } diff --git a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/dto/StateDTO.java b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/dto/StateDTO.java index 566a1b64832..0c908369f32 100644 --- a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/dto/StateDTO.java +++ b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/dto/StateDTO.java @@ -21,6 +21,8 @@ public class StateDTO { public String state; public String displayState; + public String type; + public StateDTO() { } } diff --git a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/util/SseUtil.java b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/util/SseUtil.java index f70f851814b..9dd6af379aa 100644 --- a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/util/SseUtil.java +++ b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/util/SseUtil.java @@ -51,12 +51,10 @@ public static EventDTO buildDTO(final Event event) { * @return a new OutboundEvent */ public static OutboundSseEvent buildEvent(OutboundSseEvent.Builder eventBuilder, EventDTO event) { - final OutboundSseEvent sseEvent = eventBuilder.name("message") // + return eventBuilder.name("message") // .mediaType(MediaType.APPLICATION_JSON_TYPE) // .data(event) // .build(); - - return sseEvent; } /** diff --git a/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationResource.java b/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationResource.java index fb4197b0792..9e9942864ca 100644 --- a/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationResource.java +++ b/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationResource.java @@ -117,8 +117,9 @@ public Response getTransformationServices() { try { Collection> refs = bundleContext .getServiceReferences(TransformationService.class, null); - Stream services = refs.stream().map(ref -> (String) ref.getProperty("openhab.transform")) - .filter(Objects::nonNull).map(Objects::requireNonNull); + Stream services = refs.stream() + .map(ref -> (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME)) + .filter(Objects::nonNull).map(Objects::requireNonNull).sorted(); return Response.ok(new Stream2JSONInputStream(services)).build(); } catch (InvalidSyntaxException e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); diff --git a/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java b/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java index 166a3863dd5..eb34f73ce11 100644 --- a/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java +++ b/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java @@ -13,7 +13,11 @@ package org.openhab.core.io.rest.ui.internal; import java.security.InvalidParameterException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -27,13 +31,17 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; +import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTResource; import org.openhab.core.io.rest.Stream2JSONInputStream; @@ -45,6 +53,7 @@ import org.openhab.core.ui.tiles.TileProvider; 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; @@ -84,6 +93,9 @@ public class UIResource implements RESTResource { private final UIComponentRegistryFactory componentRegistryFactory; private final TileProvider tileProvider; + private Map lastModifiedDates = new HashMap<>(); + private Map> registryChangeListeners = new HashMap<>(); + @Activate public UIResource( // final @Reference UIComponentRegistryFactory componentRegistryFactory, @@ -92,6 +104,14 @@ public UIResource( // this.tileProvider = tileProvider; } + @Deactivate + public void deactivate() { + registryChangeListeners.forEach((n, l) -> { + UIComponentRegistry registry = componentRegistryFactory.getRegistry(n); + registry.removeRegistryChangeListener(l); + }); + } + @GET @Path("/tiles") @Produces({ MediaType.APPLICATION_JSON }) @@ -107,7 +127,7 @@ public Response getAll() { @Produces({ MediaType.APPLICATION_JSON }) @Operation(operationId = "getRegisteredUIComponentsInNamespace", summary = "Get all registered UI components in the specified namespace.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RootUIComponent.class)))) }) - public Response getAllComponents(@PathParam("namespace") String namespace, + public Response getAllComponents(@Context Request request, @PathParam("namespace") String namespace, @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) { UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace); Stream components = registry.getAll().stream(); @@ -126,8 +146,33 @@ public Response getAllComponents(@PathParam("namespace") String namespace, } return component; }); + return Response.ok(new Stream2JSONInputStream(components)).build(); + } else { + if (!registryChangeListeners.containsKey(namespace)) { + RegistryChangeListener changeListener = new ResetLastModifiedChangeListener(namespace); + registryChangeListeners.put(namespace, changeListener); + registry.addRegistryChangeListener(changeListener); + } + + Date lastModifiedDate = Date.from(Instant.now()); + if (lastModifiedDates.containsKey(namespace)) { + lastModifiedDate = lastModifiedDates.get(namespace); + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModifiedDate); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + lastModifiedDate = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + lastModifiedDates.put(namespace, lastModifiedDate); + } + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + return Response.ok(new Stream2JSONInputStream(components)).lastModified(lastModifiedDate).cacheControl(cc) + .build(); } - return Response.ok(new Stream2JSONInputStream(components)).build(); } @GET @@ -208,4 +253,32 @@ public Response deleteComponent(@PathParam("namespace") String namespace, private TileDTO toTileDTO(Tile tile) { return new TileDTO(tile.getName(), tile.getUrl(), tile.getOverlay(), tile.getImageUrl()); } + + private void resetLastModifiedDate(String namespace) { + lastModifiedDates.remove(namespace); + } + + private class ResetLastModifiedChangeListener implements RegistryChangeListener { + + private String namespace; + + ResetLastModifiedChangeListener(String namespace) { + this.namespace = namespace; + } + + @Override + public void added(RootUIComponent element) { + resetLastModifiedDate(namespace); + } + + @Override + public void removed(RootUIComponent element) { + resetLastModifiedDate(namespace); + } + + @Override + public void updated(RootUIComponent oldElement, RootUIComponent element) { + resetLastModifiedDate(namespace); + } + } } diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/JSONResponse.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/JSONResponse.java index b9df1cdd69b..d0123394085 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/JSONResponse.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/JSONResponse.java @@ -210,8 +210,8 @@ public Response toResponse(Exception e) { Response.StatusType status = Response.Status.INTERNAL_SERVER_ERROR; // in case the Exception is a WebApplicationException, it already carries a Status - if (e instanceof WebApplicationException) { - status = ((WebApplicationException) e).getResponse().getStatusInfo(); + if (e instanceof WebApplicationException exception) { + status = exception.getResponse().getStatusInfo(); } JsonElement ret = INSTANCE.createErrorJson(e.getMessage(), status, null, e); diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/LocaleServiceImpl.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/LocaleServiceImpl.java index 383a3b2fac0..385053eec81 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/LocaleServiceImpl.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/LocaleServiceImpl.java @@ -30,7 +30,7 @@ * @author Dennis Nobel - Initial contribution * @author Markus Rathgeb - Use locale provider * @author Martin Herbst - Support of different language definition variants - * @authro Lyubomir Papazov - Add component annotation, rename the class to LocaleService and add method tryGetLocale + * @author Lyubomir Papazov - Add component annotation, rename the class to LocaleService and add method tryGetLocale */ @Component @NonNullByDefault @@ -81,7 +81,7 @@ private Locale tryGetLocale() { if (provider != null) { return provider.getLocale(); } else { - logger.error("There should ALWAYS be a local provider available, as it is provided by the core."); + logger.error("There should ALWAYS be a locale provider available, as it is provided by the core."); return Locale.US; } } diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/filter/CorsFilter.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/filter/CorsFilter.java index d5217d38b92..5d6bdf8d149 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/filter/CorsFilter.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/filter/CorsFilter.java @@ -159,7 +159,7 @@ private boolean processPreflight(ContainerRequestContext requestContext, Contain if (values == null || values.isEmpty()) { return null; } - return values.get(0).toString(); + return values.get(0); } /** diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/SystemInfoResource.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/SystemInfoResource.java index 434c2a55f1e..f419bb4361c 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/SystemInfoResource.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/SystemInfoResource.java @@ -23,9 +23,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.auth.Role; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTResource; import org.openhab.core.io.rest.internal.resources.beans.SystemInfoBean; +import org.openhab.core.io.rest.internal.resources.beans.UoMInfoBean; import org.openhab.core.service.StartLevelService; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -64,10 +66,12 @@ public class SystemInfoResource implements RESTResource { public static final String PATH_SYSTEMINFO = "systeminfo"; private final StartLevelService startLevelService; + private final UnitProvider unitProvider; @Activate - public SystemInfoResource(@Reference StartLevelService startLevelService) { + public SystemInfoResource(@Reference StartLevelService startLevelService, @Reference UnitProvider unitProvider) { this.startLevelService = startLevelService; + this.unitProvider = unitProvider; } @GET @@ -79,4 +83,14 @@ public Response getSystemInfo(@Context UriInfo uriInfo) { final SystemInfoBean bean = new SystemInfoBean(startLevelService.getStartLevel()); return Response.ok(bean).build(); } + + @GET + @Path("/uom") + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getUoMInformation", summary = "Get all supported dimensions and their system units.", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = UoMInfoBean.class))) }) + public Response getUoMInfo(@Context UriInfo uriInfo) { + final UoMInfoBean bean = new UoMInfoBean(unitProvider); + return Response.ok(bean).build(); + } } diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/beans/UoMInfoBean.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/beans/UoMInfoBean.java new file mode 100644 index 00000000000..225d19221d4 --- /dev/null +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/beans/UoMInfoBean.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.internal.resources.beans; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import javax.measure.Quantity; +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.types.util.UnitUtils; + +/** + * This is a java bean that is used to define UoM information for the REST interface. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class UoMInfoBean { + + public final UoMInfo uomInfo; + + public static class UoMInfo { + public final List dimensions; + + public static class DimensionInfo { + public final String dimension; + public final String systemUnit; + + public DimensionInfo(String dimension, String systemUnit) { + this.dimension = dimension; + this.systemUnit = systemUnit; + } + } + + @SuppressWarnings({ "unchecked" }) + public UoMInfo(UnitProvider unitProvider) { + dimensions = unitProvider.getAllDimensions().stream().map(dimension -> { + Unit unit = unitProvider.getUnit((Class) dimension); + String dimensionName = Objects.requireNonNull(UnitUtils.getDimensionName(unit)); + return new DimensionInfo(dimensionName, unit.toString()); + }).sorted(Comparator.comparing(a -> a.dimension)).toList(); + } + } + + public UoMInfoBean(UnitProvider unitProvider) { + uomInfo = new UoMInfo(unitProvider); + } +} diff --git a/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSClient.java b/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSClient.java index b4d3011b3c4..8ea31733ff2 100644 --- a/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSClient.java +++ b/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSClient.java @@ -33,7 +33,7 @@ public interface MDNSClient { * * @return a set of JmDNS instances */ - public Set getClientInstances(); + Set getClientInstances(); /** * Listen for services of a given type @@ -41,7 +41,7 @@ public interface MDNSClient { * @param type full qualified service type * @param listener listener for service updates */ - public void addServiceListener(String type, ServiceListener listener); + void addServiceListener(String type, ServiceListener listener); /** * Remove listener for services of a given type @@ -49,27 +49,27 @@ public interface MDNSClient { * @param type full qualified service type * @param listener listener for service updates */ - public void removeServiceListener(String type, ServiceListener listener); + void removeServiceListener(String type, ServiceListener listener); /** * Register a service * * @param description service to register, described by (@link ServiceDescription) */ - public void registerService(ServiceDescription description) throws IOException; + void registerService(ServiceDescription description) throws IOException; /** * Unregister a service. The service should have been registered. * * @param description service to remove, described by (@link ServiceDescription) */ - public void unregisterService(ServiceDescription description); + void unregisterService(ServiceDescription description); /** * Unregister all services * */ - public void unregisterAllServices(); + void unregisterAllServices(); /** * Returns a list of service infos of the specified type @@ -77,7 +77,7 @@ public interface MDNSClient { * @param type service type name * @return an array of service instances */ - public ServiceInfo[] list(String type); + ServiceInfo[] list(String type); /** * Returns a list of service infos of the specified type within timeout @@ -86,11 +86,11 @@ public interface MDNSClient { * @param timeout the amount of time it should wait if no service info is found. * @return an array of service instances */ - public ServiceInfo[] list(String type, Duration timeout); + ServiceInfo[] list(String type, Duration timeout); /** * Close properly JmDNS instances * */ - public void close(); + void close(); } diff --git a/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSService.java b/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSService.java index 791dd74dd13..28331c81d22 100644 --- a/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSService.java +++ b/bundles/org.openhab.core.io.transport.mdns/src/main/java/org/openhab/core/io/transport/mdns/MDNSService.java @@ -26,12 +26,12 @@ public interface MDNSService { * * @param serviceDescription the {@link ServiceDescription} instance with all details to identify the service */ - public void registerService(ServiceDescription description); + void registerService(ServiceDescription description); /** * This method unregisters a service not to be announced through Bonjour/MDNS * * @param serviceDescription the {@link ServiceDescription} instance with all details to identify the service */ - public void unregisterService(ServiceDescription description); + void unregisterService(ServiceDescription description); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/BitArray.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/BitArray.java index 9713d41a0ad..370b4e122d9 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/BitArray.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/BitArray.java @@ -113,8 +113,7 @@ public int size() { @Override public String toString() { - return new StringBuilder("BitArray(bits=").append(length == 0 ? "" : toBinaryString()).append(")") - .toString(); + return "BitArray(bits=" + (length == 0 ? "" : toBinaryString()) + ")"; } @Override diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusBitUtilities.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusBitUtilities.java index 7972773aec8..be2ca28b505 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusBitUtilities.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusBitUtilities.java @@ -229,8 +229,7 @@ public static int extractBit(byte[] bytes, int registerIndex, int bitIndexWithin */ public static byte extractSInt8(byte[] bytes, int registerIndex, boolean hiByte) { int byteIndex = 2 * registerIndex + (hiByte ? 0 : 1); - byte signed = extractSInt8(bytes, byteIndex); - return signed; + return extractSInt8(bytes, byteIndex); } /** @@ -248,8 +247,7 @@ public static byte extractSInt8(byte[] bytes, int registerIndex, boolean hiByte) */ public static byte extractSInt8(byte[] bytes, int index) { assertIndexAndType(bytes, index, ValueType.INT8); - byte signed = bytes[index]; - return signed; + return bytes[index]; } /** @@ -263,8 +261,7 @@ public static byte extractSInt8(byte[] bytes, int index) { */ public static short extractUInt8(byte[] bytes, int registerIndex, boolean hiByte) { int byteIndex = 2 * registerIndex + (hiByte ? 0 : 1); - short unsigned = extractUInt8(bytes, byteIndex); - return unsigned; + return extractUInt8(bytes, byteIndex); } /** @@ -302,8 +299,7 @@ public static short extractSInt16(byte[] bytes, int index) { assertIndexAndType(bytes, index, ValueType.INT16); int hi = (bytes[index] & 0xff); int lo = (bytes[index + 1] & 0xff); - short signed = (short) ((hi << 8) | lo); - return signed; + return (short) ((hi << 8) | lo); } /** @@ -340,8 +336,7 @@ public static int extractSInt32(byte[] bytes, int index) { int lo1 = bytes[index + 1] & 0xff; int hi2 = bytes[index + 2] & 0xff; int lo2 = bytes[index + 3] & 0xff; - int signed = (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; - return signed; + return (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; } /** @@ -381,8 +376,7 @@ public static int extractSInt32Swap(byte[] bytes, int index) { int lo1 = bytes[index + 3] & 0xff; int hi2 = bytes[index + 0] & 0xff; int lo2 = bytes[index + 1] & 0xff; - int signed = (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; - return signed; + return (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; } /** @@ -633,10 +627,10 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons if (command instanceof OnOffType || command instanceof OpenClosedType) { numericCommand = translateCommand2Boolean(command).get() ? new DecimalType(BigDecimal.ONE) : DecimalType.ZERO; - } else if (command instanceof DecimalType) { - numericCommand = (DecimalType) command; - } else if (command instanceof QuantityType) { - QuantityType qtCommand = ((QuantityType) command).toUnit(Units.ONE); + } else if (command instanceof DecimalType decimalType) { + numericCommand = decimalType; + } else if (command instanceof QuantityType quantityType) { + QuantityType qtCommand = quantityType.toUnit(Units.ONE); if (qtCommand == null) { throw new IllegalArgumentException( String.format("Command '%s' of class '%s' cannot be converted to bare number.", command, diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusCommunicationInterface.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusCommunicationInterface.java index ffbeca1f4b5..27f72d7301f 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusCommunicationInterface.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusCommunicationInterface.java @@ -38,7 +38,7 @@ public interface ModbusCommunicationInterface extends AutoCloseable { * * @return modbus slave endpoint */ - public ModbusSlaveEndpoint getEndpoint(); + ModbusSlaveEndpoint getEndpoint(); /** * Submit one-time poll task. The method returns immediately, and the execution of the poll task will happen in @@ -50,7 +50,7 @@ public interface ModbusCommunicationInterface extends AutoCloseable { * @return future representing the polled task * @throws IllegalStateException when this communication has been closed already */ - public Future submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusReadCallback resultCallback, + Future submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusReadCallback resultCallback, ModbusFailureCallback failureCallback); /** @@ -67,9 +67,8 @@ public Future submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusRea * @return poll task representing the regular poll * @throws IllegalStateException when this communication has been closed already */ - public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pollPeriodMillis, - long initialDelayMillis, ModbusReadCallback resultCallback, - ModbusFailureCallback failureCallback); + PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pollPeriodMillis, long initialDelayMillis, + ModbusReadCallback resultCallback, ModbusFailureCallback failureCallback); /** * Unregister regularly polled task @@ -80,7 +79,7 @@ public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pol * @return whether poll task was unregistered. Poll task is not unregistered in case of unexpected errors or * in the case where the poll task is not registered in the first place */ - public boolean unregisterRegularPoll(PollTask task); + boolean unregisterRegularPoll(PollTask task); /** * Submit one-time write task. The method returns immediately, and the execution of the task will happen in @@ -92,7 +91,7 @@ public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pol * @return future representing the task * @throws IllegalStateException when this communication has been closed already */ - public Future submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusWriteCallback resultCallback, + Future submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusWriteCallback resultCallback, ModbusFailureCallback failureCallback); /** @@ -105,5 +104,5 @@ public Future submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusW * */ @Override - public void close() throws Exception; + void close() throws Exception; } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusManager.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusManager.java index 4595e54f4e8..e8d23f26a6b 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusManager.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusManager.java @@ -34,7 +34,7 @@ public interface ModbusManager { * @throws IllegalArgumentException if there is already open communication interface with same endpoint but * differing configuration */ - public ModbusCommunicationInterface newModbusCommunicationInterface(ModbusSlaveEndpoint endpoint, + ModbusCommunicationInterface newModbusCommunicationInterface(ModbusSlaveEndpoint endpoint, @Nullable EndpointPoolConfiguration configuration) throws IllegalArgumentException; /** @@ -45,5 +45,5 @@ public ModbusCommunicationInterface newModbusCommunicationInterface(ModbusSlaveE * @param endpoint endpoint to query * @return general connection settings of the given endpoint */ - public EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint); + EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusRegisterArray.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusRegisterArray.java index cf72c82ce35..63902f8d1e1 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusRegisterArray.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusRegisterArray.java @@ -94,8 +94,7 @@ public String toString() { if (bytes.length == 0) { return "ModbusRegisterArray()"; } - return new StringBuilder(bytes.length).append("ModbusRegisterArray(").append(toHexString()).append(')') - .toString(); + return "ModbusRegisterArray(" + toHexString() + ')'; } /** diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusResponse.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusResponse.java index f1de8d7385c..9d59a7cbc57 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusResponse.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusResponse.java @@ -32,5 +32,5 @@ public interface ModbusResponse { * * @return function code of the response */ - public int getFunctionCode(); + int getFunctionCode(); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusWriteRequestBlueprintVisitor.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusWriteRequestBlueprintVisitor.java index 0794760ccaa..4f66ae34a93 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusWriteRequestBlueprintVisitor.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/ModbusWriteRequestBlueprintVisitor.java @@ -29,12 +29,12 @@ public interface ModbusWriteRequestBlueprintVisitor { * * @param blueprint */ - public void visit(ModbusWriteCoilRequestBlueprint blueprint); + void visit(ModbusWriteCoilRequestBlueprint blueprint); /** * Visit request writing register data * * @param blueprint */ - public void visit(ModbusWriteRegisterRequestBlueprint blueprint); + void visit(ModbusWriteRegisterRequestBlueprint blueprint); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/endpoint/ModbusSlaveEndpoint.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/endpoint/ModbusSlaveEndpoint.java index d980d94fd53..fda03c56b08 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/endpoint/ModbusSlaveEndpoint.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/endpoint/ModbusSlaveEndpoint.java @@ -27,5 +27,5 @@ */ @NonNullByDefault public interface ModbusSlaveEndpoint { - public R accept(ModbusSlaveEndpointVisitor visitor); + R accept(ModbusSlaveEndpointVisitor visitor); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusConnectionPool.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusConnectionPool.java index d084a105145..8f4e8373210 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusConnectionPool.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusConnectionPool.java @@ -30,14 +30,15 @@ * */ @NonNullByDefault -public class ModbusConnectionPool extends GenericKeyedObjectPool { +public class ModbusConnectionPool extends GenericKeyedObjectPool { - public ModbusConnectionPool(KeyedPooledObjectFactory factory) { + public ModbusConnectionPool( + KeyedPooledObjectFactory factory) { super(factory, new ModbusPoolConfig()); } @Override - public void setConfig(@Nullable GenericKeyedObjectPoolConfig conf) { + public void setConfig(@Nullable GenericKeyedObjectPoolConfig<@Nullable ModbusSlaveConnection> conf) { if (conf == null) { return; } else if (!(conf instanceof ModbusPoolConfig)) { diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusLibraryWrapper.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusLibraryWrapper.java index 7c83e6cc986..b0a9a56cb92 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusLibraryWrapper.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusLibraryWrapper.java @@ -213,12 +213,12 @@ public static ModbusTransaction createTransactionForEndpoint(ModbusSlaveEndpoint // We disable modbus library retries and handle in the Manager implementation transaction.setRetries(0); transaction.setRetryDelayMillis(0); - if (transaction instanceof ModbusSerialTransaction) { - ((ModbusSerialTransaction) transaction).setSerialConnection((SerialConnection) connection); - } else if (transaction instanceof ModbusUDPTransaction) { - ((ModbusUDPTransaction) transaction).setTerminal(((UDPMasterConnection) connection).getTerminal()); - } else if (transaction instanceof ModbusTCPTransaction) { - ((ModbusTCPTransaction) transaction).setConnection((TCPMasterConnection) connection); + if (transaction instanceof ModbusSerialTransaction serialTransaction) { + serialTransaction.setSerialConnection((SerialConnection) connection); + } else if (transaction instanceof ModbusUDPTransaction pTransaction) { + pTransaction.setTerminal(((UDPMasterConnection) connection).getTerminal()); + } else if (transaction instanceof ModbusTCPTransaction pTransaction) { + pTransaction.setConnection((TCPMasterConnection) connection); } else { throw new IllegalStateException(); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusManagerImpl.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusManagerImpl.java index 6af5e320582..8c75042aaee 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusManagerImpl.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusManagerImpl.java @@ -298,39 +298,38 @@ public void accept(AggregateStopWatch timer, WriteTask task, ModbusSlaveConnecti */ private static final long WARN_QUEUE_SIZE = 500; private static final long MONITOR_QUEUE_INTERVAL_MILLIS = 10000; - private static final Function DEFAULT_POOL_CONFIGURATION = endpoint -> { - return endpoint.accept(new ModbusSlaveEndpointVisitor() { - - @Override - public @NonNull EndpointPoolConfiguration visit(ModbusTCPSlaveEndpoint modbusIPSlavePoolingKey) { - EndpointPoolConfiguration endpointPoolConfig = new EndpointPoolConfiguration(); - endpointPoolConfig.setInterTransactionDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS); - endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); - endpointPoolConfig.setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS); - return endpointPoolConfig; - } + private static final Function DEFAULT_POOL_CONFIGURATION = endpoint -> endpoint + .accept(new ModbusSlaveEndpointVisitor() { + + @Override + public @NonNull EndpointPoolConfiguration visit(ModbusTCPSlaveEndpoint modbusIPSlavePoolingKey) { + EndpointPoolConfiguration endpointPoolConfig = new EndpointPoolConfiguration(); + endpointPoolConfig.setInterTransactionDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS); + endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); + endpointPoolConfig.setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS); + return endpointPoolConfig; + } - @Override - public @NonNull EndpointPoolConfiguration visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) { - EndpointPoolConfiguration endpointPoolConfig = new EndpointPoolConfiguration(); - // never "disconnect" (close/open serial port) serial connection between borrows - endpointPoolConfig.setReconnectAfterMillis(-1); - endpointPoolConfig.setInterTransactionDelayMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS); - endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); - endpointPoolConfig.setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS); - return endpointPoolConfig; - } + @Override + public @NonNull EndpointPoolConfiguration visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) { + EndpointPoolConfiguration endpointPoolConfig = new EndpointPoolConfiguration(); + // never "disconnect" (close/open serial port) serial connection between borrows + endpointPoolConfig.setReconnectAfterMillis(-1); + endpointPoolConfig.setInterTransactionDelayMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS); + endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); + endpointPoolConfig.setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS); + return endpointPoolConfig; + } - @Override - public @NonNull EndpointPoolConfiguration visit(ModbusUDPSlaveEndpoint modbusUDPSlavePoolingKey) { - EndpointPoolConfiguration endpointPoolConfig = new EndpointPoolConfiguration(); - endpointPoolConfig.setInterTransactionDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS); - endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); - endpointPoolConfig.setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS); - return endpointPoolConfig; - } - }); - }; + @Override + public @NonNull EndpointPoolConfiguration visit(ModbusUDPSlaveEndpoint modbusUDPSlavePoolingKey) { + EndpointPoolConfiguration endpointPoolConfig = new EndpointPoolConfiguration(); + endpointPoolConfig.setInterTransactionDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS); + endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); + endpointPoolConfig.setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS); + return endpointPoolConfig; + } + }); private final PollOperation pollOperation = new PollOperation(); private final WriteOperation writeOperation = new WriteOperation(); @@ -347,7 +346,7 @@ public void accept(AggregateStopWatch timer, WriteTask task, ModbusSlaveConnecti * - https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/ */ - private volatile @Nullable KeyedObjectPool connectionPool; + private volatile @Nullable KeyedObjectPool connectionPool; private volatile @Nullable ModbusSlaveConnectionFactoryImpl connectionFactory; private volatile Map> scheduledPollTasks = new ConcurrentHashMap<>(); /** @@ -360,7 +359,7 @@ public void accept(AggregateStopWatch timer, WriteTask task, ModbusSlaveConnecti private void constructConnectionPool() { ModbusSlaveConnectionFactoryImpl connectionFactory = new ModbusSlaveConnectionFactoryImpl( DEFAULT_POOL_CONFIGURATION); - GenericKeyedObjectPool genericKeyedObjectPool = new ModbusConnectionPool( + GenericKeyedObjectPool genericKeyedObjectPool = new ModbusConnectionPool( connectionFactory); genericKeyedObjectPool.setSwallowedExceptionListener(new SwallowedExceptionListener() { @@ -379,7 +378,7 @@ public void onSwallowException(@Nullable Exception e) { private Optional borrowConnection(ModbusSlaveEndpoint endpoint) { Optional connection = Optional.empty(); - KeyedObjectPool pool = connectionPool; + KeyedObjectPool pool = connectionPool; if (pool == null) { return connection; } @@ -405,7 +404,7 @@ private Optional borrowConnection(ModbusSlaveEndpoint end } private void invalidate(ModbusSlaveEndpoint endpoint, Optional connection) { - KeyedObjectPool pool = connectionPool; + KeyedObjectPool pool = connectionPool; if (pool == null) { return; } @@ -423,7 +422,7 @@ private void invalidate(ModbusSlaveEndpoint endpoint, Optional connection) { - KeyedObjectPool pool = connectionPool; + KeyedObjectPool pool = connectionPool; if (pool == null) { return; } @@ -454,7 +453,7 @@ private void returnConnection(ModbusSlaveEndpoint endpoint, Optional, T extends TaskWithEndpoint> Optional getConnection( AggregateStopWatch timer, boolean oneOffTask, @NonNull T task) throws PollTaskUnregistered { - KeyedObjectPool connectionPool = this.connectionPool; + KeyedObjectPool connectionPool = this.connectionPool; if (connectionPool == null) { return Optional.empty(); } @@ -476,7 +475,7 @@ private , timer.connection.timeRunnable(() -> invalidate(endpoint, connection)); return Optional.empty(); } - if (!connection.isPresent()) { + if (connection.isEmpty()) { logger.warn("Could not connect to endpoint {} -- aborting request {} [operation ID {}]", endpoint, request, operationId); timer.callback.timeRunnable( @@ -562,7 +561,7 @@ private , connection = getConnection(timer, oneOffTask, task); logger.trace("Operation with task {}. Got a connection {} [operation ID {}]", task, connection.isPresent() ? "successfully" : "which was unconnected (connection issue)", operationId); - if (!connection.isPresent()) { + if (connection.isEmpty()) { // Could not acquire connection, time to abort // Error logged already, error callback called as well logger.trace("Initial connection was not successful, aborting. [operation ID {}]", operationId); @@ -585,7 +584,7 @@ private , Long lastTryMillis = null; while (tryIndex < maxTries) { logger.trace("Try {} out of {} [operation ID {}]", tryIndex + 1, maxTries, operationId); - if (!connection.isPresent()) { + if (connection.isEmpty()) { // Connection was likely reseted with previous try, and connection was not successfully // re-established. Error has been logged, time to abort. logger.trace("Try {} out of {}. Connection was not successful, aborting. [operation ID {}]", @@ -597,8 +596,8 @@ private , return; } // Check poll task is still registered (this is all asynchronous) - if (!oneOffTask && task instanceof PollTask) { - verifyTaskIsRegistered((PollTask) task); + if (!oneOffTask && task instanceof PollTask pollTask) { + verifyTaskIsRegistered(pollTask); } // Let's ensure that enough time is between the retries logger.trace( @@ -716,7 +715,7 @@ private , lastTryMillis = System.currentTimeMillis(); // Connection was reseted in error handling and needs to be reconnected. // Try to re-establish connection. - if (willRetry && !connection.isPresent()) { + if (willRetry && connection.isEmpty()) { connection = getConnection(timer, oneOffTask, task); } } @@ -778,13 +777,12 @@ public Future submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusRea long scheduleTime = System.currentTimeMillis(); BasicPollTask task = new BasicPollTask(endpoint, request, resultCallback, failureCallback); logger.debug("Scheduling one-off poll task {}", task); - Future future = executor.submit(() -> { + return executor.submit(() -> { long millisInThreadPoolWaiting = System.currentTimeMillis() - scheduleTime; logger.debug("Will now execute one-off poll task {}, waited in thread pool for {}", task, millisInThreadPoolWaiting); executeOperation(task, true, pollOperation); }); - return future; } @Override @@ -871,13 +869,12 @@ public Future submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusW WriteTask task = new BasicWriteTask(endpoint, request, resultCallback, failureCallback); long scheduleTime = System.currentTimeMillis(); logger.debug("Scheduling one-off write task {}", task); - Future future = localScheduledThreadPoolExecutor.submit(() -> { + return localScheduledThreadPoolExecutor.submit(() -> { long millisInThreadPoolWaiting = System.currentTimeMillis() - scheduleTime; logger.debug("Will now execute one-off write task {}, waited in thread pool for {}", task, millisInThreadPoolWaiting); executeOperation(task, true, writeOperation); }); - return future; } @Override @@ -983,7 +980,7 @@ protected void activate(Map configProperties) { @Deactivate protected void deactivate() { synchronized (this) { - KeyedObjectPool connectionPool = this.connectionPool; + KeyedObjectPool connectionPool = this.connectionPool; if (connectionPool != null) { for (ModbusCommunicationInterface commInterface : this.communicationInterfaces) { try { @@ -1028,8 +1025,7 @@ private void logTaskQueueInfo() { task.getRequest().getDataLength(), future.isDone(), future.isCancelled(), future.getDelay(TimeUnit.MILLISECONDS), task); }); - if (scheduledThreadPoolExecutor instanceof ThreadPoolExecutor) { - ThreadPoolExecutor executor = ((ThreadPoolExecutor) scheduledThreadPoolExecutor); + if (scheduledThreadPoolExecutor instanceof ThreadPoolExecutor executor) { pollMonitorLogger.trace( "POLL MONITOR: scheduledThreadPoolExecutor queue size: {}, remaining space {}. Active threads {}", executor.getQueue().size(), executor.getQueue().remainingCapacity(), executor.getActiveCount()); diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusPoolConfig.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusPoolConfig.java index 6e8c61c7616..8707e30191d 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusPoolConfig.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusPoolConfig.java @@ -32,7 +32,7 @@ * */ @NonNullByDefault -public class ModbusPoolConfig extends GenericKeyedObjectPoolConfig { +public class ModbusPoolConfig extends GenericKeyedObjectPoolConfig<@Nullable ModbusSlaveConnection> { @SuppressWarnings("unused") private EvictionPolicy evictionPolicy = new DefaultEvictionPolicy<>(); diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java index 1a81b421de0..b1e9a8c7e08 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java @@ -43,16 +43,14 @@ public ModbusSlaveIOExceptionImpl(IOException e) { @Override public @Nullable String getMessage() { return String.format("Modbus IO Error with cause=%s, EOF=%s, message='%s', cause2=%s", - error.getClass().getSimpleName(), - error instanceof ModbusIOException ? ((ModbusIOException) error).isEOF() : "?", error.getMessage(), - error.getCause()); + error.getClass().getSimpleName(), error instanceof ModbusIOException mioe ? mioe.isEOF() : "?", + error.getMessage(), error.getCause()); } @Override public String toString() { return String.format("ModbusSlaveIOException(cause=%s, EOF=%s, message='%s', cause2=%s)", - error.getClass().getSimpleName(), - error instanceof ModbusIOException ? ((ModbusIOException) error).isEOF() : "?", error.getMessage(), - error.getCause()); + error.getClass().getSimpleName(), error instanceof ModbusIOException mioe ? mioe.isEOF() : "?", + error.getMessage(), error.getCause()); } } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java index 1c56f17ae57..2ffca70177d 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/main/java/org/openhab/core/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java @@ -59,14 +59,14 @@ */ @NonNullByDefault public class ModbusSlaveConnectionFactoryImpl - extends BaseKeyedPooledObjectFactory { + extends BaseKeyedPooledObjectFactory { - class PooledConnection extends DefaultPooledObject { + class PooledConnection extends DefaultPooledObject<@Nullable ModbusSlaveConnection> { private volatile long lastConnected; private volatile @Nullable ModbusSlaveEndpoint endpoint; - public PooledConnection(ModbusSlaveConnection object) { + public PooledConnection(@Nullable ModbusSlaveConnection object) { super(object); } @@ -97,6 +97,9 @@ public boolean maybeResetConnection(String activityName) { long localLastConnected = lastConnected; ModbusSlaveConnection connection = getObject(); + if (connection == null) { + return false; + } EndpointPoolConfiguration configuration = getEndpointPoolConfiguration(localEndpoint); long reconnectAfterMillis = configuration.getReconnectAfterMillis(); @@ -147,8 +150,8 @@ public ModbusSlaveConnectionFactoryImpl( } @Override - public ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Exception { - return endpoint.accept(new ModbusSlaveEndpointVisitor() { + public @Nullable ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Exception { + return endpoint.accept(new ModbusSlaveEndpointVisitor<@Nullable ModbusSlaveConnection>() { @Override public @Nullable ModbusSlaveConnection visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) { SerialConnection connection = new SerialConnection(modbusSerialSlavePoolingKey.getSerialParameters()); @@ -183,27 +186,34 @@ public ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Excepti } @Override - public PooledObject wrap(ModbusSlaveConnection connection) { + public PooledObject<@Nullable ModbusSlaveConnection> wrap(@Nullable ModbusSlaveConnection connection) { return new PooledConnection(connection); } @Override - public void destroyObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject obj) { + public void destroyObject(ModbusSlaveEndpoint endpoint, + @Nullable PooledObject<@Nullable ModbusSlaveConnection> obj) { if (obj == null) { return; } - logger.trace("destroyObject for connection {} and endpoint {} -> closing the connection", obj.getObject(), - endpoint); - obj.getObject().resetConnection(); + ModbusSlaveConnection connection = obj.getObject(); + if (connection == null) { + return; + } + logger.trace("destroyObject for connection {} and endpoint {} -> closing the connection", connection, endpoint); + connection.resetConnection(); } @Override - public void activateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject obj) - throws Exception { + public void activateObject(ModbusSlaveEndpoint endpoint, + @Nullable PooledObject<@Nullable ModbusSlaveConnection> obj) throws Exception { if (obj == null) { return; } ModbusSlaveConnection connection = obj.getObject(); + if (connection == null) { + return; + } try { EndpointPoolConfiguration config = getEndpointPoolConfiguration(endpoint); if (!connection.isConnected()) { @@ -226,7 +236,8 @@ public void activateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject< } @Override - public void passivateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject obj) { + public void passivateObject(ModbusSlaveEndpoint endpoint, + @Nullable PooledObject<@Nullable ModbusSlaveConnection> obj) { if (obj == null) { return; } @@ -238,8 +249,9 @@ public void passivateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject } @Override - public boolean validateObject(ModbusSlaveEndpoint key, @Nullable PooledObject p) { - boolean valid = p != null && p.getObject().isConnected(); + public boolean validateObject(ModbusSlaveEndpoint key, @Nullable PooledObject<@Nullable ModbusSlaveConnection> p) { + @SuppressWarnings("null") // p.getObject() cannot be null due to short-circuiting boolean condition + boolean valid = p != null && p.getObject() != null && p.getObject().isConnected(); ModbusSlaveConnection slaveConnection = p != null ? p.getObject() : null; logger.trace("Validating endpoint {} connection {} -> {}", key, slaveConnection, valid); return valid; @@ -272,7 +284,7 @@ public EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoin .orElseGet(() -> defaultPoolConfigurationFactory.apply(endpoint)); } - private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject obj, + private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject<@Nullable ModbusSlaveConnection> obj, ModbusSlaveConnection connection, EndpointPoolConfiguration config) throws Exception { if (connection.isConnected()) { return; diff --git a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java index b9c131404c3..00d99baa6c0 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java @@ -75,11 +75,11 @@ private static class Args implements Arguments { private Object expected; public Args(Object command, ValueType valueType, Object expected) { - this.command = command instanceof String ? new DecimalType((String) command) : (Command) command; + this.command = command instanceof String s ? new DecimalType(s) : (Command) command; this.valueType = valueType; // validating type with cast - if (expected instanceof Integer) { - this.expected = shorts((int) expected); + if (expected instanceof Integer integer) { + this.expected = shorts(integer); } else if (expected instanceof Class) { this.expected = expected; } else { @@ -118,8 +118,8 @@ private static Stream concatTestArgs(Object... objects) { for (Object obj : objects) { if (obj instanceof Args) { builder.add(obj); - } else if (obj instanceof Stream) { - ((Stream) obj).forEach(builder::add); + } else if (obj instanceof Stream stream) { + stream.forEach(builder::add); } else { throw new IllegalArgumentException("Illegal parameter " + obj.toString()); } @@ -326,8 +326,8 @@ public static Stream data() { @ParameterizedTest @MethodSource("data") public void testCommandToRegisters(Command command, ValueType type, Object expectedResult) { - if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { - assertThrows((Class) expectedResult, () -> ModbusBitUtilities.commandToRegisters(command, type)); + if (expectedResult instanceof Class class1 && Exception.class.isAssignableFrom(class1)) { + assertThrows(class1, () -> ModbusBitUtilities.commandToRegisters(command, type)); return; } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java index 3f460711d33..710294134e0 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java @@ -105,10 +105,10 @@ private void testIndividual(Object expectedResult, ValueType type, byte[] bytes, String testExplanation = String.format("bytes=%s, byteIndex=%d, type=%s", Arrays.toString(bytes), byteIndex, type); final Object expectedNumber; - if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { + if (expectedResult instanceof Class class1 && Exception.class.isAssignableFrom(class1)) { assertThrows((Class) expectedResult, () -> methodUnderTest.get()); - } else if (expectedResult instanceof Optional) { - assertTrue(!((Optional) expectedResult).isPresent()); + } else if (expectedResult instanceof Optional optional) { + assertTrue(optional.isEmpty()); if (defaultWhenEmptyOptional == null) { fail("Should provide defaultWhenEmptyOptional"); } diff --git a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java index 156e3f8c87c..ecfd9cdee1d 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java @@ -359,16 +359,14 @@ ValueType.INT64_SWAP, shortArrayToRegisterArray(0x0, 0x0, 0x8, 0x0), 0 }, @MethodSource("data") public void testextractStateFromRegisters(Object expectedResult, ValueType type, ModbusRegisterArray registers, int index) { - if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { - assertThrows((Class) expectedResult, - () -> ModbusBitUtilities.extractStateFromRegisters(registers, index, type)); + if (expectedResult instanceof Class class1 && Exception.class.isAssignableFrom(class1)) { + assertThrows(class1, () -> ModbusBitUtilities.extractStateFromRegisters(registers, index, type)); return; } Optional actualState = ModbusBitUtilities.extractStateFromRegisters(registers, index, type); // Wrap given expectedResult to Optional, if necessary - Optional expectedStateWrapped = expectedResult instanceof DecimalType - ? Optional.of((DecimalType) expectedResult) + Optional expectedStateWrapped = expectedResult instanceof DecimalType dt ? Optional.of(dt) : (Optional) expectedResult; assertThat(String.format("registers=%s, index=%d, type=%s", registers, index, type), actualState, is(equalTo(expectedStateWrapped))); diff --git a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStringTest.java b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStringTest.java index 3156083fd0e..e4e14729211 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStringTest.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/BitUtilitiesExtractStringTest.java @@ -99,8 +99,7 @@ public static Stream dataWithByteVariations() { streamBuilder.add( new Object[] { expected, offset, bytesOffsetted, origByteIndex + offset, length, charset }); } - Stream variations = streamBuilder.build(); - return variations; + return streamBuilder.build(); }); } @@ -109,8 +108,8 @@ public static Stream dataWithByteVariations() { @MethodSource("data") public void testExtractStringFromRegisters(Object expectedResult, ModbusRegisterArray registers, int index, int length, Charset charset) { - if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { - assertThrows((Class) expectedResult, + if (expectedResult instanceof Class class1 && Exception.class.isAssignableFrom(class1)) { + assertThrows(class1, () -> ModbusBitUtilities.extractStringFromRegisters(registers, index, length, charset)); return; } else { @@ -125,9 +124,8 @@ public void testExtractStringFromRegisters(Object expectedResult, ModbusRegister @MethodSource("dataWithByteVariations") public void testExtractStringFromBytes(Object expectedResult, int byteOffset, byte[] bytes, int byteIndex, int length, Charset charset) { - if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { - assertThrows((Class) expectedResult, - () -> ModbusBitUtilities.extractStringFromBytes(bytes, byteIndex, length, charset)); + if (expectedResult instanceof Class class1 && Exception.class.isAssignableFrom(class1)) { + assertThrows(class1, () -> ModbusBitUtilities.extractStringFromBytes(bytes, byteIndex, length, charset)); return; } else { String actualState = ModbusBitUtilities.extractStringFromBytes(bytes, byteIndex, length, charset); diff --git a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/IntegrationTestSupport.java b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/IntegrationTestSupport.java index dc3943a98df..0ec22cf398d 100644 --- a/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/IntegrationTestSupport.java +++ b/bundles/org.openhab.core.io.transport.modbus/src/test/java/org/openhab/core/io/transport/modbus/test/IntegrationTestSupport.java @@ -328,7 +328,7 @@ public UDPSlaveTerminal create(@NonNullByDefault({}) InetAddress interfac, int p public class SerialConnectionFactoryImpl implements SerialConnectionFactory { @Override public SerialConnection create(@NonNullByDefault({}) SerialParameters parameters) { - SerialConnection serialConnection = new SerialConnection(parameters) { + return new SerialConnection(parameters) { @Override public ModbusTransport getModbusTransport() { ModbusTransport transport = spy(super.getModbusTransport()); @@ -336,7 +336,6 @@ public ModbusTransport getModbusTransport() { return transport; } }; - return serialConnection; } } diff --git a/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttActionCallback.java b/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttActionCallback.java index 3a5d7d85886..aacae8ab688 100644 --- a/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttActionCallback.java +++ b/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttActionCallback.java @@ -22,7 +22,7 @@ */ @NonNullByDefault public interface MqttActionCallback { - public void onSuccess(String topic); + void onSuccess(String topic); - public void onFailure(String topic, Throwable error); + void onFailure(String topic, Throwable error); } diff --git a/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttConnectionObserver.java b/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttConnectionObserver.java index 9e460abc55d..392f34ea09f 100644 --- a/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttConnectionObserver.java +++ b/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttConnectionObserver.java @@ -33,5 +33,5 @@ public interface MqttConnectionObserver { * @param error An exception object (might be a MqttException) with the reason why * a connection failed. */ - public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error); + void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error); } diff --git a/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttMessageSubscriber.java b/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttMessageSubscriber.java index 41737725d19..4c3a7246d34 100644 --- a/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttMessageSubscriber.java +++ b/bundles/org.openhab.core.io.transport.mqtt/src/main/java/org/openhab/core/io/transport/mqtt/MqttMessageSubscriber.java @@ -28,5 +28,5 @@ public interface MqttMessageSubscriber { * @param topic The mqtt topic on which the message was received. * @param payload content of the message. */ - public void processMessage(String topic, byte[] payload); + void processMessage(String topic, byte[] payload); } diff --git a/bundles/org.openhab.core.io.transport.mqtt/src/test/java/org/openhab/core/io/transport/mqtt/MqttBrokerConnectionEx.java b/bundles/org.openhab.core.io.transport.mqtt/src/test/java/org/openhab/core/io/transport/mqtt/MqttBrokerConnectionEx.java index 3cb0c461560..e5b612ca6be 100644 --- a/bundles/org.openhab.core.io.transport.mqtt/src/test/java/org/openhab/core/io/transport/mqtt/MqttBrokerConnectionEx.java +++ b/bundles/org.openhab.core.io.transport.mqtt/src/test/java/org/openhab/core/io/transport/mqtt/MqttBrokerConnectionEx.java @@ -99,9 +99,7 @@ protected MqttAsyncClientWrapper createClient() { } }).when(mockedClient).unsubscribe(any()); // state - doAnswer(i -> { - return MqttClientState.CONNECTED; - }).when(mockedClient).getState(); + doAnswer(i -> MqttClientState.CONNECTED).when(mockedClient).getState(); return mockedClient; } diff --git a/bundles/org.openhab.core.io.transport.serial.javacomm/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java b/bundles/org.openhab.core.io.transport.serial.javacomm/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java index fc0ef548548..fe49877dfd7 100644 --- a/bundles/org.openhab.core.io.transport.serial.javacomm/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java +++ b/bundles/org.openhab.core.io.transport.serial.javacomm/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java @@ -50,8 +50,8 @@ public String getName() { public SerialPort open(String owner, int timeout) throws PortInUseException { try { final CommPort cp = id.open(owner, timeout); - if (cp instanceof javax.comm.SerialPort) { - return new SerialPortImpl((javax.comm.SerialPort) cp); + if (cp instanceof javax.comm.SerialPort port) { + return new SerialPortImpl(port); } else { throw new IllegalStateException( String.format("We expect a serial port instead of '%s'", cp.getClass())); diff --git a/bundles/org.openhab.core.io.transport.serial.rxtx/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java b/bundles/org.openhab.core.io.transport.serial.rxtx/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java index b41d63e5903..f0d34267171 100644 --- a/bundles/org.openhab.core.io.transport.serial.rxtx/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java +++ b/bundles/org.openhab.core.io.transport.serial.rxtx/src/main/java/org/openhab/core/io/transport/serial/internal/SerialPortIdentifierImpl.java @@ -51,8 +51,8 @@ public String getName() { public SerialPort open(String owner, int timeout) throws PortInUseException { try { final CommPort cp = id.open(owner, timeout); - if (cp instanceof gnu.io.SerialPort) { - return new RxTxSerialPort((gnu.io.SerialPort) cp); + if (cp instanceof gnu.io.SerialPort port) { + return new RxTxSerialPort(port); } else { throw new IllegalStateException( String.format("We expect a serial port instead of '%s'", cp.getClass())); diff --git a/bundles/org.openhab.core.io.transport.serial/src/main/java/org/openhab/core/io/transport/serial/SerialPortProvider.java b/bundles/org.openhab.core.io.transport.serial/src/main/java/org/openhab/core/io/transport/serial/SerialPortProvider.java index 21bd7f13d85..caf68b4fe47 100644 --- a/bundles/org.openhab.core.io.transport.serial/src/main/java/org/openhab/core/io/transport/serial/SerialPortProvider.java +++ b/bundles/org.openhab.core.io.transport.serial/src/main/java/org/openhab/core/io/transport/serial/SerialPortProvider.java @@ -35,14 +35,15 @@ public interface SerialPortProvider { * @throws UnsupportedCommOperationException * @throws PortInUseException */ - public @Nullable SerialPortIdentifier getPortIdentifier(URI portName); + @Nullable + SerialPortIdentifier getPortIdentifier(URI portName); /** * Gets all protocol types which this provider is able to create. * * @return The protocol type. */ - public Stream getAcceptedProtocols(); + Stream getAcceptedProtocols(); /** * Gets all the available {@link SerialPortIdentifier}s for this {@link SerialPortProvider}. @@ -51,5 +52,5 @@ public interface SerialPortProvider { * * @return The available ports */ - public Stream getSerialPortIdentifiers(); + Stream getSerialPortIdentifiers(); } diff --git a/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOParticipant.java b/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOParticipant.java index 0fc80310d30..f85db550044 100644 --- a/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOParticipant.java +++ b/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOParticipant.java @@ -22,10 +22,10 @@ public interface UpnpIOParticipant { /** Get the UDN of the participant **/ - public String getUDN(); + String getUDN(); /** Called when the UPNP IO service receives a {variable,value} tuple for the given UPNP service **/ - public void onValueReceived(String variable, String value, String service); + void onValueReceived(String variable, String value, String service); /** * Called to notify if a GENA subscription succeeded or failed. @@ -33,7 +33,7 @@ public interface UpnpIOParticipant { * @param service the UPnP service subscribed * @param succeeded true if the subscription succeeded; false if failed */ - public void onServiceSubscribed(String service, boolean succeeded); + void onServiceSubscribed(String service, boolean succeeded); /** * Called when the UPNP IO service is unable to poll the UDN of the participant, given that @@ -42,5 +42,5 @@ public interface UpnpIOParticipant { * @param status false, if the poll fails when the polling was previously successful; true if the poll succeeds * when the polling was previously failing */ - public void onStatusChanged(boolean status); + void onStatusChanged(boolean status); } diff --git a/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOService.java b/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOService.java index 5686b403230..36168b44855 100644 --- a/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOService.java +++ b/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/UpnpIOService.java @@ -32,7 +32,7 @@ public interface UpnpIOService { * @param actionID the Action to invoke * @param inputs a map of {variable,values} to parameterize the Action that will be invoked */ - public Map invokeAction(UpnpIOParticipant participant, String serviceID, String actionID, + Map invokeAction(UpnpIOParticipant participant, String serviceID, String actionID, Map inputs); /** @@ -42,7 +42,7 @@ public Map invokeAction(UpnpIOParticipant participant, String se * @param serviceID the UPNP service we want to subscribe to * @param duration the duration of the subscription */ - public void addSubscription(UpnpIOParticipant participant, String serviceID, int duration); + void addSubscription(UpnpIOParticipant participant, String serviceID, int duration); /** * Unsubscribe from a GENA subscription @@ -50,7 +50,7 @@ public Map invokeAction(UpnpIOParticipant participant, String se * @param participant the participant of the subscription * @param serviceID the UPNP service we want to unsubscribe from */ - public void removeSubscription(UpnpIOParticipant participant, String serviceID); + void removeSubscription(UpnpIOParticipant participant, String serviceID); /** * Verify if the a participant is registered @@ -58,21 +58,21 @@ public Map invokeAction(UpnpIOParticipant participant, String se * @param participant the participant whom's participation we want to verify * @return true of the participant is registered with the UpnpIOService */ - public boolean isRegistered(UpnpIOParticipant participant); + boolean isRegistered(UpnpIOParticipant participant); /** * Register a participant with the UPNP IO Service * * @param participant the participant whose participation we want to register */ - public void registerParticipant(UpnpIOParticipant participant); + void registerParticipant(UpnpIOParticipant participant); /** * Unregister a participant with the UPNP IO Service * * @param participant the participant whose participation we want to unregister */ - public void unregisterParticipant(UpnpIOParticipant participant); + void unregisterParticipant(UpnpIOParticipant participant); /** * Retrieves the descriptor url for the participant @@ -80,7 +80,7 @@ public Map invokeAction(UpnpIOParticipant participant, String se * @param participant the participant whom's descriptor url is requested * @return the url of the descriptor as provided by the upnp device */ - public URL getDescriptorURL(UpnpIOParticipant participant); + URL getDescriptorURL(UpnpIOParticipant participant); /** * Establish a polling mechanism to check the status of a specific UDN device. The polling mechanism @@ -92,12 +92,12 @@ public Map invokeAction(UpnpIOParticipant participant, String se * @param actionID the action to call * @param interval the interval in seconds */ - public void addStatusListener(UpnpIOParticipant participant, String serviceID, String actionID, int interval); + void addStatusListener(UpnpIOParticipant participant, String serviceID, String actionID, int interval); /** * Stops the polling mechanism to check the status of a specific UDN device. * * @param participant the participant for whom we want to remove the polling */ - public void removeStatusListener(UpnpIOParticipant participant); + void removeStatusListener(UpnpIOParticipant participant); } diff --git a/bundles/org.openhab.core.io.websocket/pom.xml b/bundles/org.openhab.core.io.websocket/pom.xml index 4104b939b21..3d4b017b3b3 100644 --- a/bundles/org.openhab.core.io.websocket/pom.xml +++ b/bundles/org.openhab.core.io.websocket/pom.xml @@ -20,6 +20,11 @@ org.openhab.core ${project.version} + + org.openhab.core.bundles + org.openhab.core.io.rest.auth + ${project.version} + diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java new file mode 100644 index 00000000000..7937ef0e39b --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.websocket; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.server.WebSocketServerFactory; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.servlet.WebSocketServlet; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.openhab.core.auth.AuthenticationException; +import org.openhab.core.auth.Role; +import org.openhab.core.io.rest.auth.AuthFilter; +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; +import org.osgi.service.http.NamespaceException; +import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletName; +import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link CommonWebSocketServlet} provides the servlet for WebSocket connections + * + * @author Jan N. Klug - Initial contribution + * @author Miguel Álvarez Díez - Refactor into a common servlet + */ +@NonNullByDefault +@HttpWhiteboardServletName(CommonWebSocketServlet.SERVLET_PATH) +@HttpWhiteboardServletPattern(CommonWebSocketServlet.SERVLET_PATH + "/*") +@Component(immediate = true, service = { Servlet.class }) +public class CommonWebSocketServlet extends WebSocketServlet { + private static final long serialVersionUID = 1L; + + public static final String SERVLET_PATH = "/ws"; + + public static final String DEFAULT_ADAPTER_ID = EventWebSocketAdapter.ADAPTER_ID; + + private final Map connectionHandlers = new HashMap<>(); + private final AuthFilter authFilter; + + @SuppressWarnings("unused") + private @Nullable WebSocketServerFactory importNeeded; + + @Activate + public CommonWebSocketServlet(@Reference AuthFilter authFilter) throws ServletException, NamespaceException { + this.authFilter = authFilter; + } + + @Override + public void configure(@NonNullByDefault({}) WebSocketServletFactory webSocketServletFactory) { + webSocketServletFactory.getPolicy().setIdleTimeout(10000); + webSocketServletFactory.setCreator(new CommonWebSocketCreator()); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addWebSocketAdapter(WebSocketAdapter wsAdapter) { + this.connectionHandlers.put(wsAdapter.getId(), wsAdapter); + } + + protected void removeWebSocketAdapter(WebSocketAdapter wsAdapter) { + this.connectionHandlers.remove(wsAdapter.getId()); + } + + private class CommonWebSocketCreator implements WebSocketCreator { + private final Logger logger = LoggerFactory.getLogger(CommonWebSocketCreator.class); + + @Override + public @Nullable Object createWebSocket(@Nullable ServletUpgradeRequest servletUpgradeRequest, + @Nullable ServletUpgradeResponse servletUpgradeResponse) { + if (servletUpgradeRequest == null || servletUpgradeResponse == null) { + return null; + } + if (isAuthorizedRequest(servletUpgradeRequest)) { + String requestPath = servletUpgradeRequest.getRequestURI().getPath(); + String pathPrefix = SERVLET_PATH + "/"; + boolean useDefaultAdapter = requestPath.equals(pathPrefix) || !requestPath.startsWith(pathPrefix); + WebSocketAdapter wsAdapter; + if (!useDefaultAdapter) { + String adapterId = requestPath.substring(pathPrefix.length()); + wsAdapter = connectionHandlers.get(adapterId); + if (wsAdapter == null) { + logger.warn("Missing WebSocket adapter for path {}", adapterId); + return null; + } + } else { + wsAdapter = connectionHandlers.get(DEFAULT_ADAPTER_ID); + if (wsAdapter == null) { + logger.warn("Default WebSocket adapter is missing"); + return null; + } + } + logger.debug("New connection handled by {}", wsAdapter.getId()); + return wsAdapter.createWebSocket(servletUpgradeRequest, servletUpgradeResponse); + } else { + logger.warn("Unauthenticated request to create a websocket from {}.", + servletUpgradeRequest.getRemoteAddress()); + } + return null; + } + + private boolean isAuthorizedRequest(ServletUpgradeRequest servletUpgradeRequest) { + try { + var securityContext = authFilter.getSecurityContext(servletUpgradeRequest.getHttpServletRequest(), + true); + return securityContext != null + && (securityContext.isUserInRole(Role.USER) || securityContext.isUserInRole(Role.ADMIN)); + } catch (AuthenticationException | IOException e) { + logger.warn("Error handling WebSocket authorization", e); + return false; + } + } + } +} diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java index 78d833c285b..f37bb4cc8e2 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java @@ -52,7 +52,7 @@ public class EventWebSocket { private final Logger logger = LoggerFactory.getLogger(EventWebSocket.class); - private final EventWebSocketServlet servlet; + private final EventWebSocketAdapter wsAdapter; private final Gson gson; private final EventPublisher eventPublisher; private final ItemEventUtility itemEventUtility; @@ -64,9 +64,9 @@ public class EventWebSocket { private List typeFilter = List.of(); private List sourceFilter = List.of(); - public EventWebSocket(Gson gson, EventWebSocketServlet servlet, ItemEventUtility itemEventUtility, + public EventWebSocket(Gson gson, EventWebSocketAdapter wsAdapter, ItemEventUtility itemEventUtility, EventPublisher eventPublisher) { - this.servlet = servlet; + this.wsAdapter = wsAdapter; this.gson = gson; this.itemEventUtility = itemEventUtility; this.eventPublisher = eventPublisher; @@ -74,7 +74,7 @@ public EventWebSocket(Gson gson, EventWebSocketServlet servlet, ItemEventUtility @OnWebSocketClose public void onClose(int statusCode, String reason) { - this.servlet.unregisterListener(this); + this.wsAdapter.unregisterListener(this); remoteIdentifier = ""; this.session = null; this.remoteEndpoint = null; @@ -86,7 +86,7 @@ public void onConnect(Session session) { RemoteEndpoint remoteEndpoint = session.getRemote(); this.remoteEndpoint = remoteEndpoint; this.remoteIdentifier = remoteEndpoint.getInetSocketAddress().toString(); - this.servlet.registerListener(this); + this.wsAdapter.registerListener(this); } @OnWebSocketMessage diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketAdapter.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketAdapter.java new file mode 100644 index 00000000000..6ef61b804cb --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketAdapter.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.websocket; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.events.EventSubscriber; +import org.openhab.core.items.ItemRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.Gson; + +/** + * The {@link EventWebSocketAdapter} allows subscription to oh events over WebSocket + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { EventSubscriber.class, WebSocketAdapter.class }) +public class EventWebSocketAdapter implements EventSubscriber, WebSocketAdapter { + public static final String ADAPTER_ID = "event-subscriber"; + private final Gson gson = new Gson(); + private final EventPublisher eventPublisher; + + private final ItemEventUtility itemEventUtility; + private final Set webSockets = new CopyOnWriteArraySet<>(); + + @Activate + public EventWebSocketAdapter(@Reference EventPublisher eventPublisher, @Reference ItemRegistry itemRegistry) { + this.eventPublisher = eventPublisher; + itemEventUtility = new ItemEventUtility(gson, itemRegistry); + } + + @Override + public Set getSubscribedEventTypes() { + return Set.of(EventSubscriber.ALL_EVENT_TYPES); + } + + @Override + public void receive(Event event) { + webSockets.forEach(ws -> ws.processEvent(event)); + } + + public void registerListener(EventWebSocket eventWebSocket) { + webSockets.add(eventWebSocket); + } + + public void unregisterListener(EventWebSocket eventWebSocket) { + webSockets.remove(eventWebSocket); + } + + @Override + public String getId() { + return ADAPTER_ID; + } + + @Override + public Object createWebSocket(ServletUpgradeRequest servletUpgradeRequest, + ServletUpgradeResponse servletUpgradeResponse) { + return new EventWebSocket(gson, EventWebSocketAdapter.this, itemEventUtility, eventPublisher); + } +} diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketServlet.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketServlet.java deleted file mode 100644 index 1eb176860e3..00000000000 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketServlet.java +++ /dev/null @@ -1,160 +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.io.websocket; - -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -import javax.servlet.Servlet; -import javax.servlet.ServletException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.websocket.server.WebSocketServerFactory; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; -import org.openhab.core.auth.Authentication; -import org.openhab.core.auth.AuthenticationException; -import org.openhab.core.auth.Credentials; -import org.openhab.core.auth.Role; -import org.openhab.core.auth.User; -import org.openhab.core.auth.UserApiTokenCredentials; -import org.openhab.core.auth.UserRegistry; -import org.openhab.core.auth.UsernamePasswordCredentials; -import org.openhab.core.events.Event; -import org.openhab.core.events.EventPublisher; -import org.openhab.core.events.EventSubscriber; -import org.openhab.core.items.ItemRegistry; -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.http.NamespaceException; -import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletName; -import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; - -/** - * The {@link EventWebSocketServlet} provides the servlet for WebSocket connections - * - * @author Jan N. Klug - Initial contribution - */ -@NonNullByDefault -@HttpWhiteboardServletName(EventWebSocketServlet.SERVLET_PATH) -@HttpWhiteboardServletPattern(EventWebSocketServlet.SERVLET_PATH + "/*") -@Component(immediate = true, service = { EventSubscriber.class, Servlet.class }) -public class EventWebSocketServlet extends WebSocketServlet implements EventSubscriber { - private static final long serialVersionUID = 1L; - - public static final String SERVLET_PATH = "/ws"; - private final Gson gson = new Gson(); - private final UserRegistry userRegistry; - private final EventPublisher eventPublisher; - - private final ItemEventUtility itemEventUtility; - private final Set webSockets = new CopyOnWriteArraySet<>(); - - @SuppressWarnings("unused") - private @Nullable WebSocketServerFactory importNeeded; - - @Activate - public EventWebSocketServlet(@Reference UserRegistry userRegistry, @Reference EventPublisher eventPublisher, - @Reference ItemRegistry itemRegistry) throws ServletException, NamespaceException { - this.userRegistry = userRegistry; - this.eventPublisher = eventPublisher; - - itemEventUtility = new ItemEventUtility(gson, itemRegistry); - } - - @Override - public void configure(@NonNullByDefault({}) WebSocketServletFactory webSocketServletFactory) { - webSocketServletFactory.getPolicy().setIdleTimeout(10000); - webSocketServletFactory.setCreator(new EventWebSocketCreator()); - } - - @Override - public Set getSubscribedEventTypes() { - return Set.of(EventSubscriber.ALL_EVENT_TYPES); - } - - @Override - public void receive(Event event) { - webSockets.forEach(ws -> ws.processEvent(event)); - } - - public void registerListener(EventWebSocket eventWebSocket) { - webSockets.add(eventWebSocket); - } - - public void unregisterListener(EventWebSocket eventWebSocket) { - webSockets.remove(eventWebSocket); - } - - private class EventWebSocketCreator implements WebSocketCreator { - private static final String API_TOKEN_PREFIX = "oh."; - - private final Logger logger = LoggerFactory.getLogger(EventWebSocketCreator.class); - - @Override - public @Nullable Object createWebSocket(@Nullable ServletUpgradeRequest servletUpgradeRequest, - @Nullable ServletUpgradeResponse servletUpgradeResponse) { - if (servletUpgradeRequest == null) { - return null; - } - - Map> parameterMap = servletUpgradeRequest.getParameterMap(); - List accessToken = parameterMap.getOrDefault("accessToken", List.of()); - if (accessToken.size() == 1 && authenticateAccessToken(accessToken.get(0))) { - return new EventWebSocket(gson, EventWebSocketServlet.this, itemEventUtility, eventPublisher); - } else { - logger.warn("Unauthenticated request to create a websocket from {}.", - servletUpgradeRequest.getRemoteAddress()); - } - - return null; - } - - private boolean authenticateAccessToken(String token) { - Credentials credentials = null; - if (token.startsWith(API_TOKEN_PREFIX)) { - credentials = new UserApiTokenCredentials(token); - } else { - // try BasicAuthentication - String[] decodedParts = new String(Base64.getDecoder().decode(token)).split(":"); - if (decodedParts.length == 2) { - credentials = new UsernamePasswordCredentials(decodedParts[0], decodedParts[1]); - } - } - - if (credentials != null) { - try { - Authentication auth = userRegistry.authenticate(credentials); - User user = userRegistry.get(auth.getUsername()); - return user != null - && (user.getRoles().contains(Role.USER) || user.getRoles().contains(Role.ADMIN)); - } catch (AuthenticationException ignored) { - } - } - - return false; - } - } -} diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java index 89ea5cd8464..e8f1d2b9ec9 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java @@ -56,10 +56,10 @@ public Event createCommandEvent(EventDTO eventDTO) throws EventProcessingExcepti Matcher matcher = getTopicMatcher(eventDTO.topic, "command"); Item item = getItem(matcher.group("entity")); Type command = parseType(eventDTO.payload); - if (command instanceof Command) { + if (command instanceof Command command1) { List> acceptedItemCommandTypes = item.getAcceptedCommandTypes(); if (acceptedItemCommandTypes.contains(command.getClass())) { - return ItemEventFactory.createCommandEvent(item.getName(), (Command) command, eventDTO.source); + return ItemEventFactory.createCommandEvent(item.getName(), command1, eventDTO.source); } } throw new EventProcessingException("Incompatible datatype, rejected."); @@ -69,10 +69,10 @@ public Event createStateEvent(EventDTO eventDTO) throws EventProcessingException Matcher matcher = getTopicMatcher(eventDTO.topic, "state"); Item item = getItem(matcher.group("entity")); Type state = parseType(eventDTO.payload); - if (state instanceof State) { + if (state instanceof State state1) { List> acceptedItemStateTypes = item.getAcceptedDataTypes(); if (acceptedItemStateTypes.contains(state.getClass())) { - return ItemEventFactory.createStateEvent(item.getName(), (State) state, eventDTO.source); + return ItemEventFactory.createStateEvent(item.getName(), state1, eventDTO.source); } } throw new EventProcessingException("Incompatible datatype, rejected."); diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/WebSocketAdapter.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/WebSocketAdapter.java new file mode 100644 index 00000000000..5007e1faf10 --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/WebSocketAdapter.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.websocket; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; + +/** + * The {@link WebSocketAdapter} can be implemented to register an adapter for a websocket connection. + * It will be accessible on the path /ws/ADAPTER_ID of your server. + * Security is handled by the {@link CommonWebSocketServlet}. + * + * @author Miguel Álvarez Díez - Initial contribution + */ +@NonNullByDefault +public interface WebSocketAdapter { + /** + * The adapter id. + * In combination with the base path {@link CommonWebSocketServlet#SERVLET_PATH} defines the adapter path. + * + * @return the adapter id. + */ + String getId(); + + /** + * Creates a websocket instance. + * It should use the {@link org.eclipse.jetty.websocket.api.annotations} or implement + * {@link org.eclipse.jetty.websocket.api.WebSocketListener}. + * + * @return a websocket instance. + */ + Object createWebSocket(ServletUpgradeRequest servletUpgradeRequest, ServletUpgradeResponse servletUpgradeResponse); +} diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java new file mode 100644 index 00000000000..17a1cd85fb0 --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.websocket; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; + +import javax.servlet.ServletException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.websocket.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.auth.AuthenticationException; +import org.openhab.core.io.rest.auth.AnonymousUserSecurityContext; +import org.openhab.core.io.rest.auth.AuthFilter; +import org.osgi.service.http.NamespaceException; + +/** + * The {@link CommonWebSocketServletTest} contains tests for the {@link EventWebSocket} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class CommonWebSocketServletTest { + private final String testAdapterId = "test-adapter-id"; + + private @NonNullByDefault({}) CommonWebSocketServlet servlet; + private @Mock @NonNullByDefault({}) AuthFilter authFilter; + private @Mock @NonNullByDefault({}) WebSocketServletFactory factory; + private @Mock @NonNullByDefault({}) WebSocketAdapter testDefaultWsAdapter; + private @Mock @NonNullByDefault({}) WebSocketAdapter testWsAdapter; + + private @Mock @NonNullByDefault({}) WebSocketPolicy wsPolicy; + private @Mock @NonNullByDefault({}) ServletUpgradeRequest request; + private @Mock @NonNullByDefault({}) ServletUpgradeResponse response; + private @Captor @NonNullByDefault({}) ArgumentCaptor webSocketCreatorAC; + + @BeforeEach + public void setup() throws ServletException, NamespaceException, AuthenticationException, IOException { + servlet = new CommonWebSocketServlet(authFilter); + when(factory.getPolicy()).thenReturn(wsPolicy); + servlet.configure(factory); + verify(factory).setCreator(webSocketCreatorAC.capture()); + var params = new HashMap>(); + when(request.getParameterMap()).thenReturn(params); + when(authFilter.getSecurityContext(any(), anyBoolean())).thenReturn(new AnonymousUserSecurityContext()); + when(testDefaultWsAdapter.getId()).thenReturn(CommonWebSocketServlet.DEFAULT_ADAPTER_ID); + when(testWsAdapter.getId()).thenReturn(testAdapterId); + servlet.addWebSocketAdapter(testDefaultWsAdapter); + servlet.addWebSocketAdapter(testWsAdapter); + } + + @Test + public void createWebsocketUsingDefaultAdapterPath() throws URISyntaxException { + when(request.getRequestURI()).thenReturn(new URI("http://127.0.0.1:8080/ws")); + webSocketCreatorAC.getValue().createWebSocket(request, response); + verify(testDefaultWsAdapter, times(1)).createWebSocket(request, response); + } + + @Test + public void createWebsocketUsingAdapterPath() throws URISyntaxException { + when(request.getRequestURI()).thenReturn(new URI("http://127.0.0.1:8080/ws/"+ testAdapterId)); + webSocketCreatorAC.getValue().createWebSocket(request, response); + verify(testWsAdapter, times(1)).createWebSocket(request, response); + } +} diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java index 9c525b96788..49eb37596de 100644 --- a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java @@ -63,7 +63,7 @@ public class EventWebSocketTest { private Gson gson = new Gson(); - private @Mock @NonNullByDefault({}) EventWebSocketServlet servlet; + private @Mock @NonNullByDefault({}) EventWebSocketAdapter servlet; private @Mock @NonNullByDefault({}) ItemRegistry itemRegistry; private @Mock @NonNullByDefault({}) EventPublisher eventPublisher; private @Mock @NonNullByDefault({}) Session session; diff --git a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/FeatureInstaller.java b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/FeatureInstaller.java index 00eaacfd788..4613a77c98d 100644 --- a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/FeatureInstaller.java +++ b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/FeatureInstaller.java @@ -83,8 +83,6 @@ public class FeatureInstaller implements ConfigurationListener { protected static final String CONFIG_URI = "system:addons"; public static final String PREFIX = "openhab-"; - public static final String PREFIX_PACKAGE = "package-"; - public static final String MINIMAL_PACKAGE = "minimal"; private static final String CFG_REMOTE = "remote"; private static final String PAX_URL_PID = "org.ops4j.pax.url.mvn"; @@ -191,13 +189,6 @@ private synchronized void processConfigQueue() { waitForConfigUpdateEvent(); } - if (installPackage(config)) { - changed = true; - // our package selection has changed, so let's wait for the values to be available in config admin - // which we will receive as another call to modified - continue; - } - if (installAddons(config)) { changed = true; } @@ -278,7 +269,7 @@ private void waitForConfigUpdateEvent() { private void changeAddonConfig(String type, String id, BiFunction, String, Boolean> method) throws IOException { Configuration cfg = configurationAdmin.getConfiguration(OpenHAB.ADDONS_SERVICE_PID, null); - Dictionary props = cfg.getProperties(); + Dictionary props = Objects.requireNonNullElse(cfg.getProperties(), new Hashtable<>()); Object typeProp = props.get(type); String[] addonIds = typeProp != null ? typeProp.toString().split(",") : new String[0]; Set normalizedIds = new HashSet<>(); // sets don't allow duplicates @@ -322,8 +313,8 @@ private boolean getOnlineRepositoryMode() { return false; } Object repos = properties.get(PROPERTY_MVN_REPOS); - if (repos instanceof String) { - return List.of(((String) repos).split(",")).contains(onlineRepoUrl); + if (repos instanceof String string) { + return List.of(string.split(",")).contains(onlineRepoUrl); } } catch (IOException e) { logger.error("Failed getting the add-on management online/offline mode: {}", e.getMessage(), @@ -350,8 +341,8 @@ private boolean setOnlineRepositoryMode(boolean enabled) { new Hashtable<>()); List repoCfg = new ArrayList<>(); Object repos = properties.get(PROPERTY_MVN_REPOS); - if (repos instanceof String) { - repoCfg.addAll(Arrays.asList(((String) repos).split(","))); + if (repos instanceof String string) { + repoCfg.addAll(Arrays.asList(string.split(","))); repoCfg.remove(""); } if (enabled && !repoCfg.contains(onlineRepoUrl)) { @@ -512,27 +503,6 @@ private void uninstallFeature(String name) { } } - private boolean installPackage(final Map config) { - boolean configChanged = false; - Object packageName = config.get(OpenHAB.CFG_PACKAGE); - if (packageName instanceof String currentPackage) { - String fullName = PREFIX + PREFIX_PACKAGE + currentPackage.strip(); - if (!MINIMAL_PACKAGE.equals(currentPackage)) { - configChanged = installFeature(fullName); - } - - // uninstall all other packages - try { - Stream.of(featuresService.listFeatures()).map(Feature::getName) - .filter(feature -> feature.startsWith(PREFIX + PREFIX_PACKAGE) && !feature.equals(fullName)) - .forEach(this::uninstallFeature); - } catch (Exception e) { - logger.error("Failed retrieving features: {}", e.getMessage(), debugException(e)); - } - } - return configChanged; - } - private void postInstalledEvent(String featureName) { String extensionId = featureName.substring(PREFIX.length()); Event event = AddonEventFactory.createAddonInstalledEvent(extensionId); diff --git a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/jaas/ManagedUserBackingEngine.java b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/jaas/ManagedUserBackingEngine.java index 00224ebc3eb..7e774814db9 100644 --- a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/jaas/ManagedUserBackingEngine.java +++ b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/jaas/ManagedUserBackingEngine.java @@ -103,8 +103,7 @@ public List listRoles(Principal principal) { @Override public void addRole(String username, String role) { User user = userRegistry.get(username); - if (user instanceof ManagedUser) { - ManagedUser managedUser = (ManagedUser) user; + if (user instanceof ManagedUser managedUser) { managedUser.getRoles().add(role); userRegistry.update(managedUser); } @@ -113,8 +112,7 @@ public void addRole(String username, String role) { @Override public void deleteRole(String username, String role) { User user = userRegistry.get(username); - if (user instanceof ManagedUser) { - ManagedUser managedUser = (ManagedUser) user; + if (user instanceof ManagedUser managedUser) { managedUser.getRoles().remove(role); userRegistry.update(managedUser); } diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepositoryChangeListener.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepositoryChangeListener.java index 519195ee43d..9b8c736853a 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepositoryChangeListener.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepositoryChangeListener.java @@ -25,5 +25,5 @@ public interface ModelRepositoryChangeListener { * Performs dispatch of all binding configs and * fires all {@link ItemsChangeListener}s if {@code modelName} ends with "items". */ - public void modelChanged(String modelName, EventType type); + void modelChanged(String modelName, EventType type); } diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java index 112d9ee405b..4b016f4d312 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java @@ -170,12 +170,10 @@ public Iterable getAllModelNamesOfType(final String modelType) { // Make a copy to avoid ConcurrentModificationException List resourceListCopy = new ArrayList<>(resourceSet.getResources()); - return resourceListCopy.stream().filter(input -> { - return input.getURI().lastSegment().contains(".") && input.isLoaded() - && modelType.equalsIgnoreCase(input.getURI().fileExtension()); - }).map(from -> { - return from.getURI().path(); - }).collect(Collectors.toList()); + return resourceListCopy.stream() + .filter(input -> input.getURI().lastSegment().contains(".") && input.isLoaded() + && modelType.equalsIgnoreCase(input.getURI().fileExtension())) + .map(from -> from.getURI().path()).collect(Collectors.toList()); } } diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java index ed4d2f77f3d..f1c181d20d9 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java @@ -12,8 +12,7 @@ */ package org.openhab.core.model.core.internal.folder; -import static org.openhab.core.service.WatchService.Kind.CREATE; -import static org.openhab.core.service.WatchService.Kind.MODIFY; +import static org.openhab.core.service.WatchService.Kind.*; import java.io.File; import java.io.FilenameFilter; @@ -162,7 +161,7 @@ private void addModelsToRepo() { if (!folderFileExtMap.isEmpty()) { for (String folderName : folderFileExtMap.keySet()) { final List validExtension = folderFileExtMap.get(folderName); - if (validExtension != null && validExtension.size() > 0) { + if (validExtension != null && !validExtension.isEmpty()) { File folder = watchService.getWatchPath().resolve(folderName).toFile(); File[] files = folder.listFiles(new FileExtensionsFilter(validExtension)); diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/valueconverter/ValueTypeToStringConverter.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/valueconverter/ValueTypeToStringConverter.java index 6f7ce5b7ec6..ba2cb13c6ce 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/valueconverter/ValueTypeToStringConverter.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/valueconverter/ValueTypeToStringConverter.java @@ -57,15 +57,14 @@ public String toString(@Nullable Object value) throws ValueConverterException { if (value == null) { throw new ValueConverterException("Value may not be null.", null, null); } - if (value instanceof String) { - return toEscapedString((String) value); + if (value instanceof String string) { + return toEscapedString(string); } - if (value instanceof BigDecimal) { - BigDecimal decimalValue = (BigDecimal) value; + if (value instanceof BigDecimal decimalValue) { return decimalValue.toPlainString(); } - if (value instanceof Boolean) { - return ((Boolean) value).toString(); + if (value instanceof Boolean boolean1) { + return boolean1.toString(); } throw new ValueConverterException("Unknown value type: " + value.getClass().getSimpleName(), null, null); } diff --git a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/Items.xtext b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/Items.xtext index 02ca3871528..ce2d709d1fa 100644 --- a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/Items.xtext +++ b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/Items.xtext @@ -16,7 +16,7 @@ ItemModel: ModelItem: (ModelNormalItem | ModelGroupItem) name=ID (label=STRING)? - ('<' icon=(ID|STRING) '>')? + ('<' icon=Icon '>')? ('(' groups+=ID (',' groups+=ID)* ')')? ('[' tags+=(ID|STRING) (',' tags+=(ID|STRING))* ']')? ('{' bindings+=ModelBinding (',' bindings+=ModelBinding)* '}')? @@ -64,6 +64,10 @@ NUMBER returns ecore::EBigDecimal: ('-')? ID ('.' ID )? ; +Icon: + (ID ':' (ID ':')?)? ID +; + terminal ID: '^'?('a'..'z'|'A'..'Z'|'_'|'0'..'9') ('a'..'z'|'A'..'Z'|'_'|'-'|'0'..'9')*; terminal STRING: diff --git a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java index 024ed4ead90..f4063d55c8e 100644 --- a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java +++ b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java @@ -223,8 +223,7 @@ private void processBindingConfigsFromModel(String modelName, EventType type) { private @Nullable Item createItemFromModelItem(ModelItem modelItem) { Item item; - if (modelItem instanceof ModelGroupItem) { - ModelGroupItem modelGroupItem = (ModelGroupItem) modelItem; + if (modelItem instanceof ModelGroupItem modelGroupItem) { Item baseItem; try { baseItem = createItemOfType(modelGroupItem.getType(), modelGroupItem.getName()); @@ -250,7 +249,7 @@ private void processBindingConfigsFromModel(String modelName, EventType type) { return null; } } - if (item instanceof ActiveItem) { + if (item instanceof ActiveItem activeItem) { String label = modelItem.getLabel(); String format = extractFormat(label); if (format != null) { @@ -258,9 +257,9 @@ private void processBindingConfigsFromModel(String modelName, EventType type) { stateDescriptionFragments.put(modelItem.getName(), StateDescriptionFragmentBuilder.create().withPattern(format).build()); } - ((ActiveItem) item).setLabel(label); - ((ActiveItem) item).setCategory(modelItem.getIcon()); - assignTags(modelItem, (ActiveItem) item); + activeItem.setLabel(label); + activeItem.setCategory(modelItem.getIcon()); + assignTags(modelItem, activeItem); return item; } else { return null; @@ -445,11 +444,11 @@ private boolean hasGroupItemChanged(Item item1, Item item2) { GroupItem gItem1 = null; GroupItem gItem2 = null; - if (item1 instanceof GroupItem) { - gItem1 = (GroupItem) item1; + if (item1 instanceof GroupItem item) { + gItem1 = item; } - if (item2 instanceof GroupItem) { - gItem2 = (GroupItem) item2; + if (item2 instanceof GroupItem item) { + gItem2 = item; } if (gItem1 == null && gItem2 == null) { diff --git a/bundles/org.openhab.core.model.lazygen/src/main/java/org/openhab/core/model/lazygen/LazyStandaloneSetup.java b/bundles/org.openhab.core.model.lazygen/src/main/java/org/openhab/core/model/lazygen/LazyStandaloneSetup.java index c8bd91787ef..5052a4b5bff 100644 --- a/bundles/org.openhab.core.model.lazygen/src/main/java/org/openhab/core/model/lazygen/LazyStandaloneSetup.java +++ b/bundles/org.openhab.core.model.lazygen/src/main/java/org/openhab/core/model/lazygen/LazyStandaloneSetup.java @@ -146,8 +146,7 @@ private URI createURI(String path) { URI uri = URI.createURI(path); if (uri.isRelative()) { - URI resolvedURI = URI.createFileURI(new File(path).getAbsolutePath()); - return resolvedURI; + return URI.createFileURI(new File(path).getAbsolutePath()); } return uri; } diff --git a/bundles/org.openhab.core.model.lsp/src/main/java/org/openhab/core/model/lsp/internal/ModelServer.java b/bundles/org.openhab.core.model.lsp/src/main/java/org/openhab/core/model/lsp/internal/ModelServer.java index 1e4e4e3ad66..7880c12c6ff 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/java/org/openhab/core/model/lsp/internal/ModelServer.java +++ b/bundles/org.openhab.core.model.lsp/src/main/java/org/openhab/core/model/lsp/internal/ModelServer.java @@ -48,12 +48,12 @@ */ @Component(immediate = true, service = ModelServer.class, configurationPid = ModelServer.CONFIGURATION_PID, // property = Constants.SERVICE_PID + "=org.openhab.lsp") -@ConfigurableService(category = "misc", label = "Language Server (LSP)", description_uri = ModelServer.CONFIG_URI) +@ConfigurableService(category = "system", label = "Language Server (LSP)", description_uri = ModelServer.CONFIG_URI) @NonNullByDefault public class ModelServer { public static final String CONFIGURATION_PID = "org.openhab.lsp"; - protected static final String CONFIG_URI = "misc:lsp"; + protected static final String CONFIG_URI = "system:lsp"; private static final String KEY_PORT = "port"; private static final int DEFAULT_PORT = 5007; diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/config/lsp.xml b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/config/lsp.xml index 507810d6ec3..70b29e25704 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/config/lsp.xml +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/config/lsp.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + The port the language server listens to. diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp.properties index e6bee1709dd..a1fb00d9e2b 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = The port the language server listens to. +system.config.lsp.port.label = Port +system.config.lsp.port.description = The port the language server listens to. -service.misc.lsp.label = Language Server (LSP) +service.system.lsp.label = Language Server (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_cs.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_cs.properties index 6c2af8f9ccb..04831656fd3 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_cs.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_cs.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Port, který jazykový server používá. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Port, který jazykový server používá. -service.misc.lsp.label = Jazykový server (LSP) +service.system.lsp.label = Jazykový server (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_da.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_da.properties index 1953d0fba59..ab39460d890 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_da.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_da.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Porten som sprogserveren lytter til. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Porten som sprogserveren lytter til. -service.misc.lsp.label = Sprogserver (LSP) +service.system.lsp.label = Sprogserver (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_de.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_de.properties index 319ecee5c9c..eff3513e2db 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_de.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_de.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Der Port, auf den der Sprachserver lauscht. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Der Port, auf den der Sprachserver lauscht. -service.misc.lsp.label = Sprachserver (LSP) +service.system.lsp.label = Sprachserver (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_el.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_el.properties index ee8da43f595..8b21b7fe529 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_el.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_el.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Θύρα -misc.config.lsp.port.description = Η θύρα που ακούει ο διακομιστής γλώσσας. +system.config.lsp.port.label = Θύρα +system.config.lsp.port.description = Η θύρα που ακούει ο διακομιστής γλώσσας. -service.misc.lsp.label = Διακομιστής Γλώσσας (LSP) +service.system.lsp.label = Διακομιστής Γλώσσας (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fi.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fi.properties index daae62c58ed..a7394a0ea9b 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fi.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fi.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Portti -misc.config.lsp.port.description = Portti, jota kieli palvelin kuuntelee. +system.config.lsp.port.label = Portti +system.config.lsp.port.description = Portti, jota kieli palvelin kuuntelee. -service.misc.lsp.label = Kielipalvelin (LSP) +service.system.lsp.label = Kielipalvelin (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fr.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fr.properties index cdcabf0ef09..af911827b10 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fr.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_fr.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Le port d'écoute du serveur de langage. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Le port d'écoute du serveur de langage. -service.misc.lsp.label = Serveur de langage (LSP) +service.system.lsp.label = Serveur de langage (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_he.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_he.properties index 2710e0d5d40..94590ad9591 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_he.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_he.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = יציאה -misc.config.lsp.port.description = הפורט ששרת השפה יאזין אליו. +system.config.lsp.port.label = יציאה +system.config.lsp.port.description = הפורט ששרת השפה יאזין אליו. -service.misc.lsp.label = שרת השפה (LSP) +service.system.lsp.label = שרת השפה (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_hu.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_hu.properties index 85f26e99041..ce2337df0cf 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_hu.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_hu.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = A nyelvi kiszolgáló portja. +system.config.lsp.port.label = Port +system.config.lsp.port.description = A nyelvi kiszolgáló portja. -service.misc.lsp.label = Nyelvi kiszolgáló (LSP) +service.system.lsp.label = Nyelvi kiszolgáló (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_it.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_it.properties index db03a4d8a05..211de11a7af 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_it.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_it.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Porta -misc.config.lsp.port.description = La porta usata dal server della traduzione. +system.config.lsp.port.label = Porta +system.config.lsp.port.description = La porta che il server della traduzione ascolta. -service.misc.lsp.label = Server Traduzione (LSP) +service.system.lsp.label = Server Traduzione (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_nl.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_nl.properties index 8f82d4b9c5a..9652114f63c 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_nl.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_nl.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Poort -misc.config.lsp.port.description = De poort waarnaar de taalserver luistert. +system.config.lsp.port.label = Poort +system.config.lsp.port.description = De poort waarnaar de taalserver luistert. -service.misc.lsp.label = Taalserver (LSP) +service.system.lsp.label = Taalserver (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_no.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_no.properties index ae862f5d5af..e59cd166d8e 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_no.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_no.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Porten språkserveren lytter til. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Porten språkserveren lytter til. -service.misc.lsp.label = Språkserver (LSP) +service.system.lsp.label = Språkserver (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pl.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pl.properties index 08b30d2127a..87f3a245c37 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pl.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pl.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Port na którym nasłuchuje serwer języka. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Port na którym nasłuchuje serwer języka. -service.misc.lsp.label = Serwer języka (LSP) +service.system.lsp.label = Serwer języka (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pt.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pt.properties index 8e0ff03285f..09382c0de4a 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pt.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_pt.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Porta -misc.config.lsp.port.description = A porta que o servidor de idioma escuta. +system.config.lsp.port.label = Porta +system.config.lsp.port.description = A porta que o servidor de idioma escuta. -service.misc.lsp.label = Servidor de Idioma (LSP) +service.system.lsp.label = Servidor de Idioma (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_ru.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_ru.properties index 5ca338e72ec..e14e07a7d9a 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_ru.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_ru.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Порт -misc.config.lsp.port.description = Порт языкового сервера. +system.config.lsp.port.label = Порт +system.config.lsp.port.description = Порт языкового сервера. -service.misc.lsp.label = Языковой сервер (LSP) +service.system.lsp.label = Языковой сервер (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sl.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sl.properties index 7bd4363f6e7..c56b851aaef 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sl.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sl.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Vrata -misc.config.lsp.port.description = Vrata, ki jim prisluškuje jezikovni strežnik. +system.config.lsp.port.label = Vrata +system.config.lsp.port.description = Vrata, ki jim prisluškuje jezikovni strežnik. -service.misc.lsp.label = jezikovni strežnik (LSP) +service.system.lsp.label = jezikovni strežnik (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sv.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sv.properties index 3008b5aafa6..3e40c806178 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sv.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_sv.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Port -misc.config.lsp.port.description = Porten som språkservern lyssnar på. +system.config.lsp.port.label = Port +system.config.lsp.port.description = Porten som språkservern lyssnar på. -service.misc.lsp.label = Språkserver (LSP) +service.system.lsp.label = Språkserver (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_uk.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_uk.properties index b0e70c659f5..11727d0361e 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_uk.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_uk.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = Порт -misc.config.lsp.port.description = Порт, до якого слухає мова. +system.config.lsp.port.label = Порт +system.config.lsp.port.description = Порт, до якого слухає мова. -service.misc.lsp.label = Мовний сервер (LSP) +service.system.lsp.label = Мовний сервер (LSP) diff --git a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_zh.properties b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_zh.properties index 9d89949c6c2..7cac582d089 100644 --- a/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_zh.properties +++ b/bundles/org.openhab.core.model.lsp/src/main/resources/OH-INF/i18n/lsp_zh.properties @@ -1,4 +1,4 @@ -misc.config.lsp.port.label = 端口 -misc.config.lsp.port.description = 语言服务器监听的端口。 +system.config.lsp.port.label = 端口 +system.config.lsp.port.description = 语言服务器监听的端口。 -service.misc.lsp.label = 语言服务器 (LSP) +service.system.lsp.label = 语言服务器 (LSP) diff --git a/bundles/org.openhab.core.model.persistence/bnd.bnd b/bundles/org.openhab.core.model.persistence/bnd.bnd index caef2b8c1c8..c5067b3cc13 100644 --- a/bundles/org.openhab.core.model.persistence/bnd.bnd +++ b/bundles/org.openhab.core.model.persistence/bnd.bnd @@ -21,6 +21,8 @@ Import-Package: \ org.openhab.core.persistence,\ org.openhab.core.persistence.config,\ org.openhab.core.persistence.strategy,\ + org.openhab.core.persistence.filter,\ + org.openhab.core.persistence.registry,\ org.openhab.core.types,\ org.openhab.core.model.core,\ com.google.common.*;version="14",\ diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext index 9bdb11e1137..7699c6e7405 100644 --- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext +++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext @@ -27,17 +27,34 @@ Filter: ; FilterDetails: - ThresholdFilter | TimeFilter + ThresholdFilter | TimeFilter | EqualsFilter | NotEqualsFilter | IncludeFilter | NotIncludeFilter ; ThresholdFilter: - '>' value=DECIMAL percent?='%' + '>' (relative?='%')? value=DECIMAL unit=STRING? ; TimeFilter: - value=INT unit=('s' | 'm' | 'h' | 'd') + 'T' value=INT unit=('s' | 'm' | 'h' | 'd') ; +EqualsFilter: + '=' values+=STRING (',' values+=STRING)* +; + +NotEqualsFilter: + '!' values+=STRING (',' values+=STRING)* +; + +IncludeFilter: + '[]' lower=DECIMAL upper=DECIMAL unit=STRING? +; + +NotIncludeFilter: + '][' lower=DECIMAL upper=DECIMAL unit=STRING? +; + + PersistenceConfiguration: items+=(AllConfig | ItemConfig | GroupConfig) (',' items+=(AllConfig | ItemConfig | GroupConfig))* ('->' alias=STRING)? ((':' ('strategy' '=' strategies+=[Strategy|ID] (',' strategies+=[Strategy|ID])*)? diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java index 4f45501276a..9b5d2b24de6 100644 --- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java +++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java @@ -12,33 +12,50 @@ */ package org.openhab.core.model.persistence.internal; +import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.emf.ecore.EObject; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractProvider; import org.openhab.core.model.core.EventType; import org.openhab.core.model.core.ModelRepository; import org.openhab.core.model.core.ModelRepositoryChangeListener; import org.openhab.core.model.persistence.persistence.AllConfig; import org.openhab.core.model.persistence.persistence.CronStrategy; +import org.openhab.core.model.persistence.persistence.EqualsFilter; import org.openhab.core.model.persistence.persistence.Filter; import org.openhab.core.model.persistence.persistence.GroupConfig; +import org.openhab.core.model.persistence.persistence.IncludeFilter; import org.openhab.core.model.persistence.persistence.ItemConfig; +import org.openhab.core.model.persistence.persistence.NotEqualsFilter; +import org.openhab.core.model.persistence.persistence.NotIncludeFilter; import org.openhab.core.model.persistence.persistence.PersistenceConfiguration; import org.openhab.core.model.persistence.persistence.PersistenceModel; import org.openhab.core.model.persistence.persistence.Strategy; -import org.openhab.core.persistence.PersistenceFilter; +import org.openhab.core.model.persistence.persistence.ThresholdFilter; +import org.openhab.core.model.persistence.persistence.TimeFilter; import org.openhab.core.persistence.PersistenceItemConfiguration; -import org.openhab.core.persistence.PersistenceManager; import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceConfiguration; import org.openhab.core.persistence.config.PersistenceAllConfig; import org.openhab.core.persistence.config.PersistenceConfig; import org.openhab.core.persistence.config.PersistenceGroupConfig; import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.filter.PersistenceEqualsFilter; +import org.openhab.core.persistence.filter.PersistenceFilter; +import org.openhab.core.persistence.filter.PersistenceIncludeFilter; +import org.openhab.core.persistence.filter.PersistenceThresholdFilter; +import org.openhab.core.persistence.filter.PersistenceTimeFilter; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationProvider; import org.openhab.core.persistence.strategy.PersistenceCronStrategy; import org.openhab.core.persistence.strategy.PersistenceStrategy; +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; /** @@ -47,75 +64,71 @@ * * @author Kai Kreuzer - Initial contribution * @author Markus Rathgeb - Move non-model logic to core.persistence + * @author Jan N. Klug - Refactored to {@link PersistenceServiceConfigurationProvider} */ -@Component(immediate = true) -public class PersistenceModelManager implements ModelRepositoryChangeListener { - - private ModelRepository modelRepository; - - private PersistenceManager manager; - - public PersistenceModelManager() { - } +@Component(immediate = true, service = PersistenceServiceConfigurationProvider.class) +@NonNullByDefault +public class PersistenceModelManager extends AbstractProvider + implements ModelRepositoryChangeListener, PersistenceServiceConfigurationProvider { + private final Map configurations = new ConcurrentHashMap<>(); + private final ModelRepository modelRepository; + + @Activate + public PersistenceModelManager(@Reference ModelRepository modelRepository) { + this.modelRepository = modelRepository; - protected void activate() { modelRepository.addModelRepositoryChangeListener(this); - for (String modelName : modelRepository.getAllModelNamesOfType("persist")) { - addModel(modelName); - } + modelRepository.getAllModelNamesOfType("persist") + .forEach(modelName -> modelChanged(modelName, EventType.ADDED)); } + @Deactivate protected void deactivate() { modelRepository.removeModelRepositoryChangeListener(this); - for (String modelName : modelRepository.getAllModelNamesOfType("persist")) { - removeModel(modelName); - } - } - - @Reference - protected void setModelRepository(ModelRepository modelRepository) { - this.modelRepository = modelRepository; - } - - protected void unsetModelRepository(ModelRepository modelRepository) { - this.modelRepository = null; - } - - @Reference - protected void setPersistenceManager(final PersistenceManager manager) { - this.manager = manager; - } - - protected void unsetPersistenceManager(final PersistenceManager manager) { - this.manager = null; + modelRepository.getAllModelNamesOfType("persist") + .forEach(modelName -> modelChanged(modelName, EventType.REMOVED)); } @Override public void modelChanged(String modelName, EventType type) { if (modelName.endsWith(".persist")) { - if (type == EventType.REMOVED || type == EventType.MODIFIED) { - removeModel(modelName); - } - if (type == EventType.ADDED || type == EventType.MODIFIED) { - addModel(modelName); - } - } - } - - private void addModel(String modelName) { - final PersistenceModel model = (PersistenceModel) modelRepository.getModel(modelName); - if (model != null) { String serviceName = serviceName(modelName); - manager.addConfig(serviceName, new PersistenceServiceConfiguration(mapConfigs(model.getConfigs()), - mapStrategies(model.getDefaults()), mapStrategies(model.getStrategies()))); + if (type == EventType.REMOVED) { + PersistenceServiceConfiguration removed = configurations.remove(serviceName); + notifyListenersAboutRemovedElement(removed); + } else { + final PersistenceModel model = (PersistenceModel) modelRepository.getModel(modelName); + + if (model != null) { + PersistenceServiceConfiguration newConfiguration = new PersistenceServiceConfiguration(serviceName, + mapConfigs(model.getConfigs()), mapStrategies(model.getDefaults()), + mapStrategies(model.getStrategies()), mapFilters(model.getFilters())); + PersistenceServiceConfiguration oldConfiguration = configurations.put(serviceName, + newConfiguration); + if (oldConfiguration == null) { + if (type != EventType.ADDED) { + logger.warn( + "Model {} is inconsistent: An updated event was sent, but there is no old configuration. Adding it now.", + modelName); + } + notifyListenersAboutAddedElement(newConfiguration); + } else { + if (type != EventType.MODIFIED) { + logger.warn( + "Model {} is inconsistent: An added event was sent, but there is an old configuration. Replacing it now.", + modelName); + } + notifyListenersAboutUpdatedElement(oldConfiguration, newConfiguration); + } + } else { + logger.error( + "The model repository reported a {} event for model '{}' but the model could not be found in the repository. ", + type, modelName); + } + } } } - private void removeModel(String modelName) { - String serviceName = serviceName(modelName); - manager.removeConfig(serviceName); - } - private String serviceName(String modelName) { return modelName.substring(0, modelName.length() - ".persist".length()); } @@ -133,10 +146,10 @@ private PersistenceItemConfiguration mapConfig(PersistenceConfiguration config) for (final EObject item : config.getItems()) { if (item instanceof AllConfig) { items.add(new PersistenceAllConfig()); - } else if (item instanceof GroupConfig) { - items.add(new PersistenceGroupConfig(((GroupConfig) item).getGroup())); - } else if (item instanceof ItemConfig) { - items.add(new PersistenceItemConfig(((ItemConfig) item).getItem())); + } else if (item instanceof GroupConfig groupConfig) { + items.add(new PersistenceGroupConfig(groupConfig.getGroup())); + } else if (item instanceof ItemConfig itemConfig) { + items.add(new PersistenceItemConfig(itemConfig.getItem())); } } return new PersistenceItemConfiguration(items, config.getAlias(), mapStrategies(config.getStrategies()), @@ -152,8 +165,8 @@ private List mapStrategies(List strategies) { } private PersistenceStrategy mapStrategy(Strategy strategy) { - if (strategy instanceof CronStrategy) { - return new PersistenceCronStrategy(strategy.getName(), ((CronStrategy) strategy).getCronExpression()); + if (strategy instanceof CronStrategy cronStrategy) { + return new PersistenceCronStrategy(strategy.getName(), cronStrategy.getCronExpression()); } else { return new PersistenceStrategy(strategy.getName()); } @@ -168,6 +181,27 @@ private List mapFilters(List filters) { } private PersistenceFilter mapFilter(Filter filter) { - return new PersistenceFilter(); + if (filter.getDefinition() instanceof TimeFilter timeFilter) { + return new PersistenceTimeFilter(filter.getName(), timeFilter.getValue(), timeFilter.getUnit()); + } else if (filter.getDefinition() instanceof ThresholdFilter thresholdFilter) { + return new PersistenceThresholdFilter(filter.getName(), thresholdFilter.getValue(), + thresholdFilter.getUnit(), thresholdFilter.isRelative()); + } else if (filter.getDefinition() instanceof EqualsFilter equalsFilter) { + return new PersistenceEqualsFilter(filter.getName(), equalsFilter.getValues(), false); + } else if (filter.getDefinition() instanceof NotEqualsFilter notEqualsFilter) { + return new PersistenceEqualsFilter(filter.getName(), notEqualsFilter.getValues(), true); + } else if (filter.getDefinition() instanceof IncludeFilter includeFilter) { + return new PersistenceIncludeFilter(filter.getName(), includeFilter.getLower(), includeFilter.getUpper(), + includeFilter.getUnit(), false); + } else if (filter.getDefinition() instanceof NotIncludeFilter notIncludeFilter) { + return new PersistenceIncludeFilter(filter.getName(), notIncludeFilter.getLower(), + notIncludeFilter.getUpper(), notIncludeFilter.getUnit(), true); + } + throw new IllegalArgumentException("Unknown filter type " + filter.getClass()); + } + + @Override + public Collection getAll() { + return List.copyOf(configurations.values()); } } diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/GlobalStrategies.java b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/GlobalStrategies.java index a17be5af765..5d4e901edcf 100644 --- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/GlobalStrategies.java +++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/GlobalStrategies.java @@ -23,21 +23,21 @@ */ public class GlobalStrategies { - static final public Strategy UPDATE = new StrategyImpl() { + public static final Strategy UPDATE = new StrategyImpl() { @Override public String getName() { return "everyUpdate"; }; }; - static final public Strategy CHANGE = new StrategyImpl() { + public static final Strategy CHANGE = new StrategyImpl() { @Override public String getName() { return "everyChange"; }; }; - static final public Strategy RESTORE = new StrategyImpl() { + public static final Strategy RESTORE = new StrategyImpl() { @Override public String getName() { return "restoreOnStartup"; diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/PersistenceGlobalScopeProvider.java b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/PersistenceGlobalScopeProvider.java index cbdfbf37b32..746d932a13e 100644 --- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/PersistenceGlobalScopeProvider.java +++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/PersistenceGlobalScopeProvider.java @@ -31,7 +31,7 @@ public class PersistenceGlobalScopeProvider extends AbstractGlobalScopeProvider { - static protected Resource res = new ResourceImpl(); + protected static Resource res = new ResourceImpl(); static { res.setURI(URI.createURI("virtual://openhab.org/persistence/strategy.global")); @@ -46,8 +46,7 @@ protected IScope getScope(Resource resource, boolean ignoreCase, EClass type, IScope parentScope = super.getScope(resource, ignoreCase, type, predicate); List descs = new ArrayList<>(); for (EObject eObj : res.getContents()) { - if (eObj instanceof Strategy) { - Strategy strategy = (Strategy) eObj; + if (eObj instanceof Strategy strategy) { descs.add(EObjectDescription.create(strategy.getName(), strategy)); } } diff --git a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java index a1ea1663852..c96a0ca5282 100644 --- a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java +++ b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java @@ -138,8 +138,7 @@ public void modelChanged(String modelFileName, EventType type) { switch (type) { case ADDED: EObject model = modelRepository.getModel(modelFileName); - if (model instanceof RuleModel) { - RuleModel ruleModel = (RuleModel) model; + if (model instanceof RuleModel ruleModel) { int index = 1; for (org.openhab.core.model.rule.rules.Rule rule : ruleModel.getRules()) { addRule(toRule(ruleModelName, rule, index)); @@ -152,8 +151,7 @@ public void modelChanged(String modelFileName, EventType type) { case MODIFIED: removeRuleModel(ruleModelName); EObject modifiedModel = modelRepository.getModel(modelFileName); - if (modifiedModel instanceof RuleModel) { - RuleModel ruleModel = (RuleModel) modifiedModel; + if (modifiedModel instanceof RuleModel ruleModel) { int index = 1; for (org.openhab.core.model.rule.rules.Rule rule : ruleModel.getRules()) { Rule newRule = toRule(ruleModelName, rule, index); @@ -180,8 +178,8 @@ public void modelChanged(String modelFileName, EventType type) { } case ADDED: EObject model = modelRepository.getModel(modelFileName); - if (model instanceof Script) { - addRule(toRule(modelFileName, ((Script) model))); + if (model instanceof Script script) { + addRule(toRule(modelFileName, script)); } break; case REMOVED: @@ -330,8 +328,7 @@ private String removeIndentation(String script) { cfg.put("startlevel", 20); return TriggerBuilder.create().withId(Integer.toString(triggerId++)) .withTypeUID("core.SystemStartlevelTrigger").withConfiguration(cfg).build(); - } else if (t instanceof SystemStartlevelTrigger) { - SystemStartlevelTrigger slTrigger = (SystemStartlevelTrigger) t; + } else if (t instanceof SystemStartlevelTrigger slTrigger) { Configuration cfg = new Configuration(); cfg.put("startlevel", slTrigger.getLevel()); return TriggerBuilder.create().withId(Integer.toString(triggerId++)) @@ -339,8 +336,7 @@ private String removeIndentation(String script) { } else if (t instanceof SystemOnShutdownTrigger) { logger.warn("System shutdown rule triggers are no longer supported!"); return null; - } else if (t instanceof CommandEventTrigger) { - CommandEventTrigger ceTrigger = (CommandEventTrigger) t; + } else if (t instanceof CommandEventTrigger ceTrigger) { Configuration cfg = new Configuration(); cfg.put("itemName", ceTrigger.getItem()); if (ceTrigger.getCommand() != null) { @@ -348,8 +344,7 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)).withTypeUID("core.ItemCommandTrigger") .withConfiguration(cfg).build(); - } else if (t instanceof GroupMemberCommandEventTrigger) { - GroupMemberCommandEventTrigger ceTrigger = (GroupMemberCommandEventTrigger) t; + } else if (t instanceof GroupMemberCommandEventTrigger ceTrigger) { Configuration cfg = new Configuration(); cfg.put("groupName", ceTrigger.getGroup()); if (ceTrigger.getCommand() != null) { @@ -357,8 +352,7 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)).withTypeUID("core.GroupCommandTrigger") .withConfiguration(cfg).build(); - } else if (t instanceof UpdateEventTrigger) { - UpdateEventTrigger ueTrigger = (UpdateEventTrigger) t; + } else if (t instanceof UpdateEventTrigger ueTrigger) { Configuration cfg = new Configuration(); cfg.put("itemName", ueTrigger.getItem()); if (ueTrigger.getState() != null) { @@ -366,8 +360,7 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)) .withTypeUID("core.ItemStateUpdateTrigger").withConfiguration(cfg).build(); - } else if (t instanceof GroupMemberUpdateEventTrigger) { - GroupMemberUpdateEventTrigger ueTrigger = (GroupMemberUpdateEventTrigger) t; + } else if (t instanceof GroupMemberUpdateEventTrigger ueTrigger) { Configuration cfg = new Configuration(); cfg.put("groupName", ueTrigger.getGroup()); if (ueTrigger.getState() != null) { @@ -375,8 +368,7 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)) .withTypeUID("core.GroupStateUpdateTrigger").withConfiguration(cfg).build(); - } else if (t instanceof ChangedEventTrigger) { - ChangedEventTrigger ceTrigger = (ChangedEventTrigger) t; + } else if (t instanceof ChangedEventTrigger ceTrigger) { Configuration cfg = new Configuration(); cfg.put("itemName", ceTrigger.getItem()); if (ceTrigger.getNewState() != null) { @@ -387,8 +379,7 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)) .withTypeUID("core.ItemStateChangeTrigger").withConfiguration(cfg).build(); - } else if (t instanceof GroupMemberChangedEventTrigger) { - GroupMemberChangedEventTrigger ceTrigger = (GroupMemberChangedEventTrigger) t; + } else if (t instanceof GroupMemberChangedEventTrigger ceTrigger) { Configuration cfg = new Configuration(); cfg.put("groupName", ceTrigger.getGroup()); if (ceTrigger.getNewState() != null) { @@ -399,8 +390,7 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)) .withTypeUID("core.GroupStateChangeTrigger").withConfiguration(cfg).build(); - } else if (t instanceof TimerTrigger) { - TimerTrigger tt = (TimerTrigger) t; + } else if (t instanceof TimerTrigger tt) { Configuration cfg = new Configuration(); String id; if (tt.getCron() != null) { @@ -419,15 +409,13 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)).withTypeUID("timer.GenericCronTrigger") .withConfiguration(cfg).build(); - } else if (t instanceof DateTimeTrigger) { - DateTimeTrigger tt = (DateTimeTrigger) t; + } else if (t instanceof DateTimeTrigger tt) { Configuration cfg = new Configuration(); cfg.put("itemName", tt.getItem()); cfg.put("timeOnly", tt.isTimeOnly()); return TriggerBuilder.create().withId(Integer.toString((triggerId++))).withTypeUID("timer.DateTimeTrigger") .withConfiguration(cfg).build(); - } else if (t instanceof EventEmittedTrigger) { - EventEmittedTrigger eeTrigger = (EventEmittedTrigger) t; + } else if (t instanceof EventEmittedTrigger eeTrigger) { Configuration cfg = new Configuration(); cfg.put("channelUID", eeTrigger.getChannel()); if (eeTrigger.getTrigger() != null) { @@ -435,15 +423,13 @@ private String removeIndentation(String script) { } return TriggerBuilder.create().withId(Integer.toString(triggerId++)).withTypeUID("core.ChannelEventTrigger") .withConfiguration(cfg).build(); - } else if (t instanceof ThingStateUpdateEventTrigger) { - ThingStateUpdateEventTrigger tsuTrigger = (ThingStateUpdateEventTrigger) t; + } else if (t instanceof ThingStateUpdateEventTrigger tsuTrigger) { Configuration cfg = new Configuration(); cfg.put("thingUID", tsuTrigger.getThing()); cfg.put("status", tsuTrigger.getState()); return TriggerBuilder.create().withId(Integer.toString(triggerId++)) .withTypeUID("core.ThingStatusUpdateTrigger").withConfiguration(cfg).build(); - } else if (t instanceof ThingStateChangedEventTrigger) { - ThingStateChangedEventTrigger tscTrigger = (ThingStateChangedEventTrigger) t; + } else if (t instanceof ThingStateChangedEventTrigger tscTrigger) { Configuration cfg = new Configuration(); cfg.put("thingUID", tscTrigger.getThing()); cfg.put("status", tscTrigger.getNewState()); @@ -461,8 +447,7 @@ public void onReadyMarkerAdded(ReadyMarker readyMarker) { for (String ruleFileName : modelRepository.getAllModelNamesOfType("rules")) { EObject model = modelRepository.getModel(ruleFileName); String ruleModelName = ruleFileName.substring(0, ruleFileName.indexOf(".")); - if (model instanceof RuleModel) { - RuleModel ruleModel = (RuleModel) model; + if (model instanceof RuleModel ruleModel) { int index = 1; for (org.openhab.core.model.rule.rules.Rule rule : ruleModel.getRules()) { addRule(toRule(ruleModelName, rule, index)); diff --git a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/RuleContextHelper.java b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/RuleContextHelper.java index 6986eaba626..b7b01cc2cf1 100644 --- a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/RuleContextHelper.java +++ b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/RuleContextHelper.java @@ -50,8 +50,8 @@ public static synchronized IEvaluationContext getContext(RuleModel ruleModel) { // check if a context already exists on the resource for (Adapter adapter : ruleModel.eAdapters()) { - if (adapter instanceof RuleContextAdapter) { - return ((RuleContextAdapter) adapter).getContext(); + if (adapter instanceof RuleContextAdapter contextAdapter) { + return contextAdapter.getContext(); } } Provider<@NonNull IEvaluationContext> contextProvider = injector.getProvider(IEvaluationContext.class); diff --git a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java index 1f5a0812496..7ae6cc6c51f 100644 --- a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java +++ b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/DSLScriptEngine.java @@ -32,6 +32,7 @@ import org.eclipse.xtext.xbase.interpreter.IEvaluationContext; import org.eclipse.xtext.xbase.interpreter.impl.DefaultEvaluationContext; import org.openhab.core.automation.module.script.ScriptExtensionAccessor; +import org.openhab.core.items.Item; import org.openhab.core.items.events.ItemEvent; import org.openhab.core.model.script.engine.Script; import org.openhab.core.model.script.engine.ScriptExecutionException; @@ -63,7 +64,8 @@ public class DSLScriptEngine implements javax.script.ScriptEngine { private static final Map IMPLICIT_VARS = Map.of("command", ScriptJvmModelInferrer.VAR_RECEIVED_COMMAND, "state", ScriptJvmModelInferrer.VAR_NEW_STATE, "newState", ScriptJvmModelInferrer.VAR_NEW_STATE, "oldState", ScriptJvmModelInferrer.VAR_PREVIOUS_STATE, - "triggeringItem", ScriptJvmModelInferrer.VAR_TRIGGERING_ITEM, "input", ScriptJvmModelInferrer.VAR_INPUT); + "triggeringItem", ScriptJvmModelInferrer.VAR_TRIGGERING_ITEM, "triggeringGroup", + ScriptJvmModelInferrer.VAR_TRIGGERING_GROUP, "input", ScriptJvmModelInferrer.VAR_INPUT); private final Logger logger = LoggerFactory.getLogger(DSLScriptEngine.class); @@ -142,12 +144,12 @@ public Object eval(String script) throws ScriptException { private DefaultEvaluationContext createEvaluationContext(Script script, IEvaluationContext specificContext) { IEvaluationContext parentContext = specificContext; - if (specificContext == null && script instanceof ScriptImpl) { - XExpression xExpression = ((ScriptImpl) script).getXExpression(); + if (specificContext == null && script instanceof ScriptImpl impl) { + XExpression xExpression = impl.getXExpression(); if (xExpression != null) { Resource resource = xExpression.eResource(); - if (resource instanceof XtextResource) { - IResourceServiceProvider provider = ((XtextResource) resource).getResourceServiceProvider(); + if (resource instanceof XtextResource xtextResource) { + IResourceServiceProvider provider = xtextResource.getResourceServiceProvider(); parentContext = provider.get(IEvaluationContext.class); } } @@ -171,19 +173,21 @@ private DefaultEvaluationContext createEvaluationContext(Script script, IEvaluat evalContext.newValue(QualifiedName.create("privateCache"), cachePreset.get("privateCache")); // now add specific implicit vars, where we have to map the right content Object value = context.getAttribute(OUTPUT_EVENT); - if (value instanceof ChannelTriggeredEvent) { - ChannelTriggeredEvent event = (ChannelTriggeredEvent) value; + if (value instanceof ChannelTriggeredEvent event) { evalContext.newValue(QualifiedName.create(ScriptJvmModelInferrer.VAR_RECEIVED_EVENT), event.getEvent()); evalContext.newValue(QualifiedName.create(ScriptJvmModelInferrer.VAR_TRIGGERING_CHANNEL), event.getChannel().getAsString()); } - if (value instanceof ItemEvent) { - ItemEvent event = (ItemEvent) value; + if (value instanceof ItemEvent event) { evalContext.newValue(QualifiedName.create(ScriptJvmModelInferrer.VAR_TRIGGERING_ITEM_NAME), event.getItemName()); + Object group = context.getAttribute(ScriptJvmModelInferrer.VAR_TRIGGERING_GROUP); + if (group instanceof Item groupItem) { + evalContext.newValue(QualifiedName.create(ScriptJvmModelInferrer.VAR_TRIGGERING_GROUP_NAME), + groupItem.getName()); + } } - if (value instanceof ThingStatusInfoChangedEvent) { - ThingStatusInfoChangedEvent event = (ThingStatusInfoChangedEvent) value; + if (value instanceof ThingStatusInfoChangedEvent event) { evalContext.newValue(QualifiedName.create(ScriptJvmModelInferrer.VAR_TRIGGERING_THING), event.getThingUID().toString()); evalContext.newValue(QualifiedName.create(ScriptJvmModelInferrer.VAR_PREVIOUS_STATUS), diff --git a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/ScriptImpl.java b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/ScriptImpl.java index 92be06f4a0e..985ed287af0 100644 --- a/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/ScriptImpl.java +++ b/bundles/org.openhab.core.model.script.runtime/src/org/openhab/core/model/script/runtime/internal/engine/ScriptImpl.java @@ -54,8 +54,8 @@ public Object execute() throws ScriptExecutionException { if (xExpression != null) { Resource resource = xExpression.eResource(); IEvaluationContext evaluationContext = null; - if (resource instanceof XtextResource) { - IResourceServiceProvider provider = ((XtextResource) resource).getResourceServiceProvider(); + if (resource instanceof XtextResource xtextResource) { + IResourceServiceProvider provider = xtextResource.getResourceServiceProvider(); evaluationContext = provider.get(IEvaluationContext.class); } return execute(evaluationContext); @@ -69,8 +69,8 @@ public Object execute(final IEvaluationContext evaluationContext) throws ScriptE if (xExpression != null) { Resource resource = xExpression.eResource(); IExpressionInterpreter interpreter = null; - if (resource instanceof XtextResource) { - IResourceServiceProvider provider = ((XtextResource) resource).getResourceServiceProvider(); + if (resource instanceof XtextResource xtextResource) { + IResourceServiceProvider provider = xtextResource.getResourceServiceProvider(); interpreter = provider.get(IExpressionInterpreter.class); } if (interpreter == null) { @@ -89,8 +89,8 @@ public Object execute(final IEvaluationContext evaluationContext) throws ScriptE } return result.getResult(); } catch (Throwable e) { - if (e instanceof ScriptExecutionException) { - throw (ScriptExecutionException) e; + if (e instanceof ScriptExecutionException exception) { + throw exception; } else { throw new ScriptExecutionException( "An error occurred during the script execution: " + e.getMessage(), e); diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/config/hli.xml b/bundles/org.openhab.core.model.script/resources/OH-INF/config/hli.xml index 374a3c292f1..eb0c526a0f8 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/config/hli.xml +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/config/hli.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + item String diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli.properties index 8fc054f3290..92b501469ef 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Voice Command Item -voice.config.rulehli.item.description = The String Item to pass voice commands to. +system.config.rulehli.item.label = Voice Command Item +system.config.rulehli.item.description = The String Item to pass voice commands to. -service.voice.rulehli.label = Rule Voice Interpreter +service.system.rulehli.label = Rule Voice Interpreter diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_cs.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_cs.properties index c19c263e058..b38bc645412 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_cs.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_cs.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item hlasového příkazu -voice.config.rulehli.item.description = String Item, do které se vloží hlasový příkaz. +system.config.rulehli.item.label = Item hlasového příkazu +system.config.rulehli.item.description = String Item, do které se vloží hlasový příkaz. -service.voice.rulehli.label = Hlasový tlumočník +service.system.rulehli.label = Hlasový tlumočník diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_da.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_da.properties index 9bef37e15c4..f5d8d8eb9cb 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_da.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_da.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Stemmekommando-item -voice.config.rulehli.item.description = Streng-item'et der skal sendes stemmekommandoer til. +system.config.rulehli.item.label = Stemmekommando-item +system.config.rulehli.item.description = Streng-item'et der skal sendes stemmekommandoer til. -service.voice.rulehli.label = Regel-stemmefortolker +service.system.rulehli.label = Regel-stemmefortolker diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_de.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_de.properties index 75a09edd818..6d8864489eb 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_de.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_de.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item für Sprachanweisungen -voice.config.rulehli.item.description = Das String-Item an das Sprachanweisungen übergeben werden sollen. +system.config.rulehli.item.label = Item für Sprachanweisungen +system.config.rulehli.item.description = Das String-Item an das Sprachanweisungen übergeben werden sollen. -service.voice.rulehli.label = Regelbasierter Sprachinterpreter +service.system.rulehli.label = Regelbasierter Sprachinterpreter diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_el.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_el.properties index be8e8687cbd..abdf9b8a44b 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_el.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_el.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Στοιχείο Φωνητικής Εντολής -voice.config.rulehli.item.description = Το στοιχείο κειμένου για να περάσει τις φωνητικές εντολές. +system.config.rulehli.item.label = Στοιχείο Φωνητικής Εντολής +system.config.rulehli.item.description = Το στοιχείο κειμένου για να περάσει τις φωνητικές εντολές. -service.voice.rulehli.label = Διερμηνέας Κανόνων Φωνής +service.system.rulehli.label = Διερμηνέας Κανόνων Φωνής diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fi.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fi.properties index 835267deeb0..a41f5218206 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fi.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fi.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Äänikomennon Item -voice.config.rulehli.item.description = Tekstimuotoinen Item, jolle äänikomennot välitetään. +system.config.rulehli.item.label = Äänikomennon Item +system.config.rulehli.item.description = Tekstimuotoinen Item, jolle äänikomennot välitetään. -service.voice.rulehli.label = Sääntöpohjainen äänitulkki +service.system.rulehli.label = Sääntöpohjainen äänitulkki diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fr.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fr.properties index 497269634f2..37c3d8a91c6 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fr.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_fr.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item de commande vocale -voice.config.rulehli.item.description = L'item pour passer des commandes vocales. +system.config.rulehli.item.label = Item de commande vocale +system.config.rulehli.item.description = L'item pour passer des commandes vocales. -service.voice.rulehli.label = Interprêteur de règle vocale +service.system.rulehli.label = Interpréteur de règle vocale diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_he.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_he.properties index 3d297fb1b78..c2c79e22310 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_he.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_he.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = פקודה קולית -voice.config.rulehli.item.description = ה-Item אליו יועברו הפקודות הקוליות. +system.config.rulehli.item.label = פקודה קולית +system.config.rulehli.item.description = ה-Item אליו יועברו הפקודות הקוליות. -service.voice.rulehli.label = מתורגמן כללים קולי +service.system.rulehli.label = מתורגמן כללים קולי diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_hu.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_hu.properties index c95bf2bad82..e25c5b0cf75 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_hu.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_hu.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Hangvezérlés elem -voice.config.rulehli.item.description = A szöveg elem, amely a hangüzeneteket fogadja. +system.config.rulehli.item.label = Hangvezérlés elem +system.config.rulehli.item.description = A szöveg elem, amely a hangüzeneteket fogadja. -service.voice.rulehli.label = Szabály hang értelmező +service.system.rulehli.label = Szabály hang értelmező diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_it.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_it.properties index dc0f67fe27e..f7349bb7db7 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_it.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_it.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item Comando Vocale -voice.config.rulehli.item.description = La Stringa dell'Item passata al comando vocale. +system.config.rulehli.item.label = Item Comando Vocale +system.config.rulehli.item.description = L'elemento stringa a cui passare i comandi vocali. -service.voice.rulehli.label = Regola Inteprete Vocale +service.system.rulehli.label = Regola Interprete Vocale diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_nl.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_nl.properties index df6e7e9fce6..d78532e0a5a 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_nl.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_nl.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Spraakopdracht Item -voice.config.rulehli.item.description = Het String Item om spraakopdrachten naar door te geven. +system.config.rulehli.item.label = Spraakopdracht Item +system.config.rulehli.item.description = Het String Item om spraakopdrachten naar door te geven. -service.voice.rulehli.label = Rule Spraak Interpreter +service.system.rulehli.label = Rule Spraak Interpreter diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_no.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_no.properties index 541440e9f5e..4d704b2470d 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_no.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_no.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Stemmekommando Item -voice.config.rulehli.item.description = Streng Item'et stemmekommandoer skal sendes til. +system.config.rulehli.item.label = Stemmekommando Item +system.config.rulehli.item.description = Streng Item'et stemmekommandoer skal sendes til. -service.voice.rulehli.label = Regelbasert Stemmetolk +service.system.rulehli.label = Regelbasert Stemmetolk diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pl.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pl.properties index 0e5be3ec0fd..76996fa937a 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pl.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pl.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Element komendy głosowej -voice.config.rulehli.item.description = Element do którego mają być przekazywane polecenia głosowe. +system.config.rulehli.item.label = Element komendy głosowej +system.config.rulehli.item.description = Element do którego mają być przekazywane komendy głosowe. -service.voice.rulehli.label = Interpretator reguł głosu +service.system.rulehli.label = Interpretator reguł głosu diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pt.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pt.properties index 334767d6519..09cae541aa2 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pt.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_pt.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item de Comando de Voz -voice.config.rulehli.item.description = O Item de texto para passar comandos de voz. +system.config.rulehli.item.label = Item de Comando de Voz +system.config.rulehli.item.description = O Item de texto para passar comandos de voz. -service.voice.rulehli.label = Interpretador de voz +service.system.rulehli.label = Interpretador de voz diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_ru.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_ru.properties index b94ca001fc9..173ae4efb39 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_ru.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_ru.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item голосовых команд -voice.config.rulehli.item.description = Строковый Item для передачи голосовых команд. +system.config.rulehli.item.label = Item голосовых команд +system.config.rulehli.item.description = Строковый Item для передачи голосовых команд. -service.voice.rulehli.label = Управление голосовым интерпретатором +service.system.rulehli.label = Управление голосовым интерпретатором diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sl.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sl.properties index be2a7c4459b..1de53e55bd2 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sl.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sl.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Predmet glasovnega ukaza -voice.config.rulehli.item.description = Besedilni predmet, ki se mu naj posreduje ukaz. +system.config.rulehli.item.label = Predmet glasovnega ukaza +system.config.rulehli.item.description = Besedilni predmet, ki se mu naj posreduje ukaz. -service.voice.rulehli.label = Tolmač glasovnih ukazov +service.system.rulehli.label = Tolmač glasovnih ukazov diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sv.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sv.properties index a829304e159..4d176002d27 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sv.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_sv.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Item för röstkommando -voice.config.rulehli.item.description = Sträng-Item att skicka röstkommandon till. +system.config.rulehli.item.label = Item för röstkommando +system.config.rulehli.item.description = Sträng-Item att skicka röstkommandon till. -service.voice.rulehli.label = Rösttolkare för regler +service.system.rulehli.label = Rösttolkare för regler diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_uk.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_uk.properties index fc90a8ba89d..aa82b3c9125 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_uk.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_uk.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = Голосова команда -voice.config.rulehli.item.description = Пункт для передачі голосових команд. +system.config.rulehli.item.label = Голосова команда +system.config.rulehli.item.description = Пункт для передачі голосових команд. -service.voice.rulehli.label = Правило голосового інтерпретера +service.system.rulehli.label = Правило голосового інтерпретера diff --git a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_zh.properties b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_zh.properties index 03189a9b1b1..736ede14255 100644 --- a/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_zh.properties +++ b/bundles/org.openhab.core.model.script/resources/OH-INF/i18n/hli_zh.properties @@ -1,4 +1,4 @@ -voice.config.rulehli.item.label = 语音命令 -voice.config.rulehli.item.description = 要传递语音命令的字符串。 +system.config.rulehli.item.label = 语音命令 +system.config.rulehli.item.description = 要传递语音命令的字符串。 -service.voice.rulehli.label = 语音规则解释器 +service.system.rulehli.label = 语音规则解释器 diff --git a/bundles/org.openhab.core.model.script/src.moved/test/java/org/openhab/core/model/script/actions/SemanticsTest.java b/bundles/org.openhab.core.model.script/src.moved/test/java/org/openhab/core/model/script/actions/SemanticsTest.java index 9e934ed887a..be07ec03be5 100644 --- a/bundles/org.openhab.core.model.script/src.moved/test/java/org/openhab/core/model/script/actions/SemanticsTest.java +++ b/bundles/org.openhab.core.model.script/src.moved/test/java/org/openhab/core/model/script/actions/SemanticsTest.java @@ -12,12 +12,14 @@ */ package org.openhab.core.model.script.actions; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.when; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,16 +27,17 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.model.script.internal.engine.action.SemanticsActionService; -import org.openhab.core.semantics.model.equipment.Battery; -import org.openhab.core.semantics.model.equipment.CleaningRobot; -import org.openhab.core.semantics.model.location.Bathroom; -import org.openhab.core.semantics.model.location.Indoor; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.internal.SemanticTagRegistryImpl; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; /** * This are tests for {@link Semantics} actions. @@ -43,20 +46,28 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault public class SemanticsTest { - private @Mock ItemRegistry mockedItemRegistry; + private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; + private @Mock @NonNullByDefault({}) ManagedSemanticTagProvider managedSemanticTagProviderMock; + + private @NonNullByDefault({}) GroupItem indoorLocationItem; + private @NonNullByDefault({}) GroupItem bathroomLocationItem; + private @NonNullByDefault({}) GroupItem equipmentItem; + private @NonNullByDefault({}) GenericItem temperaturePointItem; + private @NonNullByDefault({}) GenericItem humidityPointItem; + private @NonNullByDefault({}) GenericItem subEquipmentItem; - private GroupItem indoorLocationItem; - private GroupItem bathroomLocationItem; - private GroupItem equipmentItem; - private GenericItem temperaturePointItem; - private GenericItem humidityPointItem; - private GenericItem subEquipmentItem; + private @NonNullByDefault({}) Class indoorTagClass; + private @NonNullByDefault({}) Class bathroomTagClass; + private @NonNullByDefault({}) Class cleaningRobotTagClass; + private @NonNullByDefault({}) Class batteryTagClass; @BeforeEach public void setup() throws ItemNotFoundException { - CoreItemFactory itemFactory = new CoreItemFactory(); + CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock); indoorLocationItem = new GroupItem("TestHouse"); indoorLocationItem.addTag("Indoor"); @@ -94,13 +105,22 @@ public void setup() throws ItemNotFoundException { equipmentItem.addMember(subEquipmentItem); subEquipmentItem.addGroupName(equipmentItem.getName()); - when(mockedItemRegistry.getItem("TestHouse")).thenReturn(indoorLocationItem); - when(mockedItemRegistry.getItem("TestBathRoom")).thenReturn(bathroomLocationItem); - when(mockedItemRegistry.getItem("Test08")).thenReturn(equipmentItem); - when(mockedItemRegistry.getItem("TestTemperature")).thenReturn(temperaturePointItem); - when(mockedItemRegistry.getItem("TestHumidity")).thenReturn(humidityPointItem); + when(managedSemanticTagProviderMock.getAll()).thenReturn(List.of()); + SemanticTagRegistryImpl semanticTagRegistryImpl = new SemanticTagRegistryImpl(new DefaultSemanticTagProvider(), + managedSemanticTagProviderMock); + + indoorTagClass = semanticTagRegistryImpl.getTagClassById("Location_Indoor"); + bathroomTagClass = semanticTagRegistryImpl.getTagClassById("Location_Indoor_Room_Bathroom"); + cleaningRobotTagClass = semanticTagRegistryImpl.getTagClassById("Equipment_CleaningRobot"); + batteryTagClass = semanticTagRegistryImpl.getTagClassById("Equipment_Battery"); + + when(itemRegistryMock.getItem("TestHouse")).thenReturn(indoorLocationItem); + when(itemRegistryMock.getItem("TestBathRoom")).thenReturn(bathroomLocationItem); + when(itemRegistryMock.getItem("Test08")).thenReturn(equipmentItem); + when(itemRegistryMock.getItem("TestTemperature")).thenReturn(temperaturePointItem); + when(itemRegistryMock.getItem("TestHumidity")).thenReturn(humidityPointItem); - new SemanticsActionService(mockedItemRegistry); + new SemanticsActionService(itemRegistryMock); } @Test @@ -118,9 +138,9 @@ public void testGetLocation() { @Test public void testGetLocationType() { - assertThat(Semantics.getLocationType(indoorLocationItem), is(Indoor.class)); + assertThat(Semantics.getLocationType(indoorLocationItem), is(indoorTagClass)); - assertThat(Semantics.getLocationType(bathroomLocationItem), is(Bathroom.class)); + assertThat(Semantics.getLocationType(bathroomLocationItem), is(bathroomTagClass)); assertNull(Semantics.getLocationType(humidityPointItem)); } @@ -138,11 +158,11 @@ public void testGetEquipment() { @Test public void testGetEquipmentType() { - assertThat(Semantics.getEquipmentType(equipmentItem), is(CleaningRobot.class)); + assertThat(Semantics.getEquipmentType(equipmentItem), is(cleaningRobotTagClass)); - assertThat(Semantics.getEquipmentType(temperaturePointItem), is(CleaningRobot.class)); + assertThat(Semantics.getEquipmentType(temperaturePointItem), is(cleaningRobotTagClass)); - assertThat(Semantics.getEquipmentType(subEquipmentItem), is(Battery.class)); + assertThat(Semantics.getEquipmentType(subEquipmentItem), is(batteryTagClass)); assertNull(Semantics.getEquipmentType(humidityPointItem)); } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/BusEvent.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/BusEvent.java index 68b0f54db21..c2cf8e1a3b1 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/BusEvent.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/BusEvent.java @@ -47,7 +47,7 @@ public class BusEvent { * @param item the item to send the command to * @param commandString the command to send */ - static public Object sendCommand(Item item, String commandString) { + public static Object sendCommand(Item item, String commandString) { if (item != null) { return sendCommand(item.getName(), commandString); } else { @@ -61,7 +61,7 @@ static public Object sendCommand(Item item, String commandString) { * @param item the item to send the command to * @param number the number to send as a command */ - static public Object sendCommand(Item item, Number number) { + public static Object sendCommand(Item item, Number number) { if (item != null && number != null) { return sendCommand(item.getName(), number.toString()); } else { @@ -75,7 +75,7 @@ static public Object sendCommand(Item item, Number number) { * @param itemName the name of the item to send the command to * @param commandString the command to send */ - static public Object sendCommand(String itemName, String commandString) { + public static Object sendCommand(String itemName, String commandString) { ItemRegistry registry = ScriptServiceUtil.getItemRegistry(); EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); if (publisher != null && registry != null) { @@ -107,7 +107,7 @@ private static List getAcceptedCommandNames(Item item) * @param item the item to send the command to * @param command the command to send */ - static public Object sendCommand(Item item, Command command) { + public static Object sendCommand(Item item, Command command) { EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); if (publisher != null && item != null) { publisher.post(ItemEventFactory.createCommandEvent(item.getName(), command)); @@ -121,7 +121,7 @@ static public Object sendCommand(Item item, Command command) { * @param item the item to send the status update for * @param state the new state of the item as a number */ - static public Object postUpdate(Item item, Number state) { + public static Object postUpdate(Item item, Number state) { if (item != null && state != null) { return postUpdate(item.getName(), state.toString()); } else { @@ -135,7 +135,7 @@ static public Object postUpdate(Item item, Number state) { * @param item the item to send the status update for * @param stateAsString the new state of the item */ - static public Object postUpdate(Item item, String stateAsString) { + public static Object postUpdate(Item item, String stateAsString) { if (item != null) { return postUpdate(item.getName(), stateAsString); } else { @@ -149,7 +149,7 @@ static public Object postUpdate(Item item, String stateAsString) { * @param itemName the name of the item to send the status update for * @param stateAsString the new state of the item */ - static public Object postUpdate(String itemName, String stateString) { + public static Object postUpdate(String itemName, String stateString) { ItemRegistry registry = ScriptServiceUtil.getItemRegistry(); EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); if (publisher != null && registry != null) { @@ -181,7 +181,7 @@ private static List getAcceptedDataTypeNames(Item item * @param item the item to send the status update for * @param state the new state of the item */ - static public Object postUpdate(Item item, State state) { + public static Object postUpdate(Item item, State state) { EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); if (publisher != null && item != null) { publisher.post(ItemEventFactory.createStateEvent(item.getName(), state)); @@ -196,12 +196,11 @@ static public Object postUpdate(Item item, State state) { * @param items the items for which the state should be stored * @return the map of items with their states */ - static public Map storeStates(Item... items) { + public static Map storeStates(Item... 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()); } @@ -222,7 +221,7 @@ static public Map storeStates(Item... items) { * @param statesMap a map with ({@link Item}, {@link State}) entries * @return null */ - static public Object restoreStates(Map statesMap) { + public static Object restoreStates(Map statesMap) { if (statesMap != null) { for (Entry entry : statesMap.entrySet()) { if (entry.getValue() instanceof Command) { diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/HTTP.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/HTTP.java index 30db3358257..6c589d241b7 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/HTTP.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/HTTP.java @@ -43,7 +43,7 @@ public class HTTP { * @param url the URL to be used for the GET request. * @return the response body or NULL when the request went wrong */ - static public String sendHttpGetRequest(String url) { + public static String sendHttpGetRequest(String url) { return sendHttpGetRequest(url, 5000); } @@ -54,7 +54,7 @@ static public String sendHttpGetRequest(String url) { * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpGetRequest(String url, int timeout) { + public static String sendHttpGetRequest(String url, int timeout) { String response = null; try { return HttpUtil.executeUrl(HttpMethod.GET.name(), url, timeout); @@ -89,7 +89,7 @@ public static String sendHttpGetRequest(String url, Map headers, * @param url the URL to be used for the PUT request. * @return the response body or NULL when the request went wrong */ - static public String sendHttpPutRequest(String url) { + public static String sendHttpPutRequest(String url) { return sendHttpPutRequest(url, 1000); } @@ -100,7 +100,7 @@ static public String sendHttpPutRequest(String url) { * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpPutRequest(String url, int timeout) { + public static String sendHttpPutRequest(String url, int timeout) { String response = null; try { response = HttpUtil.executeUrl(HttpMethod.PUT.name(), url, timeout); @@ -119,7 +119,7 @@ static public String sendHttpPutRequest(String url, int timeout) { * send. * @return the response body or NULL when the request went wrong */ - static public String sendHttpPutRequest(String url, String contentType, String content) { + public static String sendHttpPutRequest(String url, String contentType, String content) { return sendHttpPutRequest(url, contentType, content, 1000); } @@ -133,7 +133,7 @@ static public String sendHttpPutRequest(String url, String contentType, String c * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpPutRequest(String url, String contentType, String content, int timeout) { + public static String sendHttpPutRequest(String url, String contentType, String content, int timeout) { String response = null; try { response = HttpUtil.executeUrl(HttpMethod.PUT.name(), url, @@ -155,7 +155,7 @@ static public String sendHttpPutRequest(String url, String contentType, String c * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpPutRequest(String url, String contentType, String content, Map headers, + public static String sendHttpPutRequest(String url, String contentType, String content, Map headers, int timeout) { try { Properties headerProperties = new Properties(); @@ -174,7 +174,7 @@ static public String sendHttpPutRequest(String url, String contentType, String c * @param url the URL to be used for the POST request. * @return the response body or NULL when the request went wrong */ - static public String sendHttpPostRequest(String url) { + public static String sendHttpPostRequest(String url) { return sendHttpPostRequest(url, 1000); } @@ -185,7 +185,7 @@ static public String sendHttpPostRequest(String url) { * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpPostRequest(String url, int timeout) { + public static String sendHttpPostRequest(String url, int timeout) { String response = null; try { response = HttpUtil.executeUrl(HttpMethod.POST.name(), url, timeout); @@ -204,7 +204,7 @@ static public String sendHttpPostRequest(String url, int timeout) { * send. * @return the response body or NULL when the request went wrong */ - static public String sendHttpPostRequest(String url, String contentType, String content) { + public static String sendHttpPostRequest(String url, String contentType, String content) { return sendHttpPostRequest(url, contentType, content, 1000); } @@ -218,7 +218,7 @@ static public String sendHttpPostRequest(String url, String contentType, String * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpPostRequest(String url, String contentType, String content, int timeout) { + public static String sendHttpPostRequest(String url, String contentType, String content, int timeout) { String response = null; try { response = HttpUtil.executeUrl(HttpMethod.POST.name(), url, @@ -259,7 +259,7 @@ public static String sendHttpPostRequest(String url, String contentType, String * @param url the URL to be used for the DELETE request. * @return the response body or NULL when the request went wrong */ - static public String sendHttpDeleteRequest(String url) { + public static String sendHttpDeleteRequest(String url) { return sendHttpDeleteRequest(url, 1000); } @@ -270,7 +270,7 @@ static public String sendHttpDeleteRequest(String url) { * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpDeleteRequest(String url, int timeout) { + public static String sendHttpDeleteRequest(String url, int timeout) { String response = null; try { response = HttpUtil.executeUrl(HttpMethod.DELETE.name(), url, timeout); @@ -288,7 +288,7 @@ static public String sendHttpDeleteRequest(String url, int timeout) { * @param timeout timeout in ms * @return the response body or NULL when the request went wrong */ - static public String sendHttpDeleteRequest(String url, Map headers, int timeout) { + public static String sendHttpDeleteRequest(String url, Map headers, int timeout) { try { Properties headerProperties = new Properties(); headerProperties.putAll(headers); diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/Log.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/Log.java index b293f1b38c6..fb44dec1a9f 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/Log.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/Log.java @@ -35,7 +35,7 @@ public class Log { * * @see Logger */ - static public void logDebug(String loggerName, String format, Object... args) { + public static void logDebug(String loggerName, String format, Object... args) { LoggerFactory.getLogger(LOGGER_NAME_PREFIX.concat(loggerName)).debug(format, args); } @@ -49,7 +49,7 @@ static public void logDebug(String loggerName, String format, Object... args) { * * @see Logger */ - static public void logInfo(String loggerName, String format, Object... args) { + public static void logInfo(String loggerName, String format, Object... args) { LoggerFactory.getLogger(LOGGER_NAME_PREFIX.concat(loggerName)).info(format, args); } @@ -63,7 +63,7 @@ static public void logInfo(String loggerName, String format, Object... args) { * * @see Logger */ - static public void logWarn(String loggerName, String format, Object... args) { + public static void logWarn(String loggerName, String format, Object... args) { LoggerFactory.getLogger(LOGGER_NAME_PREFIX.concat(loggerName)).warn(format, args); } @@ -77,7 +77,7 @@ static public void logWarn(String loggerName, String format, Object... args) { * * @see Logger */ - static public void logError(String loggerName, String format, Object... args) { + public static void logError(String loggerName, String format, Object... args) { LoggerFactory.getLogger(LOGGER_NAME_PREFIX.concat(loggerName)).error(format, args); } } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/Script.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/Script.java index 0e461f43e75..7f821da605c 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/Script.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/Script.java @@ -22,7 +22,7 @@ @SuppressWarnings("restriction") public interface Script { - public static final String SCRIPT_FILEEXT = "script"; + static final String SCRIPT_FILEEXT = "script"; /** * Executes the script instance and returns the execution result @@ -30,7 +30,7 @@ public interface Script { * @return the execution result or null, if the script does not have a return value * @throws ScriptExecutionException if an error occurs during the execution */ - public Object execute() throws ScriptExecutionException; + Object execute() throws ScriptExecutionException; /** * Executes the script instance with a given evaluation context and returns the execution result @@ -40,5 +40,5 @@ public interface Script { * @return the execution result or null, if the script does not have a return value * @throws ScriptExecutionException if an error occurs during the execution */ - public Object execute(IEvaluationContext evaluationContext) throws ScriptExecutionException; + Object execute(IEvaluationContext evaluationContext) throws ScriptExecutionException; } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptEngine.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptEngine.java index 4582061c527..cf4c1ed24bb 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptEngine.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptEngine.java @@ -29,7 +29,7 @@ public interface ScriptEngine { * @return Script object, which can be executed * @throws ScriptParsingException */ - public Script newScriptFromString(final String scriptAsString) throws ScriptParsingException; + Script newScriptFromString(final String scriptAsString) throws ScriptParsingException; /** * Executes a script that is passed as a string @@ -39,7 +39,7 @@ public interface ScriptEngine { * @throws ScriptParsingException * @throws ScriptExecutionException */ - public Object executeScript(final String scriptAsString) throws ScriptParsingException, ScriptExecutionException; + Object executeScript(final String scriptAsString) throws ScriptParsingException, ScriptExecutionException; /** * Wraps an Xbase XExpression in a Script instance @@ -47,5 +47,5 @@ public interface ScriptEngine { * @param expression the XExpression * @return the Script instance containing the expression */ - public Script newScriptFromXExpression(final XExpression expression); + Script newScriptFromXExpression(final XExpression expression); } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptParsingException.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptParsingException.java index 88e07d16732..758f77927c3 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptParsingException.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/engine/ScriptParsingException.java @@ -38,11 +38,9 @@ public ScriptParsingException(String message, String scriptAsString, Throwable t public ScriptParsingException addDiagnosticErrors(List errors) { for (Diagnostic emfDiagnosticError : errors) { - if (emfDiagnosticError instanceof AbstractDiagnostic) { - AbstractDiagnostic e = (AbstractDiagnostic) emfDiagnosticError; + if (emfDiagnosticError instanceof AbstractDiagnostic e) { this.getErrors().add(new ScriptError(e.getMessage(), e.getLine(), e.getOffset(), e.getLength())); - } else if (emfDiagnosticError instanceof ExceptionDiagnostic) { - ExceptionDiagnostic e = (ExceptionDiagnostic) emfDiagnosticError; + } else if (emfDiagnosticError instanceof ExceptionDiagnostic e) { this.getErrors().add(new ScriptError(e.getMessage(), e.getLine(), e.getOffset(), e.getLength())); } else { this.getErrors().add(new ScriptError(emfDiagnosticError.getMessage(), -1, -1, -1)); diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/RuleHumanLanguageInterpreter.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/RuleHumanLanguageInterpreter.java index 3f4e90b57ce..928b3047a4f 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/RuleHumanLanguageInterpreter.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/RuleHumanLanguageInterpreter.java @@ -47,13 +47,13 @@ @NonNullByDefault @Component(immediate = true, service = HumanLanguageInterpreter.class, configurationPid = "org.openhab.rulehli", // property = Constants.SERVICE_PID + "=org.openhab.rulehli") -@ConfigurableService(category = "voice", label = "Rule Voice Interpreter", description_uri = RuleHumanLanguageInterpreter.CONFIG_URI) +@ConfigurableService(category = "system", label = "Rule Voice Interpreter", description_uri = RuleHumanLanguageInterpreter.CONFIG_URI) public class RuleHumanLanguageInterpreter implements HumanLanguageInterpreter { private final Logger logger = LoggerFactory.getLogger(RuleHumanLanguageInterpreter.class); // constants for the configuration properties - protected static final String CONFIG_URI = "voice:rulehli"; + protected static final String CONFIG_URI = "system:rulehli"; private String itemName = "VoiceCommand"; diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/ThingActionService.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/ThingActionService.java index 04c4d06e150..b127c2b425f 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/ThingActionService.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/ThingActionService.java @@ -80,8 +80,7 @@ public Class getActionClass() { if (thing != null) { ThingHandler handler = thing.getHandler(); if (handler != null) { - ThingActions thingActions = THING_ACTIONS_MAP.get(getKey(scope, thingUid)); - return thingActions; + return THING_ACTIONS_MAP.get(getKey(scope, thingUid)); } } return null; diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend index dc4b3759123..5d871849367 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend @@ -44,6 +44,12 @@ class ScriptJvmModelInferrer extends AbstractModelInferrer { /** Variable name for the input string in a "script transformation" or "script profile" */ public static final String VAR_INPUT = "input"; + /** Variable name for the group in a "member of state triggered" or "member of command triggered" rule */ + public static final String VAR_TRIGGERING_GROUP = "triggeringGroup"; + + /** Variable name for the group in a "member of state triggered" or "member of command triggered" rule */ + public static final String VAR_TRIGGERING_GROUP_NAME = "triggeringGroupName"; + /** Variable name for the item in a "state triggered" or "command triggered" rule */ public static final String VAR_TRIGGERING_ITEM = "triggeringItem"; @@ -131,6 +137,10 @@ class ScriptJvmModelInferrer extends AbstractModelInferrer { static = true val inputTypeRef = script.newTypeRef(String) parameters += script.toParameter(VAR_INPUT, inputTypeRef) + val groupTypeRef = script.newTypeRef(Item) + parameters += script.toParameter(VAR_TRIGGERING_GROUP, groupTypeRef) + val groupNameRef = script.newTypeRef(String) + parameters += script.toParameter(VAR_TRIGGERING_GROUP_NAME, groupNameRef) val itemTypeRef = script.newTypeRef(Item) parameters += script.toParameter(VAR_TRIGGERING_ITEM, itemTypeRef) val itemNameRef = script.newTypeRef(String) diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptTypeComputer.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptTypeComputer.java index 60b5f07ed71..1da6596e0a0 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptTypeComputer.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptTypeComputer.java @@ -32,8 +32,8 @@ public class ScriptTypeComputer extends XbaseTypeComputer { @Override public void computeTypes(XExpression expression, ITypeComputationState state) { - if (expression instanceof QuantityLiteral) { - _computeTypes((QuantityLiteral) expression, state); + if (expression instanceof QuantityLiteral literal) { + _computeTypes(literal, state); } else { super.computeTypes(expression, state); } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/lib/NumberExtensions.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/lib/NumberExtensions.java index 8dd94aff595..4b05df4e84b 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/lib/NumberExtensions.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/lib/NumberExtensions.java @@ -175,22 +175,22 @@ public static boolean operator_greaterEqualsThan(Number left, Number right) { // Comparison operators between types and numbers public static boolean operator_equals(Type type, Number x) { - if (type instanceof QuantityType && x instanceof QuantityType) { - return operator_equals((QuantityType) type, (QuantityType) x); + if (type instanceof QuantityType qtype && x instanceof QuantityType qx) { + return operator_equals(qtype, qx); } - if (type != null && type instanceof DecimalType && x != null) { - return ((DecimalType) type).toBigDecimal().compareTo(numberToBigDecimal(x)) == 0; + if (type != null && type instanceof DecimalType decimalType && x != null) { + return decimalType.toBigDecimal().compareTo(numberToBigDecimal(x)) == 0; } else { return type == x; // both might be null, then we should return true } } public static boolean operator_notEquals(Type type, Number x) { - if (type instanceof QuantityType && x instanceof QuantityType) { - return operator_notEquals((QuantityType) type, (QuantityType) x); + if (type instanceof QuantityType qtype && x instanceof QuantityType qx) { + return operator_notEquals(qtype, qx); } - if (type != null && type instanceof DecimalType && x != null) { - return ((DecimalType) type).toBigDecimal().compareTo(numberToBigDecimal(x)) != 0; + if (type != null && type instanceof DecimalType decimalType && x != null) { + return decimalType.toBigDecimal().compareTo(numberToBigDecimal(x)) != 0; } else { return type != x; // both might be null, then we should return // false, otherwise true @@ -198,44 +198,44 @@ public static boolean operator_notEquals(Type type, Number x) { } public static boolean operator_greaterThan(Type type, Number x) { - if (type instanceof QuantityType && x instanceof QuantityType) { - return operator_greaterThan((QuantityType) type, (QuantityType) x); + if (type instanceof QuantityType qtype && x instanceof QuantityType qx) { + return operator_greaterThan(qtype, qx); } - if (type != null && type instanceof DecimalType && x != null) { - return ((DecimalType) type).toBigDecimal().compareTo(numberToBigDecimal(x)) > 0; + if (type != null && type instanceof DecimalType decimalType && x != null) { + return decimalType.toBigDecimal().compareTo(numberToBigDecimal(x)) > 0; } else { return false; } } public static boolean operator_greaterEqualsThan(Type type, Number x) { - if (type instanceof QuantityType && x instanceof QuantityType) { - return operator_greaterEqualsThan((QuantityType) type, (QuantityType) x); + if (type instanceof QuantityType qtype && x instanceof QuantityType qx) { + return operator_greaterEqualsThan(qtype, qx); } - if (type != null && type instanceof DecimalType && x != null) { - return ((DecimalType) type).toBigDecimal().compareTo(numberToBigDecimal(x)) >= 0; + if (type != null && type instanceof DecimalType decimalType && x != null) { + return decimalType.toBigDecimal().compareTo(numberToBigDecimal(x)) >= 0; } else { return false; } } public static boolean operator_lessThan(Type type, Number x) { - if (type instanceof QuantityType && x instanceof QuantityType) { - return operator_lessThan((QuantityType) type, (QuantityType) x); + if (type instanceof QuantityType qtype && x instanceof QuantityType qx) { + return operator_lessThan(qtype, qx); } - if (type != null && type instanceof DecimalType && x != null) { - return ((DecimalType) type).toBigDecimal().compareTo(numberToBigDecimal(x)) < 0; + if (type != null && type instanceof DecimalType decimalType && x != null) { + return decimalType.toBigDecimal().compareTo(numberToBigDecimal(x)) < 0; } else { return false; } } public static boolean operator_lessEqualsThan(Type type, Number x) { - if (type instanceof QuantityType && x instanceof QuantityType) { - return operator_lessEqualsThan((QuantityType) type, (QuantityType) x); + if (type instanceof QuantityType qtype && x instanceof QuantityType qx) { + return operator_lessEqualsThan(qtype, qx); } - if (type != null && type instanceof DecimalType && x != null) { - return ((DecimalType) type).toBigDecimal().compareTo(numberToBigDecimal(x)) <= 0; + if (type != null && type instanceof DecimalType decimalType && x != null) { + return decimalType.toBigDecimal().compareTo(numberToBigDecimal(x)) <= 0; } else { return false; } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ActionClassLoader.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ActionClassLoader.java index ba00db0ca1f..49be1a7ec4f 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ActionClassLoader.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ActionClassLoader.java @@ -22,7 +22,7 @@ * * @author Kai Kreuzer - Initial contribution */ -final public class ActionClassLoader extends ClassLoader { +public final class ActionClassLoader extends ClassLoader { public ActionClassLoader(ClassLoader cl) { super(cl); @@ -31,8 +31,7 @@ public ActionClassLoader(ClassLoader cl) { @Override public Class loadClass(String name) throws ClassNotFoundException { try { - Class clazz = getParent().loadClass(name); - return clazz; + return getParent().loadClass(name); } catch (ClassNotFoundException e) { for (ActionService actionService : ScriptServiceUtil.getActionServices()) { if (actionService.getActionClassName().equals(name)) { diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext index d36e7f4c2d0..101297bf8bf 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext @@ -54,7 +54,7 @@ Image: Video: 'Video' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & ('icon=' icon=Icon)? & - ('url=' url=(STRING)) & ('encoding=' encoding=(STRING))? & + ('url=' url=STRING) & ('encoding=' encoding=STRING)? & ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? & ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? & ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)* ']'))? & @@ -62,7 +62,7 @@ Video: Chart: 'Chart' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & ('icon=' icon=Icon)? & - ('service=' service=(STRING))? & ('refresh=' refresh=INT)? & ('period=' period=ID) & + ('service=' service=STRING)? & ('refresh=' refresh=INT)? & ('period=' period=ID) & ('legend=' legend=BOOLEAN_OBJECT)? & ('forceasitem=' forceAsItem=BOOLEAN_OBJECT)? & ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? & ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? & @@ -72,7 +72,7 @@ Chart: Webview: 'Webview' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & ('icon=' icon=Icon)? & - ('height=' height=INT)? & ('url=' url=(STRING)) & + ('height=' height=INT)? & ('url=' url=STRING) & ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? & ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? & ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)* ']'))? & @@ -129,6 +129,7 @@ Colorpicker: Input: 'Input' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & ('icon=' icon=Icon)? & + ('inputHint=' inputHint=STRING)? & ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? & ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? & ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)* ']'))? & @@ -146,7 +147,7 @@ Mapping: cmd=Command '=' label=(ID | STRING); VisibilityRule: - (item=ID) (condition=("==" | ">" | "<" | ">=" | "<=" | "!=")) (sign=('-' | '+'))? (state=XState); + (item=ID) (condition=('==' | '>' | '<' | '>=' | '<=' | '!=')) (sign=('-' | '+'))? (state=XState); ItemRef: ID; @@ -155,10 +156,14 @@ GroupItemRef: ID; Icon returns ecore::EString: - STRING | ID; + STRING | (ID ':' (ID ':')?)? IconName; + +// Allow hyphen inside an icon name +IconName: + (ID '-')* ID; ColorArray: - ((item=ID)? (condition=("==" | ">" | "<" | ">=" | "<=" | "!="))? (sign=('-' | '+'))? (state=XState) '=')? + ((item=ID)? (condition=('==' | '>' | '<' | '>=' | '<=' | '!='))? (sign=('-' | '+'))? (state=XState) '=')? (arg=STRING); Command returns ecore::EString: @@ -170,6 +175,7 @@ Number returns ecore::EBigDecimal: XState returns ecore::EString: INT | ID | STRING | FLOAT; +@Override terminal ID: ('^'? ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*) | (('0'..'9')+ ('a'..'z' | 'A'..'Z' | '_') ('0'..'9' | 'a'..'z' | 'A'..'Z' | '_')*); diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/SitemapProviderImpl.java b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/SitemapProviderImpl.java index adbf92a5e3d..16242356752 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/SitemapProviderImpl.java +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/SitemapProviderImpl.java @@ -97,8 +97,8 @@ public void modelChanged(String modelName, EventType type) { } else { EObject sitemap = modelRepo.getModel(modelName); // if the sitemap file is empty it will not be in the repo and thus there is no need to cache it here - if (sitemap instanceof Sitemap) { - sitemapModelCache.put(modelName, (Sitemap) sitemap); + if (sitemap instanceof Sitemap sitemap1) { + sitemapModelCache.put(modelName, sitemap1); } } } diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend index 17df83991a3..9aa2c13a320 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend @@ -23,6 +23,9 @@ import org.openhab.core.model.sitemap.sitemap.SitemapPackage import org.openhab.core.model.sitemap.sitemap.Widget import org.eclipse.xtext.validation.Check import java.math.BigDecimal +import org.openhab.core.model.sitemap.sitemap.Input +import org.eclipse.xtext.nodemodel.INode +import org.eclipse.xtext.nodemodel.util.NodeModelUtils //import org.eclipse.xtext.validation.Check /** @@ -32,6 +35,8 @@ import java.math.BigDecimal */ class SitemapValidator extends AbstractSitemapValidator { + val ALLOWED_HINTS = #["text", "number", "date", "time", "datetime"] + @Check def void checkFramesInFrame(Frame frame) { for (Widget w : frame.children) { @@ -102,4 +107,14 @@ class SitemapValidator extends AbstractSitemapValidator { SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__MIN_VALUE)); } } + + @Check + def void checkInputHintParameter(Input i) { + if (i.inputHint !== null && !ALLOWED_HINTS.contains(i.inputHint)) { + val node = NodeModelUtils.getNode(i) + val line = node.getStartLine() + error("Input on item '" + i.item + "' has invalid inputHint '" + i.inputHint + "' at line " + line, + SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.INPUT__INPUT_HINT)) + } + } } diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/GenericThingProvider.xtend b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/GenericThingProvider.xtend index b8559cc88da..e07b8fd5dc4 100644 --- a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/GenericThingProvider.xtend +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/GenericThingProvider.xtend @@ -602,19 +602,20 @@ class GenericThingProvider extends AbstractProviderLazyNullness implement } def private createThingsFromModelForThingHandlerFactory(String modelName, ThingHandlerFactory factory) { - if (!loadedXmlThingTypes.contains(factory.bundleName)) { + if (!loadedXmlThingTypes.contains(factory.bundleName) || modelRepository == null) { return } val oldThings = thingsMap.get(modelName).clone val newThings = newArrayList() - if (modelRepository !== null) { - val model = modelRepository.getModel(modelName) as ThingModel - if (model !== null) { - flattenModelThings(model.things).forEach [ - createThing(newThings, factory) - ] - } + + val model = modelRepository.getModel(modelName) as ThingModel + if (model !== null) { + flattenModelThings(model.things).forEach [ + createThing(newThings, factory) + ] } + thingsMap.put(modelName, newThings) + newThings.forEach [ newThing | val oldThing = oldThings.findFirst[it.UID == newThing.UID] if (oldThing !== null) { @@ -624,7 +625,6 @@ class GenericThingProvider extends AbstractProviderLazyNullness implement } } else { logger.debug("Adding thing '{}' from model '{}'.", newThing.UID, modelName); - thingsMap.get(modelName).add(newThing) newThing.notifyListenersAboutAddedElement } ] diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java index aaf2b5a458c..132660ea71b 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java @@ -14,6 +14,8 @@ import java.time.ZonedDateTime; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; /** @@ -36,6 +38,7 @@ * @author Lyubomir Papazov - Deprecate methods using java.util and add methods * that use Java8's ZonedDateTime */ +@NonNullByDefault public class FilterCriteria { /** Enumeration with all possible compare options */ @@ -65,13 +68,13 @@ public enum Ordering { } /** filter result to only contain entries for the given item */ - private String itemName; + private @Nullable String itemName; /** filter result to only contain entries that are equal to or after the given datetime */ - private ZonedDateTime beginDate; + private @Nullable ZonedDateTime beginDate; /** filter result to only contain entries that are equal to or before the given datetime */ - private ZonedDateTime endDate; + private @Nullable ZonedDateTime endDate; /** return the result list from starting index pageNumber*pageSize only */ private int pageNumber = 0; @@ -86,17 +89,17 @@ public enum Ordering { private Ordering ordering = Ordering.DESCENDING; /** Filter result to only contain entries that evaluate to true with the given operator and state */ - private State state; + private @Nullable State state; - public String getItemName() { + public @Nullable String getItemName() { return itemName; } - public ZonedDateTime getBeginDate() { + public @Nullable ZonedDateTime getBeginDate() { return beginDate; } - public ZonedDateTime getEndDate() { + public @Nullable ZonedDateTime getEndDate() { return endDate; } @@ -116,7 +119,7 @@ public Ordering getOrdering() { return ordering; } - public State getState() { + public @Nullable State getState() { return state; } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java index aab98c2ef56..a6c9bc24dd5 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java @@ -15,7 +15,6 @@ import java.time.ZonedDateTime; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.items.Item; import org.openhab.core.types.State; /** @@ -23,7 +22,8 @@ * with a certain state at a given point in time. * *

- * Note that this interface does not extend {@link Item} as the persistence services could not provide an implementation + * Note that this interface does not extend {@link org.openhab.core.items.Item} as the persistence services could not + * provide an implementation * that correctly implement getAcceptedXTypes() and getGroupNames(). * * @author Kai Kreuzer - Initial contribution diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java index 10c577a36ed..2feb4f4660f 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java @@ -49,11 +49,11 @@ public interface ModifiablePersistenceService extends QueryablePersistenceServic /** * Removes data associated with an item from a persistence service. * If all data is removed for the specified item, the persistence service should free any resources associated with - * the item (eg. remove any tables or delete files from the storage). + * the item (e.g. remove any tables or delete files from the storage). * * @param filter the filter to apply to the data removal. ItemName can not be null. * @return true if the query executed successfully - * @throws {@link IllegalArgumentException} if item name is null. + * @throws IllegalArgumentException if item name is null. */ boolean remove(FilterCriteria filter) throws IllegalArgumentException; } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java index 30b5f935595..36d462333d4 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.filter.PersistenceFilter; import org.openhab.core.persistence.strategy.PersistenceStrategy; /** @@ -26,12 +27,8 @@ * @author Markus Rathgeb - Initial contribution */ @NonNullByDefault -public class PersistenceItemConfiguration { - - private final List items; - private final @Nullable String alias; - private final List strategies; - private final List filters; +public record PersistenceItemConfiguration(List items, @Nullable String alias, + List strategies, List filters) { public PersistenceItemConfiguration(final List items, @Nullable final String alias, @Nullable final List strategies, @Nullable final List filters) { @@ -40,26 +37,4 @@ public PersistenceItemConfiguration(final List items, @Nullab this.strategies = Objects.requireNonNullElse(strategies, List.of()); this.filters = Objects.requireNonNullElse(filters, List.of()); } - - public List getItems() { - return items; - } - - public @Nullable String getAlias() { - return alias; - } - - public List getStrategies() { - return strategies; - } - - public List getFilters() { - return filters; - } - - @Override - public String toString() { - return String.format("%s [items=%s, alias=%s, strategies=%s, filters=%s]", getClass().getSimpleName(), items, - alias, strategies, filters); - } } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceManager.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceManager.java deleted file mode 100644 index 6e8d5b80c5a..00000000000 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceManager.java +++ /dev/null @@ -1,39 +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.persistence; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * A persistence manager service which could be used to start event handling or supply configuration for persistence - * services. - * - * @author Markus Rathgeb - Initial contribution - */ -@NonNullByDefault -public interface PersistenceManager { - /** - * Add a configuration for a persistence service. - * - * @param dbId the database id used by the persistence service - * @param config the configuration of the persistence service - */ - void addConfig(String dbId, PersistenceServiceConfiguration config); - - /** - * Remove a configuration for a persistence service. - * - * @param dbId the database id used by the persistence service - */ - void removeConfig(String dbId); -} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java index 6b8c2dc56c3..f07bdcfec6f 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java @@ -15,7 +15,6 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.items.Item; /** * A queryable persistence service which can be used to store and retrieve @@ -37,7 +36,8 @@ public interface QueryablePersistenceService extends PersistenceService { /** * Returns a set of {@link PersistenceItemInfo} about items that are stored in the persistence service. This allows - * the persistence service to return information about items that are no long available as an {@link Item} in + * the persistence service to return information about items that are no long available as an + * {@link org.openhab.core.items.Item} in * openHAB. If it is not possible to retrieve the information an empty set should be returned. * * @return a set of information about the persisted items diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java index c493405766b..949805fe686 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java @@ -45,9 +45,9 @@ public ItemHistoryDTO() { public void addData(Long time, State state) { HistoryDataBean newVal = new HistoryDataBean(); newVal.time = time; - if (state instanceof QuantityType) { + if (state instanceof QuantityType quantityState) { // we strip the unit from the state, since historic item states are expected to be all in the default unit - newVal.state = ((QuantityType) state).toBigDecimal().toString(); + newVal.state = quantityState.toBigDecimal().toString(); } else { newVal.state = state.toString(); } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Tilt.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceCronStrategyDTO.java similarity index 59% rename from bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Tilt.java rename to bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceCronStrategyDTO.java index 23a86709233..50c098618c7 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Tilt.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceCronStrategyDTO.java @@ -10,17 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.semantics.model.point; +package org.openhab.core.persistence.dto; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; /** - * This class defines a Tilt. + * The {@link PersistenceCronStrategyDTO} is used for transferring persistence cron + * strategies * - * @author Generated from generateTagClasses.groovy - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Point_Status_Tilt", label = "Tilt", synonyms = "", description = "") -public interface Tilt extends Status { +public class PersistenceCronStrategyDTO { + public String name = ""; + public String cronExpression = ""; } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java new file mode 100644 index 00000000000..097e92204d7 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.dto; + +import java.math.BigDecimal; +import java.util.List; + +/** + * The {@link org.openhab.core.persistence.dto.PersistenceFilterDTO} is used for transferring persistence filter + * configurations + * + * @author Jan N. Klug - Initial contribution + */ +public class PersistenceFilterDTO { + public String name; + + // threshold and time + public BigDecimal value; + + // threshold + public Boolean relative; + + // threshold, include/exclude + public String unit; + + // include/exclude + public BigDecimal lower; + public BigDecimal upper; + + // equals/not equals + public List values; + + // equals/not equals, include/exclude + public Boolean inverted; +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceItemConfigurationDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceItemConfigurationDTO.java new file mode 100644 index 00000000000..e1b0e082e60 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceItemConfigurationDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.dto; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO} is used for transferring persistence + * item configurations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceItemConfigurationDTO { + public Collection items = List.of(); + public Collection strategies = List.of(); + public Collection filters = List.of(); + public @Nullable String alias; +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java new file mode 100644 index 00000000000..e4faa9ed20f --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.dto; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PersistenceServiceConfigurationDTO} is used for transferring persistence service configurations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceServiceConfigurationDTO { + public String serviceId = ""; + public Collection configs = List.of(); + public Collection defaults = List.of(); + public Collection cronStrategies = List.of(); + public Collection thresholdFilters = List.of(); + public Collection timeFilters = List.of(); + public Collection equalsFilters = List.of(); + public Collection includeFilters = List.of(); + + public boolean editable = false; +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceStrategyDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceStrategyDTO.java new file mode 100644 index 00000000000..67bc881c0f4 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceStrategyDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PersistenceStrategyDTO} is used for transferring persistence strategies. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceStrategyDTO { + public String type; + public String configuration; + + // do not remove - needed by GSON + PersistenceStrategyDTO() { + this("", ""); + } + + public PersistenceStrategyDTO(String type, String configuration) { + this.type = type; + this.configuration = configuration; + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java index 10d3ea86723..a1d536c1d47 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java @@ -14,8 +14,9 @@ import java.math.BigDecimal; import java.math.MathContext; -import java.time.Instant; +import java.time.Duration; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -51,8 +52,6 @@ @Component(immediate = true) public class PersistenceExtensions { - private static final BigDecimal BIG_DECIMAL_TWO = BigDecimal.valueOf(2); - private static PersistenceServiceRegistry registry; @Activate @@ -449,7 +448,7 @@ public static boolean updatedBetween(Item item, ZonedDateTime begin, ZonedDateTi private static @Nullable HistoricItem internalMaximum(final Item item, ZonedDateTime begin, @Nullable ZonedDateTime end, String serviceId) { - Iterable result = getAllStatesBetween(item, begin, end, serviceId); + Iterable result = getAllStatesBetweenWithBoundaries(item, begin, end, serviceId); Iterator it = result.iterator(); HistoricItem maximumHistoricItem = null; // include current state only if no end time is given @@ -530,7 +529,7 @@ public static boolean updatedBetween(Item item, ZonedDateTime begin, ZonedDateTi private static @Nullable HistoricItem internalMinimum(final Item item, ZonedDateTime begin, @Nullable ZonedDateTime end, String serviceId) { - Iterable result = getAllStatesBetween(item, begin, end, serviceId); + Iterable result = getAllStatesBetweenWithBoundaries(item, begin, end, serviceId); Iterator it = result.iterator(); HistoricItem minimumHistoricItem = null; DecimalType minimum = end == null ? item.getStateAs(DecimalType.class) : null; @@ -610,9 +609,9 @@ public static boolean updatedBetween(Item item, ZonedDateTime begin, ZonedDateTi private static @Nullable DecimalType internalVariance(Item item, ZonedDateTime begin, @Nullable ZonedDateTime end, String serviceId) { - Iterable result = getAllStatesBetween(item, begin, end, serviceId); + Iterable result = getAllStatesBetweenWithBoundaries(item, begin, end, serviceId); Iterator it = result.iterator(); - DecimalType averageSince = internalAverage(item, it, true); + DecimalType averageSince = internalAverage(item, it, end); if (averageSince != null) { BigDecimal average = averageSince.toBigDecimal(), sum = BigDecimal.ZERO; @@ -767,9 +766,7 @@ public static boolean updatedBetween(Item item, ZonedDateTime begin, ZonedDateTi * calculation. */ public static @Nullable DecimalType averageSince(Item item, ZonedDateTime timestamp, String serviceId) { - Iterable result = getAllStatesBetween(item, timestamp, null, serviceId); - Iterator it = result.iterator(); - return internalAverage(item, it, true); + return averageBetween(item, timestamp, null, serviceId); } /** @@ -786,54 +783,47 @@ public static boolean updatedBetween(Item item, ZonedDateTime begin, ZonedDateTi */ public static @Nullable DecimalType averageBetween(Item item, ZonedDateTime begin, ZonedDateTime end, String serviceId) { - Iterable result = getAllStatesBetween(item, begin, end, serviceId); + Iterable result = getAllStatesBetweenWithBoundaries(item, begin, end, serviceId); Iterator it = result.iterator(); - return internalAverage(item, it, false); + return internalAverage(item, it, end); } @SuppressWarnings("null") - private static @Nullable DecimalType internalAverage(Item item, Iterator it, boolean includeNow) { - BigDecimal total = BigDecimal.ZERO; + private static @Nullable DecimalType internalAverage(Item item, Iterator it, ZonedDateTime endTime) { + if (endTime == null) { + endTime = ZonedDateTime.now(); + } + + BigDecimal sum = BigDecimal.ZERO; - DecimalType lastState = null, thisState; - BigDecimal firstTimestamp = null, lastTimestamp = null, thisTimestamp = null; + HistoricItem lastItem = null; + ZonedDateTime firstTimestamp = null; while (it.hasNext()) { HistoricItem thisItem = it.next(); - thisState = thisItem.getState().as(DecimalType.class); - if (thisState != null) { - thisTimestamp = BigDecimal.valueOf(thisItem.getTimestamp().toInstant().toEpochMilli()); - if (firstTimestamp == null) { - firstTimestamp = thisTimestamp; - } else { - BigDecimal average = thisState.toBigDecimal().add(lastState.toBigDecimal()).divide(BIG_DECIMAL_TWO, - MathContext.DECIMAL64); - BigDecimal timeSpan = thisTimestamp.subtract(lastTimestamp, MathContext.DECIMAL64); - total = total.add(average.multiply(timeSpan, MathContext.DECIMAL64)); - } - lastTimestamp = thisTimestamp; - lastState = thisState; + if (lastItem != null) { + BigDecimal value = lastItem.getState().as(DecimalType.class).toBigDecimal(); + BigDecimal weight = BigDecimal + .valueOf(Duration.between(lastItem.getTimestamp(), thisItem.getTimestamp()).toMillis()); + sum = sum.add(value.multiply(weight)); } - } - if (lastState != null && includeNow) { - thisState = item.getStateAs(DecimalType.class); - if (thisState != null) { - thisTimestamp = BigDecimal.valueOf(Instant.now().toEpochMilli()); - BigDecimal average = thisState.toBigDecimal().add(lastState.toBigDecimal()).divide(BIG_DECIMAL_TWO, - MathContext.DECIMAL64); - BigDecimal timeSpan = thisTimestamp.subtract(lastTimestamp, MathContext.DECIMAL64); - total = total.add(average.multiply(timeSpan, MathContext.DECIMAL64)); + if (firstTimestamp == null) { + firstTimestamp = thisItem.getTimestamp(); } + lastItem = thisItem; } - if (thisTimestamp != null) { - BigDecimal timeSpan = thisTimestamp.subtract(firstTimestamp, MathContext.DECIMAL64); - // avoid ArithmeticException if timeSpan is zero - if (!BigDecimal.ZERO.equals(timeSpan)) { - BigDecimal average = total.divide(timeSpan, MathContext.DECIMAL64); - return new DecimalType(average); - } + if (lastItem != null) { + BigDecimal value = lastItem.getState().as(DecimalType.class).toBigDecimal(); + BigDecimal weight = BigDecimal.valueOf(Duration.between(lastItem.getTimestamp(), endTime).toMillis()); + sum = sum.add(value.multiply(weight)); + } + + if (firstTimestamp != null) { + BigDecimal totalDuration = BigDecimal.valueOf(Duration.between(firstTimestamp, endTime).toMillis()); + return totalDuration.signum() == 0 ? null + : new DecimalType(sum.divide(totalDuration, MathContext.DECIMAL64)); } return null; @@ -1140,8 +1130,8 @@ public static long countBetween(Item item, ZonedDateTime begin, @Nullable ZonedD */ public static long countBetween(Item item, ZonedDateTime begin, @Nullable ZonedDateTime end, String serviceId) { Iterable historicItems = getAllStatesBetween(item, begin, end, serviceId); - if (historicItems instanceof Collection) { - return ((Collection) historicItems).size(); + if (historicItems instanceof Collection collection) { + return collection.size(); } else { return StreamSupport.stream(historicItems.spliterator(), false).count(); } @@ -1218,15 +1208,10 @@ public static long countStateChangesBetween(Item item, ZonedDateTime begin, @Nul } private static @Nullable PersistenceService getService(String serviceId) { - PersistenceService service = null; if (registry != null) { - if (serviceId != null) { - service = registry.get(serviceId); - } else { - service = registry.getDefault(); - } + return serviceId != null ? registry.get(serviceId) : registry.getDefault(); } - return service; + return null; } private static @Nullable String getDefaultServiceId() { @@ -1245,11 +1230,65 @@ public static long countStateChangesBetween(Item item, ZonedDateTime begin, @Nul return null; } - private static Iterable getAllStatesBetween(Item item, ZonedDateTime begin, + /** + * Retrieves the historic items for a given item since a certain point in time. + * The default persistence service is used. + * + * @param item the item for which to retrieve the historic item + * @param timestamp the point in time from which to retrieve the states + * @return the historic items since the given point in time, or null if no historic items could be + * found. + */ + public static Iterable getAllStatesSince(Item item, ZonedDateTime timestamp) { + return getAllStatesBetween(item, timestamp, null); + } + + /** + * Retrieves the historic items for a given item since a certain point in time + * through a {@link PersistenceService} identified by the serviceId. + * + * @param item the item for which to retrieve the historic item + * @param timestamp the point in time from which to retrieve the states + * @param serviceId the name of the {@link PersistenceService} to use + * @return the historic items since the given point in time, or null if no historic items could be + * found or if the provided serviceId does not refer to an available + * {@link QueryablePersistenceService} + */ + public static Iterable getAllStatesSince(Item item, ZonedDateTime timestamp, String serviceId) { + return getAllStatesBetween(item, timestamp, null, serviceId); + } + + /** + * Retrieves the historic items for a given item beetween two certain points in time. + * The default persistence service is used. + * + * @param item the item for which to retrieve the historic item + * @param begin the point in time from which to retrieve the states + * @param end the point in time to which to retrieve the states + * @return the historic items between the given points in time, or null if no historic items could be + * found. + */ + public static Iterable getAllStatesBetween(Item item, ZonedDateTime begin, + @Nullable ZonedDateTime end) { + return getAllStatesBetween(item, begin, end, getDefaultServiceId()); + } + + /** + * Retrieves the historic items for a given item beetween two certain points in time + * through a {@link PersistenceService} identified by the serviceId. + * + * @param item the item for which to retrieve the historic item + * @param begin the point in time from which to retrieve the states + * @param end the point in time to which to retrieve the states + * @param serviceId the name of the {@link PersistenceService} to use + * @return the historic items between the given points in time, or null if no historic items could be + * found or if the provided serviceId does not refer to an available + * {@link QueryablePersistenceService} + */ + public static Iterable getAllStatesBetween(Item item, ZonedDateTime begin, @Nullable ZonedDateTime end, String serviceId) { PersistenceService service = getService(serviceId); - if (service instanceof QueryablePersistenceService) { - QueryablePersistenceService qService = (QueryablePersistenceService) service; + if (service instanceof QueryablePersistenceService qService) { FilterCriteria filter = new FilterCriteria(); filter.setBeginDate(begin); if (end != null) { @@ -1257,6 +1296,7 @@ private static Iterable getAllStatesBetween(Item item, ZonedDateTi } filter.setItemName(item.getName()); filter.setOrdering(Ordering.ASCENDING); + return qService.query(filter); } else { LoggerFactory.getLogger(PersistenceExtensions.class) @@ -1265,6 +1305,41 @@ private static Iterable getAllStatesBetween(Item item, ZonedDateTi } } + private static Iterable getAllStatesBetweenWithBoundaries(Item item, ZonedDateTime begin, + @Nullable ZonedDateTime end, String serviceId) { + Iterable betweenItems = getAllStatesBetween(item, begin, end, serviceId); + + List betweenItemsList = new ArrayList<>(); + for (HistoricItem historicItem : betweenItems) { + betweenItemsList.add(historicItem); + } + + // add HistoricItem at begin + if (betweenItemsList.isEmpty() || !betweenItemsList.get(0).getTimestamp().equals(begin)) { + if (!begin.isAfter(ZonedDateTime.now())) { + HistoricItem first = historicState(item, begin, serviceId); + + if (first != null) { + betweenItemsList.add(0, new RetimedHistoricItem(first, begin)); + } + } + } + + // add HistoricItem at end + if (end != null && !end.isAfter(ZonedDateTime.now())) { + if (betweenItemsList.isEmpty() + || !betweenItemsList.get(betweenItemsList.size() - 1).getTimestamp().equals(end)) { + HistoricItem last = historicState(item, end, serviceId); + + if (last != null) { + betweenItemsList.add(new RetimedHistoricItem(last, end)); + } + } + } + + return betweenItemsList; + } + private static @Nullable HistoricItem historicItemOrCurrentState(Item item, HistoricItem historicItem, DecimalType value) { if (historicItem == null && value != null) { @@ -1290,4 +1365,35 @@ public String getName() { return historicItem; } } + + private static class RetimedHistoricItem implements HistoricItem { + + private final HistoricItem originItem; + private final ZonedDateTime timestamp; + + public RetimedHistoricItem(HistoricItem originItem, ZonedDateTime timestamp) { + this.originItem = originItem; + this.timestamp = timestamp; + } + + @Override + public ZonedDateTime getTimestamp() { + return timestamp; + } + + @Override + public State getState() { + return originItem.getState(); + } + + @Override + public String getName() { + return originItem.getName(); + } + + @Override + public String toString() { + return "RetimedHistoricItem [originItem=" + originItem + ", timestamp=" + timestamp + "]"; + } + } } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceEqualsFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceEqualsFilter.java new file mode 100644 index 00000000000..fdceb2863a8 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceEqualsFilter.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import java.util.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; + +/** + * The {@link PersistenceEqualsFilter} is a filter that allows only specific values to pass + *

+ * The filter returns {@code false} if the string representation of the item's state is not in the given list + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceEqualsFilter extends PersistenceFilter { + private final Collection values; + private final boolean inverted; + + public PersistenceEqualsFilter(String name, Collection values, boolean inverted) { + super(name); + this.values = values; + this.inverted = inverted; + } + + public Collection getValues() { + return values; + } + + public boolean getInverted() { + return inverted; + } + + @Override + public boolean apply(Item item) { + return values.contains(item.getState().toFullString()) != inverted; + } + + @Override + public void persisted(Item item) { + } + + @Override + public String toString() { + return String.format("%s [name=%s, value=%s, inverted=]", getClass().getSimpleName(), getName(), values); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceFilter.java new file mode 100644 index 00000000000..8ee2c8559c2 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceFilter.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.persistence.filter; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; + +/** + * The {@link PersistenceFilter} is the base class for implementing persistence filters. + * + * @author Markus Rathgeb - Initial contribution + */ +@NonNullByDefault +public abstract class PersistenceFilter { + private final String name; + + public PersistenceFilter(final String name) { + this.name = name; + } + + /** + * Get the name of this filter + * + * @return a unique name + */ + public String getName() { + return name; + } + + /** + * Apply this filter to an item + * + * @param item the item to check + * @return true if the filter allows persisting this value + */ + public abstract boolean apply(Item item); + + /** + * Notify filter that item was persisted + * + * @param item the persisted item + */ + public abstract void persisted(Item item); + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + name.hashCode(); + return result; + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof PersistenceFilter)) { + return false; + } + final PersistenceFilter other = (PersistenceFilter) obj; + return Objects.equals(name, other.name); + } + + @Override + public String toString() { + return String.format("%s [name=%s]", getClass().getSimpleName(), name); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceIncludeFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceIncludeFilter.java new file mode 100644 index 00000000000..c25aa181a3d --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceIncludeFilter.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PersistenceIncludeFilter} is a filter that allows only specific values to pass + *

+ * The filter returns {@code false} if the string representation of the item's state is not in the given list + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceIncludeFilter extends PersistenceFilter { + private final Logger logger = LoggerFactory.getLogger(PersistenceIncludeFilter.class); + + private final BigDecimal lower; + private final BigDecimal upper; + private final String unit; + private final boolean inverted; + + public PersistenceIncludeFilter(String name, BigDecimal lower, BigDecimal upper, String unit, boolean inverted) { + super(name); + this.lower = lower; + this.upper = upper; + this.unit = unit; + this.inverted = inverted; + } + + public BigDecimal getLower() { + return lower; + } + + public BigDecimal getUpper() { + return upper; + } + + public String getUnit() { + return unit; + } + + public boolean getInverted() { + return inverted; + } + + @Override + public boolean apply(Item item) { + State state = item.getState(); + BigDecimal compareValue = null; + if (state instanceof DecimalType decimalType) { + compareValue = decimalType.toBigDecimal(); + } else if (state instanceof QuantityType quantityType) { + if (!unit.isBlank()) { + QuantityType convertedQuantity = quantityType.toUnit(unit); + if (convertedQuantity != null) { + compareValue = convertedQuantity.toBigDecimal(); + } + } + } + if (compareValue == null) { + logger.warn("Cannot compare {} to range {}{} - {}{} ", state, lower, unit, upper, unit); + return true; + } + + if (inverted) { + return compareValue.compareTo(lower) <= 0 || compareValue.compareTo(upper) >= 0; + } else { + return compareValue.compareTo(lower) >= 0 && compareValue.compareTo(upper) <= 0; + } + } + + @Override + public void persisted(Item item) { + } + + @Override + public String toString() { + return String.format("%s [name=%s, lower=%s, upper=%s, unit=%s, inverted=%b]", getClass().getSimpleName(), + getName(), lower, upper, unit, inverted); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java new file mode 100644 index 00000000000..269c40db9d8 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Map; + +import javax.measure.UnconvertibleException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PersistenceThresholdFilter} is a filter to prevent persistence based on a threshold. + *

+ * The filter returns {@code false} if the new value deviates by less than {@link #value}. If unit is "%" is + * {@code true}, the filter returns {@code false} if the relative deviation is less than {@link #value}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceThresholdFilter extends PersistenceFilter { + private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); + + private final Logger logger = LoggerFactory.getLogger(PersistenceThresholdFilter.class); + + private final BigDecimal value; + private final String unit; + private final boolean relative; + + private final transient Map valueCache = new HashMap<>(); + + public PersistenceThresholdFilter(String name, BigDecimal value, String unit, boolean relative) { + super(name); + this.value = value; + this.unit = unit; + this.relative = relative; + } + + public BigDecimal getValue() { + return value; + } + + public String getUnit() { + return unit; + } + + public boolean isRelative() { + return relative; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean apply(Item item) { + String itemName = item.getName(); + State state = item.getState(); + if (!(state instanceof DecimalType || state instanceof QuantityType)) { + return true; + } + + State cachedState = valueCache.get(itemName); + + if (cachedState == null || !state.getClass().equals(cachedState.getClass())) { + return true; + } + + if (state instanceof DecimalType) { + BigDecimal oldState = ((DecimalType) cachedState).toBigDecimal(); + BigDecimal delta = oldState.subtract(((DecimalType) state).toBigDecimal()); + if (relative && !BigDecimal.ZERO.equals(oldState)) { + delta = delta.multiply(HUNDRED).divide(oldState, 2, RoundingMode.HALF_UP); + } + return delta.abs().compareTo(value) > 0; + } else { + try { + QuantityType oldState = (QuantityType) cachedState; + QuantityType delta = oldState.subtract((QuantityType) state); + if (relative) { + if (BigDecimal.ZERO.equals(oldState.toBigDecimal())) { + // value is different and old value is 0 -> always above relative threshold + return true; + } else { + // calculate percent + delta = delta.multiply(HUNDRED).divide(oldState); + } + } else if (!unit.isBlank()) { + // consider unit only if not relative threshold + delta = delta.toUnit(unit); + if (delta == null) { + throw new UnconvertibleException(""); + } + } + + return delta.toBigDecimal().abs().compareTo(value) > 0; + } catch (UnconvertibleException e) { + logger.warn("Cannot compare {} to {}", cachedState, state); + return true; + } + } + } + + @Override + public void persisted(Item item) { + valueCache.put(item.getName(), item.getState()); + } + + @Override + public String toString() { + return String.format("%s [name=%s, value=%s, unit=%s, relative=%b]", getClass().getSimpleName(), getName(), + value, unit, relative); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java new file mode 100644 index 00000000000..524ef2bc342 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.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.persistence.filter; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; + +/** + * The {@link PersistenceTimeFilter} is a filter to prevent persistence base on intervals. + *

+ * The filter returns {@code false} if the time between now and the time of the last persisted value is less than + * {@link #duration} {@link #unit} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceTimeFilter extends PersistenceFilter { + private final int value; + private final String unit; + + private transient @Nullable Duration duration; + private final transient Map nextPersistenceTimes = new HashMap<>(); + + public PersistenceTimeFilter(String name, int value, String unit) { + super(name); + this.value = value; + this.unit = unit; + } + + public int getValue() { + return value; + } + + public String getUnit() { + return unit; + } + + @Override + public boolean apply(Item item) { + String itemName = item.getName(); + + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime nextPersistenceTime = nextPersistenceTimes.get(itemName); + + return nextPersistenceTime == null || !now.isBefore(nextPersistenceTime); + } + + @Override + public void persisted(Item item) { + Duration duration = this.duration; + if (duration == null) { + duration = switch (unit) { + case "m" -> Duration.of(value, ChronoUnit.MINUTES); + case "h" -> Duration.of(value, ChronoUnit.HOURS); + case "d" -> Duration.of(value, ChronoUnit.DAYS); + default -> Duration.of(value, ChronoUnit.SECONDS); + }; + + this.duration = duration; + } + nextPersistenceTimes.put(item.getName(), ZonedDateTime.now().plus(duration)); + } + + @Override + public String toString() { + return String.format("%s [name=%s, value=%s, unit=%s]", getClass().getSimpleName(), getName(), value, unit); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistItemsJob.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistItemsJob.java deleted file mode 100644 index 586525ad9a2..00000000000 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistItemsJob.java +++ /dev/null @@ -1,91 +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.persistence.internal; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.items.Item; -import org.openhab.core.persistence.PersistenceItemConfiguration; -import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceConfiguration; -import org.openhab.core.persistence.strategy.PersistenceStrategy; -import org.openhab.core.scheduler.SchedulerRunnable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of a persistence job that could be executed e.g. for specific cron expressions. - * - * @author Kai Kreuzer - Initial contribution - * @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage. - */ -@NonNullByDefault -public class PersistItemsJob implements SchedulerRunnable { - - private final Logger logger = LoggerFactory.getLogger(PersistItemsJob.class); - - private final PersistenceManagerImpl manager; - private final String dbId; - private final String strategyName; - - public PersistItemsJob(final PersistenceManagerImpl manager, final String dbId, final String strategyName) { - this.manager = manager; - this.dbId = dbId; - this.strategyName = strategyName; - } - - @Override - public void run() { - synchronized (manager.persistenceServiceConfigs) { - final PersistenceService persistenceService = manager.persistenceServices.get(dbId); - final PersistenceServiceConfiguration config = manager.persistenceServiceConfigs.get(dbId); - - if (persistenceService != null) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config.getDefaults(), itemConfig, strategyName)) { - for (Item item : manager.getAllItems(itemConfig)) { - long startTime = System.nanoTime(); - persistenceService.store(item, itemConfig.getAlias()); - logger.trace("Storing item '{}' with persistence service '{}' took {}ms", item.getName(), - dbId, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); - } - } - - } - } - } - } - - private boolean hasStrategy(List defaults, PersistenceItemConfiguration config, - String strategyName) { - // check if the strategy is directly defined on the config - for (PersistenceStrategy strategy : config.getStrategies()) { - if (strategyName.equals(strategy.getName())) { - return true; - } - } - // if no strategies are given, check the default strategies to use - return config.getStrategies().isEmpty() && isDefault(defaults, strategyName); - } - - private boolean isDefault(List defaults, String strategyName) { - for (PersistenceStrategy strategy : defaults) { - if (strategy.getName().equals(strategyName)) { - return true; - } - } - return false; - } -} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManager.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManager.java new file mode 100644 index 00000000000..9f0fc9b94ab --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManager.java @@ -0,0 +1,465 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.internal; + +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.common.SafeCaller; +import org.openhab.core.items.GenericItem; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.ItemRegistryChangeListener; +import org.openhab.core.items.StateChangeListener; +import org.openhab.core.persistence.FilterCriteria; +import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.PersistenceService; +import org.openhab.core.persistence.QueryablePersistenceService; +import org.openhab.core.persistence.config.PersistenceAllConfig; +import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.config.PersistenceGroupConfig; +import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistryChangeListener; +import org.openhab.core.persistence.strategy.PersistenceCronStrategy; +import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.ReadyMarkerFilter; +import org.openhab.core.service.ReadyService; +import org.openhab.core.service.ReadyService.ReadyTracker; +import org.openhab.core.service.StartLevelService; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements a persistence manager to manage all persistence services etc. + * + * @author Kai Kreuzer - Initial contribution + * @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage. + * @author Jan N. Klug - Refactored to use service configuration registry + */ +@Component(immediate = true) +@NonNullByDefault +public class PersistenceManager implements ItemRegistryChangeListener, StateChangeListener, ReadyTracker, + PersistenceServiceConfigurationRegistryChangeListener { + + private final Logger logger = LoggerFactory.getLogger(PersistenceManager.class); + + private final ReadyMarker marker = new ReadyMarker("persistence", "restore"); + + // the scheduler used for timer events + private final CronScheduler scheduler; + private final ItemRegistry itemRegistry; + private final SafeCaller safeCaller; + private final ReadyService readyService; + private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry; + + private volatile boolean started = false; + + private final Map persistenceServiceContainers = new ConcurrentHashMap<>(); + + @Activate + public PersistenceManager(final @Reference CronScheduler scheduler, final @Reference ItemRegistry itemRegistry, + final @Reference SafeCaller safeCaller, final @Reference ReadyService readyService, + final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry) { + this.scheduler = scheduler; + this.itemRegistry = itemRegistry; + this.safeCaller = safeCaller; + this.readyService = readyService; + this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry; + + persistenceServiceConfigurationRegistry.addRegistryChangeListener(this); + readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) + .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL))); + } + + @Deactivate + protected void deactivate() { + itemRegistry.removeRegistryChangeListener(this); + persistenceServiceConfigurationRegistry.removeRegistryChangeListener(this); + started = false; + + persistenceServiceContainers.values().forEach(PersistenceServiceContainer::cancelPersistJobs); + + // remove item state change listeners + itemRegistry.stream().filter(GenericItem.class::isInstance) + .forEach(item -> ((GenericItem) item).removeStateChangeListener(this)); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addPersistenceService(PersistenceService persistenceService) { + String serviceId = persistenceService.getId(); + logger.debug("Initializing {} persistence service.", serviceId); + PersistenceServiceContainer container = new PersistenceServiceContainer(persistenceService, + persistenceServiceConfigurationRegistry.get(serviceId)); + + PersistenceServiceContainer oldContainer = persistenceServiceContainers.put(serviceId, container); + + if (oldContainer != null) { // cancel all jobs if the persistence service is set and an old configuration is + // already present + oldContainer.cancelPersistJobs(); + } + + if (started) { + startEventHandling(container); + } + } + + protected void removePersistenceService(PersistenceService persistenceService) { + PersistenceServiceContainer container = persistenceServiceContainers.remove(persistenceService.getId()); + if (container != null) { + container.cancelPersistJobs(); + } + } + + /** + * Calls all persistence services which use change or update policy for the given item + * + * @param item the item to persist + * @param changed true, if it has the change strategy, false otherwise + */ + private void handleStateEvent(Item item, boolean changed) { + PersistenceStrategy changeStrategy = changed ? PersistenceStrategy.Globals.CHANGE + : PersistenceStrategy.Globals.UPDATE; + + persistenceServiceContainers.values() + .forEach(container -> container.getMatchingConfigurations(changeStrategy) + .filter(itemConfig -> appliesToItem(itemConfig, item)) + .filter(itemConfig -> itemConfig.filters().stream().allMatch(filter -> filter.apply(item))) + .forEach(itemConfig -> { + itemConfig.filters().forEach(filter -> filter.persisted(item)); + container.getPersistenceService().store(item, itemConfig.alias()); + })); + } + + /** + * Checks if a given persistence configuration entry is relevant for an item + * + * @param itemConfig the persistence configuration entry + * @param item to check if the configuration applies to + * @return true, if the configuration applies to the item + */ + private boolean appliesToItem(PersistenceItemConfiguration itemConfig, Item item) { + for (PersistenceConfig itemCfg : itemConfig.items()) { + if (itemCfg instanceof PersistenceAllConfig) { + return true; + } else if (itemCfg instanceof PersistenceItemConfig) { + if (item.getName().equals(((PersistenceItemConfig) itemCfg).getItem())) { + return true; + } + } else if (itemCfg instanceof PersistenceGroupConfig) { + try { + Item gItem = itemRegistry.getItem(((PersistenceGroupConfig) itemCfg).getGroup()); + if (gItem instanceof GroupItem gItem2 && gItem2.getAllMembers().contains(item)) { + return true; + } + } catch (ItemNotFoundException e) { + // do nothing + } + } + } + return false; + } + + /** + * Retrieves all items for which the persistence configuration applies to. + * + * @param config the persistence configuration entry + * @return all items that this configuration applies to + */ + private Iterable getAllItems(PersistenceItemConfiguration config) { + // first check, if we should return them all + if (config.items().stream().anyMatch(PersistenceAllConfig.class::isInstance)) { + return itemRegistry.getItems(); + } + + // otherwise, go through the detailed definitions + Set items = new HashSet<>(); + for (Object itemCfg : config.items()) { + if (itemCfg instanceof PersistenceItemConfig) { + String itemName = ((PersistenceItemConfig) itemCfg).getItem(); + try { + items.add(itemRegistry.getItem(itemName)); + } catch (ItemNotFoundException e) { + logger.debug("Item '{}' does not exist.", itemName); + } + } + if (itemCfg instanceof PersistenceGroupConfig) { + String groupName = ((PersistenceGroupConfig) itemCfg).getGroup(); + try { + Item gItem = itemRegistry.getItem(groupName); + if (gItem instanceof GroupItem groupItem) { + items.addAll(groupItem.getAllMembers()); + } + } catch (ItemNotFoundException e) { + logger.debug("Item group '{}' does not exist.", groupName); + } + } + } + return items; + } + + /** + * Handles the "restoreOnStartup" strategy for the item. + * If the item state is still undefined when entering this method, all persistence configurations are checked, + * if they have the "restoreOnStartup" strategy configured for the item. If so, the item state will be set + * to its last persisted value. + * + * @param item the item to restore the state for + */ + private void restoreItemStateIfNeeded(Item item) { + // get the last persisted state from the persistence service if no state is yet set + if (UnDefType.NULL.equals(item.getState()) && item instanceof GenericItem) { + List matchingContainers = persistenceServiceContainers.values().stream() // + .filter(container -> container.getPersistenceService() instanceof QueryablePersistenceService) // + .filter(container -> container.getMatchingConfigurations(PersistenceStrategy.Globals.RESTORE) + .anyMatch(itemConfig -> appliesToItem(itemConfig, item))) + .toList(); + + for (PersistenceServiceContainer container : matchingContainers) { + QueryablePersistenceService queryService = (QueryablePersistenceService) container + .getPersistenceService(); + FilterCriteria filter = new FilterCriteria().setItemName(item.getName()).setPageSize(1); + Iterator result = safeCaller.create(queryService, QueryablePersistenceService.class) + .onTimeout(() -> logger.warn("Querying persistence service '{}' takes more than {}ms.", + queryService.getId(), SafeCaller.DEFAULT_TIMEOUT)) + .onException(e -> logger.error("Exception occurred while querying persistence service '{}': {}", + queryService.getId(), e.getMessage(), e)) + .build().query(filter).iterator(); + if (result.hasNext()) { + HistoricItem historicItem = result.next(); + GenericItem genericItem = (GenericItem) item; + genericItem.removeStateChangeListener(this); + genericItem.setState(historicItem.getState()); + genericItem.addStateChangeListener(this); + if (logger.isDebugEnabled()) { + logger.debug("Restored item state from '{}' for item '{}' -> '{}'", + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(historicItem.getTimestamp()), + item.getName(), historicItem.getState()); + } + return; + } + } + } + } + + private void startEventHandling(PersistenceServiceContainer serviceContainer) { + serviceContainer.getMatchingConfigurations(PersistenceStrategy.Globals.RESTORE) + .forEach(itemConfig -> getAllItems(itemConfig).forEach(this::restoreItemStateIfNeeded)); + serviceContainer.schedulePersistJobs(); + } + + // ItemStateChangeListener methods + + @Override + public void allItemsChanged(Collection oldItemNames) { + itemRegistry.getItems().forEach(this::added); + } + + @Override + public void added(Item item) { + restoreItemStateIfNeeded(item); + if (item instanceof GenericItem genericItem) { + genericItem.addStateChangeListener(this); + } + } + + @Override + public void removed(Item item) { + if (item instanceof GenericItem genericItem) { + genericItem.removeStateChangeListener(this); + } + } + + @Override + public void updated(Item oldItem, Item item) { + removed(oldItem); + added(item); + } + + @Override + public void stateChanged(Item item, State oldState, State newState) { + handleStateEvent(item, true); + } + + @Override + public void stateUpdated(Item item, State state) { + handleStateEvent(item, false); + } + + @Override + public void onReadyMarkerAdded(ReadyMarker readyMarker) { + ExecutorService scheduler = Executors.newSingleThreadExecutor(new NamedThreadFactory("persistenceManager")); + scheduler.submit(() -> { + allItemsChanged(Set.of()); + persistenceServiceContainers.values().forEach(this::startEventHandling); + started = true; + readyService.markReady(marker); + itemRegistry.addRegistryChangeListener(this); + }); + scheduler.shutdown(); + } + + @Override + public void onReadyMarkerRemoved(ReadyMarker readyMarker) { + readyService.unmarkReady(marker); + } + + @Override + public void added(PersistenceServiceConfiguration element) { + PersistenceServiceContainer container = persistenceServiceContainers.get(element.getUID()); + if (container != null) { + container.setConfiguration(element); + if (started) { + startEventHandling(container); + } + } + } + + @Override + public void removed(PersistenceServiceConfiguration element) { + PersistenceServiceContainer container = persistenceServiceContainers.get(element.getUID()); + if (container != null) { + container.setConfiguration(null); + if (started) { + startEventHandling(container); + } + } + } + + @Override + public void updated(PersistenceServiceConfiguration oldElement, PersistenceServiceConfiguration element) { + // no need to remove before, configuration is overwritten if possible + added(element); + } + + private class PersistenceServiceContainer { + private final PersistenceService persistenceService; + private final Set> jobs = new HashSet<>(); + + private PersistenceServiceConfiguration configuration; + + public PersistenceServiceContainer(PersistenceService persistenceService, + @Nullable PersistenceServiceConfiguration configuration) { + this.persistenceService = persistenceService; + this.configuration = Objects.requireNonNullElseGet(configuration, this::getDefaultConfig); + } + + public PersistenceService getPersistenceService() { + return persistenceService; + } + + /** + * Set a new configuration for this persistence service (also cancels all cron jobs) + * + * @param configuration the new {@link PersistenceServiceConfiguration}, if {@code null} the default + * configuration of the service is used + */ + public void setConfiguration(@Nullable PersistenceServiceConfiguration configuration) { + cancelPersistJobs(); + this.configuration = Objects.requireNonNullElseGet(configuration, this::getDefaultConfig); + } + + /** + * Get all item configurations from this service that match a certain strategy + * + * @param strategy the {@link PersistenceStrategy} to look for + * @return a @link Stream} of the result + */ + public Stream getMatchingConfigurations(PersistenceStrategy strategy) { + boolean matchesDefaultStrategies = configuration.getDefaults().contains(strategy); + return configuration.getConfigs().stream().filter(itemConfig -> itemConfig.strategies().contains(strategy) + || (itemConfig.strategies().isEmpty() && matchesDefaultStrategies)); + } + + private PersistenceServiceConfiguration getDefaultConfig() { + List strategies = persistenceService.getDefaultStrategies(); + List configs = List + .of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), null, strategies, null)); + return new PersistenceServiceConfiguration(persistenceService.getId(), configs, strategies, strategies, + List.of()); + } + + /** + * Cancel all scheduled cron jobs / strategies for this service + */ + public void cancelPersistJobs() { + synchronized (jobs) { + jobs.forEach(job -> job.cancel(true)); + jobs.clear(); + } + logger.debug("Removed scheduled cron job for persistence service '{}'", configuration.getUID()); + } + + /** + * Schedule all necessary cron jobs / strategies for this service + */ + public void schedulePersistJobs() { + configuration.getStrategies().stream().filter(PersistenceCronStrategy.class::isInstance) + .forEach(strategy -> { + PersistenceCronStrategy cronStrategy = (PersistenceCronStrategy) strategy; + String cronExpression = cronStrategy.getCronExpression(); + List itemConfigs = getMatchingConfigurations(strategy) + .collect(Collectors.toList()); + jobs.add(scheduler.schedule(() -> persistJob(itemConfigs), cronExpression)); + + logger.debug("Scheduled strategy {} with cron expression {} for service {}", + cronStrategy.getName(), cronExpression, configuration.getUID()); + + }); + } + + private void persistJob(List itemConfigs) { + itemConfigs.forEach(itemConfig -> { + for (Item item : getAllItems(itemConfig)) { + if (itemConfig.filters().stream().allMatch(filter -> filter.apply(item))) { + long startTime = System.nanoTime(); + itemConfig.filters().forEach(filter -> filter.persisted(item)); + persistenceService.store(item, itemConfig.alias()); + logger.trace("Storing item '{}' with persistence service '{}' took {}ms", item.getName(), + configuration.getUID(), TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); + } + } + }); + } + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java deleted file mode 100644 index dade0f3df82..00000000000 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java +++ /dev/null @@ -1,500 +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.persistence.internal; - -import java.time.format.DateTimeFormatter; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.common.NamedThreadFactory; -import org.openhab.core.common.SafeCaller; -import org.openhab.core.items.GenericItem; -import org.openhab.core.items.GroupItem; -import org.openhab.core.items.Item; -import org.openhab.core.items.ItemNotFoundException; -import org.openhab.core.items.ItemRegistry; -import org.openhab.core.items.ItemRegistryChangeListener; -import org.openhab.core.items.StateChangeListener; -import org.openhab.core.persistence.FilterCriteria; -import org.openhab.core.persistence.HistoricItem; -import org.openhab.core.persistence.PersistenceItemConfiguration; -import org.openhab.core.persistence.PersistenceManager; -import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceConfiguration; -import org.openhab.core.persistence.QueryablePersistenceService; -import org.openhab.core.persistence.config.PersistenceAllConfig; -import org.openhab.core.persistence.config.PersistenceConfig; -import org.openhab.core.persistence.config.PersistenceGroupConfig; -import org.openhab.core.persistence.config.PersistenceItemConfig; -import org.openhab.core.persistence.strategy.PersistenceCronStrategy; -import org.openhab.core.persistence.strategy.PersistenceStrategy; -import org.openhab.core.scheduler.CronScheduler; -import org.openhab.core.scheduler.ScheduledCompletableFuture; -import org.openhab.core.service.ReadyMarker; -import org.openhab.core.service.ReadyMarkerFilter; -import org.openhab.core.service.ReadyService; -import org.openhab.core.service.ReadyService.ReadyTracker; -import org.openhab.core.service.StartLevelService; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class implements a persistence manager to manage all persistence services etc. - * - * @author Kai Kreuzer - Initial contribution - * @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage. - */ -@Component(immediate = true, service = PersistenceManager.class) -@NonNullByDefault -public class PersistenceManagerImpl - implements ItemRegistryChangeListener, PersistenceManager, StateChangeListener, ReadyTracker { - - private final Logger logger = LoggerFactory.getLogger(PersistenceManagerImpl.class); - - private final ReadyMarker marker = new ReadyMarker("persistence", "restore"); - - // the scheduler used for timer events - private final CronScheduler scheduler; - private final ItemRegistry itemRegistry; - private final SafeCaller safeCaller; - private final ReadyService readyService; - private volatile boolean started = false; - - final Map persistenceServices = new HashMap<>(); - final Map persistenceServiceConfigs = new HashMap<>(); - private final Map>> persistenceJobs = new HashMap<>(); - - @Activate - public PersistenceManagerImpl(final @Reference CronScheduler scheduler, final @Reference ItemRegistry itemRegistry, - final @Reference SafeCaller safeCaller, final @Reference ReadyService readyService) { - this.scheduler = scheduler; - this.itemRegistry = itemRegistry; - this.safeCaller = safeCaller; - this.readyService = readyService; - } - - @Activate - protected void activate() { - readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) - .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL))); - } - - @Deactivate - protected void deactivate() { - itemRegistry.removeRegistryChangeListener(this); - started = false; - removeTimers(); - removeItemStateChangeListeners(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - protected void addPersistenceService(PersistenceService persistenceService) { - logger.debug("Initializing {} persistence service.", persistenceService.getId()); - persistenceServices.put(persistenceService.getId(), persistenceService); - persistenceServiceConfigs.putIfAbsent(persistenceService.getId(), getDefaultConfig(persistenceService)); - if (started) { - stopEventHandling(persistenceService.getId()); - startEventHandling(persistenceService.getId()); - } - } - - protected void removePersistenceService(PersistenceService persistenceService) { - stopEventHandling(persistenceService.getId()); - persistenceServices.remove(persistenceService.getId()); - } - - /** - * Calls all persistence services which use change or update policy for the given item - * - * @param item the item to persist - * @param onlyChanges true, if it has the change strategy, false otherwise - */ - private void handleStateEvent(Item item, boolean onlyChanges) { - synchronized (persistenceServiceConfigs) { - for (Entry entry : persistenceServiceConfigs - .entrySet()) { - final String serviceName = entry.getKey(); - final PersistenceServiceConfiguration config = entry.getValue(); - if (config != null && persistenceServices.containsKey(serviceName)) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, onlyChanges ? PersistenceStrategy.Globals.CHANGE - : PersistenceStrategy.Globals.UPDATE)) { - if (appliesToItem(itemConfig, item)) { - persistenceServices.get(serviceName).store(item, itemConfig.getAlias()); - } - } - } - } - } - } - } - - /** - * Checks if a given persistence configuration entry has a certain strategy for the given service - * - * @param config the configuration to check for - * @param itemConfig the persistence configuration entry - * @param strategy the strategy to check for - * @return true, if it has the given strategy - */ - private boolean hasStrategy(PersistenceServiceConfiguration config, PersistenceItemConfiguration itemConfig, - PersistenceStrategy strategy) { - if (config.getDefaults().contains(strategy) && itemConfig.getStrategies().isEmpty()) { - return true; - } else { - for (PersistenceStrategy s : itemConfig.getStrategies()) { - if (s.equals(strategy)) { - return true; - } - } - return false; - } - } - - /** - * Checks if a given persistence configuration entry is relevant for an item - * - * @param config the persistence configuration entry - * @param item to check if the configuration applies to - * @return true, if the configuration applies to the item - */ - private boolean appliesToItem(PersistenceItemConfiguration config, Item item) { - for (PersistenceConfig itemCfg : config.getItems()) { - if (itemCfg instanceof PersistenceAllConfig) { - return true; - } - if (itemCfg instanceof PersistenceItemConfig) { - PersistenceItemConfig singleItemConfig = (PersistenceItemConfig) itemCfg; - if (item.getName().equals(singleItemConfig.getItem())) { - return true; - } - } - if (itemCfg instanceof PersistenceGroupConfig) { - PersistenceGroupConfig groupItemConfig = (PersistenceGroupConfig) itemCfg; - try { - Item gItem = itemRegistry.getItem(groupItemConfig.getGroup()); - if (gItem instanceof GroupItem) { - GroupItem groupItem = (GroupItem) gItem; - if (groupItem.getAllMembers().contains(item)) { - return true; - } - } - } catch (ItemNotFoundException e) { - // do nothing - } - } - } - return false; - } - - /** - * Retrieves all items for which the persistence configuration applies to. - * - * @param config the persistence configuration entry - * @return all items that this configuration applies to - */ - Iterable getAllItems(PersistenceItemConfiguration config) { - // first check, if we should return them all - for (Object itemCfg : config.getItems()) { - if (itemCfg instanceof PersistenceAllConfig) { - return itemRegistry.getItems(); - } - } - - // otherwise, go through the detailed definitions - Set items = new HashSet<>(); - for (Object itemCfg : config.getItems()) { - if (itemCfg instanceof PersistenceItemConfig) { - PersistenceItemConfig singleItemConfig = (PersistenceItemConfig) itemCfg; - String itemName = singleItemConfig.getItem(); - try { - items.add(itemRegistry.getItem(itemName)); - } catch (ItemNotFoundException e) { - logger.debug("Item '{}' does not exist.", itemName); - } - } - if (itemCfg instanceof PersistenceGroupConfig) { - PersistenceGroupConfig groupItemConfig = (PersistenceGroupConfig) itemCfg; - String groupName = groupItemConfig.getGroup(); - try { - Item gItem = itemRegistry.getItem(groupName); - if (gItem instanceof GroupItem) { - GroupItem groupItem = (GroupItem) gItem; - items.addAll(groupItem.getAllMembers()); - } - } catch (ItemNotFoundException e) { - logger.debug("Item group '{}' does not exist.", groupName); - } - } - } - return items; - } - - /** - * Handles the "restoreOnStartup" strategy for the item. - * If the item state is still undefined when entering this method, all persistence configurations are checked, - * if they have the "restoreOnStartup" strategy configured for the item. If so, the item state will be set - * to its last persisted value. - * - * @param item the item to restore the state for - */ - @SuppressWarnings("null") - private void initialize(Item item) { - // get the last persisted state from the persistence service if no state is yet set - if (UnDefType.NULL.equals(item.getState()) && item instanceof GenericItem) { - for (Entry entry : persistenceServiceConfigs - .entrySet()) { - final String serviceName = entry.getKey(); - final PersistenceServiceConfiguration config = entry.getValue(); - if (config != null) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, PersistenceStrategy.Globals.RESTORE)) { - if (appliesToItem(itemConfig, item)) { - PersistenceService service = persistenceServices.get(serviceName); - if (service instanceof QueryablePersistenceService) { - QueryablePersistenceService queryService = (QueryablePersistenceService) service; - FilterCriteria filter = new FilterCriteria().setItemName(item.getName()) - .setPageSize(1); - Iterable result = safeCaller - .create(queryService, QueryablePersistenceService.class).onTimeout(() -> { - logger.warn("Querying persistence service '{}' takes more than {}ms.", - queryService.getId(), SafeCaller.DEFAULT_TIMEOUT); - }).onException(e -> { - logger.error( - "Exception occurred while querying persistence service '{}': {}", - queryService.getId(), e.getMessage(), e); - }).build().query(filter); - if (result != null) { - Iterator it = result.iterator(); - if (it.hasNext()) { - HistoricItem historicItem = it.next(); - GenericItem genericItem = (GenericItem) item; - genericItem.removeStateChangeListener(this); - genericItem.setState(historicItem.getState()); - genericItem.addStateChangeListener(this); - if (logger.isDebugEnabled()) { - logger.debug("Restored item state from '{}' for item '{}' -> '{}'", - DateTimeFormatter.ISO_ZONED_DATE_TIME - .format(historicItem.getTimestamp()), - item.getName(), historicItem.getState()); - } - return; - } - } - } else if (service != null) { - logger.warn( - "Failed to restore item states as persistence service '{}' cannot be queried.", - serviceName); - } - } - } - } - } - } - } - } - - private void removeItemStateChangeListeners() { - for (Item item : itemRegistry.getAll()) { - if (item instanceof GenericItem) { - ((GenericItem) item).removeStateChangeListener(this); - } - } - } - - /** - * Creates new {@link ScheduledCompletableFuture}s in the group dbId for the given collection of - * {@link PersistenceStrategy strategies}. - * - * @param dbId the database id used by the persistence service - * @param strategies a collection of strategies - */ - private void createTimers(final String dbId, List strategies) { - for (PersistenceStrategy strategy : strategies) { - if (strategy instanceof PersistenceCronStrategy) { - PersistenceCronStrategy cronStrategy = (PersistenceCronStrategy) strategy; - String cronExpression = cronStrategy.getCronExpression(); - - final PersistItemsJob job = new PersistItemsJob(this, dbId, cronStrategy.getName()); - ScheduledCompletableFuture schedule = scheduler.schedule(job, cronExpression); - if (persistenceJobs.containsKey(dbId)) { - persistenceJobs.get(dbId).add(schedule); - } else { - final Set> jobs = new HashSet<>(); - jobs.add(schedule); - persistenceJobs.put(dbId, jobs); - } - - logger.debug("Scheduled strategy {} with cron expression {}", cronStrategy.getName(), cronExpression); - } - } - } - - /** - * Deletes all {@link ScheduledCompletableFuture}s of the group dbId. - * - * @param dbId the database id used by the persistence service - */ - private void removeTimers(String dbId) { - if (!persistenceJobs.containsKey(dbId)) { - return; - } - for (final ScheduledCompletableFuture job : persistenceJobs.get(dbId)) { - job.cancel(true); - logger.debug("Removed scheduled cron job for persistence service '{}'", dbId); - } - persistenceJobs.remove(dbId); - } - - private void removeTimers() { - Set dbIds = new HashSet<>(persistenceJobs.keySet()); - for (String dbId : dbIds) { - removeTimers(dbId); - } - } - - @Override - public void addConfig(final String dbId, final PersistenceServiceConfiguration config) { - synchronized (persistenceServiceConfigs) { - if (started && persistenceServiceConfigs.containsKey(dbId)) { - stopEventHandling(dbId); - } - persistenceServiceConfigs.put(dbId, config); - if (started && persistenceServices.containsKey(dbId)) { - startEventHandling(dbId); - } - } - } - - @Override - public void removeConfig(final String dbId) { - synchronized (persistenceServiceConfigs) { - stopEventHandling(dbId); - PersistenceService persistenceService = persistenceServices.get(dbId); - if (persistenceService != null) { - persistenceServiceConfigs.put(dbId, getDefaultConfig(persistenceService)); - startEventHandling(dbId); - } else { - persistenceServiceConfigs.remove(dbId); - } - } - } - - private void startEventHandling(final String serviceName) { - synchronized (persistenceServiceConfigs) { - final PersistenceServiceConfiguration config = persistenceServiceConfigs.get(serviceName); - if (config != null) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, PersistenceStrategy.Globals.RESTORE)) { - for (Item item : getAllItems(itemConfig)) { - initialize(item); - } - } - } - createTimers(serviceName, config.getStrategies()); - } - } - } - - private void stopEventHandling(String dbId) { - synchronized (persistenceServiceConfigs) { - removeTimers(dbId); - } - } - - private @Nullable PersistenceServiceConfiguration getDefaultConfig(PersistenceService persistenceService) { - List strategies = persistenceService.getDefaultStrategies(); - List configs = List - .of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), null, strategies, null)); - return new PersistenceServiceConfiguration(configs, strategies, strategies); - } - - @Override - public void allItemsChanged(Collection oldItemNames) { - for (Item item : itemRegistry.getItems()) { - added(item); - } - } - - @Override - public void added(Item item) { - initialize(item); - if (item instanceof GenericItem) { - GenericItem genericItem = (GenericItem) item; - genericItem.addStateChangeListener(this); - } - } - - @Override - public void removed(Item item) { - if (item instanceof GenericItem) { - GenericItem genericItem = (GenericItem) item; - genericItem.removeStateChangeListener(this); - } - } - - @Override - public void updated(Item oldItem, Item item) { - removed(oldItem); - added(item); - } - - @Override - public void stateChanged(Item item, State oldState, State newState) { - handleStateEvent(item, true); - } - - @Override - public void stateUpdated(Item item, State state) { - handleStateEvent(item, false); - } - - @Override - public void onReadyMarkerAdded(ReadyMarker readyMarker) { - ExecutorService scheduler = Executors.newSingleThreadExecutor(new NamedThreadFactory("persistenceManager")); - scheduler.submit(() -> { - allItemsChanged(Collections.emptySet()); - for (String dbId : persistenceServices.keySet()) { - startEventHandling(dbId); - } - started = true; - readyService.markReady(marker); - itemRegistry.addRegistryChangeListener(this); - }); - scheduler.shutdown(); - } - - @Override - public void onReadyMarkerRemoved(ReadyMarker readyMarker) { - readyService.unmarkReady(marker); - } -} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceServiceConfigurationRegistryImpl.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceServiceConfigurationRegistryImpl.java new file mode 100644 index 00000000000..69e4cc151fa --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceServiceConfigurationRegistryImpl.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.internal; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractRegistry; +import org.openhab.core.common.registry.Provider; +import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistryChangeListener; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PersistenceServiceConfigurationRegistryImpl} implements the + * {@link PersistenceServiceConfigurationRegistry} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = PersistenceServiceConfigurationRegistry.class) +public class PersistenceServiceConfigurationRegistryImpl + extends AbstractRegistry + implements PersistenceServiceConfigurationRegistry { + private final Logger logger = LoggerFactory.getLogger(PersistenceServiceConfigurationRegistryImpl.class); + private final Map> serviceToProvider = new HashMap<>(); + private final Set registryChangeListeners = new CopyOnWriteArraySet<>(); + + public PersistenceServiceConfigurationRegistryImpl() { + super(PersistenceServiceConfigurationProvider.class); + } + + @Override + public void added(Provider provider, PersistenceServiceConfiguration element) { + if (serviceToProvider.containsKey(element.getUID())) { + logger.warn("Tried to add strategy container with serviceId '{}', but it was already added before.", + element.getUID()); + } else { + super.added(provider, element); + } + } + + @Override + public void removed(Provider provider, PersistenceServiceConfiguration element) { + if (!provider.equals(serviceToProvider.getOrDefault(element.getUID(), provider))) { + logger.warn("Tried to remove strategy container with serviceId '{}', but it was added by another provider.", + element.getUID()); + } else { + super.removed(provider, element); + } + } + + @Override + public void updated(Provider provider, PersistenceServiceConfiguration oldelement, + PersistenceServiceConfiguration element) { + if (!provider.equals(serviceToProvider.getOrDefault(element.getUID(), provider))) { + logger.warn("Tried to update strategy container with serviceId '{}', but it was added by another provider.", + element.getUID()); + } else { + super.updated(provider, oldelement, element); + } + } + + protected void notifyListenersAboutAddedElement(PersistenceServiceConfiguration element) { + registryChangeListeners.forEach(listener -> listener.added(element)); + super.notifyListenersAboutAddedElement(element); + } + + protected void notifyListenersAboutRemovedElement(PersistenceServiceConfiguration element) { + registryChangeListeners.forEach(listener -> listener.removed(element)); + super.notifyListenersAboutRemovedElement(element); + } + + protected void notifyListenersAboutUpdatedElement(PersistenceServiceConfiguration oldElement, + PersistenceServiceConfiguration element) { + registryChangeListeners.forEach(listener -> listener.updated(oldElement, element)); + } + + @Override + public void addRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener) { + registryChangeListeners.add(listener); + } + + @Override + public void removeRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener) { + registryChangeListeners.remove(listener); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setManagedProvider(ManagedPersistenceServiceConfigurationProvider provider) { + super.setManagedProvider(provider); + } + + protected void unsetManagedProvider(ManagedPersistenceServiceConfigurationProvider provider) { + super.unsetManagedProvider(provider); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/ManagedPersistenceServiceConfigurationProvider.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/ManagedPersistenceServiceConfigurationProvider.java new file mode 100644 index 00000000000..4bbd84d9626 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/ManagedPersistenceServiceConfigurationProvider.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.AbstractManagedProvider; +import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; +import org.openhab.core.storage.StorageService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link ManagedPersistenceServiceConfigurationProvider} implements a + * {@link PersistenceServiceConfigurationProvider} for managed configurations which are stored in a JSON database + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { PersistenceServiceConfigurationProvider.class, + ManagedPersistenceServiceConfigurationProvider.class }) +public class ManagedPersistenceServiceConfigurationProvider + extends AbstractManagedProvider + implements PersistenceServiceConfigurationProvider { + private static final String STORAGE_NAME = "org.openhab.core.persistence.PersistenceServiceConfiguration"; + + @Activate + public ManagedPersistenceServiceConfigurationProvider(@Reference StorageService storageService) { + super(storageService); + } + + @Override + protected String getStorageName() { + return STORAGE_NAME; + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + protected @Nullable PersistenceServiceConfiguration toElement(String key, + PersistenceServiceConfigurationDTO persistableElement) { + return PersistenceServiceConfigurationDTOMapper.map(persistableElement); + } + + @Override + protected PersistenceServiceConfigurationDTO toPersistableElement(PersistenceServiceConfiguration element) { + return PersistenceServiceConfigurationDTOMapper.map(element); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceServiceConfiguration.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfiguration.java similarity index 51% rename from bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceServiceConfiguration.java rename to bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfiguration.java index b31930ad896..214d584a83d 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceServiceConfiguration.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfiguration.java @@ -10,32 +10,43 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.persistence; +package org.openhab.core.persistence.registry; import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Identifiable; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.filter.PersistenceFilter; import org.openhab.core.persistence.strategy.PersistenceStrategy; /** - * This class represents the configuration for a persistence service. + * The {@link PersistenceServiceConfiguration} represents the configuration for a persistence service. * - * @author Markus Rathgeb - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class PersistenceServiceConfiguration { +public class PersistenceServiceConfiguration implements Identifiable { + private final String serviceId; private final List configs; private final List defaults; private final List strategies; + private final List filters; - public PersistenceServiceConfiguration(final Collection configs, - final Collection defaults, final Collection strategies) { - this.configs = Collections.unmodifiableList(new LinkedList<>(configs)); - this.defaults = Collections.unmodifiableList(new LinkedList<>(defaults)); - this.strategies = Collections.unmodifiableList(new LinkedList<>(strategies)); + public PersistenceServiceConfiguration(String serviceId, Collection configs, + Collection defaults, Collection strategies, + Collection filters) { + this.serviceId = serviceId; + this.configs = List.copyOf(configs); + this.defaults = List.copyOf(defaults); + this.strategies = List.copyOf(strategies); + this.filters = List.copyOf(filters); + } + + @Override + public String getUID() { + return serviceId; } /** @@ -64,4 +75,13 @@ public List getDefaults() { public List getStrategies() { return strategies; } + + /** + * Get all defined filters. + * + * @return an unmodifiable list of the defined filters + */ + public List getFilters() { + return filters; + } } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java new file mode 100644 index 00000000000..a20dbcc6d39 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.registry; + +import static org.openhab.core.persistence.strategy.PersistenceStrategy.Globals.STRATEGIES; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.config.PersistenceAllConfig; +import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.config.PersistenceGroupConfig; +import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.dto.PersistenceCronStrategyDTO; +import org.openhab.core.persistence.dto.PersistenceFilterDTO; +import org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO; +import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; +import org.openhab.core.persistence.filter.PersistenceEqualsFilter; +import org.openhab.core.persistence.filter.PersistenceFilter; +import org.openhab.core.persistence.filter.PersistenceIncludeFilter; +import org.openhab.core.persistence.filter.PersistenceThresholdFilter; +import org.openhab.core.persistence.filter.PersistenceTimeFilter; +import org.openhab.core.persistence.strategy.PersistenceCronStrategy; +import org.openhab.core.persistence.strategy.PersistenceStrategy; + +/** + * The {@link PersistenceServiceConfigurationDTOMapper} is a utility class to map persistence configurations for storage + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceServiceConfigurationDTOMapper { + + private PersistenceServiceConfigurationDTOMapper() { + // prevent initialization + } + + public static PersistenceServiceConfigurationDTO map( + PersistenceServiceConfiguration persistenceServiceConfiguration) { + PersistenceServiceConfigurationDTO dto = new PersistenceServiceConfigurationDTO(); + dto.serviceId = persistenceServiceConfiguration.getUID(); + dto.configs = persistenceServiceConfiguration.getConfigs().stream() + .map(PersistenceServiceConfigurationDTOMapper::mapPersistenceItemConfig).toList(); + dto.defaults = persistenceServiceConfiguration.getDefaults().stream().map(PersistenceStrategy::getName) + .toList(); + dto.cronStrategies = filterList(persistenceServiceConfiguration.getStrategies(), PersistenceCronStrategy.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceCronStrategy); + dto.thresholdFilters = filterList(persistenceServiceConfiguration.getFilters(), + PersistenceThresholdFilter.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceThresholdFilter); + dto.timeFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceTimeFilter.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceTimeFilter); + dto.equalsFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceEqualsFilter.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceEqualsFilter); + dto.includeFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceIncludeFilter.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceIncludeFilter); + + return dto; + } + + public static PersistenceServiceConfiguration map(PersistenceServiceConfigurationDTO dto) { + Map strategyMap = dto.cronStrategies.stream() + .collect(Collectors.toMap(e -> e.name, e -> new PersistenceCronStrategy(e.name, e.cronExpression))); + + Map filterMap = Stream.of( + dto.thresholdFilters.stream() + .map(f -> new PersistenceThresholdFilter(f.name, f.value, f.unit, f.relative)), + dto.timeFilters.stream().map(f -> new PersistenceTimeFilter(f.name, f.value.intValue(), f.unit)), + dto.equalsFilters.stream().map(f -> new PersistenceEqualsFilter(f.name, f.values, f.inverted)), + dto.includeFilters.stream() + .map(f -> new PersistenceIncludeFilter(f.name, f.lower, f.upper, f.unit, f.inverted))) + .flatMap(Function.identity()).collect(Collectors.toMap(PersistenceFilter::getName, e -> e)); + + List defaults = dto.defaults.stream() + .map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList(); + + List configs = dto.configs.stream().map(config -> { + List items = config.items.stream() + .map(PersistenceServiceConfigurationDTOMapper::stringToPersistenceConfig).toList(); + List strategies = config.strategies.stream() + .map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList(); + List filters = config.filters.stream() + .map(str -> stringToPersistenceFilter(str, filterMap, dto.serviceId)).toList(); + return new PersistenceItemConfiguration(items, config.alias, strategies, filters); + }).toList(); + + return new PersistenceServiceConfiguration(dto.serviceId, configs, defaults, strategyMap.values(), + filterMap.values()); + } + + private static Collection filterList(Collection list, Class clazz, Function mapper) { + return list.stream().filter(clazz::isInstance).map(clazz::cast).map(mapper).toList(); + } + + private static PersistenceConfig stringToPersistenceConfig(String string) { + if ("*".equals(string)) { + return new PersistenceAllConfig(); + } else if (string.endsWith("*")) { + return new PersistenceGroupConfig(string.substring(0, string.length() - 1)); + } else { + return new PersistenceItemConfig(string); + } + } + + private static PersistenceStrategy stringToPersistenceStrategy(String string, + Map strategyMap, String serviceId) { + PersistenceStrategy strategy = strategyMap.get(string); + if (strategy != null) { + return strategy; + } + strategy = STRATEGIES.get(string); + if (strategy != null) { + return strategy; + } + throw new IllegalArgumentException("Strategy '" + string + "' unknown for service '" + serviceId + "'"); + } + + private static PersistenceFilter stringToPersistenceFilter(String string, Map filterMap, + String serviceId) { + PersistenceFilter filter = filterMap.get(string); + if (filter != null) { + return filter; + } + + throw new IllegalArgumentException("Filter '" + string + "' unknown for service '" + serviceId + "'"); + } + + private static String persistenceConfigToString(PersistenceConfig config) { + if (config instanceof PersistenceAllConfig) { + return "*"; + } else if (config instanceof PersistenceGroupConfig persistenceGroupConfig) { + return persistenceGroupConfig.getGroup() + "*"; + } else if (config instanceof PersistenceItemConfig persistenceItemConfig) { + return persistenceItemConfig.getItem(); + } + throw new IllegalArgumentException("Unknown persistence config class " + config.getClass()); + } + + private static PersistenceItemConfigurationDTO mapPersistenceItemConfig(PersistenceItemConfiguration config) { + PersistenceItemConfigurationDTO itemDto = new PersistenceItemConfigurationDTO(); + itemDto.items = config.items().stream().map(PersistenceServiceConfigurationDTOMapper::persistenceConfigToString) + .toList(); + itemDto.strategies = config.strategies().stream().map(PersistenceStrategy::getName).toList(); + itemDto.filters = config.filters().stream().map(PersistenceFilter::getName).toList(); + itemDto.alias = config.alias(); + return itemDto; + } + + private static PersistenceCronStrategyDTO mapPersistenceCronStrategy(PersistenceCronStrategy cronStrategy) { + PersistenceCronStrategyDTO cronStrategyDTO = new PersistenceCronStrategyDTO(); + cronStrategyDTO.name = cronStrategy.getName(); + cronStrategyDTO.cronExpression = cronStrategy.getCronExpression(); + return cronStrategyDTO; + } + + private static PersistenceFilterDTO mapPersistenceThresholdFilter(PersistenceThresholdFilter thresholdFilter) { + PersistenceFilterDTO filterDTO = new PersistenceFilterDTO(); + filterDTO.name = thresholdFilter.getName(); + filterDTO.value = thresholdFilter.getValue(); + filterDTO.unit = thresholdFilter.getUnit(); + filterDTO.relative = thresholdFilter.isRelative(); + return filterDTO; + } + + private static PersistenceFilterDTO mapPersistenceTimeFilter(PersistenceTimeFilter persistenceTimeFilter) { + PersistenceFilterDTO filterDTO = new PersistenceFilterDTO(); + filterDTO.name = persistenceTimeFilter.getName(); + filterDTO.value = new BigDecimal(persistenceTimeFilter.getValue()); + filterDTO.unit = persistenceTimeFilter.getUnit(); + return filterDTO; + } + + private static PersistenceFilterDTO mapPersistenceEqualsFilter(PersistenceEqualsFilter persistenceEqualsFilter) { + PersistenceFilterDTO filterDTO = new PersistenceFilterDTO(); + filterDTO.name = persistenceEqualsFilter.getName(); + filterDTO.values = persistenceEqualsFilter.getValues().stream().toList(); + filterDTO.inverted = persistenceEqualsFilter.getInverted(); + return filterDTO; + } + + private static PersistenceFilterDTO mapPersistenceIncludeFilter(PersistenceIncludeFilter persistenceIncludeFilter) { + PersistenceFilterDTO filterDTO = new PersistenceFilterDTO(); + filterDTO.name = persistenceIncludeFilter.getName(); + filterDTO.lower = persistenceIncludeFilter.getLower(); + filterDTO.upper = persistenceIncludeFilter.getUpper(); + filterDTO.unit = persistenceIncludeFilter.getUnit(); + filterDTO.inverted = persistenceIncludeFilter.getInverted(); + return filterDTO; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/BackDoor.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationProvider.java similarity index 55% rename from bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/BackDoor.java rename to bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationProvider.java index f693b7df429..942097f0900 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/BackDoor.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationProvider.java @@ -10,17 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.semantics.model.equipment; +package org.openhab.core.persistence.registry; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; +import org.openhab.core.common.registry.Provider; /** - * This class defines a Back Door. + * The {@link PersistenceServiceConfigurationProvider} is an interface for persistence service configuration providers * - * @author Generated from generateTagClasses.groovy - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Equipment_Door_BackDoor", label = "Back Door", synonyms = "Back Doors", description = "") -public interface BackDoor extends Door { +public interface PersistenceServiceConfigurationProvider extends Provider { } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistry.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistry.java new file mode 100644 index 00000000000..5435f5b8559 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistry.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Registry; + +/** + * The {@link PersistenceServiceConfigurationRegistry} is the central place to store persistence service configurations. + * Configurations are registered through {@link PersistenceServiceConfigurationProvider}. + * Because the {@link org.openhab.core.persistence.internal.PersistenceManager} implementation needs to listen to + * different registries, the {@link PersistenceServiceConfigurationRegistryChangeListener} can be used to add listeners + * to this registry. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface PersistenceServiceConfigurationRegistry extends Registry { + void addRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener); + + void removeRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener); +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistryChangeListener.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistryChangeListener.java new file mode 100644 index 00000000000..4acf4d6adf1 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistryChangeListener.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PersistenceServiceConfigurationRegistryChangeListener} is an interface that can be implemented by services + * that need to listen to the {@link PersistenceServiceConfigurationRegistry} when more than one registry with different + * types is used. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface PersistenceServiceConfigurationRegistryChangeListener { + /** + * Notifies the listener that a single element has been added. + * + * @param element the element that has been added + */ + void added(PersistenceServiceConfiguration element); + + /** + * Notifies the listener that a single element has been removed. + * + * @param element the element that has been removed + */ + void removed(PersistenceServiceConfiguration element); + + /** + * Notifies the listener that a single element has been updated. + * + * @param element the new element + * @param oldElement the element that has been updated + */ + void updated(PersistenceServiceConfiguration oldElement, PersistenceServiceConfiguration element); +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java index 375f43e4024..4cbf6ca659a 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java @@ -12,6 +12,7 @@ */ package org.openhab.core.persistence.strategy; +import java.util.Map; import java.util.Objects; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -28,6 +29,9 @@ public static class Globals { public static final PersistenceStrategy UPDATE = new PersistenceStrategy("everyUpdate"); public static final PersistenceStrategy CHANGE = new PersistenceStrategy("everyChange"); public static final PersistenceStrategy RESTORE = new PersistenceStrategy("restoreOnStartup"); + + public static final Map STRATEGIES = Map.of(UPDATE.name, UPDATE, CHANGE.name, + CHANGE, RESTORE.name, RESTORE); } private final String name; @@ -44,7 +48,7 @@ public String getName() { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + name.hashCode(); return result; } @@ -56,14 +60,10 @@ public boolean equals(final @Nullable Object obj) { if (obj == null) { return false; } - if (!(obj instanceof PersistenceStrategy)) { - return false; - } - final PersistenceStrategy other = (PersistenceStrategy) obj; - if (!Objects.equals(name, other.name)) { + if (!(obj instanceof final PersistenceStrategy other)) { return false; } - return true; + return Objects.equals(name, other.name); } @Override diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java index c8371f1fff6..df375db1650 100644 --- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java @@ -12,17 +12,26 @@ */ package org.openhab.core.persistence.extensions; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.closeTo; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; -import java.time.Instant; +import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Set; +import java.util.stream.DoubleStream; import java.util.stream.IntStream; import javax.measure.quantity.Temperature; @@ -73,11 +82,10 @@ public class PersistenceExtensionsTest { public void setUp() { when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - CoreItemFactory itemFactory = new CoreItemFactory(); + CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock); numberItem = itemFactory.createItem(CoreItemFactory.NUMBER, TEST_NUMBER); quantityItem = itemFactory.createItem(CoreItemFactory.NUMBER + ItemUtil.EXTENSION_SEPARATOR + "Temperature", TEST_QUANTITY_NUMBER); - quantityItem.setUnitProvider(unitProviderMock); switchItem = itemFactory.createItem(CoreItemFactory.SWITCH, TEST_SWITCH); when(itemRegistryMock.get(TEST_NUMBER)).thenReturn(numberItem); @@ -192,7 +200,7 @@ public void testMaximumSinceDecimalType() { ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()), TestPersistenceService.ID); assertNotNull(historicItem); assertThat(historicItem.getState(), is(instanceOf(DecimalType.class))); - assertEquals("1", historicItem.getState().toString()); + assertEquals("2012", historicItem.getState().toString()); historicItem = PersistenceExtensions.maximumSince(numberItem, ZonedDateTime.of(2005, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()), TestPersistenceService.ID); @@ -229,7 +237,7 @@ public void testMaximumSinceQuantityType() { ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()), TestPersistenceService.ID); assertThat(historicItem, is(notNullValue())); assertThat(historicItem.getState(), is(instanceOf(QuantityType.class))); - assertThat(historicItem.getState().toString(), is("1 °C")); + assertThat(historicItem.getState().toString(), is("2012 °C")); historicItem = PersistenceExtensions.maximumSince(quantityItem, ZonedDateTime.of(2005, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()), TestPersistenceService.ID); @@ -279,7 +287,7 @@ public void testMaximumSinceSwitch() { historicItem = PersistenceExtensions.maximumSince(switchItem, now, TestPersistenceService.ID); assertNotNull(historicItem); - assertEquals(OnOffType.OFF, historicItem.getState()); + assertEquals(OnOffType.ON, historicItem.getState()); historicItem = PersistenceExtensions.maximumSince(switchItem, now.plusHours(1), TestPersistenceService.ID); assertNotNull(historicItem); @@ -393,10 +401,12 @@ public void testVarianceSince() { ZonedDateTime startStored = ZonedDateTime.of(2003, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); ZonedDateTime endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); - long storedInterval = endStored.toInstant().toEpochMilli() - startStored.toInstant().toEpochMilli(); - long recentInterval = Instant.now().toEpochMilli() - endStored.toInstant().toEpochMilli(); - double expectedAverage = (2007.4994 * storedInterval + 2518.5 * recentInterval) + + long storedInterval = Duration.between(startStored, endStored).toDays(); + long recentInterval = Duration.between(endStored, ZonedDateTime.now()).toDays(); + double expectedAverage = ((2003.0 + 2011.0) / 2.0 * storedInterval + 2012.0 * recentInterval) / (storedInterval + recentInterval); + double expected = IntStream.of(2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012) .mapToDouble(i -> Double.parseDouble(Integer.toString(i))).map(d -> Math.pow(d - expectedAverage, 2)) .sum() / 10d; @@ -413,10 +423,14 @@ public void testVarianceSince() { public void testVarianceBetween() { ZonedDateTime startStored = ZonedDateTime.of(2005, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); ZonedDateTime endStored = ZonedDateTime.of(2011, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); + + double expected = DoubleStream.of(2005, 2006, 2007, 2008, 2009, 2010, 2011) + .map(d -> Math.pow(d - (2005.0 + 2010.0) / 2.0, 2)).sum() / 7d; + DecimalType variance = PersistenceExtensions.varianceBetween(numberItem, startStored, endStored, TestPersistenceService.ID); assertThat(variance, is(notNullValue())); - assertThat(variance.doubleValue(), is(closeTo(4, 0.01))); + assertThat(variance.doubleValue(), is(closeTo(expected, 0.01))); // default persistence service variance = PersistenceExtensions.varianceBetween(numberItem, startStored, endStored); @@ -425,17 +439,16 @@ public void testVarianceBetween() { @Test public void testDeviationSinceDecimalType() { - numberItem.setState(new DecimalType(3025)); - ZonedDateTime startStored = ZonedDateTime.of(2003, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); ZonedDateTime endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); - long storedInterval = endStored.toInstant().toEpochMilli() - startStored.toInstant().toEpochMilli(); - long recentInterval = Instant.now().toEpochMilli() - endStored.toInstant().toEpochMilli(); - double expectedAverage = (2007.4994 * storedInterval + 2518.5 * recentInterval) + + long storedInterval = Duration.between(startStored, endStored).toDays(); + long recentInterval = Duration.between(endStored, ZonedDateTime.now()).toDays(); + double expectedAverage = ((2003.0 + 2011.0) / 2.0 * storedInterval + 2012.0 * recentInterval) / (storedInterval + recentInterval); - double expected = Math.sqrt(IntStream.of(2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012) - .mapToDouble(i -> Double.parseDouble(Integer.toString(i))).map(d -> Math.pow(d - expectedAverage, 2)) - .sum() / 10d); + + double expected = Math.sqrt(DoubleStream.of(2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012) + .map(d -> Math.pow(d - expectedAverage, 2)).sum() / 10d); DecimalType deviation = PersistenceExtensions.deviationSince(numberItem, startStored, TestPersistenceService.ID); assertNotNull(deviation); @@ -450,10 +463,14 @@ public void testDeviationSinceDecimalType() { public void testDeviationBetweenDecimalType() { ZonedDateTime startStored = ZonedDateTime.of(2005, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); ZonedDateTime endStored = ZonedDateTime.of(2011, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); + + double expected = Math.sqrt(DoubleStream.of(2005, 2006, 2007, 2008, 2009, 2010, 2011) + .map(d -> Math.pow(d - (2005.0 + 2010.0) / 2.0, 2)).sum() / 7d); + DecimalType deviation = PersistenceExtensions.deviationBetween(numberItem, startStored, endStored, TestPersistenceService.ID); assertThat(deviation, is(notNullValue())); - assertThat(deviation.doubleValue(), is(closeTo(2, 0.01))); + assertThat(deviation.doubleValue(), is(closeTo(expected, 0.01))); // default persistence service deviation = PersistenceExtensions.deviationBetween(numberItem, startStored, endStored); @@ -466,13 +483,15 @@ public void testDeviationSinceQuantityType() { ZonedDateTime startStored = ZonedDateTime.of(2003, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); ZonedDateTime endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); - long storedInterval = endStored.toInstant().toEpochMilli() - startStored.toInstant().toEpochMilli(); - long recentInterval = Instant.now().toEpochMilli() - endStored.toInstant().toEpochMilli(); - double expectedAverage = (2007.4994 * storedInterval + 2518.5 * recentInterval) + + long storedInterval = Duration.between(startStored, endStored).toDays(); + long recentInterval = Duration.between(endStored, ZonedDateTime.now()).toDays(); + double expectedAverage = ((2003.0 + 2011.0) / 2.0 * storedInterval + 2012.0 * recentInterval) / (storedInterval + recentInterval); - double expected = Math.sqrt(IntStream.of(2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012) - .mapToDouble(i -> Double.parseDouble(Integer.toString(i))).map(d -> Math.pow(d - expectedAverage, 2)) - .sum() / 10d); + + double expected = Math.sqrt(DoubleStream.of(2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012) + .map(d -> Math.pow(d - expectedAverage, 2)).sum() / 10d); + DecimalType deviation = PersistenceExtensions.deviationSince(quantityItem, startStored, TestPersistenceService.ID); assertNotNull(deviation); @@ -487,10 +506,14 @@ public void testDeviationSinceQuantityType() { public void testDeviationBetweenQuantityType() { ZonedDateTime startStored = ZonedDateTime.of(2005, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); ZonedDateTime endStored = ZonedDateTime.of(2011, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); + + double expected = Math.sqrt(DoubleStream.of(2005, 2006, 2007, 2008, 2009, 2010, 2011) + .map(d -> Math.pow(d - (2005.0 + 2010.0) / 2.0, 2)).sum() / 7d); + DecimalType deviation = PersistenceExtensions.deviationBetween(quantityItem, startStored, endStored, TestPersistenceService.ID); assertThat(deviation, is(notNullValue())); - assertThat(deviation.doubleValue(), is(closeTo(2, 0.01))); + assertThat(deviation.doubleValue(), is(closeTo(expected, 0.01))); // default persistence service deviation = PersistenceExtensions.deviationBetween(quantityItem, startStored, endStored); @@ -499,17 +522,17 @@ public void testDeviationBetweenQuantityType() { @Test public void testAverageSinceDecimalType() { - numberItem.setState(new DecimalType(3025)); - ZonedDateTime startStored = ZonedDateTime.of(1940, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); DecimalType average = PersistenceExtensions.averageSince(numberItem, startStored, TestPersistenceService.ID); assertNull(average); startStored = ZonedDateTime.of(2003, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); - Instant endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()).toInstant(); - long storedInterval = endStored.toEpochMilli() - startStored.toInstant().toEpochMilli(); - long recentInterval = Instant.now().toEpochMilli() - endStored.toEpochMilli(); - double expected = (2007.4994 * storedInterval + 2518.5 * recentInterval) / (storedInterval + recentInterval); + ZonedDateTime endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); + long storedInterval = Duration.between(startStored, endStored).toDays(); + long recentInterval = Duration.between(endStored, ZonedDateTime.now()).toDays(); + double expected = ((2003.0 + 2011.0) / 2.0 * storedInterval + 2012.0 * recentInterval) + / (storedInterval + recentInterval); + average = PersistenceExtensions.averageSince(numberItem, startStored, TestPersistenceService.ID); assertNotNull(average); assertEquals(expected, average.doubleValue(), 0.01); @@ -519,6 +542,56 @@ public void testAverageSinceDecimalType() { assertNull(average); } + @Test + public void testAverageSinceDecimalTypeIrregularTimespans() { + TestCachedValuesPersistenceService persistenceService = new TestCachedValuesPersistenceService(); + new PersistenceExtensions(new PersistenceServiceRegistry() { + + @Override + public @Nullable String getDefaultId() { + // not available + return null; + } + + @Override + public @Nullable PersistenceService getDefault() { + // not available + return null; + } + + @Override + public Set getAll() { + return Set.of(persistenceService); + } + + @Override + public @Nullable PersistenceService get(@Nullable String serviceId) { + return TestCachedValuesPersistenceService.ID.equals(serviceId) ? persistenceService : null; + } + }); + + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime beginStored = now.minusHours(27); + + persistenceService.addHistoricItem(beginStored, new DecimalType(0), TEST_NUMBER); + persistenceService.addHistoricItem(beginStored.plusHours(1), new DecimalType(100), TEST_NUMBER); + persistenceService.addHistoricItem(beginStored.plusHours(2), new DecimalType(0), TEST_NUMBER); + persistenceService.addHistoricItem(beginStored.plusHours(25), new DecimalType(50), TEST_NUMBER); + persistenceService.addHistoricItem(beginStored.plusHours(26), new DecimalType(0), TEST_NUMBER); + + DecimalType average = PersistenceExtensions.averageSince(numberItem, beginStored, + TestCachedValuesPersistenceService.ID); + assertThat(average.doubleValue(), is(closeTo((100.0 + 50.0) / 27.0, 0.01))); + + average = PersistenceExtensions.averageSince(numberItem, beginStored.plusHours(3), + TestCachedValuesPersistenceService.ID); + assertThat(average.doubleValue(), is(closeTo(50.0 / 24.0, 0.01))); + + average = PersistenceExtensions.averageSince(numberItem, now.minusMinutes(30), + TestCachedValuesPersistenceService.ID); + assertThat(average.doubleValue(), is(closeTo(0, 0.01))); + } + @Test public void testAverageBetweenDecimalType() { ZonedDateTime beginStored = ZonedDateTime.of(2005, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); @@ -527,7 +600,7 @@ public void testAverageBetweenDecimalType() { TestPersistenceService.ID); assertThat(average, is(notNullValue())); - assertThat(average.doubleValue(), is(closeTo(2008, 0.01))); + assertThat(average.doubleValue(), is(closeTo((2005.0 + 2010.0) / 2.0, 0.01))); // default persistence service average = PersistenceExtensions.averageBetween(quantityItem, beginStored, endStored); @@ -543,10 +616,12 @@ public void testAverageSinceQuantityType() { assertNull(average); startStored = ZonedDateTime.of(2003, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); - Instant endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()).toInstant(); - long storedInterval = endStored.toEpochMilli() - startStored.toInstant().toEpochMilli(); - long recentInterval = Instant.now().toEpochMilli() - endStored.toEpochMilli(); - double expected = (2007.4994 * storedInterval + 2518.5 * recentInterval) / (storedInterval + recentInterval); + ZonedDateTime endStored = ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); + long storedInterval = Duration.between(startStored, endStored).toDays(); + long recentInterval = Duration.between(endStored, ZonedDateTime.now()).toDays(); + double expected = ((2003.0 + 2011.0) / 2.0 * storedInterval + 2012.0 * recentInterval) + / (storedInterval + recentInterval); + average = PersistenceExtensions.averageSince(quantityItem, startStored, TestPersistenceService.ID); assertNotNull(average); assertEquals(expected, average.doubleValue(), 0.01); @@ -564,7 +639,7 @@ public void testAverageBetweenQuantityType() { TestPersistenceService.ID); assertThat(average, is(notNullValue())); - assertThat(average.doubleValue(), is(closeTo(2008, 0.01))); + assertThat(average.doubleValue(), is(closeTo((2005.0 + 2010.0) / 2, 0.01))); // default persistence service average = PersistenceExtensions.averageBetween(quantityItem, beginStored, endStored); @@ -573,17 +648,17 @@ public void testAverageBetweenQuantityType() { @Test public void testAverageSinceSwitch() { - switchItem.setState(OnOffType.ON); + // switch is 5h ON, 6h OFF, and 4h ON (until now) ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.MINUTES); DecimalType average = PersistenceExtensions.averageSince(switchItem, now.minusHours(15), TestPersistenceService.ID); assertThat(average, is(notNullValue())); - assertThat(average.doubleValue(), is(closeTo(0.625, 0.04))); + assertThat(average.doubleValue(), is(closeTo(9.0 / 15.0, 0.01))); average = PersistenceExtensions.averageSince(switchItem, now.minusHours(7), TestPersistenceService.ID); assertThat(average, is(notNullValue())); - assertThat(average.doubleValue(), is(closeTo(0.714, 0.1))); + assertThat(average.doubleValue(), is(closeTo(4.0 / 7.0, 0.01))); average = PersistenceExtensions.averageSince(switchItem, now.minusHours(6), TestPersistenceService.ID); assertThat(average, is(notNullValue())); @@ -605,6 +680,15 @@ public void testAverageSinceSwitch() { assertThat(average, is(nullValue())); } + @Test + public void testAverageBetweenZeroDuration() { + ZonedDateTime now = ZonedDateTime.now(); + assertDoesNotThrow( + () -> PersistenceExtensions.averageBetween(quantityItem, now, now, TestPersistenceService.ID)); + assertThat(PersistenceExtensions.averageBetween(quantityItem, now, now, TestPersistenceService.ID), + is(nullValue())); + } + @Test public void testSumSinceDecimalType() { DecimalType sum = PersistenceExtensions.sumSince(numberItem, diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestCachedValuesPersistenceService.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestCachedValuesPersistenceService.java new file mode 100644 index 00000000000..27563dd7b7d --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestCachedValuesPersistenceService.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.extensions; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; +import org.openhab.core.persistence.FilterCriteria; +import org.openhab.core.persistence.FilterCriteria.Ordering; +import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.PersistenceItemInfo; +import org.openhab.core.persistence.QueryablePersistenceService; +import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.openhab.core.types.State; + +/** + * A simple persistence service working with cached HistoricItems used for unit tests. + * + * @author Florian Binder - Initial contribution + */ +@NonNullByDefault +public class TestCachedValuesPersistenceService implements QueryablePersistenceService { + + public static final String ID = "testCachedHistoricItems"; + + private final List historicItems = new ArrayList<>(); + + public TestCachedValuesPersistenceService() { + } + + public void addHistoricItem(ZonedDateTime timestamp, State state, String itemName) { + historicItems.add(new CachedHistoricItem(timestamp, state, itemName)); + } + + @Override + public String getId() { + return ID; + } + + @Override + public void store(Item item) { + } + + @Override + public void store(Item item, @Nullable String alias) { + } + + @Override + public Iterable query(FilterCriteria filter) { + Stream stream = historicItems.stream(); + + if (filter.getState() != null) { + throw new UnsupportedOperationException("state filtering is not supported yet"); + } + + if (filter.getItemName() != null) { + stream = stream.filter(hi -> filter.getItemName().equals(hi.getName())); + } + + if (filter.getBeginDate() != null) { + stream = stream.filter(hi -> !filter.getBeginDate().isAfter(hi.getTimestamp())); + } + + if (filter.getEndDate() != null) { + stream = stream.filter(hi -> !filter.getEndDate().isBefore(hi.getTimestamp())); + } + + if (filter.getOrdering() == Ordering.ASCENDING) { + stream = stream.sorted(((o1, o2) -> o1.getTimestamp().compareTo(o2.getTimestamp()))); + } else if (filter.getOrdering() == Ordering.DESCENDING) { + stream = stream.sorted(((o1, o2) -> -o1.getTimestamp().compareTo(o2.getTimestamp()))); + } + + if (filter.getPageNumber() > 0) { + stream = stream.skip(filter.getPageSize() * filter.getPageNumber()); + } + + if (filter.getPageSize() != Integer.MAX_VALUE) { + stream = stream.limit(filter.getPageSize()); + } + + return stream.toList(); + } + + @Override + public Set getItemInfo() { + return Set.of(); + } + + @Override + public String getLabel(@Nullable Locale locale) { + return "Test Label"; + } + + @Override + public List getDefaultStrategies() { + return List.of(); + } + + private static class CachedHistoricItem implements HistoricItem { + private final ZonedDateTime timestamp; + private final State state; + private final String name; + + public CachedHistoricItem(ZonedDateTime timestamp, State state, String name) { + this.timestamp = timestamp; + this.state = state; + this.name = name; + } + + @Override + public ZonedDateTime getTimestamp() { + return timestamp; + } + + @Override + public State getState() { + return state; + } + + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return "CachedHistoricItem [timestamp=" + timestamp + ", state=" + state + ", name=" + name + "]"; + } + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java index 8c5aec88c24..f497a542559 100644 --- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import javax.measure.Unit; @@ -97,7 +98,7 @@ public State getState() { @Override public String getName() { - return filter.getItemName(); + return Objects.requireNonNull(filter.getItemName()); } }); } @@ -132,14 +133,14 @@ public ZonedDateTime getTimestamp() { @Override public State getState() { - Item item = itemRegistry.get(filter.getItemName()); - Unit unit = item instanceof NumberItem ? ((NumberItem) item).getUnit() : null; + Item item = itemRegistry.get(Objects.requireNonNull(filter.getItemName())); + Unit unit = item instanceof NumberItem ni ? ni.getUnit() : null; return unit == null ? new DecimalType(year) : QuantityType.valueOf(year, unit); } @Override public String getName() { - return filter.getItemName(); + return Objects.requireNonNull(filter.getItemName()); } }); } diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceEqualsFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceEqualsFilterTest.java new file mode 100644 index 00000000000..2376cc59b47 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceEqualsFilterTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.items.GenericItem; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; + +/** + * The {@link PersistenceEqualsFilterTest} contains tests for {@link PersistenceEqualsFilter} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class PersistenceEqualsFilterTest { + private static final String ITEM_NAME = "itemName"; + + private @NonNullByDefault({}) @Mock GenericItem item; + + @ParameterizedTest + @MethodSource("argumentProvider") + public void equalsFilterTest(State state, Collection values, boolean expected) { + when(item.getState()).thenReturn(state); + + PersistenceEqualsFilter filter = new PersistenceEqualsFilter("filter", values, false); + assertThat(filter.apply(item), is(expected)); + } + + @ParameterizedTest + @MethodSource("argumentProvider") + public void notEqualsFilterTest(State state, Collection values, boolean expected) { + when(item.getState()).thenReturn(state); + + PersistenceEqualsFilter filter = new PersistenceEqualsFilter("filter", values, true); + assertThat(filter.apply(item), is(not(expected))); + } + + private static Stream argumentProvider() { + return Stream.of(// + // item state, values, result + Arguments.of(new StringType("value1"), List.of("value1", "value2"), true), + Arguments.of(new StringType("value3"), List.of("value1", "value2"), false), + Arguments.of(new DecimalType(5), List.of("3", "5", "9"), true), + Arguments.of(new DecimalType(7), List.of("3", "5", "9"), false), + Arguments.of(new QuantityType<>(10, SIUnits.CELSIUS), List.of("5 °C", "10 °C", "15 °C"), true), + Arguments.of(new QuantityType<>(20, SIUnits.CELSIUS), List.of("5 °C", "10 °C", "15 °C"), false), + Arguments.of(OnOffType.ON, List.of("ON", "UNDEF", "NULL"), true), + Arguments.of(OnOffType.OFF, List.of("ON", "UNDEF", "NULL"), false)); + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceIncludeFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceIncludeFilterTest.java new file mode 100644 index 00000000000..a43b85cab41 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceIncludeFilterTest.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.items.GenericItem; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.State; + +/** + * The {@link PersistenceIncludeFilterTest} contains tests for {@link PersistenceIncludeFilter} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class PersistenceIncludeFilterTest { + private static final String ITEM_NAME = "itemName"; + + private @NonNullByDefault({}) @Mock GenericItem item; + + @ParameterizedTest + @MethodSource("argumentProvider") + public void includeFilterTest(State state, BigDecimal lower, BigDecimal upper, String unit, boolean expected) { + when(item.getState()).thenReturn(state); + + PersistenceIncludeFilter filter = new PersistenceIncludeFilter("filter", lower, upper, unit, false); + assertThat(filter.apply(item), is(expected)); + } + + @ParameterizedTest + @MethodSource("notArgumentProvider") + public void notIncludeFilterTest(State state, BigDecimal lower, BigDecimal upper, String unit, boolean expected) { + when(item.getState()).thenReturn(state); + + PersistenceIncludeFilter filter = new PersistenceIncludeFilter("filter", lower, upper, unit, true); + assertThat(filter.apply(item), is(expected)); + } + + private static Stream argumentProvider() { + return Stream.of(// + // item state, lower, upper, unit, result + // QuantityType + Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true), + Arguments.of(new QuantityType<>("10 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", false), + Arguments.of(new QuantityType<>("20 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", false), + Arguments.of(new QuantityType<>("0 °C"), BigDecimal.valueOf(270), BigDecimal.valueOf(275), "K", true), + // invalid or missing units + Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true), + Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true), + // DecimalType + Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true), + Arguments.of(new DecimalType("17"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "", true), + Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true), + Arguments.of(new DecimalType("10"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", false), + Arguments.of(new DecimalType("20"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", false)); + } + + private static Stream notArgumentProvider() { + return Stream.of(// + // item state, lower, upper, unit, result + // QuantityType + Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", false), + Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("17 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true), + Arguments.of(new QuantityType<>("10 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true), + Arguments.of(new QuantityType<>("20 °C"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "°C", true), + Arguments.of(new QuantityType<>("0 °C"), BigDecimal.valueOf(270), BigDecimal.valueOf(275), "K", false), + // invalid or missing units + Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "°C", true), + Arguments.of(new QuantityType<>("5 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true), + Arguments.of(new QuantityType<>("17 kg"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", true), + // DecimalType + Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(19), "", false), + Arguments.of(new DecimalType("17"), BigDecimal.valueOf(17), BigDecimal.valueOf(19), "", true), + Arguments.of(new DecimalType("17"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true), + Arguments.of(new DecimalType("10"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true), + Arguments.of(new DecimalType("20"), BigDecimal.valueOf(14), BigDecimal.valueOf(17), "", true)); + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java new file mode 100644 index 00000000000..e37e22c5030 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.stream.Stream; + +import javax.measure.quantity.ElectricCurrent; +import javax.measure.quantity.ElectricPotential; +import javax.measure.quantity.Length; +import javax.measure.quantity.Pressure; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; +import org.openhab.core.types.util.UnitUtils; + +import tech.units.indriya.unit.Units; + +/** + * The {@link PersistenceThresholdFilterTest} contains tests for {@link PersistenceThresholdFilter} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class PersistenceThresholdFilterTest { + private static final String ITEM_NAME_1 = "itemName1"; + private static final String ITEM_NAME_2 = "itemName2"; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; + + @BeforeEach + public void setup() { + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); + when(unitProviderMock.getUnit(Pressure.class)).thenReturn(SIUnits.PASCAL); + when(unitProviderMock.getUnit(Length.class)).thenReturn(SIUnits.METRE); + when(unitProviderMock.getUnit(ElectricCurrent.class)).thenReturn(Units.AMPERE); + when(unitProviderMock.getUnit(ElectricPotential.class)).thenReturn(Units.VOLT); + } + + @Test + public void differentItemSameValue() { + filterTest(ITEM_NAME_2, DecimalType.ZERO, DecimalType.ZERO, "", false, true); + } + + @ParameterizedTest + @MethodSource("argumentProvider") + public void filterTest(State state1, State state2, String unit, boolean relative, boolean expected) { + filterTest(ITEM_NAME_1, state1, state2, unit, relative, expected); + } + + private static Stream argumentProvider() { + return Stream.of(// + // same item, same value -> false + Arguments.of(DecimalType.ZERO, DecimalType.ZERO, "", false, false), + // plain decimal, below threshold, absolute + Arguments.of(DecimalType.ZERO, DecimalType.valueOf("5"), "", false, false), + // plain decimal, above threshold, absolute + Arguments.of(DecimalType.ZERO, DecimalType.valueOf("15"), "", false, true), + // plain decimal, below threshold, relative + Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("9.5"), "", true, false), + // plain decimal, above threshold, relative + Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("11.5"), "", true, true), + // quantity type, below threshold, relative + Arguments.of(new QuantityType<>("15 A"), new QuantityType<>("14000 mA"), "", true, false), + // quantity type, above threshold, relative + Arguments.of(new QuantityType<>("2000 mbar"), new QuantityType<>("2.6 bar"), "", true, true), + // quantity type, below threshold, absolute, no unit + Arguments.of(new QuantityType<>("100 K"), new QuantityType<>("105 K"), "", false, false), + // quantity type, above threshold, absolute, no unit + Arguments.of(new QuantityType<>("20 V"), new QuantityType<>("9000 mV"), "", false, true), + // quantity type, below threshold, absolute, with unit + Arguments.of(new QuantityType<>("10 m"), new QuantityType<>("10.002 m"), "mm", false, false), + // quantity type, above threshold, absolute, with unit + Arguments.of(new QuantityType<>("-10 °C"), new QuantityType<>("5 °C"), "K", false, true)); + } + + private void filterTest(String item2name, State state1, State state2, String unit, boolean relative, + boolean expected) { + String itemType = "Number"; + if (state1 instanceof QuantityType q) { + itemType += ":" + UnitUtils.getDimensionName(q.getUnit()); + } + + NumberItem item1 = new NumberItem(itemType, PersistenceThresholdFilterTest.ITEM_NAME_1, unitProviderMock); + NumberItem item2 = new NumberItem(itemType, item2name, unitProviderMock); + + item1.setState(state1); + item2.setState(state2); + + PersistenceFilter filter = new PersistenceThresholdFilter("test", BigDecimal.TEN, unit, relative); + + assertThat(filter.apply(item1), is(true)); + filter.persisted(item1); + assertThat(filter.apply(item2), is(expected)); + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceTimeFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceTimeFilterTest.java new file mode 100644 index 00000000000..38b2879e729 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceTimeFilterTest.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.filter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.library.items.StringItem; + +/** + * The {@link PersistenceTimeFilterTest} contains tests for {@link PersistenceTimeFilter} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceTimeFilterTest { + + @Test + public void testTimeFilter() throws InterruptedException { + PersistenceFilter filter = new PersistenceTimeFilter("test", 1, "s"); + + StringItem item = new StringItem("testItem"); + assertThat(filter.apply(item), is(true)); + filter.persisted(item); + + // immediate store returns false + assertThat(filter.apply(item), is(false)); + + // after interval returns true + Thread.sleep(1500); + assertThat(filter.apply(item), is(true)); + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java new file mode 100644 index 00000000000..b33766fbd4a --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java @@ -0,0 +1,405 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.persistence.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.common.SafeCaller; +import org.openhab.core.common.SafeCallerBuilder; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.PersistenceService; +import org.openhab.core.persistence.QueryablePersistenceService; +import org.openhab.core.persistence.config.PersistenceAllConfig; +import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.config.PersistenceGroupConfig; +import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.filter.PersistenceFilter; +import org.openhab.core.persistence.filter.PersistenceThresholdFilter; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; +import org.openhab.core.persistence.strategy.PersistenceCronStrategy; +import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.openhab.core.scheduler.SchedulerRunnable; +import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.ReadyService; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link PersistenceManagerTest} contains tests for the {@link PersistenceManager} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class PersistenceManagerTest { + private static final String TEST_ITEM_NAME = "testItem"; + private static final String TEST_ITEM2_NAME = "testItem2"; + private static final String TEST_ITEM3_NAME = "testItem3"; + private static final String TEST_GROUP_ITEM_NAME = "groupItem"; + + private static final StringItem TEST_ITEM = new StringItem(TEST_ITEM_NAME); + private static final StringItem TEST_ITEM2 = new StringItem(TEST_ITEM2_NAME); + private static final NumberItem TEST_ITEM3 = new NumberItem(TEST_ITEM3_NAME); + private static final GroupItem TEST_GROUP_ITEM = new GroupItem(TEST_GROUP_ITEM_NAME); + + private static final State TEST_STATE = new StringType("testState1"); + + private static final HistoricItem TEST_HISTORIC_ITEM = new HistoricItem() { + @Override + public ZonedDateTime getTimestamp() { + return ZonedDateTime.now().minusDays(1); + } + + @Override + public State getState() { + return TEST_STATE; + } + + @Override + public String getName() { + return TEST_ITEM_NAME; + } + }; + + private static final String TEST_PERSISTENCE_SERVICE_ID = "testPersistenceService"; + private static final String TEST_QUERYABLE_PERSISTENCE_SERVICE_ID = "testQueryablePersistenceService"; + + private @NonNullByDefault({}) @Mock CronScheduler cronSchedulerMock; + private @NonNullByDefault({}) @Mock ScheduledCompletableFuture scheduledFutureMock; + private @NonNullByDefault({}) @Mock ItemRegistry itemRegistryMock; + private @NonNullByDefault({}) @Mock SafeCaller safeCallerMock; + private @NonNullByDefault({}) @Mock SafeCallerBuilder safeCallerBuilderMock; + private @NonNullByDefault({}) @Mock ReadyService readyServiceMock; + private @NonNullByDefault({}) @Mock PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock; + + private @NonNullByDefault({}) @Mock PersistenceService persistenceServiceMock; + private @NonNullByDefault({}) @Mock QueryablePersistenceService queryablePersistenceServiceMock; + + private @NonNullByDefault({}) PersistenceManager manager; + + @BeforeEach + public void setUp() throws ItemNotFoundException { + TEST_GROUP_ITEM.addMember(TEST_ITEM); + + // set initial states + TEST_ITEM.setState(UnDefType.NULL); + TEST_ITEM2.setState(UnDefType.NULL); + TEST_ITEM3.setState(DecimalType.ZERO); + TEST_GROUP_ITEM.setState(UnDefType.NULL); + + when(itemRegistryMock.getItem(TEST_GROUP_ITEM_NAME)).thenReturn(TEST_GROUP_ITEM); + when(itemRegistryMock.getItem(TEST_ITEM_NAME)).thenReturn(TEST_ITEM); + when(itemRegistryMock.getItem(TEST_ITEM2_NAME)).thenReturn(TEST_ITEM2); + when(itemRegistryMock.getItem(TEST_ITEM3_NAME)).thenReturn(TEST_ITEM3); + when(itemRegistryMock.getItems()).thenReturn(List.of(TEST_ITEM, TEST_ITEM2, TEST_ITEM3, TEST_GROUP_ITEM)); + when(persistenceServiceMock.getId()).thenReturn(TEST_PERSISTENCE_SERVICE_ID); + when(queryablePersistenceServiceMock.getId()).thenReturn(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID); + when(queryablePersistenceServiceMock.query(any())).thenReturn(List.of(TEST_HISTORIC_ITEM)); + + manager = new PersistenceManager(cronSchedulerMock, itemRegistryMock, safeCallerMock, readyServiceMock, + persistenceServiceConfigurationRegistryMock); + manager.addPersistenceService(persistenceServiceMock); + manager.addPersistenceService(queryablePersistenceServiceMock); + + clearInvocations(persistenceServiceMock, queryablePersistenceServiceMock); + } + + @Test + public void appliesToItemWithItemConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void doesNotApplyToItemWithItemConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM2, TEST_STATE); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void appliesToGroupItemWithItemConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_GROUP_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_GROUP_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_GROUP_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void appliesToItemWithGroupConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceGroupConfig(TEST_GROUP_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void doesNotApplyToItemWithGroupConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceGroupConfig(TEST_GROUP_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM2, TEST_STATE); + manager.stateUpdated(TEST_GROUP_ITEM, TEST_STATE); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void appliesToItemWithAllConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + manager.stateUpdated(TEST_ITEM2, TEST_STATE); + manager.stateUpdated(TEST_GROUP_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verify(persistenceServiceMock).store(TEST_ITEM2, null); + verify(persistenceServiceMock).store(TEST_GROUP_ITEM, null); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void updatedStatePersistsEveryUpdate() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + manager.stateUpdated(TEST_ITEM, TEST_STATE); + + verify(persistenceServiceMock, times(2)).store(TEST_ITEM, null); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void updatedStateDoesNotPersistWithChangeStrategy() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.CHANGE, + null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void changedStatePersistsWithChangeStrategy() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.CHANGE, + null); + + manager.stateChanged(TEST_ITEM, UnDefType.UNDEF, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void changedStateDoesNotPersistWithUpdateStrategy() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + null); + + manager.stateChanged(TEST_ITEM, UnDefType.UNDEF, TEST_STATE); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void restoreOnStartupWhenItemNull() { + setupPersistence(new PersistenceAllConfig()); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + verify(readyServiceMock, timeout(1000)).markReady(any()); + + assertThat(TEST_ITEM2.getState(), is(TEST_STATE)); + assertThat(TEST_ITEM.getState(), is(TEST_STATE)); + assertThat(TEST_GROUP_ITEM.getState(), is(TEST_STATE)); + + verify(queryablePersistenceServiceMock, times(3)).query(any()); + + verifyNoMoreInteractions(queryablePersistenceServiceMock); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void noRestoreOnStartupWhenItemNotNull() { + setupPersistence(new PersistenceAllConfig()); + + // set TEST_ITEM state to a value + StringType initialValue = new StringType("value"); + TEST_ITEM.setState(initialValue); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + verify(readyServiceMock, timeout(1000)).markReady(any()); + + assertThat(TEST_ITEM.getState(), is(initialValue)); + assertThat(TEST_ITEM2.getState(), is(TEST_STATE)); + assertThat(TEST_GROUP_ITEM.getState(), is(TEST_STATE)); + + verify(queryablePersistenceServiceMock, times(2)).query(any()); + + verifyNoMoreInteractions(queryablePersistenceServiceMock); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void cronStrategyIsScheduledAndCancelledAndPersistsValue() throws Exception { + ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(SchedulerRunnable.class); + when(cronSchedulerMock.schedule(runnableCaptor.capture(), any())).thenReturn(scheduledFutureMock); + + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM3_NAME), + new PersistenceCronStrategy("withoutFilter", "0 0 * * * ?"), null); + addConfiguration(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM3_NAME), + new PersistenceCronStrategy("withFilter", "0 * * * * ?"), + new PersistenceThresholdFilter("test", BigDecimal.TEN, "", false)); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + + verify(readyServiceMock, timeout(1000)).markReady(any()); + List runnables = runnableCaptor.getAllValues(); + assertThat(runnables.size(), is(2)); + runnables.get(0).run(); + runnables.get(0).run(); + runnables.get(1).run(); + runnables.get(1).run(); + + manager.deactivate(); + + verify(cronSchedulerMock, times(2)).schedule(any(), any()); + verify(scheduledFutureMock, times(2)).cancel(true); + // no filter - persist everything + verify(persistenceServiceMock, times(2)).store(TEST_ITEM3, null); + // filter - persist filtered value + verify(queryablePersistenceServiceMock, times(1)).store(TEST_ITEM3, null); + } + + @Test + public void cronStrategyIsProperlyUpdated() { + when(cronSchedulerMock.schedule(any(), any())).thenReturn(scheduledFutureMock); + + PersistenceServiceConfiguration configuration = addConfiguration(TEST_PERSISTENCE_SERVICE_ID, + new PersistenceItemConfig(TEST_ITEM_NAME), new PersistenceCronStrategy("everyHour", "0 0 * * * ?"), + null); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + + verify(readyServiceMock, timeout(1000)).markReady(any()); + + manager.updated(configuration, configuration); + manager.deactivate(); + + verify(cronSchedulerMock, times(2)).schedule(any(), any()); + verify(scheduledFutureMock, times(2)).cancel(true); + } + + @Test + public void filterAppliesOnStateUpdate() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + new PersistenceThresholdFilter("test", BigDecimal.TEN, "", false)); + + manager.stateUpdated(TEST_ITEM3, DecimalType.ZERO); + manager.stateUpdated(TEST_ITEM3, DecimalType.ZERO); + + verify(persistenceServiceMock, times(1)).store(TEST_ITEM3, null); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + /** + * Add a configuration for restoring TEST_ITEM and mock the SafeCaller + */ + private void setupPersistence(PersistenceConfig itemConfig) { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, itemConfig, PersistenceStrategy.Globals.RESTORE, null); + addConfiguration(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID, itemConfig, PersistenceStrategy.Globals.RESTORE, null); + + when(safeCallerMock.create(queryablePersistenceServiceMock, QueryablePersistenceService.class)) + .thenReturn(safeCallerBuilderMock); + when(safeCallerBuilderMock.onTimeout(any())).thenReturn(safeCallerBuilderMock); + when(safeCallerBuilderMock.onException(any())).thenReturn(safeCallerBuilderMock); + when(safeCallerBuilderMock.build()).thenReturn(queryablePersistenceServiceMock); + } + + /** + * Add a configuration to the manager + * + * @param serviceId the persistence service id + * @param itemConfig the item configuration + * @param strategy the strategy + * @param filter a persistence filter + * @return the added strategy + */ + private PersistenceServiceConfiguration addConfiguration(String serviceId, PersistenceConfig itemConfig, + PersistenceStrategy strategy, @Nullable PersistenceFilter filter) { + List filters = filter != null ? List.of(filter) : List.of(); + + PersistenceItemConfiguration itemConfiguration = new PersistenceItemConfiguration(List.of(itemConfig), null, + List.of(strategy), filters); + + List strategies = PersistenceStrategy.Globals.STRATEGIES.containsValue(strategy) + ? List.of() + : List.of(strategy); + + PersistenceServiceConfiguration serviceConfiguration = new PersistenceServiceConfiguration(serviceId, + List.of(itemConfiguration), List.of(), strategies, filters); + manager.added(serviceConfiguration); + + return serviceConfiguration; + } +} diff --git a/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy b/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy index 557d9a36801..0012ff6be0b 100755 --- a/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy +++ b/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy @@ -22,10 +22,6 @@ baseDir = Paths.get(getClass().protectionDomain.codeSource.location.toURI()).get header = header() def tagSets = new TreeMap() -def locations = new TreeSet() -def equipments = new TreeSet() -def points = new TreeSet() -def properties = new TreeSet() def labelsFile = new FileWriter("${baseDir}/src/main/resources/tags.properties") labelsFile.write("# Generated content - do not edit!\n") @@ -36,63 +32,26 @@ for (line in parseCsv(new FileReader("${baseDir}/model/SemanticTags.csv"), separ def tagSet = (line.Parent ? tagSets.get(line.Parent) : line.Type) + "_" + line.Tag tagSets.put(line.Tag,tagSet) - createTagSetClass(line, tagSet) appendLabelsFile(labelsFile, line, tagSet) switch(line.Type) { - case "Location" : locations.add(line.Tag); break; - case "Equipment" : equipments.add(line.Tag); break; - case "Point" : points.add(line.Tag); break; - case "Property" : properties.add(line.Tag); break; + case "Location" : break; + case "Equipment" : break; + case "Point" : break; + case "Property" : break; default : println "Unrecognized type " + line.Type } } labelsFile.close() -createLocationsFile(locations) -createEquipmentsFile(equipments) -createPointsFile(points) -createPropertiesFile(properties) +createDefaultProviderFile(tagSets) println "\n\nTagSets:" for (String tagSet : tagSets) { println tagSet } -def createTagSetClass(def line, String tagSet) { - def tag = line.Tag - def type = line.Type - def label = line.Label - def synonyms = line.Synonyms - def desc = line.Description - def parent = line.Parent - def parentClass = parent ? parent : type - def pkg = type.toLowerCase() - def ch = label.toLowerCase().charAt(0) - def article = ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u' ? "an" : "a" - def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/${pkg}/${tag}.java") - file.write(header) - file.write("package org.openhab.core.semantics.model." + pkg + ";\n\n") - file.write("import org.eclipse.jdt.annotation.NonNullByDefault;\n") - if (!parent) { - file.write("import org.openhab.core.semantics." + type + ";\n") - } - file.write("""import org.openhab.core.semantics.TagInfo; - -/** - * This class defines ${article} ${label}. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "${tagSet}", label = "${label}", synonyms = "${synonyms}", description = "${desc}") -public interface ${tag} extends ${parentClass} { -} -""") - file.close() -} - def appendLabelsFile(FileWriter file, def line, String tagSet) { file.write(tagSet + "=" + line.Label) if (line.Synonyms) { @@ -101,152 +60,59 @@ def appendLabelsFile(FileWriter file, def line, String tagSet) { file.write("\n") } -def createLocationsFile(Set locations) { - def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/location/Locations.java") +def createDefaultProviderFile(def tagSets) { + def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java") file.write(header) - file.write("""package org.openhab.core.semantics.model.location; + file.write("""package org.openhab.core.semantics.model; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Location; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagProvider; +import org.osgi.service.component.annotations.Component; /** - * This class provides a stream of all defined locations. + * This class defines a provider of all default semantic tags. * * @author Generated from generateTagClasses.groovy - Initial contribution */ @NonNullByDefault -public class Locations { +@Component(immediate = true, service = { SemanticTagProvider.class, DefaultSemanticTagProvider.class }) +public class DefaultSemanticTagProvider implements SemanticTagProvider { - static final Set> LOCATIONS = new HashSet<>(); + private List defaultTags; - static { - LOCATIONS.add(Location.class); + public DefaultSemanticTagProvider() { + this.defaultTags = new ArrayList<>(); + defaultTags.add(new SemanticTagImpl("Equipment", "", "", "")); + defaultTags.add(new SemanticTagImpl("Location", "", "", "")); + defaultTags.add(new SemanticTagImpl("Point", "", "", "")); + defaultTags.add(new SemanticTagImpl("Property", "", "", "")); """) - for (String location : locations) { - file.write(" LOCATIONS.add(${location}.class);\n") - } - file.write(""" } - - public static Stream> stream() { - return LOCATIONS.stream(); - } -} -""") - file.close() -} - -def createEquipmentsFile(Set equipments) { - def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java") - file.write(header) - file.write("""package org.openhab.core.semantics.model.equipment; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; - -/** - * This class provides a stream of all defined equipments. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Equipments { - - static final Set> EQUIPMENTS = new HashSet<>(); - - static { - EQUIPMENTS.add(Equipment.class); -""") - for (String equipment : equipments) { - file.write(" EQUIPMENTS.add(${equipment}.class);\n") - } - file.write(""" } - - public static Stream> stream() { - return EQUIPMENTS.stream(); - } -} + for (line in parseCsv(new FileReader("${baseDir}/model/SemanticTags.csv"), separator: ',')) { + def tagId = (line.Parent ? tagSets.get(line.Parent) : line.Type) + "_" + line.Tag + file.write(""" defaultTags.add(new SemanticTagImpl("${tagId}", // + "${line.Label}", "${line.Description}", "${line.Synonyms}")); """) - file.close() -} - -def createPointsFile(Set points) { - def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/point/Points.java") - file.write(header) - file.write("""package org.openhab.core.semantics.model.point; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; - -/** - * This class provides a stream of all defined points. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Points { - - static final Set> POINTS = new HashSet<>(); - - static { - POINTS.add(Point.class); -""") - for (String point : points) { - file.write(" POINTS.add(${point}.class);\n") } file.write(""" } - public static Stream> stream() { - return POINTS.stream(); + @Override + public Collection getAll() { + return defaultTags; } -} -""") - file.close() -} - -def createPropertiesFile(Set properties) { - def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/property/Properties.java") - file.write(header) - file.write("""package org.openhab.core.semantics.model.property; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -/** - * This class provides a stream of all defined properties. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Properties { - - static final Set> PROPERTIES = new HashSet<>(); - - static { - PROPERTIES.add(Property.class); -""") - for (String property : properties) { - file.write(" PROPERTIES.add(${property}.class);\n") + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { } - file.write(""" } - public static Stream> stream() { - return PROPERTIES.stream(); + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { } } """) diff --git a/bundles/org.openhab.core.semantics/pom.xml b/bundles/org.openhab.core.semantics/pom.xml index 92a08fd1236..1ebf3cec206 100644 --- a/bundles/org.openhab.core.semantics/pom.xml +++ b/bundles/org.openhab.core.semantics/pom.xml @@ -20,6 +20,12 @@ org.openhab.core ${project.version} + + org.ow2.asm + asm + 9.4 + provided + diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Equipment.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Equipment.java index f3de8702dc0..096093cfa31 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Equipment.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Equipment.java @@ -22,7 +22,6 @@ * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Equipment") public interface Equipment extends Tag { @Nullable diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Location.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Location.java index 591d307c212..cc5a890dfb3 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Location.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Location.java @@ -22,10 +22,9 @@ * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Location") public interface Location extends Tag { - public static String name() { + static String name() { return "Location"; } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java new file mode 100644 index 00000000000..fbb7be82559 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java @@ -0,0 +1,72 @@ +/** + * 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.semantics; + +import java.util.Collection; +import java.util.Comparator; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.AbstractManagedProvider; +import org.openhab.core.semantics.dto.SemanticTagDTO; +import org.openhab.core.semantics.dto.SemanticTagDTOMapper; +import org.openhab.core.storage.StorageService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link ManagedSemanticTagProvider} is an OSGi service, that allows to add or remove + * semantic tags at runtime by calling {@link ManagedSemanticTagProvider#add(SemanticTag)} + * or {@link ManagedSemanticTagProvider#remove(String)}. + * An added semantic tag is automatically exposed to the {@link SemanticTagRegistry}. + * Persistence of added semantic tags is handled by a {@link StorageService}. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { SemanticTagProvider.class, ManagedSemanticTagProvider.class }) +public class ManagedSemanticTagProvider extends AbstractManagedProvider + implements SemanticTagProvider { + + @Activate + public ManagedSemanticTagProvider(final @Reference StorageService storageService) { + super(storageService); + } + + @Override + protected String getStorageName() { + return SemanticTag.class.getName(); + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + public Collection getAll() { + // Sort tags by uid to be sure that tag classes will be created in the right order + return super.getAll().stream().sorted(Comparator.comparing(SemanticTag::getUID)).toList(); + } + + @Override + protected @Nullable SemanticTag toElement(String uid, SemanticTagDTO persistedTag) { + return SemanticTagDTOMapper.map(persistedTag); + } + + @Override + protected SemanticTagDTO toPersistableElement(SemanticTag tag) { + return SemanticTagDTOMapper.map(tag); + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Point.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Point.java index a707ae9976d..8971feb32ce 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Point.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Point.java @@ -22,7 +22,6 @@ * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Point") public interface Point extends Tag { @Nullable diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Property.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Property.java index 5caca212469..d119925c14d 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Property.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/Property.java @@ -20,6 +20,5 @@ * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault -@TagInfo(id = "MeasurementProperty") public interface Property extends Tag { } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java new file mode 100644 index 00000000000..d1782911521 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java @@ -0,0 +1,71 @@ +/** + * 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.semantics; + +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Identifiable; + +/** + * This interface defines the core features of an openHAB semantic tag. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface SemanticTag extends Identifiable { + + /** + * Returns the name of the semantic tag. + * + * @return the name of the semantic tag + */ + String getName(); + + /** + * Returns the UID of the parent tag. + * + * @return the UID of the parent tag + */ + String getParentUID(); + + /** + * Returns the label of the semantic tag. + * + * @return semantic tag label or an empty string if undefined + */ + String getLabel(); + + /** + * Returns the description of the semantic tag. + * + * @return semantic tag description or an empty string if undefined + */ + String getDescription(); + + /** + * Returns the synonyms of the semantic tag. + * + * @return semantic tag synonyms as a List + */ + List getSynonyms(); + + /** + * Returns the localized semantic tag. + * + * @param locale the locale to be used + * @return the localized semantic tag + */ + SemanticTag localized(Locale locale); +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java new file mode 100644 index 00000000000..37c7cd0381d --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java @@ -0,0 +1,133 @@ +/** + * 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.semantics; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.ResourceBundle.Control; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This is the main implementing class of the {@link SemanticTag} interface. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class SemanticTagImpl implements SemanticTag { + + private static final String TAGS_BUNDLE_NAME = "tags"; + + private String uid; + private String name; + private String parent; + private String label; + private String description; + private List synonyms; + + public SemanticTagImpl(String uid, @Nullable String label, @Nullable String description, + @Nullable List synonyms) { + this(uid, label, description); + if (synonyms != null) { + this.synonyms = new ArrayList<>(); + for (String synonym : synonyms) { + this.synonyms.add(synonym.trim()); + } + } + } + + public SemanticTagImpl(String uid, @Nullable String label, @Nullable String description, + @Nullable String synonyms) { + this(uid, label, description); + if (synonyms != null && !synonyms.isBlank()) { + this.synonyms = new ArrayList<>(); + for (String synonym : synonyms.split(",")) { + this.synonyms.add(synonym.trim()); + } + } + } + + private SemanticTagImpl(String uid, @Nullable String label, @Nullable String description) { + this.uid = uid; + int idx = uid.lastIndexOf("_"); + if (idx < 0) { + this.name = uid.trim(); + this.parent = ""; + } else { + this.name = uid.substring(idx + 1).trim(); + this.parent = uid.substring(0, idx).trim(); + } + this.label = label == null ? "" : label.trim(); + this.description = description == null ? "" : description.trim(); + this.synonyms = List.of(); + } + + @Override + public String getUID() { + return uid; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getParentUID() { + return parent; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public List getSynonyms() { + return synonyms; + } + + @Override + public SemanticTag localized(Locale locale) { + ResourceBundle rb = ResourceBundle.getBundle(TAGS_BUNDLE_NAME, locale, + Control.getNoFallbackControl(Control.FORMAT_PROPERTIES)); + String label; + List synonyms; + try { + String entry = rb.getString(uid); + int idx = entry.indexOf(","); + if (idx >= 0) { + label = entry.substring(0, idx); + String synonymsCsv = entry.substring(idx + 1); + synonyms = synonymsCsv.isBlank() ? null : List.of(synonymsCsv.split(",")); + } else { + label = entry; + synonyms = null; + } + } catch (MissingResourceException e) { + label = getLabel(); + synonyms = getSynonyms(); + } + + return new SemanticTagImpl(uid, label, getDescription(), synonyms); + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Gate.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java similarity index 58% rename from bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Gate.java rename to bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java index beadc24bce1..3d4c0a5a14f 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Gate.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java @@ -10,17 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.semantics.model.equipment; +package org.openhab.core.semantics; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; +import org.openhab.core.common.registry.Provider; /** - * This class defines a Gate. + * The {@link SemanticTagProvider} is responsible for providing semantic tags. * - * @author Generated from generateTagClasses.groovy - Initial contribution + * @author Laurent Garnier - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Equipment_Door_Gate", label = "Gate", synonyms = "Gates", description = "") -public interface Gate extends Door { +public interface SemanticTagProvider extends Provider { + } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java new file mode 100644 index 00000000000..7c84ab65c2c --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java @@ -0,0 +1,80 @@ +/** + * 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.semantics; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.Registry; + +/** + * {@link SemanticTagRegistry} tracks all {@link SemanticTag}s from different {@link SemanticTagProvider}s + * and provides access to them. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface SemanticTagRegistry extends Registry { + + /** + * Retrieves the class for a given id. + * + * @param tagId the id of the tag. The id can be fully qualified (e.g. "Location_Room_Bedroom") or a segment, if + * this uniquely identifies the tag + * (e.g. "Bedroom"). + * @return the class for the id or null, if non exists. + */ + @Nullable + Class getTagClassById(String tagId); + + /** + * Checks if a tag with a given id can be added to the registry. + * + * To be added, no tag with this id must already exist in the registry, the tag name extracted from this id + * must have a valid syntax, the parent tag extracted from this id must already exists in the registry and + * should be either a default semantic tag or a managed semantic tag, and no tag with a same name must already + * exist in the registry. + * + * @param tag a tag to be added to the registry + * @return true if the tag can be added, false if not + */ + boolean canBeAdded(SemanticTag tag); + + /** + * Returns the provided tag + all tags having the provided tag as ancestor. + * + * @param tag a tag in the registry + * @return a list of all tags having the provided tag as ancestor, including the provided tag itself + */ + List getSubTree(SemanticTag tag); + + /** + * Indicates if a tag is editable. + * + * To be editable, a tag must be managed. + * + * @param tag a tag in the registry + * @return true if the provided tag is editable, false if not + */ + boolean isEditable(SemanticTag tag); + + /** + * Removes the provided tag and all tags having the provided tag as ancestor. + * + * Only removable (managed) tags are removed. + * + * @param tag a tag in the registry + */ + void removeSubTree(SemanticTag tag); +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java index 6dd51feac59..fecebc542ab 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java @@ -12,25 +12,14 @@ */ package org.openhab.core.semantics; -import java.util.List; -import java.util.Locale; +import java.util.Collections; import java.util.Map; -import java.util.MissingResourceException; -import java.util.Optional; -import java.util.ResourceBundle; -import java.util.ResourceBundle.Control; import java.util.Set; import java.util.TreeMap; -import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.Item; -import org.openhab.core.semantics.model.equipment.Equipments; -import org.openhab.core.semantics.model.location.Locations; -import org.openhab.core.semantics.model.point.Measurement; -import org.openhab.core.semantics.model.point.Points; -import org.openhab.core.semantics.model.property.Properties; import org.openhab.core.types.StateDescription; /** @@ -38,19 +27,19 @@ * For everything that is not static, the {@link SemanticsService} should be used instead. * * @author Kai Kreuzer - Initial contribution + * @author Jimmy Tanagra - Add the ability to add new tags at runtime + * @author Laurent Garnier - Several methods moved into class SemanticsService or SemanticTagRegistry */ @NonNullByDefault public class SemanticTags { - private static final String TAGS_BUNDLE_NAME = "tags"; - - private static final Map> TAGS = new TreeMap<>(); + private static final Map> TAGS = Collections.synchronizedMap(new TreeMap<>()); static { - Locations.stream().forEach(location -> addTagSet(location)); - Equipments.stream().forEach(equipment -> addTagSet(equipment)); - Points.stream().forEach(point -> addTagSet(point)); - Properties.stream().forEach(property -> addTagSet(property)); + addTagSet("Location", Location.class); + addTagSet("Equipment", Equipment.class); + addTagSet("Point", Point.class); + addTagSet("Property", Property.class); } /** @@ -65,44 +54,6 @@ public class SemanticTags { return TAGS.get(tagId); } - public static @Nullable Class getByLabel(String tagLabel, Locale locale) { - Optional> tag = TAGS.values().stream().distinct() - .filter(t -> getLabel(t, locale).equalsIgnoreCase(tagLabel)).findFirst(); - return tag.isPresent() ? tag.get() : null; - } - - public static List> getByLabelOrSynonym(String tagLabelOrSynonym, Locale locale) { - return TAGS.values().stream().distinct() - .filter(t -> getLabelAndSynonyms(t, locale).contains(tagLabelOrSynonym.toLowerCase(locale))) - .collect(Collectors.toList()); - } - - public static List getLabelAndSynonyms(Class tag, Locale locale) { - ResourceBundle rb = ResourceBundle.getBundle(TAGS_BUNDLE_NAME, locale, - Control.getNoFallbackControl(Control.FORMAT_PROPERTIES)); - try { - String entry = rb.getString(tag.getAnnotation(TagInfo.class).id()); - return List.of(entry.toLowerCase(locale).split(",")); - } catch (MissingResourceException e) { - return List.of(tag.getAnnotation(TagInfo.class).label()); - } - } - - public static String getLabel(Class tag, Locale locale) { - ResourceBundle rb = ResourceBundle.getBundle(TAGS_BUNDLE_NAME, locale, - Control.getNoFallbackControl(Control.FORMAT_PROPERTIES)); - try { - String entry = rb.getString(tag.getAnnotation(TagInfo.class).id()); - if (entry.contains(",")) { - return entry.substring(0, entry.indexOf(",")); - } else { - return entry; - } - } catch (MissingResourceException e) { - return tag.getAnnotation(TagInfo.class).label(); - } - } - /** * Determines the semantic type of an {@link Item} i.e. a sub-type of {@link Location}, {@link Equipment} or * {@link Point}. @@ -121,9 +72,9 @@ public static String getLabel(Class tag, Locale locale) { if (getProperty(item) != null) { StateDescription stateDescription = item.getStateDescription(); if (stateDescription != null && stateDescription.isReadOnly()) { - return Measurement.class; + return getById("Point_Measurement"); } else { - return org.openhab.core.semantics.model.point.Control.class; + return getById("Point_Control"); } } else { return null; @@ -199,12 +150,11 @@ public static String getLabel(Class tag, Locale locale) { return null; } - private static void addTagSet(Class tagSet) { - String id = tagSet.getAnnotation(TagInfo.class).id(); - while (id.indexOf("_") != -1) { - TAGS.put(id, tagSet); - id = id.substring(id.indexOf("_") + 1); - } + public static void addTagSet(String id, Class tagSet) { TAGS.put(id, tagSet); } + + public static void removeTagSet(String id, Class tagSet) { + TAGS.remove(id, tagSet); + } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticsService.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticsService.java index 7e321aaaf78..df36cc33cd2 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticsService.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticsService.java @@ -12,16 +12,19 @@ */ package org.openhab.core.semantics; +import java.util.List; import java.util.Locale; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.Item; /** * This interface defines a service, which offers functionality regarding semantic tags. * * @author Kai Kreuzer - Initial contribution + * @author Laurent Garnier - Few methods moved from class SemanticTags in order to use the semantic tag registry */ @NonNullByDefault public interface SemanticsService { @@ -45,4 +48,34 @@ public interface SemanticsService { * @return as set of items that are located in the given location(s) */ Set getItemsInLocation(String labelOrSynonym, Locale locale); + + /** + * Retrieves the first semantic tag having label matching the given parameter. + * Case is ignored. + * + * @param tagLabel the searched label + * @param locale the locale to be considered + * @return the tag class of the first matching semantic tag or null if no matching found + */ + @Nullable + Class getByLabel(String tagLabel, Locale locale); + + /** + * Retrieves all semantic tags having label or a synonym matching the given parameter. + * Case is ignored. + * + * @param tagLabelOrSynonym the searched label or synonym + * @param locale the locale to be considered + * @return the List of tag classes of all matching semantic tags + */ + List> getByLabelOrSynonym(String tagLabelOrSynonym, Locale locale); + + /** + * Gets the label and all synonyms of a semantic tag using the given locale. + * + * @param tagClass the tag class + * @param locale the locale to be considered + * @return the list containing the label and all synonyms of a semantic tag + */ + List getLabelAndSynonyms(Class tagClass, Locale locale); } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/TagInfo.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/TagInfo.java deleted file mode 100644 index 89b53e7e607..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/TagInfo.java +++ /dev/null @@ -1,37 +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.semantics; - -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * This is an annotation to be used on semantic tag classes for providing their ids, labels and descriptions. - * - * @author Kai Kreuzer - Initial contribution - */ -@Retention(RUNTIME) -@Target(TYPE) -public @interface TagInfo { - - String id(); - - String label() default ""; - - String synonyms() default ""; - - String description() default ""; -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java new file mode 100644 index 00000000000..db00e10f968 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.semantics.dto; + +import java.util.List; + +/** + * This is a data transfer object that is used to serialize semantic tags. + * + * @author Laurent Garnier - Initial contribution + */ +public class SemanticTagDTO { + + public String uid; + public String label; + public String description; + public List synonyms; + + public SemanticTagDTO() { + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java new file mode 100644 index 00000000000..0987bb6e1d4 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java @@ -0,0 +1,60 @@ +/** + * 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.semantics.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; + +/** + * The {@link SemanticTagDTOMapper} is an utility class to map semantic tags into + * semantic tag data transfer objects (DTOs). + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class SemanticTagDTOMapper { + + /** + * Maps semantic tag DTO into semantic tag object. + * + * @param tagDTO the DTO + * @return the semantic tag object + */ + public static @Nullable SemanticTag map(@Nullable SemanticTagDTO tagDTO) { + if (tagDTO == null) { + throw new IllegalArgumentException("The argument 'tagDTO' must not be null."); + } + if (tagDTO.uid == null) { + throw new IllegalArgumentException("The argument 'tagDTO.uid' must not be null."); + } + + return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms); + } + + /** + * Maps semantic tag object into semantic tag DTO. + * + * @param tag the semantic tag + * @return the semantic tag DTO + */ + public static SemanticTagDTO map(SemanticTag tag) { + SemanticTagDTO tagDTO = new SemanticTagDTO(); + tagDTO.uid = tag.getUID(); + tagDTO.label = tag.getLabel(); + tagDTO.description = tag.getDescription(); + tagDTO.synonyms = tag.getSynonyms(); + return tagDTO; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java new file mode 100644 index 00000000000..55a9d01151c --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java @@ -0,0 +1,285 @@ +/** + * 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.semantics.internal; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.openhab.core.common.registry.AbstractRegistry; +import org.openhab.core.common.registry.Provider; +import org.openhab.core.semantics.Equipment; +import org.openhab.core.semantics.Location; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.Point; +import org.openhab.core.semantics.Property; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagProvider; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the main implementing class of the {@link SemanticTagRegistry} interface. It + * keeps track of all declared semantic tags of all semantic tags providers and keeps + * their current state in memory. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true) +public class SemanticTagRegistryImpl extends AbstractRegistry + implements SemanticTagRegistry { + + private static final SemanticClassLoader CLASS_LOADER = new SemanticClassLoader(); + + private final Logger logger = LoggerFactory.getLogger(SemanticTagRegistryImpl.class); + + private final DefaultSemanticTagProvider defaultSemanticTagProvider; + private final ManagedSemanticTagProvider managedProvider; + + @Activate + public SemanticTagRegistryImpl(@Reference DefaultSemanticTagProvider defaultSemanticTagProvider, + @Reference ManagedSemanticTagProvider managedProvider) { + super(SemanticTagProvider.class); + this.defaultSemanticTagProvider = defaultSemanticTagProvider; + this.managedProvider = managedProvider; + // Add the default semantic tags provider first, before all others + super.addProvider(defaultSemanticTagProvider); + super.addProvider(managedProvider); + setManagedProvider(managedProvider); + } + + @Override + protected void addProvider(Provider provider) { + // Ignore the default semantic tags provider and the managed provider (they are added in the constructor) + if (!provider.equals(defaultSemanticTagProvider) && !provider.equals(managedProvider)) { + logger.trace("addProvider {} => calling super.addProvider", provider.getClass().getSimpleName()); + super.addProvider(provider); + } else { + logger.trace("addProvider {} => ignoring it", provider.getClass().getSimpleName()); + } + } + + @Override + public @Nullable Class getTagClassById(String tagId) { + return SemanticTags.getById(tagId); + } + + /** + * Builds the fully qualified id for a semantic tag class. + * + * @param tag the semantic tag class + * @return the fully qualified id + */ + public static String buildId(Class tag) { + return buildId("", tag); + } + + private static String buildId(String relativeId, Class tag) { + if (!Location.class.isAssignableFrom(tag) && !Equipment.class.isAssignableFrom(tag) + && !Point.class.isAssignableFrom(tag) && !Property.class.isAssignableFrom(tag)) { + return relativeId; + } + String id = tag.getSimpleName(); + if (!relativeId.isEmpty()) { + id += "_" + relativeId; + } + return buildId(id, tag.getInterfaces()[0]); + } + + @Override + public boolean canBeAdded(SemanticTag tag) { + String id = tag.getUID(); + // check that a tag with this id does not already exist in the registry + if (get(id) != null) { + return false; + } + // Extract the tag name and the parent tag + int lastSeparator = id.lastIndexOf("_"); + if (lastSeparator <= 0) { + return false; + } + String name = id.substring(lastSeparator + 1); + String parentId = id.substring(0, lastSeparator); + SemanticTag parent = get(parentId); + // Check that the tag name has a valid syntax and the parent tag already exists + // and is either a default tag or a managed tag + // Check also that a semantic tag class with the same name does not already exist + return name.matches("[A-Z][a-zA-Z0-9]+") && parent != null + && (managedProvider.get(parentId) != null || defaultSemanticTagProvider.getAll().contains(parent)) + && getTagClassById(name) == null; + } + + @Override + public List getSubTree(SemanticTag tag) { + List ids = getAll().stream().map(t -> t.getUID()).filter(uid -> uid.startsWith(tag.getUID() + "_")) + .toList(); + List tags = new ArrayList<>(); + tags.add(tag); + ids.forEach(id -> { + SemanticTag t = get(id); + if (t != null) { + tags.add(t); + } + }); + return tags; + } + + @Override + public boolean isEditable(SemanticTag tag) { + return managedProvider.get(tag.getUID()) != null; + } + + @Override + public void removeSubTree(SemanticTag tag) { + // Get tags id in reverse order + List ids = getSubTree(tag).stream().filter(this::isEditable).map(SemanticTag::getUID) + .sorted(Comparator.reverseOrder()).toList(); + ids.forEach(managedProvider::remove); + } + + @Override + protected void onAddElement(SemanticTag tag) throws IllegalArgumentException { + logger.trace("onAddElement {}", tag.getUID()); + super.onAddElement(tag); + + String uid = tag.getUID(); + Class tagClass = getTagClassById(uid); + if (tagClass != null) { + // Class already exists + return; + } + + String type; + String className; + Class newTag; + int lastSeparator = uid.lastIndexOf("_"); + if (lastSeparator < 0) { + switch (uid) { + case "Equipment": + newTag = Equipment.class; + break; + case "Location": + newTag = Location.class; + break; + case "Point": + newTag = Point.class; + break; + case "Property": + newTag = Property.class; + break; + default: + throw new IllegalArgumentException("Failed to create semantic tag '" + uid + + "': only Equipment, Location, Point and Property are allowed as root tags."); + } + type = uid; + className = newTag.getClass().getName(); + } else { + String name = uid.substring(lastSeparator + 1); + String parentId = uid.substring(0, lastSeparator); + Class parentTagClass = getTagClassById(parentId); + if (parentTagClass == null) { + throw new IllegalArgumentException( + "Failed to create semantic tag '" + uid + "': no existing parent tag with id " + parentId); + } + if (!name.matches("[A-Z][a-zA-Z0-9]+")) { + throw new IllegalArgumentException("Failed to create semantic tag '" + uid + + "': tag name must start with a capital letter and contain only alphanumerics."); + } + tagClass = getTagClassById(name); + if (tagClass != null) { + throw new IllegalArgumentException( + "Failed to create semantic tag '" + uid + "': tag '" + buildId(tagClass) + "' already exists."); + } + + type = parentId.split("_")[0]; + className = "org.openhab.core.semantics.model." + type.toLowerCase() + "." + name; + try { + newTag = (Class) Class.forName(className, false, CLASS_LOADER); + logger.debug("'{}' semantic {} tag already exists.", className, type); + } catch (ClassNotFoundException e) { + // Create the tag interface + ClassWriter classWriter = new ClassWriter(0); + classWriter.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE, + className.replace('.', '/'), null, "java/lang/Object", + new String[] { parentTagClass.getName().replace('.', '/') }); + classWriter.visitSource("Status.java", null); + classWriter.visitEnd(); + byte[] byteCode = classWriter.toByteArray(); + try { + newTag = (Class) CLASS_LOADER.defineClass(className, byteCode); + logger.debug("'{}' semantic {} tag created.", className, type); + } catch (Exception ex) { + logger.warn("Failed to create semantic tag '{}': {}", className, ex.getMessage()); + throw new IllegalArgumentException("Failed to create semantic tag '" + className + "'", ex); + } + } + } + + addTagSet(uid, newTag); + logger.debug("'{}' semantic {} tag added.", className, type); + } + + @Override + protected void onRemoveElement(SemanticTag tag) { + logger.trace("onRemoveElement {}", tag.getUID()); + super.onRemoveElement(tag); + removeTagSet(tag.getUID()); + } + + private void addTagSet(String tagId, Class tagSet) { + logger.trace("addTagSet {}", tagId); + String id = tagId; + while (id.indexOf("_") != -1) { + SemanticTags.addTagSet(id, tagSet); + id = id.substring(id.indexOf("_") + 1); + } + SemanticTags.addTagSet(id, tagSet); + } + + private void removeTagSet(String tagId) { + logger.trace("removeTagSet {}", tagId); + Class tagSet = getTagClassById(tagId); + if (tagSet == null) { + return; + } + String id = tagId; + while (id.indexOf("_") != -1) { + SemanticTags.removeTagSet(id, tagSet); + id = id.substring(id.indexOf("_") + 1); + } + SemanticTags.removeTagSet(id, tagSet); + } + + private static class SemanticClassLoader extends ClassLoader { + public SemanticClassLoader() { + super(SemanticTagRegistryImpl.class.getClassLoader()); + } + + public Class defineClass(String className, byte[] byteCode) { + // defineClass is protected in the normal ClassLoader + return defineClass(className, byteCode, 0, byteCode.length); + } + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java index df5b4921c5a..535fc3f4a3e 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java @@ -33,9 +33,9 @@ import org.openhab.core.semantics.Location; import org.openhab.core.semantics.Point; import org.openhab.core.semantics.Property; +import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticTags; import org.openhab.core.semantics.Tag; -import org.openhab.core.semantics.TagInfo; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -75,7 +75,8 @@ public int compare(String s1, String s2) { private final ItemRegistry itemRegistry; @Activate - public SemanticsMetadataProvider(final @Reference ItemRegistry itemRegistry) { + public SemanticsMetadataProvider(final @Reference ItemRegistry itemRegistry, + final @Reference SemanticTagRegistry semanticTagRegistry) { this.itemRegistry = itemRegistry; } @@ -111,7 +112,7 @@ private void processItem(Item item) { if (type != null) { processProperties(item, configuration); processHierarchy(item, configuration); - Metadata md = new Metadata(key, type.getAnnotation(TagInfo.class).id(), configuration); + Metadata md = new Metadata(key, SemanticTagRegistryImpl.buildId(type), configuration); Metadata oldMd = semantics.put(item.getName(), md); if (oldMd == null) { notifyListenersAboutAddedElement(md); @@ -125,8 +126,8 @@ private void processItem(Item item) { } } - if (item instanceof GroupItem) { - for (Item memberItem : ((GroupItem) item).getMembers()) { + if (item instanceof GroupItem groupItem) { + for (Item memberItem : groupItem.getMembers()) { processItem(memberItem); } } @@ -148,7 +149,7 @@ private void processProperties(Item item, Map configuration) { if (entityClass.isAssignableFrom(type)) { Class p = SemanticTags.getProperty(item); if (p != null) { - configuration.put(relation.getValue(), p.getAnnotation(TagInfo.class).id()); + configuration.put(relation.getValue(), SemanticTagRegistryImpl.buildId(p)); } } } @@ -169,8 +170,7 @@ private void processHierarchy(Item item, Map configuration) { processParent(type, parentItem, configuration); } } - if (item instanceof GroupItem) { - GroupItem gItem = (GroupItem) item; + if (item instanceof GroupItem gItem) { for (Item memberItem : gItem.getMembers()) { processMember(type, memberItem, configuration); } @@ -258,8 +258,8 @@ public void removed(Item item) { if (removedMd != null) { notifyListenersAboutRemovedElement(removedMd); - if (item instanceof GroupItem) { - for (Item memberItem : ((GroupItem) item).getMembers()) { + if (item instanceof GroupItem groupItem) { + for (Item memberItem : groupItem.getMembers()) { processItem(memberItem); } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java index 9bcf7413d5c..61a27a40b43 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java @@ -12,14 +12,19 @@ */ package org.openhab.core.semantics.internal; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemPredicates; @@ -30,7 +35,8 @@ import org.openhab.core.semantics.Equipment; import org.openhab.core.semantics.Location; import org.openhab.core.semantics.Point; -import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticsPredicates; import org.openhab.core.semantics.SemanticsService; import org.openhab.core.semantics.Tag; @@ -42,6 +48,7 @@ * The internal implementation of the {@link SemanticsService} interface, which is registered as an OSGi service. * * @author Kai Kreuzer - Initial contribution + * @author Laurent Garnier - Few methods moved from class SemanticTags in order to use the semantic tag registry */ @NonNullByDefault @Component @@ -51,12 +58,15 @@ public class SemanticsServiceImpl implements SemanticsService { private final ItemRegistry itemRegistry; private final MetadataRegistry metadataRegistry; + private final SemanticTagRegistry semanticTagRegistry; @Activate public SemanticsServiceImpl(final @Reference ItemRegistry itemRegistry, - final @Reference MetadataRegistry metadataRegistry) { + final @Reference MetadataRegistry metadataRegistry, + final @Reference SemanticTagRegistry semanticTagRegistry) { this.itemRegistry = itemRegistry; this.metadataRegistry = metadataRegistry; + this.semanticTagRegistry = semanticTagRegistry; } @Override @@ -65,8 +75,7 @@ public Set getItemsInLocation(Class locationType) { Set locationItems = itemRegistry.stream().filter(SemanticsPredicates.isA(locationType)) .collect(Collectors.toSet()); for (Item locationItem : locationItems) { - if (locationItem instanceof GroupItem) { - GroupItem gItem = (GroupItem) locationItem; + if (locationItem instanceof GroupItem gItem) { items.addAll(gItem .getMembers(SemanticsPredicates.isA(Point.class).or(SemanticsPredicates.isA(Equipment.class)))); } @@ -78,7 +87,7 @@ public Set getItemsInLocation(Class locationType) { @Override public Set getItemsInLocation(String labelOrSynonym, Locale locale) { Set items = new HashSet<>(); - List> tagList = SemanticTags.getByLabelOrSynonym(labelOrSynonym, locale); + List> tagList = getByLabelOrSynonym(labelOrSynonym, locale); if (!tagList.isEmpty()) { for (Class tag : tagList) { if (Location.class.isAssignableFrom(tag)) { @@ -89,8 +98,7 @@ public Set getItemsInLocation(String labelOrSynonym, Locale locale) { Set locationItems = itemRegistry.stream().filter(ItemPredicates.hasLabel(labelOrSynonym) .or(hasSynonym(labelOrSynonym)).and(SemanticsPredicates.isLocation())).collect(Collectors.toSet()); for (Item locationItem : locationItems) { - if (locationItem instanceof GroupItem) { - GroupItem gItem = (GroupItem) locationItem; + if (locationItem instanceof GroupItem gItem) { items.addAll(gItem.getMembers( SemanticsPredicates.isA(Point.class).or(SemanticsPredicates.isA(Equipment.class)))); } @@ -114,4 +122,40 @@ private Predicate hasSynonym(String labelOrSynonym) { return false; }; } + + @Override + public @Nullable Class getByLabel(String tagLabel, Locale locale) { + Optional tag = semanticTagRegistry.getAll().stream() + .filter(t -> t.localized(locale).getLabel().equalsIgnoreCase(tagLabel)) + .sorted(Comparator.comparing(SemanticTag::getUID)).findFirst(); + return tag.isPresent() ? semanticTagRegistry.getTagClassById(tag.get().getUID()) : null; + } + + @Override + public List> getByLabelOrSynonym(String tagLabelOrSynonym, Locale locale) { + List tags = semanticTagRegistry.getAll().stream() + .filter(t -> getLabelAndSynonyms(t, locale).contains(tagLabelOrSynonym.toLowerCase(locale))) + .sorted(Comparator.comparing(SemanticTag::getUID)).toList(); + List> tagList = new ArrayList<>(); + tags.forEach(t -> { + Class tag = semanticTagRegistry.getTagClassById(t.getUID()); + if (tag != null) { + tagList.add(tag); + } + }); + return tagList; + } + + @Override + public List getLabelAndSynonyms(Class tagClass, Locale locale) { + SemanticTag tag = semanticTagRegistry.get(SemanticTagRegistryImpl.buildId(tagClass)); + return tag == null ? List.of() : getLabelAndSynonyms(tag, locale); + } + + private List getLabelAndSynonyms(SemanticTag tag, Locale locale) { + SemanticTag localizedTag = tag.localized(locale); + Stream label = Stream.of(localizedTag.getLabel()); + Stream synonyms = localizedTag.getSynonyms().stream(); + return Stream.concat(label, synonyms).map(s -> s.toLowerCase(locale)).distinct().toList(); + } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java new file mode 100644 index 00000000000..177d0657e95 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java @@ -0,0 +1,311 @@ +/** + * 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.semantics.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagProvider; +import org.osgi.service.component.annotations.Component; + +/** + * This class defines a provider of all default semantic tags. + * + * @author Generated from generateTagClasses.groovy - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { SemanticTagProvider.class, DefaultSemanticTagProvider.class }) +public class DefaultSemanticTagProvider implements SemanticTagProvider { + + private List defaultTags; + + public DefaultSemanticTagProvider() { + this.defaultTags = new ArrayList<>(); + defaultTags.add(new SemanticTagImpl("Equipment", "", "", "")); + defaultTags.add(new SemanticTagImpl("Location", "", "", "")); + defaultTags.add(new SemanticTagImpl("Point", "", "", "")); + defaultTags.add(new SemanticTagImpl("Property", "", "", "")); + defaultTags.add(new SemanticTagImpl("Location_Indoor", // + "Indoor", "Anything that is inside a closed building", "")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Apartment", // + "Apartment", "", "Apartments")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building", // + "Building", "", "Buildings")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_Garage", // + "Garage", "", "Garages")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_House", // + "House", "", "Houses")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_Shed", // + "Shed", "", "Sheds")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_SummerHouse", // + "Summer House", "", "Summer Houses, Second Home, Second Homes")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor", // + "Floor", "", "Floors")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_GroundFloor", // + "Ground Floor", "", "Ground Floors, Downstairs")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_FirstFloor", // + "First Floor", "", "First Floors, Upstairs")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_SecondFloor", // + "Second Floor", "", "Second Floors")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_ThirdFloor", // + "Third Floor", "", "Third Floors")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_Attic", // + "Attic", "", "Attics")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_Basement", // + "Basement", "", "Basements")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Corridor", // + "Corridor", "", "Corridors, Hallway, Hallways")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room", // + "Room", "", "Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Bathroom", // + "Bathroom", "", "Bathrooms, Bath, Baths, Powder Room, Powder Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Bedroom", // + "Bedroom", "", "Bedrooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_BoilerRoom", // + "Boiler Room", "", "Boiler Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Cellar", // + "Cellar", "", "Cellars")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_DiningRoom", // + "Dining Room", "", "Dining Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Entry", // + "Entry", "", "Entries, Foyer, Foyers")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_FamilyRoom", // + "Family Room", "", "Family Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_GuestRoom", // + "Guest Room", "", "Guest Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Kitchen", // + "Kitchen", "", "Kitchens")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_LaundryRoom", // + "Laundry Room", "", "Laundry Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_LivingRoom", // + "Living Room", "", "Living Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Office", // + "Office", "", "Offices")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Veranda", // + "Veranda", "", "Verandas")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor", // + "Outdoor", "", "")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Carport", // + "Carport", "", "Carports")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Driveway", // + "Driveway", "", "Driveways")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Garden", // + "Garden", "", "Gardens")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Patio", // + "Patio", "", "Patios")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Porch", // + "Porch", "", "Porches")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Terrace", // + "Terrace", "", "Terraces, Deck, Decks")); + defaultTags.add(new SemanticTagImpl("Property_Temperature", // + "Temperature", "", "Temperatures")); + defaultTags.add(new SemanticTagImpl("Property_Light", // + "Light", "", "Lights, Lighting")); + defaultTags.add(new SemanticTagImpl("Property_ColorTemperature", // + "Color Temperature", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Humidity", // + "Humidity", "", "Moisture")); + defaultTags.add(new SemanticTagImpl("Property_Presence", // + "Presence", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Pressure", // + "Pressure", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Smoke", // + "Smoke", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Noise", // + "Noise", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Rain", // + "Rain", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Wind", // + "Wind", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Water", // + "Water", "", "")); + defaultTags.add(new SemanticTagImpl("Property_CO2", // + "CO2", "", "Carbon Dioxide")); + defaultTags.add(new SemanticTagImpl("Property_CO", // + "CO", "", "Carbon Monoxide")); + defaultTags.add(new SemanticTagImpl("Property_Energy", // + "Energy", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Power", // + "Power", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Voltage", // + "Voltage", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Current", // + "Current", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Frequency", // + "Frequency", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Gas", // + "Gas", "", "")); + defaultTags.add(new SemanticTagImpl("Property_SoundVolume", // + "Sound Volume", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Oil", // + "Oil", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Duration", // + "Duration", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Level", // + "Level", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Opening", // + "Opening", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Timestamp", // + "Timestamp", "", "")); + defaultTags.add(new SemanticTagImpl("Property_Ultraviolet", // + "Ultraviolet", "", "UV")); + defaultTags.add(new SemanticTagImpl("Property_Vibration", // + "Vibration", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Alarm", // + "Alarm", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Control", // + "Control", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Control_Switch", // + "Switch", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Measurement", // + "Measurement", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Setpoint", // + "Setpoint", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Status", // + "Status", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Status_LowBattery", // + "LowBattery", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Status_OpenLevel", // + "OpenLevel", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Status_OpenState", // + "OpenState", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Status_Tampered", // + "Tampered", "", "")); + defaultTags.add(new SemanticTagImpl("Point_Status_Tilt", // + "Tilt", "", "")); + defaultTags.add(new SemanticTagImpl("Equipment_AlarmSystem", // + "Alarm System", "", "Alarm Systems")); + defaultTags.add(new SemanticTagImpl("Equipment_Battery", // + "Battery", "", "Batteries")); + defaultTags.add(new SemanticTagImpl("Equipment_Blinds", // + "Blinds", "", "Rollershutter, Rollershutters, Roller shutter, Roller shutters, Shutter, Shutters")); + defaultTags.add(new SemanticTagImpl("Equipment_Boiler", // + "Boiler", "", "Boilers")); + defaultTags.add(new SemanticTagImpl("Equipment_Camera", // + "Camera", "", "Cameras")); + defaultTags.add(new SemanticTagImpl("Equipment_Car", // + "Car", "", "Cars")); + defaultTags.add(new SemanticTagImpl("Equipment_CleaningRobot", // + "Cleaning Robot", "", "Cleaning Robots, Vacuum robot, Vacuum robots")); + defaultTags.add(new SemanticTagImpl("Equipment_Door", // + "Door", "", "Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_BackDoor", // + "Back Door", "", "Back Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_CellarDoor", // + "Cellar Door", "", "Cellar Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_FrontDoor", // + "Front Door", "", "Front Doors, Frontdoor, Frontdoors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_GarageDoor", // + "Garage Door", "", "Garage Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_Gate", // + "Gate", "", "Gates")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_InnerDoor", // + "Inner Door", "", "Inner Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_SideDoor", // + "Side Door", "", "Side Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Doorbell", // + "Doorbell", "", "Doorbells")); + defaultTags.add(new SemanticTagImpl("Equipment_Fan", // + "Fan", "", "Fans")); + defaultTags.add(new SemanticTagImpl("Equipment_Fan_CeilingFan", // + "Ceiling Fan", "", "Ceiling Fans")); + defaultTags.add(new SemanticTagImpl("Equipment_Fan_KitchenHood", // + "Kitchen Hood", "", "Kitchen Hoods")); + defaultTags.add(new SemanticTagImpl("Equipment_HVAC", // + "HVAC", "", "Heating, Ventilation, Air Conditioning, A/C, A/Cs, AC")); + defaultTags.add(new SemanticTagImpl("Equipment_Inverter", // + "Inverter", "", "Inverters")); + defaultTags.add(new SemanticTagImpl("Equipment_LawnMower", // + "Lawn Mower", "", "Lawn Mowers")); + defaultTags.add(new SemanticTagImpl("Equipment_Lightbulb", // + "Lightbulb", "", "Lightbulbs, Bulb, Bulbs, Lamp, Lamps, Lights, Lighting")); + defaultTags.add(new SemanticTagImpl("Equipment_Lightbulb_LightStripe", // + "Light Stripe", "", "Light Stripes")); + defaultTags.add(new SemanticTagImpl("Equipment_Lock", // + "Lock", "", "Locks")); + defaultTags.add(new SemanticTagImpl("Equipment_NetworkAppliance", // + "Network Appliance", "", "Network Appliances")); + defaultTags.add(new SemanticTagImpl("Equipment_PowerOutlet", // + "Power Outlet", "", "Power Outlets, Outlet, Outlets")); + defaultTags.add(new SemanticTagImpl("Equipment_Projector", // + "Projector", "", "Projectors, Beamer, Beamers")); + defaultTags.add(new SemanticTagImpl("Equipment_Pump", // + "Pump", "", "Pumps")); + defaultTags.add(new SemanticTagImpl("Equipment_RadiatorControl", // + "Radiator Control", "", "Radiator Controls, Radiator, Radiators")); + defaultTags.add(new SemanticTagImpl("Equipment_Receiver", // + "Receiver", "", "Receivers, Audio Receiver, Audio Receivers, AV Receiver, AV Receivers")); + defaultTags.add(new SemanticTagImpl("Equipment_RemoteControl", // + "Remote Control", "", "Remote Controls")); + defaultTags.add(new SemanticTagImpl("Equipment_Screen", // + "Screen", "", "Screens")); + defaultTags.add(new SemanticTagImpl("Equipment_Screen_Television", // + "Television", "", "Televisions, TV, TVs")); + defaultTags.add(new SemanticTagImpl("Equipment_Sensor", // + "Sensor", "", "Sensors")); + defaultTags.add(new SemanticTagImpl("Equipment_Sensor_MotionDetector", // + "Motion Detector", "", "Motion Detectors, Motion sensor, Motion sensors")); + defaultTags.add(new SemanticTagImpl("Equipment_Sensor_SmokeDetector", // + "Smoke Detector", "", "Smoke Detectors")); + defaultTags.add(new SemanticTagImpl("Equipment_Siren", // + "Siren", "", "Sirens")); + defaultTags.add(new SemanticTagImpl("Equipment_Smartphone", // + "Smartphone", "", "Smartphones, Phone, Phones")); + defaultTags.add(new SemanticTagImpl("Equipment_Speaker", // + "Speaker", "", "Speakers")); + defaultTags.add(new SemanticTagImpl("Equipment_Valve", // + "Valve", "", "Valves")); + defaultTags.add(new SemanticTagImpl("Equipment_VoiceAssistant", // + "Voice Assistant", "", "Voice Assistants")); + defaultTags.add(new SemanticTagImpl("Equipment_WallSwitch", // + "Wall Switch", "", "Wall Switches")); + defaultTags.add(new SemanticTagImpl("Equipment_WebService", // + "Web Service", "", "Web Services")); + defaultTags.add(new SemanticTagImpl("Equipment_WebService_WeatherService", // + "Weather Service", "", "Weather Services")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood", // + "White Good", "", "White Goods")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Dishwasher", // + "Dishwasher", "", "Dishwashers")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Dryer", // + "Dryer", "", "Dryers")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Freezer", // + "Freezer", "", "Freezers")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Oven", // + "Oven", "", "Ovens")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Refrigerator", // + "Refrigerator", "", "Refrigerators")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_WashingMachine", // + "Washing Machine", "", "Washing Machines")); + defaultTags.add(new SemanticTagImpl("Equipment_Window", // + "Window", "", "Windows")); + } + + @Override + public Collection getAll() { + return defaultTags; + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/AlarmSystem.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/AlarmSystem.java deleted file mode 100644 index bc810386bdf..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/AlarmSystem.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Alarm System. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_AlarmSystem", label = "Alarm System", synonyms = "Alarm Systems", description = "") -public interface AlarmSystem extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Battery.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Battery.java deleted file mode 100644 index f1bdad31eda..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Battery.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Battery. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Battery", label = "Battery", synonyms = "Batteries", description = "") -public interface Battery extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Blinds.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Blinds.java deleted file mode 100644 index cf282d1ce37..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Blinds.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Blinds. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Blinds", label = "Blinds", synonyms = "Rollershutter, Rollershutters, Roller shutter, Roller shutters, Shutter, Shutters", description = "") -public interface Blinds extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Boiler.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Boiler.java deleted file mode 100644 index 0ea0d34b930..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Boiler.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Boiler. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Boiler", label = "Boiler", synonyms = "Boilers", description = "") -public interface Boiler extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Camera.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Camera.java deleted file mode 100644 index bd983ae404e..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Camera.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Camera. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Camera", label = "Camera", synonyms = "Cameras", description = "") -public interface Camera extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CeilingFan.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CeilingFan.java deleted file mode 100644 index 61628444b2b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CeilingFan.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Ceiling Fan. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Fan_CeilingFan", label = "Ceiling Fan", synonyms = "Ceiling Fans", description = "") -public interface CeilingFan extends Fan { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CellarDoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CellarDoor.java deleted file mode 100644 index c61ce7ee805..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CellarDoor.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Cellar Door. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Door_CellarDoor", label = "Cellar Door", synonyms = "Cellar Doors", description = "") -public interface CellarDoor extends Door { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CleaningRobot.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CleaningRobot.java deleted file mode 100644 index ff5c995fb1c..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/CleaningRobot.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Cleaning Robot. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_CleaningRobot", label = "Cleaning Robot", synonyms = "Cleaning Robots, Vacuum robot, Vacuum robots", description = "") -public interface CleaningRobot extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Dishwasher.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Dishwasher.java deleted file mode 100644 index ecb707ab016..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Dishwasher.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Dishwasher. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood_Dishwasher", label = "Dishwasher", synonyms = "Dishwashers", description = "") -public interface Dishwasher extends WhiteGood { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Door.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Door.java deleted file mode 100644 index 584d5ebf5c4..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Door.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Door. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Door", label = "Door", synonyms = "Doors", description = "") -public interface Door extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Doorbell.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Doorbell.java deleted file mode 100644 index 61875ce66fa..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Doorbell.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Doorbell. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Doorbell", label = "Doorbell", synonyms = "Doorbells", description = "") -public interface Doorbell extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Dryer.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Dryer.java deleted file mode 100644 index 8860419ffb9..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Dryer.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Dryer. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood_Dryer", label = "Dryer", synonyms = "Dryers", description = "") -public interface Dryer extends WhiteGood { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java deleted file mode 100644 index 30b6bbe0836..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java +++ /dev/null @@ -1,92 +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.semantics.model.equipment; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; - -/** - * This class provides a stream of all defined equipments. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Equipments { - - static final Set> EQUIPMENTS = new HashSet<>(); - - static { - EQUIPMENTS.add(Equipment.class); - EQUIPMENTS.add(AlarmSystem.class); - EQUIPMENTS.add(BackDoor.class); - EQUIPMENTS.add(Battery.class); - EQUIPMENTS.add(Blinds.class); - EQUIPMENTS.add(Boiler.class); - EQUIPMENTS.add(Camera.class); - EQUIPMENTS.add(Car.class); - EQUIPMENTS.add(CeilingFan.class); - EQUIPMENTS.add(CellarDoor.class); - EQUIPMENTS.add(CleaningRobot.class); - EQUIPMENTS.add(Dishwasher.class); - EQUIPMENTS.add(Door.class); - EQUIPMENTS.add(Doorbell.class); - EQUIPMENTS.add(Dryer.class); - EQUIPMENTS.add(Fan.class); - EQUIPMENTS.add(Freezer.class); - EQUIPMENTS.add(FrontDoor.class); - EQUIPMENTS.add(GarageDoor.class); - EQUIPMENTS.add(Gate.class); - EQUIPMENTS.add(HVAC.class); - EQUIPMENTS.add(InnerDoor.class); - EQUIPMENTS.add(Inverter.class); - EQUIPMENTS.add(KitchenHood.class); - EQUIPMENTS.add(LawnMower.class); - EQUIPMENTS.add(LightStripe.class); - EQUIPMENTS.add(Lightbulb.class); - EQUIPMENTS.add(Lock.class); - EQUIPMENTS.add(MotionDetector.class); - EQUIPMENTS.add(NetworkAppliance.class); - EQUIPMENTS.add(Oven.class); - EQUIPMENTS.add(PowerOutlet.class); - EQUIPMENTS.add(Projector.class); - EQUIPMENTS.add(Pump.class); - EQUIPMENTS.add(RadiatorControl.class); - EQUIPMENTS.add(Receiver.class); - EQUIPMENTS.add(Refrigerator.class); - EQUIPMENTS.add(RemoteControl.class); - EQUIPMENTS.add(Screen.class); - EQUIPMENTS.add(Sensor.class); - EQUIPMENTS.add(SideDoor.class); - EQUIPMENTS.add(Siren.class); - EQUIPMENTS.add(Smartphone.class); - EQUIPMENTS.add(SmokeDetector.class); - EQUIPMENTS.add(Speaker.class); - EQUIPMENTS.add(Television.class); - EQUIPMENTS.add(Valve.class); - EQUIPMENTS.add(VoiceAssistant.class); - EQUIPMENTS.add(WallSwitch.class); - EQUIPMENTS.add(WashingMachine.class); - EQUIPMENTS.add(WeatherService.class); - EQUIPMENTS.add(WebService.class); - EQUIPMENTS.add(WhiteGood.class); - EQUIPMENTS.add(Window.class); - } - - public static Stream> stream() { - return EQUIPMENTS.stream(); - } -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Fan.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Fan.java deleted file mode 100644 index 8df120d40b7..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Fan.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Fan. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Fan", label = "Fan", synonyms = "Fans", description = "") -public interface Fan extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Freezer.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Freezer.java deleted file mode 100644 index da57322726f..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Freezer.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Freezer. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood_Freezer", label = "Freezer", synonyms = "Freezers", description = "") -public interface Freezer extends WhiteGood { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/FrontDoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/FrontDoor.java deleted file mode 100644 index 4fd6ce52306..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/FrontDoor.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Front Door. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Door_FrontDoor", label = "Front Door", synonyms = "Front Doors, Frontdoor, Frontdoors", description = "") -public interface FrontDoor extends Door { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/GarageDoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/GarageDoor.java deleted file mode 100644 index 177c0eb1981..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/GarageDoor.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Garage Door. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Door_GarageDoor", label = "Garage Door", synonyms = "Garage Doors", description = "") -public interface GarageDoor extends Door { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/HVAC.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/HVAC.java deleted file mode 100644 index 0073408c846..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/HVAC.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a HVAC. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_HVAC", label = "HVAC", synonyms = "Heating, Ventilation, Air Conditioning, A/C, A/Cs, AC", description = "") -public interface HVAC extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/InnerDoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/InnerDoor.java deleted file mode 100644 index bfc3f11d2e9..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/InnerDoor.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Inner Door. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Door_InnerDoor", label = "Inner Door", synonyms = "Inner Doors", description = "") -public interface InnerDoor extends Door { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Inverter.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Inverter.java deleted file mode 100644 index 303893113d3..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Inverter.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Inverter. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Inverter", label = "Inverter", synonyms = "Inverters", description = "") -public interface Inverter extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/KitchenHood.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/KitchenHood.java deleted file mode 100644 index e53b7f56860..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/KitchenHood.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Kitchen Hood. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Fan_KitchenHood", label = "Kitchen Hood", synonyms = "Kitchen Hoods", description = "") -public interface KitchenHood extends Fan { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/LawnMower.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/LawnMower.java deleted file mode 100644 index 9b354437dbb..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/LawnMower.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Lawn Mower. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_LawnMower", label = "Lawn Mower", synonyms = "Lawn Mowers", description = "") -public interface LawnMower extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/LightStripe.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/LightStripe.java deleted file mode 100644 index 7e628d3b9cf..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/LightStripe.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Light Stripe. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Lightbulb_LightStripe", label = "Light Stripe", synonyms = "Light Stripes", description = "") -public interface LightStripe extends Lightbulb { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Lightbulb.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Lightbulb.java deleted file mode 100644 index 76eb0d4992d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Lightbulb.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Lightbulb. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Lightbulb", label = "Lightbulb", synonyms = "Lightbulbs, Bulb, Bulbs, Lamp, Lamps, Lights, Lighting", description = "") -public interface Lightbulb extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Lock.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Lock.java deleted file mode 100644 index 26c303b1329..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Lock.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Lock. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Lock", label = "Lock", synonyms = "Locks", description = "") -public interface Lock extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/MotionDetector.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/MotionDetector.java deleted file mode 100644 index 3a7c0f40ffa..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/MotionDetector.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Motion Detector. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Sensor_MotionDetector", label = "Motion Detector", synonyms = "Motion Detectors, Motion sensor, Motion sensors", description = "") -public interface MotionDetector extends Sensor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/NetworkAppliance.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/NetworkAppliance.java deleted file mode 100644 index e873a53d2a0..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/NetworkAppliance.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Network Appliance. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_NetworkAppliance", label = "Network Appliance", synonyms = "Network Appliances", description = "") -public interface NetworkAppliance extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Oven.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Oven.java deleted file mode 100644 index 18578cd0d6c..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Oven.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Oven. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood_Oven", label = "Oven", synonyms = "Ovens", description = "") -public interface Oven extends WhiteGood { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/PowerOutlet.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/PowerOutlet.java deleted file mode 100644 index bbeb001eb4f..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/PowerOutlet.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Power Outlet. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_PowerOutlet", label = "Power Outlet", synonyms = "Power Outlets, Outlet, Outlets", description = "") -public interface PowerOutlet extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Projector.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Projector.java deleted file mode 100644 index 5fb617c4692..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Projector.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Projector. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Projector", label = "Projector", synonyms = "Projectors, Beamer, Beamers", description = "") -public interface Projector extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Pump.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Pump.java deleted file mode 100644 index 02cd22bebb0..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Pump.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Pump. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Pump", label = "Pump", synonyms = "Pumps", description = "") -public interface Pump extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/RadiatorControl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/RadiatorControl.java deleted file mode 100644 index 882289fa68f..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/RadiatorControl.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Radiator Control. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_RadiatorControl", label = "Radiator Control", synonyms = "Radiator Controls, Radiator, Radiators", description = "") -public interface RadiatorControl extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Receiver.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Receiver.java deleted file mode 100644 index d1cdfe7b87a..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Receiver.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Receiver. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Receiver", label = "Receiver", synonyms = "Receivers, Audio Receiver, Audio Receivers, AV Receiver, AV Receivers", description = "") -public interface Receiver extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Refrigerator.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Refrigerator.java deleted file mode 100644 index fa855447615..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Refrigerator.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Refrigerator. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood_Refrigerator", label = "Refrigerator", synonyms = "Refrigerators", description = "") -public interface Refrigerator extends WhiteGood { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/RemoteControl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/RemoteControl.java deleted file mode 100644 index 4befed3c843..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/RemoteControl.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Remote Control. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_RemoteControl", label = "Remote Control", synonyms = "Remote Controls", description = "") -public interface RemoteControl extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Screen.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Screen.java deleted file mode 100644 index c3cb10e0c2e..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Screen.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Screen. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Screen", label = "Screen", synonyms = "Screens", description = "") -public interface Screen extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Sensor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Sensor.java deleted file mode 100644 index ef60408ada6..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Sensor.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Sensor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Sensor", label = "Sensor", synonyms = "Sensors", description = "") -public interface Sensor extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/SideDoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/SideDoor.java deleted file mode 100644 index ba2e3bb1595..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/SideDoor.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Side Door. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Door_SideDoor", label = "Side Door", synonyms = "Side Doors", description = "") -public interface SideDoor extends Door { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Siren.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Siren.java deleted file mode 100644 index e436b39ebbc..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Siren.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Siren. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Siren", label = "Siren", synonyms = "Sirens", description = "") -public interface Siren extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Smartphone.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Smartphone.java deleted file mode 100644 index fbecc781aab..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Smartphone.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Smartphone. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Smartphone", label = "Smartphone", synonyms = "Smartphones, Phone, Phones", description = "") -public interface Smartphone extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/SmokeDetector.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/SmokeDetector.java deleted file mode 100644 index 2ee50b2ea44..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/SmokeDetector.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Smoke Detector. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Sensor_SmokeDetector", label = "Smoke Detector", synonyms = "Smoke Detectors", description = "") -public interface SmokeDetector extends Sensor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Speaker.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Speaker.java deleted file mode 100644 index ce42af983c8..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Speaker.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Speaker. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Speaker", label = "Speaker", synonyms = "Speakers", description = "") -public interface Speaker extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Television.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Television.java deleted file mode 100644 index c84af6d7cc4..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Television.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Television. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Screen_Television", label = "Television", synonyms = "Televisions, TV, TVs", description = "") -public interface Television extends Screen { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Valve.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Valve.java deleted file mode 100644 index a30ad2fd9a6..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Valve.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Valve. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Valve", label = "Valve", synonyms = "Valves", description = "") -public interface Valve extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/VoiceAssistant.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/VoiceAssistant.java deleted file mode 100644 index af28a055e85..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/VoiceAssistant.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Voice Assistant. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_VoiceAssistant", label = "Voice Assistant", synonyms = "Voice Assistants", description = "") -public interface VoiceAssistant extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WallSwitch.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WallSwitch.java deleted file mode 100644 index d2d68e2d85d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WallSwitch.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Wall Switch. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WallSwitch", label = "Wall Switch", synonyms = "Wall Switches", description = "") -public interface WallSwitch extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WashingMachine.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WashingMachine.java deleted file mode 100644 index b70aa93db22..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WashingMachine.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Washing Machine. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood_WashingMachine", label = "Washing Machine", synonyms = "Washing Machines", description = "") -public interface WashingMachine extends WhiteGood { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WeatherService.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WeatherService.java deleted file mode 100644 index 2f3118d3bc0..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WeatherService.java +++ /dev/null @@ -1,26 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Weather Service. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WebService_WeatherService", label = "Weather Service", synonyms = "Weather Services", description = "") -public interface WeatherService extends WebService { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WebService.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WebService.java deleted file mode 100644 index 33f14f5500e..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WebService.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Web Service. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WebService", label = "Web Service", synonyms = "Web Services", description = "") -public interface WebService extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WhiteGood.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WhiteGood.java deleted file mode 100644 index 687e056dbd6..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/WhiteGood.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a White Good. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_WhiteGood", label = "White Good", synonyms = "White Goods", description = "") -public interface WhiteGood extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Window.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Window.java deleted file mode 100644 index d2f305cb128..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Window.java +++ /dev/null @@ -1,27 +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.semantics.model.equipment; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Equipment; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Window. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Equipment_Window", label = "Window", synonyms = "Windows", description = "") -public interface Window extends Equipment { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Apartment.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Apartment.java deleted file mode 100644 index 389e7844dc9..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Apartment.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Apartment. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Apartment", label = "Apartment", synonyms = "Apartments", description = "") -public interface Apartment extends Indoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Attic.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Attic.java deleted file mode 100644 index f766e622e1d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Attic.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Attic. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor_Attic", label = "Attic", synonyms = "Attics", description = "") -public interface Attic extends Floor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Basement.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Basement.java deleted file mode 100644 index 8fc8cb3baad..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Basement.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Basement. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor_Basement", label = "Basement", synonyms = "Basements", description = "") -public interface Basement extends Floor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Bathroom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Bathroom.java deleted file mode 100644 index 4b6c3fb78b4..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Bathroom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Bathroom. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Bathroom", label = "Bathroom", synonyms = "Bathrooms, Bath, Baths, Powder Room, Powder Rooms", description = "") -public interface Bathroom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Bedroom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Bedroom.java deleted file mode 100644 index 8c5c0fc987b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Bedroom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Bedroom. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Bedroom", label = "Bedroom", synonyms = "Bedrooms", description = "") -public interface Bedroom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/BoilerRoom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/BoilerRoom.java deleted file mode 100644 index d149ef4468d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/BoilerRoom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Boiler Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_BoilerRoom", label = "Boiler Room", synonyms = "Boiler Rooms", description = "") -public interface BoilerRoom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Building.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Building.java deleted file mode 100644 index 46577c537f6..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Building.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Building. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Building", label = "Building", synonyms = "Buildings", description = "") -public interface Building extends Indoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Carport.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Carport.java deleted file mode 100644 index 322ddfe103e..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Carport.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Carport. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor_Carport", label = "Carport", synonyms = "Carports", description = "") -public interface Carport extends Outdoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Cellar.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Cellar.java deleted file mode 100644 index e5f0ab2709b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Cellar.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Cellar. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Cellar", label = "Cellar", synonyms = "Cellars", description = "") -public interface Cellar extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Corridor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Corridor.java deleted file mode 100644 index 6509b82659e..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Corridor.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Corridor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Corridor", label = "Corridor", synonyms = "Corridors, Hallway, Hallways", description = "") -public interface Corridor extends Indoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/DiningRoom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/DiningRoom.java deleted file mode 100644 index 21c7affc1d7..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/DiningRoom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Dining Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_DiningRoom", label = "Dining Room", synonyms = "Dining Rooms", description = "") -public interface DiningRoom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Driveway.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Driveway.java deleted file mode 100644 index 44930624558..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Driveway.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Driveway. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor_Driveway", label = "Driveway", synonyms = "Driveways", description = "") -public interface Driveway extends Outdoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Entry.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Entry.java deleted file mode 100644 index f88c4f10bd7..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Entry.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Entry. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Entry", label = "Entry", synonyms = "Entries, Foyer, Foyers", description = "") -public interface Entry extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/FamilyRoom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/FamilyRoom.java deleted file mode 100644 index d4c095f059d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/FamilyRoom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Family Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_FamilyRoom", label = "Family Room", synonyms = "Family Rooms", description = "") -public interface FamilyRoom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/FirstFloor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/FirstFloor.java deleted file mode 100644 index 977606b7ea5..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/FirstFloor.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a First Floor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor_FirstFloor", label = "First Floor", synonyms = "First Floors, Upstairs", description = "") -public interface FirstFloor extends Floor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Floor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Floor.java deleted file mode 100644 index e6a71e4e6a5..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Floor.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Floor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor", label = "Floor", synonyms = "Floors", description = "") -public interface Floor extends Indoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Garage.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Garage.java deleted file mode 100644 index 8f592538f52..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Garage.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Garage. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Building_Garage", label = "Garage", synonyms = "Garages", description = "") -public interface Garage extends Building { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Garden.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Garden.java deleted file mode 100644 index 1efedd39b76..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Garden.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Garden. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor_Garden", label = "Garden", synonyms = "Gardens", description = "") -public interface Garden extends Outdoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/GroundFloor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/GroundFloor.java deleted file mode 100644 index b8b2942d71c..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/GroundFloor.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Ground Floor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor_GroundFloor", label = "Ground Floor", synonyms = "Ground Floors, Downstairs", description = "") -public interface GroundFloor extends Floor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/GuestRoom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/GuestRoom.java deleted file mode 100644 index cfafb8f99ff..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/GuestRoom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Guest Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_GuestRoom", label = "Guest Room", synonyms = "Guest Rooms", description = "") -public interface GuestRoom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/House.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/House.java deleted file mode 100644 index b22410e8c55..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/House.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a House. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Building_House", label = "House", synonyms = "Houses", description = "") -public interface House extends Building { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Indoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Indoor.java deleted file mode 100644 index 48c652aaa85..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Indoor.java +++ /dev/null @@ -1,27 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Location; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Indoor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor", label = "Indoor", synonyms = "", description = "Anything that is inside a closed building") -public interface Indoor extends Location { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Kitchen.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Kitchen.java deleted file mode 100644 index ffdd06d2786..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Kitchen.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Kitchen. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Kitchen", label = "Kitchen", synonyms = "Kitchens", description = "") -public interface Kitchen extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/LaundryRoom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/LaundryRoom.java deleted file mode 100644 index 98f46395ba3..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/LaundryRoom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Laundry Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_LaundryRoom", label = "Laundry Room", synonyms = "Laundry Rooms", description = "") -public interface LaundryRoom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/LivingRoom.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/LivingRoom.java deleted file mode 100644 index c23557395b7..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/LivingRoom.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Living Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_LivingRoom", label = "Living Room", synonyms = "Living Rooms", description = "") -public interface LivingRoom extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java deleted file mode 100644 index df755f38251..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java +++ /dev/null @@ -1,75 +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.semantics.model.location; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Location; - -/** - * This class provides a stream of all defined locations. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Locations { - - static final Set> LOCATIONS = new HashSet<>(); - - static { - LOCATIONS.add(Location.class); - LOCATIONS.add(Apartment.class); - LOCATIONS.add(Attic.class); - LOCATIONS.add(Basement.class); - LOCATIONS.add(Bathroom.class); - LOCATIONS.add(Bedroom.class); - LOCATIONS.add(BoilerRoom.class); - LOCATIONS.add(Building.class); - LOCATIONS.add(Carport.class); - LOCATIONS.add(Cellar.class); - LOCATIONS.add(Corridor.class); - LOCATIONS.add(DiningRoom.class); - LOCATIONS.add(Driveway.class); - LOCATIONS.add(Entry.class); - LOCATIONS.add(FamilyRoom.class); - LOCATIONS.add(FirstFloor.class); - LOCATIONS.add(Floor.class); - LOCATIONS.add(Garage.class); - LOCATIONS.add(Garden.class); - LOCATIONS.add(GroundFloor.class); - LOCATIONS.add(GuestRoom.class); - LOCATIONS.add(House.class); - LOCATIONS.add(Indoor.class); - LOCATIONS.add(Kitchen.class); - LOCATIONS.add(LaundryRoom.class); - LOCATIONS.add(LivingRoom.class); - LOCATIONS.add(Office.class); - LOCATIONS.add(Outdoor.class); - LOCATIONS.add(Patio.class); - LOCATIONS.add(Porch.class); - LOCATIONS.add(Room.class); - LOCATIONS.add(SecondFloor.class); - LOCATIONS.add(Shed.class); - LOCATIONS.add(SummerHouse.class); - LOCATIONS.add(Terrace.class); - LOCATIONS.add(ThirdFloor.class); - LOCATIONS.add(Veranda.class); - } - - public static Stream> stream() { - return LOCATIONS.stream(); - } -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Office.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Office.java deleted file mode 100644 index e5b29630351..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Office.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Office. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Office", label = "Office", synonyms = "Offices", description = "") -public interface Office extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Outdoor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Outdoor.java deleted file mode 100644 index 5e0b0ede4e4..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Outdoor.java +++ /dev/null @@ -1,27 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Location; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Outdoor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor", label = "Outdoor", synonyms = "", description = "") -public interface Outdoor extends Location { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Patio.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Patio.java deleted file mode 100644 index 673db0ae90f..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Patio.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Patio. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor_Patio", label = "Patio", synonyms = "Patios", description = "") -public interface Patio extends Outdoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Porch.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Porch.java deleted file mode 100644 index 239fb09fecb..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Porch.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Porch. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor_Porch", label = "Porch", synonyms = "Porches", description = "") -public interface Porch extends Outdoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Room.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Room.java deleted file mode 100644 index fcde06a6013..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Room.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Room. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room", label = "Room", synonyms = "Rooms", description = "") -public interface Room extends Indoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/SecondFloor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/SecondFloor.java deleted file mode 100644 index 9dd926a9a5d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/SecondFloor.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Second Floor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor_SecondFloor", label = "Second Floor", synonyms = "Second Floors", description = "") -public interface SecondFloor extends Floor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Shed.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Shed.java deleted file mode 100644 index 016b82fd6ce..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Shed.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Shed. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Building_Shed", label = "Shed", synonyms = "Sheds", description = "") -public interface Shed extends Building { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/SummerHouse.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/SummerHouse.java deleted file mode 100644 index 2aabcebaa04..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/SummerHouse.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Summer House. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Building_SummerHouse", label = "Summer House", synonyms = "Summer Houses, Second Home, Second Homes", description = "") -public interface SummerHouse extends Building { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Terrace.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Terrace.java deleted file mode 100644 index 9d1ac8800b2..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Terrace.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Terrace. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Outdoor_Terrace", label = "Terrace", synonyms = "Terraces, Deck, Decks", description = "") -public interface Terrace extends Outdoor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/ThirdFloor.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/ThirdFloor.java deleted file mode 100644 index 02f6fe4a2a9..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/ThirdFloor.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Third Floor. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Floor_ThirdFloor", label = "Third Floor", synonyms = "Third Floors", description = "") -public interface ThirdFloor extends Floor { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Veranda.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Veranda.java deleted file mode 100644 index 2138ef6aba3..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Veranda.java +++ /dev/null @@ -1,26 +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.semantics.model.location; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Veranda. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Location_Indoor_Room_Veranda", label = "Veranda", synonyms = "Verandas", description = "") -public interface Veranda extends Room { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Alarm.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Alarm.java deleted file mode 100644 index 580573b6c22..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Alarm.java +++ /dev/null @@ -1,27 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Alarm. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Alarm", label = "Alarm", synonyms = "", description = "") -public interface Alarm extends Point { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Control.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Control.java deleted file mode 100644 index 963d242091b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Control.java +++ /dev/null @@ -1,27 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Control. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Control", label = "Control", synonyms = "", description = "") -public interface Control extends Point { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/LowBattery.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/LowBattery.java deleted file mode 100644 index 52b9708f90a..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/LowBattery.java +++ /dev/null @@ -1,26 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a LowBattery. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Status_LowBattery", label = "LowBattery", synonyms = "", description = "") -public interface LowBattery extends Status { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Measurement.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Measurement.java deleted file mode 100644 index 7cf717f6c12..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Measurement.java +++ /dev/null @@ -1,27 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Measurement. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Measurement", label = "Measurement", synonyms = "", description = "") -public interface Measurement extends Point { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/OpenLevel.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/OpenLevel.java deleted file mode 100644 index 374d422f57d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/OpenLevel.java +++ /dev/null @@ -1,26 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an OpenLevel. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Status_OpenLevel", label = "OpenLevel", synonyms = "", description = "") -public interface OpenLevel extends Status { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/OpenState.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/OpenState.java deleted file mode 100644 index a03d0918e02..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/OpenState.java +++ /dev/null @@ -1,26 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an OpenState. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Status_OpenState", label = "OpenState", synonyms = "", description = "") -public interface OpenState extends Status { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java deleted file mode 100644 index 44f78811272..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java +++ /dev/null @@ -1,50 +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.semantics.model.point; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; - -/** - * This class provides a stream of all defined points. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Points { - - static final Set> POINTS = new HashSet<>(); - - static { - POINTS.add(Point.class); - POINTS.add(Alarm.class); - POINTS.add(Control.class); - POINTS.add(LowBattery.class); - POINTS.add(Measurement.class); - POINTS.add(OpenLevel.class); - POINTS.add(OpenState.class); - POINTS.add(Setpoint.class); - POINTS.add(Status.class); - POINTS.add(Switch.class); - POINTS.add(Tampered.class); - POINTS.add(Tilt.class); - } - - public static Stream> stream() { - return POINTS.stream(); - } -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Setpoint.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Setpoint.java deleted file mode 100644 index 68bc3136b12..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Setpoint.java +++ /dev/null @@ -1,27 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Setpoint. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Setpoint", label = "Setpoint", synonyms = "", description = "") -public interface Setpoint extends Point { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Status.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Status.java deleted file mode 100644 index d3a844a9c79..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Status.java +++ /dev/null @@ -1,27 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Point; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Status. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Status", label = "Status", synonyms = "", description = "") -public interface Status extends Point { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Tampered.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Tampered.java deleted file mode 100644 index ac65fdb63ab..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Tampered.java +++ /dev/null @@ -1,26 +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.semantics.model.point; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Tampered. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Point_Status_Tampered", label = "Tampered", synonyms = "", description = "") -public interface Tampered extends Status { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/CO.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/CO.java deleted file mode 100644 index cf5c8824a29..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/CO.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a CO. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_CO", label = "CO", synonyms = "Carbon Monoxide", description = "") -public interface CO extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/CO2.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/CO2.java deleted file mode 100644 index 13d29f113f7..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/CO2.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a CO2. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_CO2", label = "CO2", synonyms = "Carbon Dioxide", description = "") -public interface CO2 extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/ColorTemperature.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/ColorTemperature.java deleted file mode 100644 index 3c1e2ee1f89..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/ColorTemperature.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Color Temperature. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_ColorTemperature", label = "Color Temperature", synonyms = "", description = "") -public interface ColorTemperature extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Current.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Current.java deleted file mode 100644 index 2f944e0c003..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Current.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Current. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Current", label = "Current", synonyms = "", description = "") -public interface Current extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Duration.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Duration.java deleted file mode 100644 index 7ef6b3c766f..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Duration.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Duration. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Duration", label = "Duration", synonyms = "", description = "") -public interface Duration extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Energy.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Energy.java deleted file mode 100644 index e52abd5ae9c..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Energy.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Energy. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Energy", label = "Energy", synonyms = "", description = "") -public interface Energy extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Frequency.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Frequency.java deleted file mode 100644 index 5458939f559..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Frequency.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Frequency. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Frequency", label = "Frequency", synonyms = "", description = "") -public interface Frequency extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Gas.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Gas.java deleted file mode 100644 index 94d71e28c1b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Gas.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Gas. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Gas", label = "Gas", synonyms = "", description = "") -public interface Gas extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Humidity.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Humidity.java deleted file mode 100644 index 6d2781b9d87..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Humidity.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Humidity. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Humidity", label = "Humidity", synonyms = "Moisture", description = "") -public interface Humidity extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Level.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Level.java deleted file mode 100644 index 85411489ebb..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Level.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Level. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Level", label = "Level", synonyms = "", description = "") -public interface Level extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Light.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Light.java deleted file mode 100644 index 81d999cea9c..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Light.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Light. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Light", label = "Light", synonyms = "Lights, Lighting", description = "") -public interface Light extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Noise.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Noise.java deleted file mode 100644 index f59869d58bb..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Noise.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Noise. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Noise", label = "Noise", synonyms = "", description = "") -public interface Noise extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Oil.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Oil.java deleted file mode 100644 index c90daa7e054..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Oil.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Oil. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Oil", label = "Oil", synonyms = "", description = "") -public interface Oil extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Opening.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Opening.java deleted file mode 100644 index 1f1ba1dde1d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Opening.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Opening. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Opening", label = "Opening", synonyms = "", description = "") -public interface Opening extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Power.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Power.java deleted file mode 100644 index 3da7279a987..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Power.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Power. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Power", label = "Power", synonyms = "", description = "") -public interface Power extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Presence.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Presence.java deleted file mode 100644 index 1b3d2adeaf8..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Presence.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Presence. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Presence", label = "Presence", synonyms = "", description = "") -public interface Presence extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Pressure.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Pressure.java deleted file mode 100644 index c71a2a4a667..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Pressure.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Pressure. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Pressure", label = "Pressure", synonyms = "", description = "") -public interface Pressure extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java deleted file mode 100644 index 7a747b23830..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java +++ /dev/null @@ -1,66 +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.semantics.model.property; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; - -/** - * This class provides a stream of all defined properties. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -public class Properties { - - static final Set> PROPERTIES = new HashSet<>(); - - static { - PROPERTIES.add(Property.class); - PROPERTIES.add(CO.class); - PROPERTIES.add(CO2.class); - PROPERTIES.add(ColorTemperature.class); - PROPERTIES.add(Current.class); - PROPERTIES.add(Duration.class); - PROPERTIES.add(Energy.class); - PROPERTIES.add(Frequency.class); - PROPERTIES.add(Gas.class); - PROPERTIES.add(Humidity.class); - PROPERTIES.add(Level.class); - PROPERTIES.add(Light.class); - PROPERTIES.add(Noise.class); - PROPERTIES.add(Oil.class); - PROPERTIES.add(Opening.class); - PROPERTIES.add(Power.class); - PROPERTIES.add(Presence.class); - PROPERTIES.add(Pressure.class); - PROPERTIES.add(Rain.class); - PROPERTIES.add(Smoke.class); - PROPERTIES.add(SoundVolume.class); - PROPERTIES.add(Temperature.class); - PROPERTIES.add(Timestamp.class); - PROPERTIES.add(Ultraviolet.class); - PROPERTIES.add(Vibration.class); - PROPERTIES.add(Voltage.class); - PROPERTIES.add(Water.class); - PROPERTIES.add(Wind.class); - } - - public static Stream> stream() { - return PROPERTIES.stream(); - } -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Rain.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Rain.java deleted file mode 100644 index f4f53b5e98b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Rain.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Rain. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Rain", label = "Rain", synonyms = "", description = "") -public interface Rain extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Smoke.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Smoke.java deleted file mode 100644 index 4aeea0f353a..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Smoke.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Smoke. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Smoke", label = "Smoke", synonyms = "", description = "") -public interface Smoke extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/SoundVolume.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/SoundVolume.java deleted file mode 100644 index 4a820588833..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/SoundVolume.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Sound Volume. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_SoundVolume", label = "Sound Volume", synonyms = "", description = "") -public interface SoundVolume extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Temperature.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Temperature.java deleted file mode 100644 index 94a4b4cbf0f..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Temperature.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Temperature. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Temperature", label = "Temperature", synonyms = "Temperatures", description = "") -public interface Temperature extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Timestamp.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Timestamp.java deleted file mode 100644 index b429c1b4e99..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Timestamp.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Timestamp. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Timestamp", label = "Timestamp", synonyms = "", description = "") -public interface Timestamp extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Ultraviolet.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Ultraviolet.java deleted file mode 100644 index 0c6266a845d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Ultraviolet.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines an Ultraviolet. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Ultraviolet", label = "Ultraviolet", synonyms = "UV", description = "") -public interface Ultraviolet extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Vibration.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Vibration.java deleted file mode 100644 index 99f0aa58f90..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Vibration.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Vibration. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Vibration", label = "Vibration", synonyms = "", description = "") -public interface Vibration extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Voltage.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Voltage.java deleted file mode 100644 index e8e3c163d6a..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Voltage.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Voltage. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Voltage", label = "Voltage", synonyms = "", description = "") -public interface Voltage extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Water.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Water.java deleted file mode 100644 index 6bbe90ad11b..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Water.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Water. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Water", label = "Water", synonyms = "", description = "") -public interface Water extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Wind.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Wind.java deleted file mode 100644 index 92a1e7c0e9d..00000000000 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Wind.java +++ /dev/null @@ -1,27 +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.semantics.model.property; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.Property; -import org.openhab.core.semantics.TagInfo; - -/** - * This class defines a Wind. - * - * @author Generated from generateTagClasses.groovy - Initial contribution - */ -@NonNullByDefault -@TagInfo(id = "Property_Wind", label = "Wind", synonyms = "", description = "") -public interface Wind extends Property { -} diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java index 51bc065197e..d17c798ea25 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java @@ -13,35 +13,50 @@ package org.openhab.core.semantics; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; -import java.util.Locale; +import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.library.CoreItemFactory; -import org.openhab.core.semantics.model.equipment.CleaningRobot; -import org.openhab.core.semantics.model.location.Bathroom; -import org.openhab.core.semantics.model.location.Kitchen; -import org.openhab.core.semantics.model.location.Room; -import org.openhab.core.semantics.model.point.Measurement; -import org.openhab.core.semantics.model.property.Temperature; +import org.openhab.core.semantics.internal.SemanticTagRegistryImpl; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; /** * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault +@ExtendWith(MockitoExtension.class) public class SemanticTagsTest { + private static final String CUSTOM_LOCATION = "CustomLocation"; + private static final String CUSTOM_EQUIPMENT = "CustomEquipment"; + private static final String CUSTOM_POINT = "CustomPoint"; + private static final String CUSTOM_PROPERTY = "CustomProperty"; + + private @Mock @NonNullByDefault({}) ManagedSemanticTagProvider managedSemanticTagProviderMock; + private @NonNullByDefault({}) GroupItem locationItem; private @NonNullByDefault({}) GroupItem equipmentItem; private @NonNullByDefault({}) GenericItem pointItem; + private @NonNullByDefault({}) Class roomTagClass; + private @NonNullByDefault({}) Class bathroomTagClass; + private @NonNullByDefault({}) Class cleaningRobotTagClass; + private @NonNullByDefault({}) Class measurementTagClass; + private @NonNullByDefault({}) Class temperatureTagClass; + @BeforeEach public void setup() { - CoreItemFactory itemFactory = new CoreItemFactory(); + CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class)); locationItem = new GroupItem("TestBathRoom"); locationItem.addTag("Bathroom"); @@ -52,57 +67,115 @@ public void setup() { pointItem = itemFactory.createItem(CoreItemFactory.NUMBER, "TestTemperature"); pointItem.addTag("Measurement"); pointItem.addTag("Temperature"); - } - @Test - public void testByTagId() { - assertEquals(Location.class, SemanticTags.getById("Location")); - assertEquals(Room.class, SemanticTags.getById("Room")); - assertEquals(Room.class, SemanticTags.getById("Location_Indoor_Room")); - assertEquals(Bathroom.class, SemanticTags.getById("Bathroom")); - assertEquals(Bathroom.class, SemanticTags.getById("Room_Bathroom")); - assertEquals(Bathroom.class, SemanticTags.getById("Indoor_Room_Bathroom")); - assertEquals(Bathroom.class, SemanticTags.getById("Location_Indoor_Room_Bathroom")); + SemanticTag customLocationTag = new SemanticTagImpl("Location_" + CUSTOM_LOCATION, null, null, List.of()); + SemanticTag customEquipmentTag = new SemanticTagImpl("Equipment_" + CUSTOM_EQUIPMENT, null, null, List.of()); + SemanticTag customPointTag = new SemanticTagImpl("Point_" + CUSTOM_POINT, null, null, List.of()); + SemanticTag customPropertyTag = new SemanticTagImpl("Property_" + CUSTOM_PROPERTY, null, null, List.of()); + when(managedSemanticTagProviderMock.getAll()) + .thenReturn(List.of(customLocationTag, customEquipmentTag, customPointTag, customPropertyTag)); + new SemanticTagRegistryImpl(new DefaultSemanticTagProvider(), managedSemanticTagProviderMock); + + roomTagClass = SemanticTags.getById("Location_Indoor_Room"); + bathroomTagClass = SemanticTags.getById("Location_Indoor_Room_Bathroom"); + cleaningRobotTagClass = SemanticTags.getById("Equipment_CleaningRobot"); + measurementTagClass = SemanticTags.getById("Point_Measurement"); + temperatureTagClass = SemanticTags.getById("Property_Temperature"); } @Test - public void testByLabel() { - assertEquals(Kitchen.class, SemanticTags.getByLabel("Kitchen", Locale.ENGLISH)); - assertEquals(Kitchen.class, SemanticTags.getByLabel("Küche", Locale.GERMAN)); - assertNull(SemanticTags.getByLabel("Bad", Locale.GERMAN)); + public void testTagClasses() { + assertNotNull(roomTagClass); + assertNotNull(bathroomTagClass); + assertNotNull(cleaningRobotTagClass); + assertNotNull(measurementTagClass); + assertNotNull(temperatureTagClass); } @Test - public void testByLabelOrSynonym() { - assertEquals(Kitchen.class, SemanticTags.getByLabelOrSynonym("Kitchen", Locale.ENGLISH).iterator().next()); - assertEquals(Kitchen.class, SemanticTags.getByLabelOrSynonym("Küche", Locale.GERMAN).iterator().next()); - assertEquals(Bathroom.class, SemanticTags.getByLabelOrSynonym("Badezimmer", Locale.GERMAN).iterator().next()); + public void testByTagId() { + assertEquals(Location.class, SemanticTags.getById("Location")); + assertEquals(roomTagClass, SemanticTags.getById("Room")); + assertEquals(roomTagClass, SemanticTags.getById("Indoor_Room")); + assertEquals(roomTagClass, SemanticTags.getById("Location_Indoor_Room")); + assertEquals(bathroomTagClass, SemanticTags.getById("Bathroom")); + assertEquals(bathroomTagClass, SemanticTags.getById("Room_Bathroom")); + assertEquals(bathroomTagClass, SemanticTags.getById("Indoor_Room_Bathroom")); + assertEquals(bathroomTagClass, SemanticTags.getById("Location_Indoor_Room_Bathroom")); } @Test public void testGetSemanticType() { - assertEquals(Bathroom.class, SemanticTags.getSemanticType(locationItem)); - assertEquals(CleaningRobot.class, SemanticTags.getSemanticType(equipmentItem)); - assertEquals(Measurement.class, SemanticTags.getSemanticType(pointItem)); + assertEquals(bathroomTagClass, SemanticTags.getSemanticType(locationItem)); + assertEquals(cleaningRobotTagClass, SemanticTags.getSemanticType(equipmentItem)); + assertEquals(measurementTagClass, SemanticTags.getSemanticType(pointItem)); } @Test public void testGetLocation() { - assertEquals(Bathroom.class, SemanticTags.getLocation(locationItem)); + assertEquals(bathroomTagClass, SemanticTags.getLocation(locationItem)); } @Test public void testGetEquipment() { - assertEquals(CleaningRobot.class, SemanticTags.getEquipment(equipmentItem)); + assertEquals(cleaningRobotTagClass, SemanticTags.getEquipment(equipmentItem)); } @Test public void testGetPoint() { - assertEquals(Measurement.class, SemanticTags.getPoint(pointItem)); + assertEquals(measurementTagClass, SemanticTags.getPoint(pointItem)); } @Test public void testGetProperty() { - assertEquals(Temperature.class, SemanticTags.getProperty(pointItem)); + assertEquals(temperatureTagClass, SemanticTags.getProperty(pointItem)); + } + + @Test + public void testAddLocation() { + String tagName = CUSTOM_LOCATION; + Class customTag = SemanticTags.getById(tagName); + assertNotNull(customTag); + + GroupItem myItem = new GroupItem("MyLocation"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getLocation(myItem)); + } + + @Test + public void testAddEquipment() { + String tagName = CUSTOM_EQUIPMENT; + Class customTag = SemanticTags.getById(tagName); + assertNotNull(customTag); + + GroupItem myItem = new GroupItem("MyEquipment"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getEquipment(myItem)); + } + + @Test + public void testAddPoint() { + String tagName = CUSTOM_POINT; + Class customTag = SemanticTags.getById(tagName); + assertNotNull(customTag); + + GroupItem myItem = new GroupItem("MyItem"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getPoint(myItem)); + } + + @Test + public void testAddProperty() { + String tagName = CUSTOM_PROPERTY; + Class customTag = SemanticTags.getById(tagName); + assertNotNull(customTag); + + GroupItem myItem = new GroupItem("MyItem"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getProperty(myItem)); } } diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticsPredicatesTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticsPredicatesTest.java index ae1d5e964f6..947ecaca92f 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticsPredicatesTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticsPredicatesTest.java @@ -13,31 +13,42 @@ package org.openhab.core.semantics; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Objects; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.library.CoreItemFactory; -import org.openhab.core.semantics.model.property.Humidity; -import org.openhab.core.semantics.model.property.Temperature; +import org.openhab.core.semantics.internal.SemanticTagRegistryImpl; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; /** - * This are tests for {@link SemanticsPredicates}. + * These are tests for {@link SemanticsPredicates}. * * @author Christoph Weitkamp - Initial contribution */ @NonNullByDefault +@ExtendWith(MockitoExtension.class) public class SemanticsPredicatesTest { + private @Mock @NonNullByDefault({}) ManagedSemanticTagProvider managedSemanticTagProviderMock; + private @NonNullByDefault({}) GroupItem locationItem; private @NonNullByDefault({}) GroupItem equipmentItem; private @NonNullByDefault({}) GenericItem pointItem; @BeforeEach public void setup() { - CoreItemFactory itemFactory = new CoreItemFactory(); + CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class)); locationItem = new GroupItem("TestBathRoom"); locationItem.addTag("Bathroom"); @@ -48,6 +59,9 @@ public void setup() { pointItem = itemFactory.createItem(CoreItemFactory.NUMBER, "TestTemperature"); pointItem.addTag("Measurement"); pointItem.addTag("Temperature"); + + when(managedSemanticTagProviderMock.getAll()).thenReturn(List.of()); + new SemanticTagRegistryImpl(new DefaultSemanticTagProvider(), managedSemanticTagProviderMock); } @Test @@ -73,9 +87,13 @@ public void testIsPoint() { @Test public void testRelatesTo() { - assertFalse(SemanticsPredicates.relatesTo(Temperature.class).test(locationItem)); - assertFalse(SemanticsPredicates.relatesTo(Temperature.class).test(equipmentItem)); - assertTrue(SemanticsPredicates.relatesTo(Temperature.class).test(pointItem)); - assertFalse(SemanticsPredicates.relatesTo(Humidity.class).test(equipmentItem)); + Class temperatureTagClass = (Class) Objects + .requireNonNull(SemanticTags.getById("Property_Temperature")); + Class humidityTagClass = (Class) Objects + .requireNonNull(SemanticTags.getById("Property_Humidity")); + assertFalse(SemanticsPredicates.relatesTo(temperatureTagClass).test(locationItem)); + assertFalse(SemanticsPredicates.relatesTo(temperatureTagClass).test(equipmentItem)); + assertTrue(SemanticsPredicates.relatesTo(temperatureTagClass).test(pointItem)); + assertFalse(SemanticsPredicates.relatesTo(humidityTagClass).test(equipmentItem)); } } diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticTagRegistryImplTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticTagRegistryImplTest.java new file mode 100644 index 00000000000..80532c3d92c --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticTagRegistryImplTest.java @@ -0,0 +1,120 @@ +/** + * 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.semantics.internal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.semantics.Equipment; +import org.openhab.core.semantics.Location; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.Point; +import org.openhab.core.semantics.Property; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; + +/** + * @author Laurent Garnier - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class SemanticTagRegistryImplTest { + + private @Mock @NonNullByDefault({}) ManagedSemanticTagProvider managedSemanticTagProviderMock; + private @NonNullByDefault({}) SemanticTagRegistry semanticTagRegistry; + + private @NonNullByDefault({}) SemanticTag userLocationTag; + private @NonNullByDefault({}) SemanticTag userSubLocationTag; + + private @NonNullByDefault({}) Class roomTagClass; + private @NonNullByDefault({}) Class bathroomTagClass; + private @NonNullByDefault({}) Class cleaningRobotTagClass; + private @NonNullByDefault({}) Class measurementTagClass; + private @NonNullByDefault({}) Class temperatureTagClass; + + @BeforeEach + public void setup() throws Exception { + userLocationTag = new SemanticTagImpl("Location_UserLocation", "Custom label", "Custom description", + " Synonym1, Synonym2 , Synonym With Space "); + userSubLocationTag = new SemanticTagImpl("Location_UserLocation_UserSubLocation", null, null, List.of()); + when(managedSemanticTagProviderMock.getAll()).thenReturn(List.of(userLocationTag, userSubLocationTag)); + semanticTagRegistry = new SemanticTagRegistryImpl(new DefaultSemanticTagProvider(), + managedSemanticTagProviderMock); + + roomTagClass = semanticTagRegistry.getTagClassById("Location_Indoor_Room"); + bathroomTagClass = semanticTagRegistry.getTagClassById("Location_Indoor_Room_Bathroom"); + cleaningRobotTagClass = semanticTagRegistry.getTagClassById("Equipment_CleaningRobot"); + measurementTagClass = semanticTagRegistry.getTagClassById("Point_Measurement"); + temperatureTagClass = semanticTagRegistry.getTagClassById("Property_Temperature"); + } + + @Test + public void testGetById() { + assertEquals(Location.class, semanticTagRegistry.getTagClassById("Location")); + assertEquals(roomTagClass, semanticTagRegistry.getTagClassById("Room")); + assertEquals(roomTagClass, semanticTagRegistry.getTagClassById("Indoor_Room")); + assertEquals(roomTagClass, semanticTagRegistry.getTagClassById("Location_Indoor_Room")); + assertEquals(bathroomTagClass, semanticTagRegistry.getTagClassById("Bathroom")); + assertEquals(bathroomTagClass, semanticTagRegistry.getTagClassById("Room_Bathroom")); + assertEquals(bathroomTagClass, semanticTagRegistry.getTagClassById("Indoor_Room_Bathroom")); + assertEquals(bathroomTagClass, semanticTagRegistry.getTagClassById("Location_Indoor_Room_Bathroom")); + } + + @Test + public void testBuildId() { + assertEquals("Location", SemanticTagRegistryImpl.buildId(Location.class)); + assertEquals("Location_Indoor_Room", SemanticTagRegistryImpl.buildId(roomTagClass)); + assertEquals("Location_Indoor_Room_Bathroom", SemanticTagRegistryImpl.buildId(bathroomTagClass)); + assertEquals("Equipment", SemanticTagRegistryImpl.buildId(Equipment.class)); + assertEquals("Equipment_CleaningRobot", SemanticTagRegistryImpl.buildId(cleaningRobotTagClass)); + assertEquals("Point", SemanticTagRegistryImpl.buildId(Point.class)); + assertEquals("Point_Measurement", SemanticTagRegistryImpl.buildId(measurementTagClass)); + assertEquals("Property", SemanticTagRegistryImpl.buildId(Property.class)); + assertEquals("Property_Temperature", SemanticTagRegistryImpl.buildId(temperatureTagClass)); + } + + @Test + public void testIsEditable() { + when(managedSemanticTagProviderMock.get(eq("Location"))).thenReturn(null); + when(managedSemanticTagProviderMock.get(eq("Location_Indoor"))).thenReturn(null); + when(managedSemanticTagProviderMock.get(eq("Location_Indoor_Room"))).thenReturn(null); + when(managedSemanticTagProviderMock.get(eq("Location_Indoor_Room_Bathroom"))).thenReturn(null); + when(managedSemanticTagProviderMock.get(eq("Location_UserLocation"))).thenReturn(userLocationTag); + when(managedSemanticTagProviderMock.get(eq("Location_UserLocation_UserSubLocation"))) + .thenReturn(userSubLocationTag); + + assertFalse(semanticTagRegistry.isEditable(Objects.requireNonNull(semanticTagRegistry.get("Location")))); + assertFalse(semanticTagRegistry.isEditable(Objects.requireNonNull(semanticTagRegistry.get("Location_Indoor")))); + assertFalse(semanticTagRegistry + .isEditable(Objects.requireNonNull(semanticTagRegistry.get("Location_Indoor_Room")))); + assertFalse(semanticTagRegistry + .isEditable(Objects.requireNonNull(semanticTagRegistry.get("Location_Indoor_Room_Bathroom")))); + assertTrue(semanticTagRegistry + .isEditable(Objects.requireNonNull(semanticTagRegistry.get("Location_UserLocation")))); + assertTrue(semanticTagRegistry + .isEditable(Objects.requireNonNull(semanticTagRegistry.get("Location_UserLocation_UserSubLocation")))); + } +} diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsMetadataProviderTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsMetadataProviderTest.java index 90a5edc6cff..a8de24199fc 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsMetadataProviderTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsMetadataProviderTest.java @@ -16,6 +16,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import java.util.List; import java.util.Objects; import org.eclipse.jdt.annotation.NonNull; @@ -36,6 +37,9 @@ import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.Metadata; import org.openhab.core.library.items.SwitchItem; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; /** * @author Simon Lamon - Initial contribution @@ -49,14 +53,19 @@ public class SemanticsMetadataProviderTest { private static final String GROUP_ITEM_NAME = "groupItem"; - private @NonNullByDefault({}) @Mock ItemRegistry itemRegistry; - private @NonNullByDefault({}) @Mock ProviderChangeListener<@NonNull Metadata> changeListener; + private @Mock @NonNullByDefault({}) ItemRegistry itemRegistry; + private @Mock @NonNullByDefault({}) ProviderChangeListener<@NonNull Metadata> changeListener; + private @Mock @NonNullByDefault({}) ManagedSemanticTagProvider managedSemanticTagProviderMock; private @NonNullByDefault({}) SemanticsMetadataProvider semanticsMetadataProvider; @BeforeEach public void beforeEach() throws Exception { - semanticsMetadataProvider = new SemanticsMetadataProvider(itemRegistry) { + when(managedSemanticTagProviderMock.getAll()).thenReturn(List.of()); + SemanticTagRegistry semanticTagRegistry = new SemanticTagRegistryImpl(new DefaultSemanticTagProvider(), + managedSemanticTagProviderMock); + + semanticsMetadataProvider = new SemanticsMetadataProvider(itemRegistry, semanticTagRegistry) { { addProviderChangeListener(changeListener); } diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java index 9cb87fe6701..4e95ba4044d 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java @@ -12,9 +12,11 @@ */ package org.openhab.core.semantics.internal; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.util.List; import java.util.Locale; import java.util.Set; import java.util.stream.Stream; @@ -25,17 +27,24 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.MetadataRegistry; import org.openhab.core.library.CoreItemFactory; -import org.openhab.core.semantics.model.location.Bathroom; -import org.openhab.core.semantics.model.location.LivingRoom; +import org.openhab.core.semantics.Location; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.model.DefaultSemanticTagProvider; /** * @author Kai Kreuzer - Initial contribution + * @author Laurent Garnier - Tests added for methods moved from SemanticTags to SemanticsService */ @ExtendWith(MockitoExtension.class) @NonNullByDefault @@ -43,16 +52,24 @@ public class SemanticsServiceImplTest { private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) MetadataRegistry metadataRegistryMock; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; + private @Mock @NonNullByDefault({}) ManagedSemanticTagProvider managedSemanticTagProviderMock; private @NonNullByDefault({}) GroupItem locationItem; private @NonNullByDefault({}) GroupItem equipmentItem; private @NonNullByDefault({}) GenericItem pointItem; + private @NonNullByDefault({}) Class roomTagClass; + private @NonNullByDefault({}) Class bathroomTagClass; + private @NonNullByDefault({}) Class livingRoomTagClass; + private @NonNullByDefault({}) Class userLocationTagClass; + private @NonNullByDefault({}) Class cleaningRobotTagClass; + private @NonNullByDefault({}) SemanticsServiceImpl service; @BeforeEach public void setup() throws Exception { - CoreItemFactory itemFactory = new CoreItemFactory(); + CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock); locationItem = new GroupItem("TestBathRoom"); locationItem.addTag("Bathroom"); locationItem.setLabel("Joe's Room"); @@ -67,28 +84,149 @@ public void setup() throws Exception { pointItem.addGroupName(locationItem.getName()); locationItem.addMember(pointItem); - when(itemRegistryMock.stream()).thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) - .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) - .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)); + SemanticTag userLocationTag = new SemanticTagImpl("Location_UserLocation", "Custom label", "Custom description", + " Synonym1, Synonym2 , Synonym With Space "); + when(managedSemanticTagProviderMock.getAll()).thenReturn(List.of(userLocationTag)); + SemanticTagRegistry semanticTagRegistry = new SemanticTagRegistryImpl(new DefaultSemanticTagProvider(), + managedSemanticTagProviderMock); + + roomTagClass = semanticTagRegistry.getTagClassById("Location_Indoor_Room"); + bathroomTagClass = semanticTagRegistry.getTagClassById("Location_Indoor_Room_Bathroom"); + livingRoomTagClass = semanticTagRegistry.getTagClassById("Location_Indoor_Room_LivingRoom"); + userLocationTagClass = semanticTagRegistry.getTagClassById("Location_UserLocation"); + cleaningRobotTagClass = semanticTagRegistry.getTagClassById("Equipment_CleaningRobot"); - service = new SemanticsServiceImpl(itemRegistryMock, metadataRegistryMock); + service = new SemanticsServiceImpl(itemRegistryMock, metadataRegistryMock, semanticTagRegistry); } @Test public void testGetItemsInLocation() throws Exception { - Set items = service.getItemsInLocation(Bathroom.class); + when(itemRegistryMock.stream()).thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)); + + Set items = service.getItemsInLocation((Class) bathroomTagClass); + assertEquals(1, items.size()); assertTrue(items.contains(pointItem)); - items = service.getItemsInLocation("Room", Locale.ENGLISH); + items = service.getItemsInLocation((Class) roomTagClass); + assertEquals(1, items.size()); assertTrue(items.contains(pointItem)); + + items = service.getItemsInLocation((Class) livingRoomTagClass); + assertTrue(items.isEmpty()); } @Test public void testGetItemsInLocationByString() throws Exception { + when(itemRegistryMock.stream()).thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)); + when(metadataRegistryMock.get(any())).thenReturn(null); + + // Label of a location group item Set items = service.getItemsInLocation("joe's room", Locale.ENGLISH); + assertEquals(1, items.size()); assertTrue(items.contains(pointItem)); - items = service.getItemsInLocation(LivingRoom.class); + // Location tag label + items = service.getItemsInLocation("bathroom", Locale.ENGLISH); + assertEquals(1, items.size()); + assertTrue(items.contains(pointItem)); + + // Location tag synonym + items = service.getItemsInLocation("powder room", Locale.ENGLISH); + assertEquals(1, items.size()); + assertTrue(items.contains(pointItem)); + + // Location parent tag label + items = service.getItemsInLocation("Room", Locale.ENGLISH); + assertEquals(1, items.size()); + assertTrue(items.contains(pointItem)); + + // Existing item label + items = service.getItemsInLocation("my Test label", Locale.ENGLISH); + assertTrue(items.isEmpty()); + + // Unknown item label + items = service.getItemsInLocation("wrong label", Locale.ENGLISH); assertTrue(items.isEmpty()); } + + @Test + public void testGetLabelAndSynonyms() { + List result = service.getLabelAndSynonyms(bathroomTagClass, Locale.ENGLISH); + assertEquals(6, result.size()); + assertEquals("bathroom", result.get(0)); + assertEquals("bathrooms", result.get(1)); + assertEquals("bath", result.get(2)); + assertEquals("baths", result.get(3)); + assertEquals("powder room", result.get(4)); + assertEquals("powder rooms", result.get(5)); + + result = service.getLabelAndSynonyms(cleaningRobotTagClass, Locale.FRENCH); + assertEquals(4, result.size()); + assertEquals("robot de nettoyage", result.get(0)); + assertEquals("robos de nettoyage", result.get(1)); + assertEquals("robot aspirateur", result.get(2)); + assertEquals("robots aspirateur", result.get(3)); + + result = service.getLabelAndSynonyms(userLocationTagClass, Locale.ENGLISH); + assertEquals(4, result.size()); + assertEquals("custom label", result.get(0)); + assertEquals("synonym1", result.get(1)); + assertEquals("synonym2", result.get(2)); + assertEquals("synonym with space", result.get(3)); + } + + @Test + public void testGetByLabel() { + Class tag = service.getByLabel("BATHROOM", Locale.ENGLISH); + assertEquals(bathroomTagClass, tag); + tag = service.getByLabel("Bath", Locale.ENGLISH); + assertNull(tag); + + tag = service.getByLabel("ROBOT de nettoyage", Locale.FRENCH); + assertEquals(cleaningRobotTagClass, tag); + tag = service.getByLabel("Robot aspirateur", Locale.FRENCH); + assertNull(tag); + + tag = service.getByLabel("CUSTOM label", Locale.ENGLISH); + assertEquals(userLocationTagClass, tag); + tag = service.getByLabel("Synonym1", Locale.ENGLISH); + assertNull(tag); + } + + @Test + public void testGetByLabelOrSynonym() { + List> tags = service.getByLabelOrSynonym("BATHROOM", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(bathroomTagClass, tags.get(0)); + tags = service.getByLabelOrSynonym("POWDER Rooms", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(bathroomTagClass, tags.get(0)); + tags = service.getByLabelOrSynonym("other bath", Locale.ENGLISH); + assertTrue(tags.isEmpty()); + + tags = service.getByLabelOrSynonym("ROBOT de nettoyage", Locale.FRENCH); + assertEquals(1, tags.size()); + assertEquals(cleaningRobotTagClass, tags.get(0)); + tags = service.getByLabelOrSynonym("ROBOTS aspirateur", Locale.FRENCH); + assertEquals(1, tags.size()); + assertEquals(cleaningRobotTagClass, tags.get(0)); + tags = service.getByLabelOrSynonym("Robot cuiseur", Locale.FRENCH); + assertTrue(tags.isEmpty()); + + tags = service.getByLabelOrSynonym("CUSTOM label", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(userLocationTagClass, tags.get(0)); + tags = service.getByLabelOrSynonym("Synonym with space", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(userLocationTagClass, tags.get(0)); + tags = service.getByLabelOrSynonym("wrong label", Locale.ENGLISH); + assertTrue(tags.isEmpty()); + } } diff --git a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java index 0be2f7c2acb..ad21e9d8817 100644 --- a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java +++ b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java @@ -53,7 +53,7 @@ public class JsonStorageTest extends JavaTest { @BeforeEach public void setUp() throws IOException { - tmpFile = File.createTempFile("storage-debug", ".json"); + tmpFile = Files.createTempFile("storage-debug", ".json").toFile(); tmpFile.deleteOnExit(); objectStorage = new JsonStorage<>(tmpFile, this.getClass().getClassLoader(), 0, 0, 0, List.of()); } diff --git a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/MigrationTest.java b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/MigrationTest.java index 2e4dbcf1b87..cd24b2ced7f 100644 --- a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/MigrationTest.java +++ b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/MigrationTest.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import java.util.Objects; @@ -48,7 +49,7 @@ public class MigrationTest { @BeforeEach public void setup() throws IOException { - tmpFile = File.createTempFile("storage-debug", ".json"); + tmpFile = Files.createTempFile("storage-debug", ".json").toFile(); tmpFile.deleteOnExit(); // store old class diff --git a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/handler/MagicDelayedOnlineHandler.java b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/handler/MagicDelayedOnlineHandler.java index d1c60ac4a91..6bb90d6d6e2 100644 --- a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/handler/MagicDelayedOnlineHandler.java +++ b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/handler/MagicDelayedOnlineHandler.java @@ -44,9 +44,8 @@ public void initialize() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (channelUID.getId().equals("number")) { - if (command instanceof DecimalType) { - DecimalType cmd = (DecimalType) command; + if ("number".equals(channelUID.getId())) { + if (command instanceof DecimalType cmd) { int cmdInt = cmd.intValue(); ThingStatus status = cmdInt > 0 ? ThingStatus.ONLINE : ThingStatus.OFFLINE; int waitTime = Math.abs(cmd.intValue()); diff --git a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java index ce8895e0774..1e46d4bf392 100644 --- a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java +++ b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/MagicServiceConfig.java @@ -46,7 +46,7 @@ public class MagicServiceConfig { @Override public String toString() { - StringBuffer b = new StringBuffer(); + StringBuilder b = new StringBuilder(); for (Field field : this.getClass().getDeclaredFields()) { Object value; try { diff --git a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/automation/modules/MagicThingActionsService.java b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/automation/modules/MagicThingActionsService.java index ae8f4e023a7..f7a0fa956c3 100644 --- a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/automation/modules/MagicThingActionsService.java +++ b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/automation/modules/MagicThingActionsService.java @@ -59,8 +59,8 @@ public class MagicThingActionsService implements ThingActions { @Override public void setThingHandler(ThingHandler handler) { - if (handler instanceof MagicActionModuleThingHandler) { - this.handler = (MagicActionModuleThingHandler) handler; + if (handler instanceof MagicActionModuleThingHandler thingHandler) { + this.handler = thingHandler; } } diff --git a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/firmware/MagicFirmwareProvider.java b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/firmware/MagicFirmwareProvider.java index 36492d1ea67..be7486e51ae 100644 --- a/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/firmware/MagicFirmwareProvider.java +++ b/bundles/org.openhab.core.test.magic/src/main/java/org/openhab/core/magic/binding/internal/firmware/MagicFirmwareProvider.java @@ -73,8 +73,7 @@ public class MagicFirmwareProvider implements FirmwareProvider { private static Firmware createFirmware(final @Nullable String model, final String version, boolean modelRestricted) { - Firmware firmware = FirmwareBuilder.create(MagicBindingConstants.THING_TYPE_FIRMWARE_UPDATE, version) - .withModel(model).withModelRestricted(modelRestricted).build(); - return firmware; + return FirmwareBuilder.create(MagicBindingConstants.THING_TYPE_FIRMWARE_UPDATE, version).withModel(model) + .withModelRestricted(modelRestricted).build(); } } diff --git a/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/SyntheticBundleInstaller.java b/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/SyntheticBundleInstaller.java index 5731af89e39..15fa91b289f 100644 --- a/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/SyntheticBundleInstaller.java +++ b/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/SyntheticBundleInstaller.java @@ -232,10 +232,7 @@ public static Bundle installFragment(BundleContext bundleContext, String testBun String bundlePath = BUNDLE_POOL_PATH + "/" + testBundleName + "/"; byte[] syntheticBundleBytes = createSyntheticBundle(bundleContext.getBundle(), bundlePath, testBundleName, extensionsToInclude); - - Bundle syntheticBundle = bundleContext.installBundle(testBundleName, - new ByteArrayInputStream(syntheticBundleBytes)); - return syntheticBundle; + return bundleContext.installBundle(testBundleName, new ByteArrayInputStream(syntheticBundleBytes)); } private static boolean isBundleAvailable(BundleContext context, String bsn) { @@ -396,8 +393,7 @@ private static List collectEntries(Bundle bundle, String path, Set private static String convertToFileEntry(URI baseURI, URL entryURL) throws URISyntaxException { URI entryURI = entryURL.toURI(); URI relativeURI = baseURI.relativize(entryURI); - String fileEntry = relativeURI.toString(); - return fileEntry; + return relativeURI.toString(); } private static Manifest getManifest(Bundle bundle, String bundlePath) throws IOException { diff --git a/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaOSGiTest.java b/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaOSGiTest.java index dfbb0fba468..4f944c548da 100644 --- a/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaOSGiTest.java +++ b/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaOSGiTest.java @@ -111,7 +111,7 @@ public void bindBundleContext() { * @return List of OSGi services or empty List if no service can be found for the given class and filter */ protected List getServices(Class clazz, Predicate> filter) { - final ServiceReference<@Nullable T> serviceReferences[] = getServices(clazz); + final ServiceReference<@Nullable T>[] serviceReferences = getServices(clazz); if (serviceReferences == null) { new MissingServiceAnalyzer(System.out, bundleContext).printMissingServiceDetails(clazz); @@ -192,7 +192,7 @@ protected List getServices(Class clazz, Predicate> * @return List of OSGi service or empty List if no matching services can be found for the given classes */ protected List getServices(Class clazz, Class implementationClass) { - final ServiceReference<@Nullable T> serviceReferences[] = getServices(clazz); + final ServiceReference<@Nullable T>[] serviceReferences = getServices(clazz); if (serviceReferences == null) { new MissingServiceAnalyzer(System.out, bundleContext).printMissingServiceDetails(clazz); @@ -344,6 +344,15 @@ protected String getInterfaceName(final Object service) { } } + /** + * Get the OSGi {@link BundleContext} + * + * @return the bundle context + */ + protected BundleContext getBundleContext() { + return bundleContext; + } + /** * Registers a volatile storage service. */ diff --git a/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaTest.java b/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaTest.java index 90f55e7686f..5bd878314c5 100644 --- a/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaTest.java +++ b/bundles/org.openhab.core.test/src/main/java/org/openhab/core/test/java/JavaTest.java @@ -98,7 +98,7 @@ protected void assertNoLogMessage(Class clazz) { if (appender == null) { Assertions.fail("Logger for class '" + clazz + "' not found."); } - if (appender.list.size() != 0) { + if (!appender.list.isEmpty()) { Assertions.fail("Expected no log message for class '" + clazz + "', but found '" + appender.list + "'."); } } diff --git a/bundles/org.openhab.core.thing/pom.xml b/bundles/org.openhab.core.thing/pom.xml index 58cd26a9443..f969dcd2100 100644 --- a/bundles/org.openhab.core.thing/pom.xml +++ b/bundles/org.openhab.core.thing/pom.xml @@ -25,6 +25,11 @@ org.openhab.core.io.console ${project.version} + + org.openhab.core.bundles + org.openhab.core.transform + ${project.version} + org.openhab.core.bundles org.openhab.core.test diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/Channel.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/Channel.java index 9a3ec7df81e..ee1e8c9a001 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/Channel.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/Channel.java @@ -14,6 +14,7 @@ import java.util.LinkedHashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -178,4 +179,27 @@ public Set getDefaultTags() { public @Nullable AutoUpdatePolicy getAutoUpdatePolicy() { return autoUpdatePolicy; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Channel channel = (Channel) o; + return Objects.equals(acceptedItemType, channel.acceptedItemType) && kind == channel.kind + && Objects.equals(uid, channel.uid) && Objects.equals(channelTypeUID, channel.channelTypeUID) + && Objects.equals(label, channel.label) && Objects.equals(description, channel.description) + && Objects.equals(configuration, channel.configuration) + && Objects.equals(properties, channel.properties) && Objects.equals(defaultTags, channel.defaultTags) + && autoUpdatePolicy == channel.autoUpdatePolicy; + } + + @Override + public int hashCode() { + return Objects.hash(acceptedItemType, kind, uid, channelTypeUID, label, description, configuration, properties, + defaultTags, autoUpdatePolicy); + } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ManagedThingProvider.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ManagedThingProvider.java index cf953fbace3..cab11cad2a8 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ManagedThingProvider.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ManagedThingProvider.java @@ -63,6 +63,7 @@ protected String keyToString(ThingUID key) { return null; } + @Override protected ThingStorageEntity toPersistableElement(Thing element) { return new ThingStorageEntity(ThingDTOMapper.map(element), element instanceof BridgeImpl); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingManager.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingManager.java index 6f525c84c1b..28eeead5ff7 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingManager.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingManager.java @@ -31,7 +31,7 @@ public interface ThingManager { * @return {@code false} when the {@link Thing} has {@link ThingStatus} with {@link ThingStatusDetail#DISABLED}. * Returns {@code true} in all other cases. */ - public boolean isEnabled(ThingUID thingUID); + boolean isEnabled(ThingUID thingUID); /** * This method is used for changing enabled state of the {@link Thing} @@ -41,5 +41,5 @@ public interface ThingManager { * @param thingUID UID of the {@link Thing}. * @param isEnabled a new enabled / disabled state of the {@link Thing}. */ - public void setEnabled(ThingUID thingUID, boolean isEnabled); + void setEnabled(ThingUID thingUID, boolean isEnabled); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatus.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatus.java index 19f2b8eee6a..747c6b0bebd 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatus.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatus.java @@ -28,5 +28,5 @@ public enum ThingStatus { ONLINE, OFFLINE, REMOVING, - REMOVED; + REMOVED } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProvider.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProvider.java new file mode 100644 index 00000000000..1a646a5389d --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProvider.java @@ -0,0 +1,447 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding; + +import java.math.BigDecimal; +import java.net.URI; +import java.util.Collection; +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; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.storage.Storage; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.internal.type.TriggerChannelTypeBuilderImpl; +import org.openhab.core.thing.type.AutoUpdatePolicy; +import org.openhab.core.thing.type.BridgeType; +import org.openhab.core.thing.type.ChannelDefinition; +import org.openhab.core.thing.type.ChannelDefinitionBuilder; +import org.openhab.core.thing.type.ChannelGroupDefinition; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeBuilder; +import org.openhab.core.thing.type.ChannelGroupTypeProvider; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeProvider; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.StateChannelTypeBuilder; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.EventDescription; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragment; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; + +/** + * The {@link AbstractStorageBasedTypeProvider} is the base class for the implementation of a {@link Storage} based + * {@link ThingTypeProvider}, {@link ChannelTypeProvider} and {@link ChannelGroupTypeProvider} + * + * It can be subclassed by bindings that create {@link ThingType}s and {@link ChannelType}s on-the-fly and need to + * persist those for future thing initializations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractStorageBasedTypeProvider + implements ThingTypeProvider, ChannelTypeProvider, ChannelGroupTypeProvider { + + private final Storage thingTypeEntityStorage; + private final Storage channelTypeEntityStorage; + private final Storage channelGroupTypeEntityStorage; + + /** + * Instantiate a new storage based type provider. The subclass needs to be a + * {@link org.osgi.service.component.annotations.Component} and declare itself as {@link ThingTypeProvider} and/or + * {@link ChannelTypeProvider} and/or {@link ChannelGroupTypeProvider}. + * + * @param storageService a persistent {@link StorageService} + */ + public AbstractStorageBasedTypeProvider(StorageService storageService) { + String thingTypeStorageName = getClass().getName() + "-ThingType"; + String channelTypeStorageName = getClass().getName() + "-ChannelType"; + String channelGroupTypeStorageName = getClass().getName() + "-ChannelGroupType"; + ClassLoader classLoader = getClass().getClassLoader(); + thingTypeEntityStorage = storageService.getStorage(thingTypeStorageName, classLoader); + channelTypeEntityStorage = storageService.getStorage(channelTypeStorageName, classLoader); + channelGroupTypeEntityStorage = storageService.getStorage(channelGroupTypeStorageName, classLoader); + } + + /** + * Add or update a {@link ThingType} to the storage + * + * @param thingType the {@link ThingType} that needs to be stored + */ + public void putThingType(ThingType thingType) { + thingTypeEntityStorage.put(thingType.getUID().toString(), mapToEntity(thingType)); + } + + /** + * Remove a {@link ThingType} from the storage + * + * @param thingTypeUID the {@link ThingTypeUID} of the thing type + */ + public void removeThingType(ThingTypeUID thingTypeUID) { + thingTypeEntityStorage.remove(thingTypeUID.toString()); + } + + /** + * Add or update a {@link ChannelType} to the storage + * + * @param channelType the {@link ChannelType} that needs to be stored + */ + public void putChannelType(ChannelType channelType) { + channelTypeEntityStorage.put(channelType.getUID().toString(), mapToEntity(channelType)); + } + + /** + * Remove a {@link ChannelType} from the storage + * + * @param channelTypeUID the {@link ChannelTypeUID} of the channel type + */ + public void removeChannelType(ChannelTypeUID channelTypeUID) { + channelTypeEntityStorage.remove(channelTypeUID.toString()); + } + + /** + * Add or update a {@link ChannelGroupType} to the storage + * + * @param channelGroupType the {@link ChannelType} that needs to be stored + */ + public void putChannelGroupType(ChannelGroupType channelGroupType) { + channelGroupTypeEntityStorage.put(channelGroupType.getUID().toString(), mapToEntity(channelGroupType)); + } + + /** + * Remove a {@link ChannelGroupType} from the storage + * + * @param channelGroupTypeUID the {@link ChannelGroupTypeUID} of the channel type + */ + public void removeChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID) { + channelGroupTypeEntityStorage.remove(channelGroupTypeUID.toString()); + } + + @Override + public Collection getThingTypes(@Nullable Locale locale) { + return thingTypeEntityStorage.stream().map(Map.Entry::getValue).filter(Objects::nonNull) + .map(Objects::requireNonNull).map(AbstractStorageBasedTypeProvider::mapFromEntity).toList(); + } + + @Override + public @Nullable ThingType getThingType(ThingTypeUID thingTypeUID, @Nullable Locale locale) { + ThingTypeEntity entity = thingTypeEntityStorage.get(thingTypeUID.toString()); + if (entity != null) { + return mapFromEntity(entity); + } else { + return null; + } + } + + @Override + public Collection getChannelTypes(@Nullable Locale locale) { + return channelTypeEntityStorage.stream().map(Map.Entry::getValue).filter(Objects::nonNull) + .map(Objects::requireNonNull).map(AbstractStorageBasedTypeProvider::mapFromEntity).toList(); + } + + @Override + public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) { + ChannelTypeEntity entity = channelTypeEntityStorage.get(channelTypeUID.toString()); + if (entity != null) { + return mapFromEntity(entity); + } else { + return null; + } + } + + @Override + public Collection getChannelGroupTypes(@Nullable Locale locale) { + return channelGroupTypeEntityStorage.stream().map(Map.Entry::getValue).filter(Objects::nonNull) + .map(Objects::requireNonNull).map(AbstractStorageBasedTypeProvider::mapFromEntity).toList(); + } + + @Override + public @Nullable ChannelGroupType getChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID, + @Nullable Locale locale) { + ChannelGroupTypeEntity entity = channelGroupTypeEntityStorage.get(channelGroupTypeUID.toString()); + if (entity != null) { + return mapFromEntity(entity); + } else { + return null; + } + } + + static ThingTypeEntity mapToEntity(ThingType thingType) { + ThingTypeEntity entity = new ThingTypeEntity(); + entity.uid = thingType.getUID(); + entity.label = thingType.getLabel(); + entity.description = thingType.getDescription(); + + entity.supportedBridgeTypeRefs = thingType.getSupportedBridgeTypeUIDs(); + entity.configDescriptionUri = thingType.getConfigDescriptionURI(); + entity.category = thingType.getCategory(); + entity.channelGroupDefinitions = thingType.getChannelGroupDefinitions().stream() + .map(AbstractStorageBasedTypeProvider::mapToEntity).collect(Collectors.toList()); + entity.channelDefinitions = thingType.getChannelDefinitions().stream() + .map(AbstractStorageBasedTypeProvider::mapToEntity).toList(); + entity.representationProperty = thingType.getRepresentationProperty(); + entity.properties = thingType.getProperties(); + entity.isListed = thingType.isListed(); + entity.extensibleChannelTypeIds = thingType.getExtensibleChannelTypeIds(); + entity.isBridge = thingType instanceof BridgeType; + + return entity; + } + + static ChannelDefinitionEntity mapToEntity(ChannelDefinition channelDefinition) { + ChannelDefinitionEntity entity = new ChannelDefinitionEntity(); + entity.id = channelDefinition.getId(); + entity.uid = channelDefinition.getChannelTypeUID(); + entity.label = channelDefinition.getLabel(); + entity.description = channelDefinition.getDescription(); + entity.properties = channelDefinition.getProperties(); + entity.autoUpdatePolicy = channelDefinition.getAutoUpdatePolicy(); + return entity; + } + + static ChannelGroupDefinitionEntity mapToEntity(ChannelGroupDefinition channelGroupDefinition) { + ChannelGroupDefinitionEntity entity = new ChannelGroupDefinitionEntity(); + entity.id = channelGroupDefinition.getId(); + entity.typeUid = channelGroupDefinition.getTypeUID(); + entity.label = channelGroupDefinition.getLabel(); + entity.description = channelGroupDefinition.getDescription(); + return entity; + } + + static ChannelTypeEntity mapToEntity(ChannelType channelType) { + ChannelTypeEntity entity = new ChannelTypeEntity(); + entity.uid = channelType.getUID(); + entity.label = channelType.getLabel(); + entity.description = channelType.getDescription(); + entity.configDescriptionURI = channelType.getConfigDescriptionURI(); + entity.advanced = channelType.isAdvanced(); + entity.itemType = channelType.getItemType(); + entity.kind = channelType.getKind(); + entity.tags = channelType.getTags(); + entity.category = channelType.getCategory(); + StateDescription stateDescription = channelType.getState(); + if (stateDescription != null) { + StateDescriptionFragment fragment = StateDescriptionFragmentBuilder.create(stateDescription).build(); + entity.stateDescriptionFragment = mapToEntity(fragment); + } else { + entity.stateDescriptionFragment = null; + } + entity.commandDescription = channelType.getCommandDescription(); + entity.event = channelType.getEvent(); + entity.autoUpdatePolicy = channelType.getAutoUpdatePolicy(); + return entity; + } + + static ChannelGroupTypeEntity mapToEntity(ChannelGroupType channelGroupType) { + ChannelGroupTypeEntity entity = new ChannelGroupTypeEntity(); + entity.uid = channelGroupType.getUID(); + entity.label = channelGroupType.getLabel(); + entity.description = channelGroupType.getDescription(); + entity.category = channelGroupType.getCategory(); + entity.channelDefinitions = channelGroupType.getChannelDefinitions().stream() + .map(AbstractStorageBasedTypeProvider::mapToEntity).toList(); + return entity; + } + + static StateDescriptionFragmentEntity mapToEntity(StateDescriptionFragment fragment) { + StateDescriptionFragmentEntity entity = new StateDescriptionFragmentEntity(); + entity.maximum = fragment.getMaximum(); + entity.minimum = fragment.getMinimum(); + entity.step = fragment.getStep(); + entity.options = fragment.getOptions(); + entity.pattern = fragment.getPattern(); + entity.isReadOnly = fragment.isReadOnly(); + return entity; + } + + static ThingType mapFromEntity(ThingTypeEntity entity) { + ThingTypeBuilder builder = ThingTypeBuilder.instance(entity.uid, entity.label) + .withSupportedBridgeTypeUIDs(entity.supportedBridgeTypeRefs).withProperties(entity.properties) + .withChannelDefinitions(entity.channelDefinitions.stream() + .map(AbstractStorageBasedTypeProvider::mapFromEntity).toList()) + .withChannelGroupDefinitions(entity.channelGroupDefinitions.stream() + .map(AbstractStorageBasedTypeProvider::mapFromEntity).toList()) + .isListed(entity.isListed).withExtensibleChannelTypeIds(entity.extensibleChannelTypeIds); + if (entity.description != null) { + builder.withDescription(Objects.requireNonNull(entity.description)); + } + if (entity.category != null) { + builder.withCategory(Objects.requireNonNull(entity.category)); + } + if (entity.configDescriptionUri != null) { + builder.withConfigDescriptionURI(Objects.requireNonNull(entity.configDescriptionUri)); + } + if (entity.representationProperty != null) { + builder.withRepresentationProperty(Objects.requireNonNull(entity.representationProperty)); + } + return entity.isBridge ? builder.buildBridge() : builder.build(); + } + + static ChannelDefinition mapFromEntity(ChannelDefinitionEntity entity) { + return new ChannelDefinitionBuilder(entity.id, entity.uid).withLabel(entity.label) + .withDescription(entity.description).withProperties(entity.properties) + .withAutoUpdatePolicy(entity.autoUpdatePolicy).build(); + } + + static ChannelGroupDefinition mapFromEntity(ChannelGroupDefinitionEntity entity) { + return new ChannelGroupDefinition(entity.id, entity.typeUid, entity.label, entity.description); + } + + static ChannelType mapFromEntity(ChannelTypeEntity entity) { + ChannelTypeBuilder builder = (entity.kind == ChannelKind.STATE) + ? ChannelTypeBuilder.state(entity.uid, entity.label, Objects.requireNonNull(entity.itemType)) + : ChannelTypeBuilder.trigger(entity.uid, entity.label); + builder.isAdvanced(entity.advanced).withTags(entity.tags); + if (entity.description != null) { + builder.withDescription(Objects.requireNonNull(entity.description)); + } + if (entity.configDescriptionURI != null) { + builder.withConfigDescriptionURI(Objects.requireNonNull(entity.configDescriptionURI)); + } + if (entity.category != null) { + builder.withCategory(Objects.requireNonNull(entity.category)); + } + if (builder instanceof StateChannelTypeBuilder stateBuilder) { + if (entity.stateDescriptionFragment != null) { + stateBuilder.withStateDescriptionFragment( + mapFromEntity(Objects.requireNonNull(entity.stateDescriptionFragment))); + } + if (entity.commandDescription != null) { + stateBuilder.withCommandDescription(Objects.requireNonNull(entity.commandDescription)); + } + if (entity.autoUpdatePolicy != null) { + stateBuilder.withAutoUpdatePolicy(Objects.requireNonNull(entity.autoUpdatePolicy)); + } + } + if (builder instanceof TriggerChannelTypeBuilderImpl triggerBuilder) { + if (entity.event != null) { + triggerBuilder.withEventDescription(Objects.requireNonNull(entity.event)); + } + } + return builder.build(); + } + + static ChannelGroupType mapFromEntity(ChannelGroupTypeEntity entity) { + ChannelGroupTypeBuilder builder = ChannelGroupTypeBuilder.instance(entity.uid, entity.label) + .withChannelDefinitions(entity.channelDefinitions.stream() + .map(AbstractStorageBasedTypeProvider::mapFromEntity).toList()); + if (entity.description != null) { + builder.withDescription(Objects.requireNonNull(entity.description)); + } + if (entity.category != null) { + builder.withCategory(Objects.requireNonNull(entity.category)); + } + return builder.build(); + } + + static StateDescriptionFragment mapFromEntity(StateDescriptionFragmentEntity entity) { + StateDescriptionFragmentBuilder builder = StateDescriptionFragmentBuilder.create(); + if (entity.maximum != null) { + builder.withMaximum(Objects.requireNonNull(entity.maximum)); + } + if (entity.minimum != null) { + builder.withMinimum(Objects.requireNonNull(entity.minimum)); + } + if (entity.step != null) { + builder.withStep(Objects.requireNonNull(entity.step)); + } + if (entity.options != null) { + builder.withOptions(Objects.requireNonNull(entity.options)); + } + if (entity.pattern != null) { + builder.withPattern(Objects.requireNonNull(entity.pattern)); + } + builder.withReadOnly(entity.isReadOnly); + return builder.build(); + } + + static class ThingTypeEntity { + public @NonNullByDefault({}) ThingTypeUID uid; + public @NonNullByDefault({}) String label; + public @Nullable String description; + public @Nullable String category; + public @Nullable String representationProperty; + public List supportedBridgeTypeRefs = List.of(); + public @Nullable URI configDescriptionUri; + public List extensibleChannelTypeIds = List.of(); + public List channelGroupDefinitions = List.of(); + public List channelDefinitions = List.of(); + public Map properties = Map.of(); + public boolean isListed = false; + public boolean isBridge = false; + } + + static class ChannelDefinitionEntity { + public @NonNullByDefault({}) String id; + public @NonNullByDefault({}) ChannelTypeUID uid; + public @Nullable String label; + public @Nullable String description; + public Map properties = Map.of(); + public @Nullable AutoUpdatePolicy autoUpdatePolicy; + } + + static class ChannelGroupDefinitionEntity { + public @NonNullByDefault({}) String id; + public @NonNullByDefault({}) ChannelGroupTypeUID typeUid; + public @Nullable String label; + public @Nullable String description; + } + + static class ChannelTypeEntity { + public @NonNullByDefault({}) ChannelTypeUID uid; + public @NonNullByDefault({}) String label; + public @Nullable String description; + public @Nullable URI configDescriptionURI; + + public boolean advanced; + public @Nullable String itemType; + public @NonNullByDefault({}) ChannelKind kind; + public Set tags = Set.of(); + public @Nullable String category; + public @Nullable StateDescriptionFragmentEntity stateDescriptionFragment; + public @Nullable CommandDescription commandDescription; + public @Nullable EventDescription event; + public @Nullable AutoUpdatePolicy autoUpdatePolicy; + } + + static class ChannelGroupTypeEntity { + public @NonNullByDefault({}) ChannelGroupTypeUID uid; + public @NonNullByDefault({}) String label; + public @Nullable String description; + + public List channelDefinitions = List.of(); + private @Nullable String category; + } + + static class StateDescriptionFragmentEntity { + public @Nullable BigDecimal maximum; + public @Nullable BigDecimal minimum; + public @Nullable BigDecimal step; + public @Nullable List options; + public @Nullable String pattern; + public boolean isReadOnly = false; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandlerFactory.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandlerFactory.java index c15aaf5bba6..b991c7117c9 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandlerFactory.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/BaseThingHandlerFactory.java @@ -148,8 +148,8 @@ private void registerServices(Thing thing, ThingHandler thingHandler) { Object serviceInstance = c.getConstructor().newInstance(); ThingHandlerService ths = null; - if (serviceInstance instanceof ThingHandlerService) { - ths = (ThingHandlerService) serviceInstance; + if (serviceInstance instanceof ThingHandlerService service) { + ths = service; ths.setThingHandler(thingHandler); } else { logger.warn( @@ -340,9 +340,8 @@ public void removeThing(ThingUID thingUID) { ThingUID effectiveUID = thingUID != null ? thingUID : ThingFactory.generateRandomThingUID(thingTypeUID); ThingType thingType = getThingTypeByUID(thingTypeUID); if (thingType != null) { - Thing thing = ThingFactory.createThing(thingType, effectiveUID, configuration, bridgeUID, + return ThingFactory.createThing(thingType, effectiveUID, configuration, bridgeUID, getConfigDescriptionRegistry()); - return thing; } else { return null; } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/Firmware.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/Firmware.java index d8fdb6523c3..913993796fd 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/Firmware.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/Firmware.java @@ -65,7 +65,7 @@ public interface Firmware extends Comparable { /** The key for the requires a factory reset property. */ - public static final String PROPERTY_REQUIRES_FACTORY_RESET = "requiresFactoryReset"; + static final String PROPERTY_REQUIRES_FACTORY_RESET = "requiresFactoryReset"; /** * Returns the thing type UID, that this firmware is associated with. diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/ProgressStep.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/ProgressStep.java index 0bfea244cb1..6bcd881f617 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/ProgressStep.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/firmware/ProgressStep.java @@ -43,5 +43,5 @@ public enum ProgressStep { UPDATING, /** The {@link FirmwareUpdateHandler} is going to reboot the device. */ - REBOOTING; + REBOOTING } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java new file mode 100644 index 00000000000..8839167d4f9 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandler.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.Command; + +/** + * The {@link ChannelHandler} defines the interface for converting received {@link ChannelHandlerContent} + * to {@link org.openhab.core.types.State}s for posting updates to {@link org.openhab.core.thing.Channel}s and + * {@link Command}s to values for sending + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface ChannelHandler { + + /** + * called to process a given content for this channel + * + * @param content raw content to process (null results in + * {@link org.openhab.core.types.UnDefType#UNDEF}) + */ + void process(@Nullable ChannelHandlerContent content); + + /** + * called to send a command to this channel + * + * @param command + */ + void send(Command command); +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java new file mode 100644 index 00000000000..ed5b4ad3e15 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelHandlerContent.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ChannelHandlerContent} defines the pre-processed response + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ChannelHandlerContent { + private final byte[] rawContent; + private final Charset encoding; + private final @Nullable String mediaType; + + public ChannelHandlerContent(byte[] rawContent, String encoding, @Nullable String mediaType) { + this.rawContent = rawContent; + this.mediaType = mediaType; + + Charset finalEncoding = StandardCharsets.UTF_8; + try { + finalEncoding = Charset.forName(encoding); + } catch (IllegalArgumentException e) { + } + this.encoding = finalEncoding; + } + + public byte[] getRawContent() { + return rawContent; + } + + public String getAsString() { + return new String(rawContent, encoding); + } + + public @Nullable String getMediaType() { + return mediaType; + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceFilter.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java similarity index 66% rename from bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceFilter.java rename to bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java index f7ae88e1f3c..63353866747 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceFilter.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelMode.java @@ -10,16 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.persistence; +package org.openhab.core.thing.binding.generic; import org.eclipse.jdt.annotation.NonNullByDefault; /** + * The {@link ChannelMode} enum defines control modes for channels * - * - * @author Markus Rathgeb - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class PersistenceFilter { - +public enum ChannelMode { + READONLY, + READWRITE, + WRITEONLY } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java new file mode 100644 index 00000000000..e4187cdae13 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelTransformation.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.transform.TransformationException; +import org.openhab.core.transform.TransformationHelper; +import org.openhab.core.transform.TransformationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ChannelTransformation} can be used to transform an input value using one or more transformations. + * Individual transformations can be chained with and must follow the pattern + * serviceName:function where serviceName refers to a {@link TransformationService} and + * function has to be a valid transformation function for this service + * + * @author Jan N. Klug - Initial contribution + */ +public class ChannelTransformation { + private final Logger logger = LoggerFactory.getLogger(ChannelTransformation.class); + private List transformationSteps; + + public ChannelTransformation(@Nullable String transformationString) { + if (transformationString != null) { + try { + transformationSteps = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isBlank()) + .map(TransformationStep::new).toList(); + return; + } catch (IllegalArgumentException e) { + logger.warn("Transformation ignored, failed to parse {}: {}", transformationString, e.getMessage()); + } + } + transformationSteps = List.of(); + } + + public Optional apply(String value) { + Optional valueOptional = Optional.of(value); + + // process all transformations + for (TransformationStep transformationStep : transformationSteps) { + valueOptional = valueOptional.flatMap(transformationStep::apply); + } + + logger.trace("Transformed '{}' to '{}' using '{}'", value, valueOptional, transformationSteps); + return valueOptional; + } + + private static class TransformationStep { + private final Logger logger = LoggerFactory.getLogger(TransformationStep.class); + private final String serviceName; + private final String function; + + public TransformationStep(String pattern) throws IllegalArgumentException { + int index = pattern.indexOf(":"); + if (index == -1) { + throw new IllegalArgumentException( + "The transformation pattern must consist of the type and the pattern separated by a colon"); + } + this.serviceName = pattern.substring(0, index).toUpperCase().trim(); + this.function = pattern.substring(index + 1).trim(); + } + + public Optional apply(String value) { + TransformationService service = TransformationHelper.getTransformationService(serviceName); + if (service != null) { + try { + return Optional.ofNullable(service.transform(function, value)); + } catch (TransformationException e) { + logger.debug("Applying {} failed: {}", this, e.getMessage()); + } + } else { + logger.warn("Failed to use {}, service not found", this); + } + return Optional.empty(); + } + + @Override + public String toString() { + return "TransformationStep{serviceName='" + serviceName + "', function='" + function + "'}"; + } + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java new file mode 100644 index 00000000000..72dd8a8a053 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/ChannelValueConverterConfig.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.NextPreviousType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.RewindFastforwardType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.binding.generic.converter.ColorChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * The {@link ChannelValueConverterConfig} is a base class for the channel configuration of things + * using the {@link ChannelHandler}s + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class ChannelValueConverterConfig { + private final Map stringStateMap = new HashMap<>(); + private final Map commandStringMap = new HashMap<>(); + + public ChannelMode mode = ChannelMode.READWRITE; + + // number + public @Nullable String unit; + + // switch, dimmer, color + public @Nullable String onValue; + public @Nullable String offValue; + + // dimmer, color + public BigDecimal step = BigDecimal.ONE; + public @Nullable String increaseValue; + public @Nullable String decreaseValue; + + // color + public ColorChannelHandler.ColorMode colorMode = ColorChannelHandler.ColorMode.RGB; + + // contact + public @Nullable String openValue; + public @Nullable String closedValue; + + // rollershutter + public @Nullable String upValue; + public @Nullable String downValue; + public @Nullable String stopValue; + public @Nullable String moveValue; + + // player + public @Nullable String playValue; + public @Nullable String pauseValue; + public @Nullable String nextValue; + public @Nullable String previousValue; + public @Nullable String rewindValue; + public @Nullable String fastforwardValue; + + private boolean initialized = false; + + /** + * maps a command to a user-defined string + * + * @param command the command to map + * @return a string or null if no mapping found + */ + public @Nullable String commandToFixedValue(Command command) { + if (!initialized) { + createMaps(); + } + + return commandStringMap.get(command); + } + + /** + * maps a user-defined string to a state + * + * @param string the string to map + * @return the state or null if no mapping found + */ + public @Nullable State fixedValueToState(String string) { + if (!initialized) { + createMaps(); + } + + return stringStateMap.get(string); + } + + private void createMaps() { + addToMaps(this.onValue, OnOffType.ON); + addToMaps(this.offValue, OnOffType.OFF); + addToMaps(this.openValue, OpenClosedType.OPEN); + addToMaps(this.closedValue, OpenClosedType.CLOSED); + addToMaps(this.upValue, UpDownType.UP); + addToMaps(this.downValue, UpDownType.DOWN); + + commandStringMap.put(IncreaseDecreaseType.INCREASE, increaseValue); + commandStringMap.put(IncreaseDecreaseType.DECREASE, decreaseValue); + commandStringMap.put(StopMoveType.STOP, stopValue); + commandStringMap.put(StopMoveType.MOVE, moveValue); + commandStringMap.put(PlayPauseType.PLAY, playValue); + commandStringMap.put(PlayPauseType.PAUSE, pauseValue); + commandStringMap.put(NextPreviousType.NEXT, nextValue); + commandStringMap.put(NextPreviousType.PREVIOUS, previousValue); + commandStringMap.put(RewindFastforwardType.REWIND, rewindValue); + commandStringMap.put(RewindFastforwardType.FASTFORWARD, fastforwardValue); + + initialized = true; + } + + private void addToMaps(@Nullable String value, State state) { + if (value != null) { + commandStringMap.put((Command) state, value); + stringStateMap.put(value, state); + } + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java new file mode 100644 index 00000000000..c2dff0ef26f --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ColorChannelHandler.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link ColorChannelHandler} implements {@link org.openhab.core.library.items.ColorItem} conversions + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class ColorChannelHandler extends AbstractTransformingChannelHandler { + private static final BigDecimal BYTE_FACTOR = BigDecimal.valueOf(2.55); + private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); + private static final Pattern TRIPLE_MATCHER = Pattern.compile("(?\\d+),(?\\d+),(?\\d+)"); + + private State state = UnDefType.UNDEF; + + public ColorChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + } + + @Override + protected @Nullable Command toCommand(String value) { + return null; + } + + @Override + public String toString(Command command) { + String string = channelConfig.commandToFixedValue(command); + if (string != null) { + return string; + } + + if (command instanceof HSBType newState) { + state = newState; + return hsbToString(newState); + } else if (command instanceof PercentType percentCommand && state instanceof HSBType colorState) { + HSBType newState = new HSBType(colorState.getBrightness(), colorState.getSaturation(), percentCommand); + state = newState; + return hsbToString(newState); + } + + throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported"); + } + + @Override + public Optional toState(String string) { + State newState = UnDefType.UNDEF; + if (string.equals(channelConfig.onValue)) { + if (state instanceof HSBType) { + newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), + PercentType.HUNDRED); + } else { + newState = HSBType.WHITE; + } + } else if (string.equals(channelConfig.offValue)) { + if (state instanceof HSBType) { + newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), PercentType.ZERO); + } else { + newState = HSBType.BLACK; + } + } else if (string.equals(channelConfig.increaseValue) && state instanceof HSBType) { + BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().add(channelConfig.step); + if (HUNDRED.compareTo(newBrightness) < 0) { + newBrightness = HUNDRED; + } + newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), + new PercentType(newBrightness)); + } else if (string.equals(channelConfig.decreaseValue) && state instanceof HSBType) { + BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().subtract(channelConfig.step); + if (BigDecimal.ZERO.compareTo(newBrightness) > 0) { + newBrightness = BigDecimal.ZERO; + } + newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), + new PercentType(newBrightness)); + } else { + Matcher matcher = TRIPLE_MATCHER.matcher(string); + if (matcher.matches()) { + switch (channelConfig.colorMode) { + case RGB -> { + int r = Integer.parseInt(matcher.group("r")); + int g = Integer.parseInt(matcher.group("g")); + int b = Integer.parseInt(matcher.group("b")); + newState = HSBType.fromRGB(r, g, b); + } + case HSB -> newState = new HSBType(string); + } + } + } + + state = newState; + return Optional.of(newState); + } + + private String hsbToString(HSBType state) { + switch (channelConfig.colorMode) { + case RGB: + PercentType[] rgb = state.toRGB(); + return String.format("%1$d,%2$d,%3$d", rgb[0].toBigDecimal().multiply(BYTE_FACTOR).intValue(), + rgb[1].toBigDecimal().multiply(BYTE_FACTOR).intValue(), + rgb[2].toBigDecimal().multiply(BYTE_FACTOR).intValue()); + case HSB: + return state.toString(); + } + throw new IllegalStateException("Invalid colorMode setting"); + } + + public enum ColorMode { + RGB, + HSB + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java new file mode 100644 index 00000000000..f9980de7831 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/DimmerChannelHandler.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link DimmerChannelHandler} implements {@link org.openhab.core.library.items.DimmerItem} conversions + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class DimmerChannelHandler extends AbstractTransformingChannelHandler { + private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); + + private State state = UnDefType.UNDEF; + + public DimmerChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + } + + @Override + protected @Nullable Command toCommand(String value) { + return null; + } + + @Override + public String toString(Command command) { + String string = channelConfig.commandToFixedValue(command); + if (string != null) { + return string; + } + + if (command instanceof PercentType) { + return ((PercentType) command).toString(); + } + + throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported"); + } + + @Override + public Optional toState(String string) { + State newState = UnDefType.UNDEF; + + if (string.equals(channelConfig.onValue)) { + newState = PercentType.HUNDRED; + } else if (string.equals(channelConfig.offValue)) { + newState = PercentType.ZERO; + } else if (string.equals(channelConfig.increaseValue) && state instanceof PercentType) { + BigDecimal newBrightness = ((PercentType) state).toBigDecimal().add(channelConfig.step); + if (HUNDRED.compareTo(newBrightness) < 0) { + newBrightness = HUNDRED; + } + newState = new PercentType(newBrightness); + } else if (string.equals(channelConfig.decreaseValue) && state instanceof PercentType) { + BigDecimal newBrightness = ((PercentType) state).toBigDecimal().subtract(channelConfig.step); + if (BigDecimal.ZERO.compareTo(newBrightness) > 0) { + newBrightness = BigDecimal.ZERO; + } + newState = new PercentType(newBrightness); + } else { + try { + BigDecimal value = new BigDecimal(string); + if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) { + value = PercentType.HUNDRED.toBigDecimal(); + } + if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) { + value = PercentType.ZERO.toBigDecimal(); + } + newState = new PercentType(value); + } catch (NumberFormatException e) { + // ignore + } + } + + state = newState; + return Optional.of(newState); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java new file mode 100644 index 00000000000..47ea726d3b6 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/FixedValueMappingChannelHandler.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link FixedValueMappingChannelHandler} implements mapping conversions for different item-types + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class FixedValueMappingChannelHandler extends AbstractTransformingChannelHandler { + + public FixedValueMappingChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + } + + @Override + protected @Nullable Command toCommand(String value) { + return null; + } + + @Override + public String toString(Command command) { + String value = channelConfig.commandToFixedValue(command); + if (value != null) { + return value; + } + + throw new IllegalArgumentException( + "Command type '" + command.toString() + "' not supported or mapping not defined."); + } + + @Override + public Optional toState(String string) { + State state = channelConfig.fixedValueToState(string); + + return Optional.of(Objects.requireNonNullElse(state, UnDefType.UNDEF)); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java new file mode 100644 index 00000000000..aa25055270c --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/GenericChannelHandler.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link GenericChannelHandler} implements simple conversions for different item types + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class GenericChannelHandler extends AbstractTransformingChannelHandler { + private final Function toState; + + public GenericChannelHandler(Function toState, Consumer updateState, + Consumer postCommand, @Nullable Consumer sendValue, + ChannelTransformation stateTransformations, ChannelTransformation commandTransformations, + ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + this.toState = toState; + } + + protected Optional toState(String value) { + try { + return Optional.of(toState.apply(value)); + } catch (IllegalArgumentException e) { + return Optional.of(UnDefType.UNDEF); + } + } + + @Override + protected @Nullable Command toCommand(String value) { + return null; + } + + protected String toString(Command command) { + return command.toString(); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java new file mode 100644 index 00000000000..ff23ea3289b --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/ImageChannelHandler.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.RawType; +import org.openhab.core.thing.binding.generic.ChannelHandler; +import org.openhab.core.thing.binding.generic.ChannelHandlerContent; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link ImageChannelHandler} implements {@link org.openhab.core.library.items.ImageItem} conversions + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class ImageChannelHandler implements ChannelHandler { + private final Consumer updateState; + + public ImageChannelHandler(Consumer updateState) { + this.updateState = updateState; + } + + @Override + public void process(@Nullable ChannelHandlerContent content) { + if (content == null) { + updateState.accept(UnDefType.UNDEF); + return; + } + String mediaType = content.getMediaType(); + updateState.accept( + new RawType(content.getRawContent(), mediaType != null ? mediaType : RawType.DEFAULT_MIME_TYPE)); + } + + @Override + public void send(Command command) { + throw new IllegalStateException("Read-only channel"); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java new file mode 100644 index 00000000000..91a48514d4c --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/NumberChannelHandler.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.util.Optional; +import java.util.function.Consumer; + +import javax.measure.format.MeasurementParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link NumberChannelHandler} implements {@link org.openhab.core.library.items.NumberItem} conversions + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class NumberChannelHandler extends AbstractTransformingChannelHandler { + + public NumberChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + } + + @Override + protected @Nullable Command toCommand(String value) { + return null; + } + + @Override + protected Optional toState(String value) { + String trimmedValue = value.trim(); + State newState = UnDefType.UNDEF; + if (!trimmedValue.isEmpty()) { + try { + if (channelConfig.unit != null) { + // we have a given unit - use that + newState = new QuantityType<>(trimmedValue + " " + channelConfig.unit); + } else { + try { + // try if we have a simple number + newState = new DecimalType(trimmedValue); + } catch (IllegalArgumentException e1) { + // not a plain number, maybe with unit? + newState = new QuantityType<>(trimmedValue); + } + } + } catch (IllegalArgumentException | MeasurementParseException e) { + // finally failed + } + } + return Optional.of(newState); + } + + @Override + protected String toString(Command command) { + return command.toString(); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java new file mode 100644 index 00000000000..0afe6f41c18 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/PlayerChannelHandler.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.NextPreviousType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.RewindFastforwardType; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * The {@link PlayerChannelHandler} implements {@link org.openhab.core.library.items.RollershutterItem} + * conversions + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class PlayerChannelHandler extends AbstractTransformingChannelHandler { + private @Nullable String lastCommand; // store last command to prevent duplicate commands + + public PlayerChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + } + + @Override + public String toString(Command command) { + String string = channelConfig.commandToFixedValue(command); + if (string != null) { + return string; + } + + throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported"); + } + + @Override + protected @Nullable Command toCommand(String string) { + if (string.equals(lastCommand)) { + // only send commands once + return null; + } + lastCommand = string; + + if (string.equals(channelConfig.playValue)) { + return PlayPauseType.PLAY; + } else if (string.equals(channelConfig.pauseValue)) { + return PlayPauseType.PAUSE; + } else if (string.equals(channelConfig.nextValue)) { + return NextPreviousType.NEXT; + } else if (string.equals(channelConfig.previousValue)) { + return NextPreviousType.PREVIOUS; + } else if (string.equals(channelConfig.rewindValue)) { + return RewindFastforwardType.REWIND; + } else if (string.equals(channelConfig.fastforwardValue)) { + return RewindFastforwardType.FASTFORWARD; + } + + return null; + } + + @Override + public Optional toState(String string) { + // no value - we ignore state updates + return Optional.empty(); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java new file mode 100644 index 00000000000..cf0ef1df0a6 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/generic/converter/RollershutterChannelHandler.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link RollershutterChannelHandler} implements {@link org.openhab.core.library.items.RollershutterItem} + * conversions + * + * @author Jan N. Klug - Initial contribution + */ + +@NonNullByDefault +public class RollershutterChannelHandler extends AbstractTransformingChannelHandler { + + public RollershutterChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + super(updateState, postCommand, sendValue, stateTransformations, commandTransformations, channelConfig); + } + + @Override + public String toString(Command command) { + String string = channelConfig.commandToFixedValue(command); + if (string != null) { + return string; + } + + if (command instanceof PercentType) { + final String downValue = channelConfig.downValue; + final String upValue = channelConfig.upValue; + if (command.equals(PercentType.HUNDRED) && downValue != null) { + return downValue; + } else if (command.equals(PercentType.ZERO) && upValue != null) { + return upValue; + } else { + return ((PercentType) command).toString(); + } + } + + throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported"); + } + + @Override + protected @Nullable Command toCommand(String string) { + if (string.equals(channelConfig.upValue)) { + return UpDownType.UP; + } else if (string.equals(channelConfig.downValue)) { + return UpDownType.DOWN; + } else if (string.equals(channelConfig.moveValue)) { + return StopMoveType.MOVE; + } else if (string.equals(channelConfig.stopValue)) { + return StopMoveType.STOP; + } + + return null; + } + + @Override + public Optional toState(String string) { + State newState = UnDefType.UNDEF; + try { + BigDecimal value = new BigDecimal(string); + if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) { + value = PercentType.HUNDRED.toBigDecimal(); + } + if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) { + value = PercentType.ZERO.toBigDecimal(); + } + newState = new PercentType(value); + } catch (NumberFormatException e) { + // ignore + } + + return Optional.of(newState); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareStatus.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareStatus.java index 3e240984075..b7bd713739b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareStatus.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareStatus.java @@ -45,6 +45,6 @@ public enum FirmwareStatus { UPDATE_AVAILABLE, /** There is a newer firmware of the thing available and the firmware update for the thing can be executed. */ - UPDATE_EXECUTABLE; + UPDATE_EXECUTABLE } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareUpdateResult.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareUpdateResult.java index b987fac35c6..58c22154115 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareUpdateResult.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/FirmwareUpdateResult.java @@ -29,5 +29,5 @@ public enum FirmwareUpdateResult { ERROR, /** Indicates that the firmware update was canceled. */ - CANCELED; + CANCELED } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/AutoUpdateManager.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/AutoUpdateManager.java index 2acd86de2a6..e7abeef6198 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/AutoUpdateManager.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/AutoUpdateManager.java @@ -141,9 +141,7 @@ public void receiveCommand(ItemCommandEvent commandEvent, Item item) { } final String itemName = commandEvent.getItemName(); final Command command = commandEvent.getItemCommand(); - if (command instanceof State) { - final State state = (State) command; - + if (command instanceof State state) { Recommendation autoUpdate = shouldAutoUpdate(item); // consider user-override via item meta-data diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/BridgeImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/BridgeImpl.java index e4d75cbc1cc..c24b3e8433e 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/BridgeImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/BridgeImpl.java @@ -82,8 +82,8 @@ public List getThings() { public @Nullable BridgeHandler getHandler() { BridgeHandler bridgeHandler = null; ThingHandler thingHandler = super.getHandler(); - if (thingHandler instanceof BridgeHandler) { - bridgeHandler = (BridgeHandler) thingHandler; + if (thingHandler instanceof BridgeHandler handler) { + bridgeHandler = handler; } else if (thingHandler != null) { logger.warn("Handler of bridge '{}' must implement BridgeHandler interface.", getUID()); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java index b41c1ca5014..8b2bf6ba0a6 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/CommunicationManager.java @@ -24,7 +24,7 @@ import java.util.function.Consumer; import java.util.function.Function; -import javax.measure.Quantity; +import javax.measure.Unit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -35,16 +35,22 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemFactory; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemStateConverter; import org.openhab.core.items.ItemUtil; import org.openhab.core.items.events.AbstractItemRegistryEvent; +import org.openhab.core.items.events.GroupStateUpdatedEvent; import org.openhab.core.items.events.ItemCommandEvent; -import org.openhab.core.items.events.ItemStateEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; +import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -67,13 +73,10 @@ import org.openhab.core.thing.profiles.ProfileTypeUID; import org.openhab.core.thing.profiles.StateProfile; import org.openhab.core.thing.profiles.TriggerProfile; -import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.Type; -import org.openhab.core.types.util.UnitUtils; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -114,8 +117,8 @@ public void onStateUpdateFromItem(State state) { // the timeout to use for any item event processing public static final long THINGHANDLER_EVENT_TIMEOUT = TimeUnit.SECONDS.toMillis(30); - private static final Set SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemCommandEvent.TYPE, - ChannelTriggeredEvent.TYPE); + private static final Set SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateUpdatedEvent.TYPE, ItemCommandEvent.TYPE, + GroupStateUpdatedEvent.TYPE, ChannelTriggeredEvent.TYPE); private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class); @@ -128,6 +131,7 @@ public void onStateUpdateFromItem(State state) { private final EventPublisher eventPublisher; private final SafeCaller safeCaller; private final ThingRegistry thingRegistry; + private final UnitProvider unitProvider; private final ExpiringCacheMap profileSafeCallCache = new ExpiringCacheMap<>(CACHE_EXPIRATION); @@ -140,7 +144,7 @@ public CommunicationManager(final @Reference AutoUpdateManager autoUpdateManager final @Reference ItemStateConverter itemStateConverter, // final @Reference EventPublisher eventPublisher, // final @Reference SafeCaller safeCaller, // - final @Reference ThingRegistry thingRegistry) { + final @Reference ThingRegistry thingRegistry, final @Reference UnitProvider unitProvider) { this.autoUpdateManager = autoUpdateManager; this.channelTypeRegistry = channelTypeRegistry; this.defaultProfileFactory = defaultProfileFactory; @@ -150,6 +154,7 @@ public CommunicationManager(final @Reference AutoUpdateManager autoUpdateManager this.eventPublisher = eventPublisher; this.safeCaller = safeCaller; this.thingRegistry = thingRegistry; + this.unitProvider = unitProvider; itemChannelLinkRegistry.addRegistryChangeListener(this); } @@ -179,20 +184,20 @@ public Set getSubscribedEventTypes() { @Override public void receive(Event event) { - if (event instanceof ItemStateEvent) { - receiveUpdate((ItemStateEvent) event); - } else if (event instanceof ItemCommandEvent) { - receiveCommand((ItemCommandEvent) event); - } else if (event instanceof ChannelTriggeredEvent) { - receiveTrigger((ChannelTriggeredEvent) event); - } else if (event instanceof AbstractItemRegistryEvent) { - String itemName = ((AbstractItemRegistryEvent) event).getItem().name; + if (event instanceof ItemStateUpdatedEvent updatedEvent) { + receiveUpdate(updatedEvent); + } else if (event instanceof ItemCommandEvent commandEvent) { + receiveCommand(commandEvent); + } else if (event instanceof ChannelTriggeredEvent triggeredEvent) { + receiveTrigger(triggeredEvent); + } else if (event instanceof AbstractItemRegistryEvent registryEvent) { + String itemName = registryEvent.getItem().name; profiles.entrySet().removeIf(entry -> { ItemChannelLink link = itemChannelLinkRegistry.get(entry.getKey()); return link != null && itemName.equals(link.getItemName()); }); - } else if (event instanceof AbstractThingRegistryEvent) { - ThingUID thingUid = new ThingUID(((AbstractThingRegistryEvent) event).getThing().UID); + } else if (event instanceof AbstractThingRegistryEvent registryEvent) { + ThingUID thingUid = new ThingUID(registryEvent.getThing().UID); profiles.entrySet().removeIf(entry -> { ItemChannelLink link = itemChannelLinkRegistry.get(entry.getKey()); return link != null && thingUid.equals(link.getLinkedUID().getThingUID()); @@ -200,10 +205,6 @@ public void receive(Event event) { } } - private @Nullable Thing getThing(ThingUID thingUID) { - return thingRegistry.get(thingUID); - } - private Profile getProfile(ItemChannelLink link, Item item, @Nullable Thing thing) { synchronized (profiles) { Profile profile = profiles.get(link.getUID()); @@ -225,8 +226,8 @@ private Profile getProfile(ItemChannelLink link, Item item, @Nullable Thing thin } private ProfileCallback createCallback(ItemChannelLink link) { - return new ProfileCallbackImpl(eventPublisher, safeCaller, itemStateConverter, link, - thingUID -> getThing(thingUID), itemName -> getItem(itemName)); + return new ProfileCallbackImpl(eventPublisher, safeCaller, itemStateConverter, link, thingRegistry::get, + this::getItem); } private @Nullable ProfileTypeUID determineProfileTypeUID(ItemChannelLink link, Item item, @Nullable Thing thing) { @@ -269,25 +270,21 @@ private ProfileCallback createCallback(ItemChannelLink link) { String profileName = (String) link.getConfiguration() .get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); if (profileName != null && !profileName.trim().isEmpty()) { - profileName = normalizeProfileName(profileName); + if (!profileName.contains(AbstractUID.SEPARATOR)) { + profileName = ProfileTypeUID.SYSTEM_SCOPE + AbstractUID.SEPARATOR + profileName; + } return new ProfileTypeUID(profileName); } return null; } - private String normalizeProfileName(String profileName) { - if (!profileName.contains(AbstractUID.SEPARATOR)) { - return ProfileTypeUID.SYSTEM_SCOPE + AbstractUID.SEPARATOR + profileName; - } - return profileName; - } - private @Nullable Profile getProfileFromFactories(ProfileTypeUID profileTypeUID, ItemChannelLink link, ProfileCallback callback) { ProfileContext context = null; Item item = getItem(link.getItemName()); - Thing thing = getThing(link.getLinkedUID().getThingUID()); + ThingUID thingUID = link.getLinkedUID().getThingUID(); + Thing thing = thingRegistry.get(thingUID); if (item != null && thing != null) { Channel channel = thing.getChannel(link.getLinkedUID()); if (channel != null) { @@ -321,7 +318,7 @@ private String normalizeProfileName(String profileName) { } } } - logger.warn("No ProfileFactory found which supports profile '{}'", profileTypeUID); + logger.warn("No ProfileFactory found which supports profile '{}' for link '{}'", profileTypeUID, link); return null; } @@ -338,50 +335,51 @@ private void receiveCommand(ItemCommandEvent commandEvent) { autoUpdateManager.receiveCommand(commandEvent, item); } - handleEvent(itemName, command, commandEvent.getSource(), s -> acceptedCommandTypeMap.get(s), - (profile, thing, convertedCommand) -> { - if (profile instanceof StateProfile) { - int key = Objects.hash("COMMAND", profile, thing); - Profile p = profileSafeCallCache.putIfAbsentAndGet(key, () -> { - return safeCaller.create(((StateProfile) profile), StateProfile.class) // - .withAsync() // - .withIdentifier(thing) // - .withTimeout(THINGHANDLER_EVENT_TIMEOUT) // - .build(); - }); - if (p instanceof StateProfile) { - ((StateProfile) p).onCommandFromItem(convertedCommand); - } else { - throw new IllegalStateException("ExpiringCache didn't provide a StateProfile instance!"); - } - } - }); + handleEvent(itemName, command, commandEvent.getSource(), acceptedCommandTypeMap::get, + this::applyProfileForCommand); } - private void receiveUpdate(ItemStateEvent updateEvent) { + private void receiveUpdate(ItemStateUpdatedEvent updateEvent) { final String itemName = updateEvent.getItemName(); final State newState = updateEvent.getItemState(); - handleEvent(itemName, newState, updateEvent.getSource(), s -> acceptedStateTypeMap.get(s), - (profile, thing, convertedState) -> { - int key = Objects.hash("UPDATE", profile, thing); - Profile p = profileSafeCallCache.putIfAbsentAndGet(key, () -> { - return safeCaller.create(profile, Profile.class) // - .withAsync() // - .withIdentifier(thing) // - .withTimeout(THINGHANDLER_EVENT_TIMEOUT) // - .build(); - }); - if (p != null) { - p.onStateUpdateFromItem(convertedState); - } else { - throw new IllegalStateException("ExpiringCache didn't provide a Profile instance!"); - } - }); + handleEvent(itemName, newState, updateEvent.getSource(), acceptedStateTypeMap::get, + this::applyProfileForUpdate); } @FunctionalInterface - private static interface ProfileAction { - void handle(Profile profile, Thing thing, T type); + private interface ProfileAction { + void applyProfile(Profile profile, Thing thing, T type); + } + + private void applyProfileForUpdate(Profile profile, Thing thing, State convertedState) { + int key = Objects.hash("UPDATE", profile, thing); + Profile p = profileSafeCallCache.putIfAbsentAndGet(key, () -> safeCaller.create(profile, Profile.class) // + .withAsync() // + .withIdentifier(thing) // + .withTimeout(THINGHANDLER_EVENT_TIMEOUT) // + .build()); + if (p != null) { + p.onStateUpdateFromItem(convertedState); + } else { + throw new IllegalStateException("ExpiringCache didn't provide a Profile instance!"); + } + } + + private void applyProfileForCommand(Profile profile, Thing thing, Command convertedCommand) { + if (profile instanceof StateProfile stateProfile) { + int key = Objects.hash("COMMAND", profile, thing); + Profile p = profileSafeCallCache.putIfAbsentAndGet(key, + () -> safeCaller.create(stateProfile, StateProfile.class) // + .withAsync() // + .withIdentifier(thing) // + .withTimeout(THINGHANDLER_EVENT_TIMEOUT) // + .build()); + if (p instanceof StateProfile profileP) { + profileP.onCommandFromItem(convertedCommand); + } else { + throw new IllegalStateException("ExpiringCache didn't provide a StateProfile instance!"); + } + } } private void handleEvent(String itemName, T type, @Nullable String source, @@ -398,7 +396,8 @@ private void handleEvent(String itemName, T type, @Nullable Str return !link.getLinkedUID().toString().equals(source); }).forEach(link -> { ChannelUID channelUID = link.getLinkedUID(); - Thing thing = getThing(channelUID.getThingUID()); + ThingUID thingUID = channelUID.getThingUID(); + Thing thing = thingRegistry.get(thingUID); if (thing != null) { Channel channel = thing.getChannel(channelUID.getId()); if (channel != null) { @@ -407,7 +406,7 @@ private void handleEvent(String itemName, T type, @Nullable Str if (convertedType != null) { if (thing.getHandler() != null) { Profile profile = getProfile(link, item, thing); - action.handle(profile, thing, convertedType); + action.applyProfile(profile, thing, convertedType); } } else { logger.debug( @@ -428,120 +427,49 @@ private void handleEvent(String itemName, T type, @Nullable Str @SuppressWarnings("unchecked") private @Nullable T toAcceptedType(T originalType, Channel channel, Function<@Nullable String, @Nullable List>> acceptedTypesFunction, Item item) { - String acceptedItemType = channel.getAcceptedItemType(); - - // DecimalType command sent to a NumberItem with dimension defined: - if (originalType instanceof DecimalType && hasDimension(item, acceptedItemType)) { - @Nullable - QuantityType quantityType = convertToQuantityType((DecimalType) originalType, item, acceptedItemType); - if (quantityType != null) { - return (T) quantityType; - } - } + String channelAcceptedItemType = channel.getAcceptedItemType(); - // The command is sent to an item w/o dimension defined and the channel is legacy (created from a ThingType - // definition before UoM was introduced to the binding). The dimension information might now be defined on the - // current ThingType. The binding might expect us to provide a QuantityType so try to convert to the dimension - // the ChannelType provides. - // This can be removed once a suitable solution for https://github.com/eclipse/smarthome/issues/2555 (Thing - // migration) is found. - if (originalType instanceof DecimalType && !hasDimension(item, acceptedItemType) - && channelTypeDefinesDimension(channel.getChannelTypeUID())) { - ChannelType channelType = channelTypeRegistry.getChannelType(channel.getChannelTypeUID()); - - String acceptedItemTypeFromChannelType = channelType != null ? channelType.getItemType() : null; - @Nullable - QuantityType quantityType = convertToQuantityType((DecimalType) originalType, item, - acceptedItemTypeFromChannelType); - if (quantityType != null) { - return (T) quantityType; - } - } - - if (acceptedItemType == null) { + if (channelAcceptedItemType == null) { return originalType; } - List> acceptedTypes = acceptedTypesFunction.apply(acceptedItemType); - if (acceptedTypes == null) { - return originalType; + // handle Number-Channels for backward compatibility + if (CoreItemFactory.NUMBER.equals(channelAcceptedItemType) + && originalType instanceof QuantityType quantityType) { + // strip unit from QuantityType for channels that accept plain number + return (T) new DecimalType(quantityType.toBigDecimal()); } - if (acceptedTypes.contains(originalType.getClass())) { - return originalType; - } else { - // Look for class hierarchy and convert appropriately - for (Class typeClass : acceptedTypes) { - if (!typeClass.isEnum() && typeClass.isAssignableFrom(originalType.getClass()) // - && State.class.isAssignableFrom(typeClass) && originalType instanceof State) { - T ret = (T) ((State) originalType).as((Class) typeClass); - if (logger.isDebugEnabled()) { - logger.debug("Converted '{}' ({}) to accepted type '{}' ({}) for channel '{}' ", originalType, - originalType.getClass().getSimpleName(), ret, ret.getClass().getName(), - channel.getUID()); - } - return ret; - } - } - } - logger.debug("Received not accepted type '{}' for channel '{}'", originalType.getClass().getSimpleName(), - channel.getUID()); - return null; - } + String itemDimension = ItemUtil.getItemTypeExtension(item.getType()); + String channelDimension = ItemUtil.getItemTypeExtension(channelAcceptedItemType); - private boolean channelTypeDefinesDimension(@Nullable ChannelTypeUID channelTypeUID) { - if (channelTypeUID == null) { - return false; + if (originalType instanceof DecimalType decimalType && channelDimension != null + && channelDimension.equals(itemDimension)) { + // Add unit from item to DecimalType when dimensions are equal + Unit unit = Objects.requireNonNull(((NumberItem) item).getUnit()); + return (T) new QuantityType<>(decimalType.toBigDecimal(), unit); } - ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); - return channelType != null && getDimension(channelType.getItemType()) != null; - } - - private boolean hasDimension(Item item, @Nullable String acceptedItemType) { - return (item instanceof NumberItem && ((NumberItem) item).getDimension() != null) - || getDimension(acceptedItemType) != null; - } - - private @Nullable QuantityType convertToQuantityType(DecimalType originalType, Item item, - @Nullable String acceptedItemType) { - if (!(item instanceof NumberItem)) { - // PercentType command sent via DimmerItem to a channel that's dimensioned - // (such as Number:Dimensionless, expecting a %). - // We can't know the proper units to add, so just pass it through and assume - // The binding can deal with it. - return null; + // handle HSBType/PercentType + if (CoreItemFactory.DIMMER.equals(channelAcceptedItemType) && originalType instanceof HSBType hsb) { + return (T) (hsb.as(PercentType.class)); } - NumberItem numberItem = (NumberItem) item; - - // DecimalType command sent via a NumberItem with dimension: - Class> dimension = numberItem.getDimension(); - - if (dimension == null) { - // DecimalType command sent via a plain NumberItem w/o dimension. - // We try to guess the correct unit from the channel-type's expected item dimension - // or from the item's state description. - dimension = getDimension(acceptedItemType); - } - - if (dimension != null) { - return numberItem.toQuantityType(originalType, dimension); - } - - return null; - } - - private @Nullable Class> getDimension(@Nullable String acceptedItemType) { - if (acceptedItemType == null || acceptedItemType.isEmpty()) { - return null; - } - String itemTypeExtension = ItemUtil.getItemTypeExtension(acceptedItemType); - if (itemTypeExtension == null) { + // check for other cases if the type is acceptable + List> acceptedTypes = acceptedTypesFunction.apply(channelAcceptedItemType); + if (acceptedTypes == null || acceptedTypes.contains(originalType.getClass())) { + return originalType; + } else if (acceptedTypes.contains(PercentType.class) && originalType instanceof State state + && PercentType.class.isAssignableFrom(originalType.getClass())) { + return (@Nullable T) state.as(PercentType.class); + } else if (acceptedTypes.contains(OnOffType.class) && originalType instanceof State state + && PercentType.class.isAssignableFrom(originalType.getClass())) { + return (@Nullable T) state.as(OnOffType.class); + } else { + logger.debug("Received not accepted type '{}' for channel '{}'", originalType.getClass().getSimpleName(), + channel.getUID()); return null; } - - return UnitUtils.parseDimension(itemTypeExtension); } private @Nullable Item getItem(final String itemName) { @@ -551,36 +479,39 @@ private boolean hasDimension(Item item, @Nullable String acceptedItemType) { private void receiveTrigger(ChannelTriggeredEvent channelTriggeredEvent) { final ChannelUID channelUID = channelTriggeredEvent.getChannel(); final String event = channelTriggeredEvent.getEvent(); - final Thing thing = getThing(channelUID.getThingUID()); + ThingUID thingUID = channelUID.getThingUID(); + final Thing thing = thingRegistry.get(thingUID); handleCallFromHandler(channelUID, thing, profile -> { - if (profile instanceof TriggerProfile) { - ((TriggerProfile) profile).onTriggerFromHandler(event); + if (profile instanceof TriggerProfile triggerProfile) { + triggerProfile.onTriggerFromHandler(event); } }); } public void stateUpdated(ChannelUID channelUID, State state) { - final Thing thing = getThing(channelUID.getThingUID()); + ThingUID thingUID = channelUID.getThingUID(); + final Thing thing = thingRegistry.get(thingUID); handleCallFromHandler(channelUID, thing, profile -> { - if (profile instanceof StateProfile) { - ((StateProfile) profile).onStateUpdateFromHandler(state); + if (profile instanceof StateProfile stateProfile) { + stateProfile.onStateUpdateFromHandler(state); } }); } public void postCommand(ChannelUID channelUID, Command command) { - final Thing thing = getThing(channelUID.getThingUID()); + ThingUID thingUID = channelUID.getThingUID(); + final Thing thing = thingRegistry.get(thingUID); handleCallFromHandler(channelUID, thing, profile -> { - if (profile instanceof StateProfile) { - ((StateProfile) profile).onCommandFromHandler(command); + if (profile instanceof StateProfile stateProfile) { + stateProfile.onCommandFromHandler(command); } }); } - void handleCallFromHandler(ChannelUID channelUID, @Nullable Thing thing, Consumer action) { + private void handleCallFromHandler(ChannelUID channelUID, @Nullable Thing thing, Consumer action) { itemChannelLinkRegistry.getLinks(channelUID).forEach(link -> { final Item item = getItem(link.getItemName()); if (item != null) { @@ -625,9 +556,7 @@ protected void addProfileFactory(ProfileFactory profileFactory) { protected void removeProfileFactory(ProfileFactory profileFactory) { Set links = profileFactories.remove(profileFactory); synchronized (profiles) { - links.forEach(link -> { - profiles.remove(link); - }); + links.forEach(profiles::remove); } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingConfigDescriptionAliasProvider.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingConfigDescriptionAliasProvider.java index 7557703145f..3b140c0bec7 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingConfigDescriptionAliasProvider.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingConfigDescriptionAliasProvider.java @@ -88,10 +88,8 @@ public ThingConfigDescriptionAliasProvider(final @Reference ThingRegistry thingR if (thingType == null) { return null; } - // Get the config description URI for this thing type - URI configURI = thingType.getConfigDescriptionURI(); - return configURI; + return thingType.getConfigDescriptionURI(); } private @Nullable URI getChannelConfigDescriptionURI(URI uri) { @@ -117,9 +115,7 @@ public ThingConfigDescriptionAliasProvider(final @Reference ThingRegistry thingR if (channelType == null) { return null; } - // Get the config description URI for this channel type - URI configURI = channelType.getConfigDescriptionURI(); - return configURI; + return channelType.getConfigDescriptionURI(); } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java index 41a6f80fe14..3322f845f0b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java @@ -144,7 +144,7 @@ private static Channel createChannel(ChannelDefinition channelDefinition, ThingU return channelBuilder.withProperties(channelDefinition.getProperties()).build(); } - static ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelType channelType, + public static ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelType channelType, ConfigDescriptionRegistry configDescriptionRegistry) { final ChannelBuilder channelBuilder = ChannelBuilder.create(channelUID, channelType.getItemType()) // .withType(channelType.getUID()) // @@ -168,13 +168,11 @@ static ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelType ch return channelBuilder; } - static ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelDefinition channelDefinition, + public static ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelDefinition channelDefinition, ConfigDescriptionRegistry configDescriptionRegistry) { - ChannelType channelType = withChannelTypeRegistry(channelTypeRegistry -> { - return (channelTypeRegistry != null) - ? channelTypeRegistry.getChannelType(channelDefinition.getChannelTypeUID()) - : null; - }); + ChannelType channelType = withChannelTypeRegistry(channelTypeRegistry -> (channelTypeRegistry != null) + ? channelTypeRegistry.getChannelType(channelDefinition.getChannelTypeUID()) + : null); if (channelType == null) { logger.warn("Could not create channel '{}', because channel type '{}' could not be found.", channelDefinition.getId(), channelDefinition.getChannelTypeUID()); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java index 7bb229c00af..380e3cd998c 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java @@ -186,8 +186,10 @@ public ThingManagerImpl( // final @Reference StorageService storageService, // final @Reference ThingRegistry thingRegistry, final @Reference ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService, - final @Reference ThingTypeRegistry thingTypeRegistry, final @Reference BundleResolver bundleResolver, - final @Reference TranslationProvider translationProvider, final BundleContext bundleContext) { + final @Reference ThingTypeRegistry thingTypeRegistry, + final @Reference ThingUpdateInstructionReader thingUpdateInstructionReader, + final @Reference BundleResolver bundleResolver, final @Reference TranslationProvider translationProvider, + final BundleContext bundleContext) { this.channelGroupTypeRegistry = channelGroupTypeRegistry; this.channelTypeRegistry = channelTypeRegistry; this.communicationManager = communicationManager; @@ -198,7 +200,7 @@ public ThingManagerImpl( // this.readyService = readyService; this.safeCaller = safeCaller; this.thingRegistry = (ThingRegistryImpl) thingRegistry; - this.thingUpdateInstructionReader = new ThingUpdateInstructionReader(bundleResolver); + this.thingUpdateInstructionReader = thingUpdateInstructionReader; this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService; this.thingTypeRegistry = thingTypeRegistry; this.translationProvider = translationProvider; @@ -404,13 +406,13 @@ public void thingUpdated(Thing oldThing, Thing newThing, ThingTrackerEvent thing try { normalizeThingConfiguration(oldThing); } catch (ConfigValidationException e) { - logger.warn("Failed to normalize configuration for thing '{}': {}", oldThing.getUID(), + logger.debug("Failed to normalize configuration for old thing during update '{}': {}", oldThing.getUID(), e.getValidationMessages(null)); } try { normalizeThingConfiguration(newThing); } catch (ConfigValidationException e) { - logger.warn("Failed to normalize configuration´for thing '{}': {}", newThing.getUID(), + logger.warn("Failed to normalize configuration for new thing during update '{}': {}", newThing.getUID(), e.getValidationMessages(null)); } if (thingUpdatedLock.contains(thingUID)) { @@ -661,23 +663,23 @@ private void normalizeThingConfiguration(Thing thing) throws ConfigValidationExc thing.getUID()); return; } - normalizeConfiguration(thingType, thing.getUID(), thing.getConfiguration()); + normalizeConfiguration(thingType, thing.getThingTypeUID(), thing.getUID(), thing.getConfiguration()); for (Channel channel : thing.getChannels()) { ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); if (channelTypeUID != null) { ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); - normalizeConfiguration(channelType, channel.getUID(), channel.getConfiguration()); + normalizeConfiguration(channelType, channelTypeUID, channel.getUID(), channel.getConfiguration()); } } } - private void normalizeConfiguration(@Nullable AbstractDescriptionType prototype, UID targetUID, + private void normalizeConfiguration(@Nullable AbstractDescriptionType prototype, UID prototypeUID, UID targetUID, Configuration configuration) throws ConfigValidationException { if (prototype == null) { ConfigValidationMessage message = new ConfigValidationMessage("thing/channel", - "Type description for '{0}' not found although we checked the presence before.", - "type_description_missing", targetUID); + "Type description {0} for {1} not found, although we checked the presence before.", + "type_description_missing", prototypeUID.toString(), targetUID.toString()); throw new ConfigValidationException(bundleContext.getBundle(), translationProvider, List.of(message)); } @@ -691,8 +693,8 @@ private void normalizeConfiguration(@Nullable AbstractDescriptionType prototype, ConfigDescription configDescription = configDescriptionRegistry.getConfigDescription(configDescriptionURI); if (configDescription == null) { ConfigValidationMessage message = new ConfigValidationMessage("thing/channel", - "Config description for '{0}' not found also we checked the presence before.", - "config_description_missing", targetUID); + "Config description {0} for {1} not found, although we checked the presence before.", + "config_description_missing", configDescriptionURI.toString(), targetUID.toString()); throw new ConfigValidationException(bundleContext.getBundle(), translationProvider, List.of(message)); } @@ -725,7 +727,7 @@ private boolean isHandlerRegistered(Thing thing) { return null; } Thing bridge = thingRegistry.get(bridgeUID); - return bridge instanceof Bridge ? (Bridge) bridge : null; + return bridge instanceof Bridge b ? b : null; } private void unregisterHandler(Thing thing, ThingHandlerFactory thingHandlerFactory) { @@ -1262,7 +1264,7 @@ public synchronized boolean isReady() { timesChecked++; if (timesChecked > MAX_CHECK_PREREQUISITE_TIME / CHECK_INTERVAL) { logger.warn( - "Channel types or config descriptions for thing '{}' are missing in the respective registry for more than {}s. This should be fixed in the binding.", + "Channel types or config descriptions for thing '{}' are missing in the respective registry for more than {}s. In case it does not happen immediately after an upgrade, it should be fixed in the binding.", thingUID, MAX_CHECK_PREREQUISITE_TIME); channelTypeUIDs.clear(); configDescriptionUris.clear(); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingRegistryImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingRegistryImpl.java index 120cd75af17..7416b310a4f 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingRegistryImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingRegistryImpl.java @@ -149,8 +149,8 @@ protected void notifyListenersAboutUpdatedElement(Thing oldElement, Thing elemen @Override protected void onAddElement(Thing thing) throws IllegalArgumentException { addThingToBridge(thing); - if (thing instanceof Bridge) { - addThingsToBridge((Bridge) thing); + if (thing instanceof Bridge bridge) { + addThingsToBridge(bridge); } } @@ -161,8 +161,8 @@ protected void onRemoveElement(Thing thing) { ThingUID bridgeUID = thing.getBridgeUID(); if (bridgeUID != null) { Thing bridge = this.get(bridgeUID); - if (bridge instanceof BridgeImpl) { - ((BridgeImpl) bridge).removeThing(thing); + if (bridge instanceof BridgeImpl impl) { + impl.removeThing(thing); } } } @@ -187,8 +187,8 @@ private void addThingsToBridge(Bridge bridge) { forEach(thing -> { ThingUID bridgeUID = thing.getBridgeUID(); if (bridgeUID != null && bridgeUID.equals(bridge.getUID())) { - if (bridge instanceof BridgeImpl && !bridge.getThings().contains(thing)) { - ((BridgeImpl) bridge).addThing(thing); + if (bridge instanceof BridgeImpl impl && !bridge.getThings().contains(thing)) { + impl.addThing(thing); } } }); @@ -198,8 +198,8 @@ private void addThingToBridge(Thing thing) { ThingUID bridgeUID = thing.getBridgeUID(); if (bridgeUID != null) { Thing bridge = this.get(bridgeUID); - if (bridge instanceof BridgeImpl && !((Bridge) bridge).getThings().contains(thing)) { - ((BridgeImpl) bridge).addThing(thing); + if (bridge instanceof BridgeImpl impl && !impl.getThings().contains(thing)) { + impl.addThing(thing); } } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingTracker.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingTracker.java index 2ecdfddeb4a..bc4815e88c9 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingTracker.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingTracker.java @@ -29,7 +29,7 @@ @NonNullByDefault public interface ThingTracker { - public enum ThingTrackerEvent { + enum ThingTrackerEvent { THING_ADDED, THING_REMOVING, THING_REMOVED, diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java new file mode 100644 index 00000000000..b4e332f0c9a --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingChannelHandler.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.binding.generic.converter; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.binding.generic.ChannelHandler; +import org.openhab.core.thing.binding.generic.ChannelHandlerContent; +import org.openhab.core.thing.binding.generic.ChannelMode; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link AbstractTransformingChannelHandler} is a base class for an item converter with transformations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractTransformingChannelHandler implements ChannelHandler { + private final Consumer updateState; + private final Consumer postCommand; + private final @Nullable Consumer sendValue; + private final ChannelTransformation stateTransformations; + private final ChannelTransformation commandTransformations; + + protected final ChannelValueConverterConfig channelConfig; + + public AbstractTransformingChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig) { + this.updateState = updateState; + this.postCommand = postCommand; + this.sendValue = sendValue; + this.stateTransformations = stateTransformations; + this.commandTransformations = commandTransformations; + this.channelConfig = channelConfig; + } + + @Override + public void process(@Nullable ChannelHandlerContent content) { + if (content == null) { + updateState.accept(UnDefType.UNDEF); + return; + } + if (channelConfig.mode != ChannelMode.WRITEONLY) { + stateTransformations.apply(content.getAsString()).ifPresent(transformedValue -> { + Command command = toCommand(transformedValue); + if (command != null) { + postCommand.accept(command); + } else { + toState(transformedValue).ifPresent(updateState); + } + }); + } else { + throw new IllegalStateException("Write-only channel"); + } + } + + @Override + public void send(Command command) { + Consumer sendHttpValue = this.sendValue; + if (sendHttpValue != null && channelConfig.mode != ChannelMode.READONLY) { + commandTransformations.apply(toString(command)).ifPresent(sendHttpValue); + } else { + throw new IllegalStateException("Read-only channel"); + } + } + + /** + * check if this converter received a value that needs to be sent as command + * + * @param value the value + * @return the command or null + */ + protected abstract @Nullable Command toCommand(String value); + + /** + * convert the received value to a state + * + * @param value the value + * @return the state that represents the value of UNDEF if conversion failed + */ + protected abstract Optional toState(String value); + + /** + * convert a command to a string + * + * @param command the command + * @return the string representation of the command + */ + protected abstract String toString(Command command); + + @FunctionalInterface + public interface Factory { + ChannelHandler create(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendHttpValue, ChannelTransformation stateTransformations, + ChannelTransformation commandTransformations, ChannelValueConverterConfig channelConfig); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java index 3e91ea4b99d..b102afba530 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/FirmwareUpdateConsoleCommandExtension.java @@ -132,7 +132,7 @@ private void listFirmwareStatus(Console console, String[] args) { FirmwareStatusInfo firmwareStatusInfo = firmwareUpdateService.getFirmwareStatusInfo(thingUID); if (firmwareStatusInfo != null) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); sb.append(String.format("Firmware status for thing with UID %s is %s.", thingUID, firmwareStatusInfo.getFirmwareStatus())); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/LinkConsoleCommandExtension.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/LinkConsoleCommandExtension.java index 23cdab935cf..21b2e3d4971 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/LinkConsoleCommandExtension.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/console/LinkConsoleCommandExtension.java @@ -77,7 +77,7 @@ public void execute(String[] args, Console console) { list(console, itemChannelLinkRegistry.getAll()); return; case SUBCMD_ORPHAN: - if (args.length == 2 && (args[1].equals("list") || args[1].equals("purge"))) { + if (args.length == 2 && ("list".equals(args[1]) || "purge".equals(args[1]))) { orphan(console, args[1], itemChannelLinkRegistry.getAll(), thingRegistry.getAll(), itemRegistry.getAll()); } else { diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceImpl.java index 6c6d40954d4..a52ee0b030c 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceImpl.java @@ -265,8 +265,7 @@ public Set getSubscribedEventTypes() { @Override public void receive(Event event) { - if (event instanceof ThingStatusInfoChangedEvent) { - ThingStatusInfoChangedEvent changedEvent = (ThingStatusInfoChangedEvent) event; + if (event instanceof ThingStatusInfoChangedEvent changedEvent) { if (changedEvent.getStatusInfo().getStatus() != ThingStatus.ONLINE) { return; } @@ -327,11 +326,10 @@ private synchronized void processFirmwareStatusInfo(FirmwareUpdateHandler firmwa eventPublisher.post(FirmwareEventFactory.createFirmwareStatusInfoEvent(newFirmwareStatusInfo)); if (newFirmwareStatusInfo.getFirmwareStatus() == FirmwareStatus.UPDATE_AVAILABLE - && firmwareUpdateHandler instanceof FirmwareUpdateBackgroundTransferHandler + && firmwareUpdateHandler instanceof FirmwareUpdateBackgroundTransferHandler handler && !firmwareUpdateHandler.isUpdateExecutable()) { if (latestFirmware != null) { - transferLatestFirmware((FirmwareUpdateBackgroundTransferHandler) firmwareUpdateHandler, - latestFirmware, previousFirmwareStatusInfo); + transferLatestFirmware(handler, latestFirmware, previousFirmwareStatusInfo); } } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/ProgressCallbackImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/ProgressCallbackImpl.java index 5ca8f5b3de1..28db0c576da 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/ProgressCallbackImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/firmware/ProgressCallbackImpl.java @@ -190,8 +190,7 @@ void failedInternal(String errorMessageKey) { private String getMessage(Class clazz, String errorMessageKey, Object... arguments) { Bundle bundle = bundleResolver.resolveBundle(clazz); - String errorMessage = i18nProvider.getText(bundle, errorMessageKey, null, locale, arguments); - return errorMessage; + return i18nProvider.getText(bundle, errorMessageKey, null, locale, arguments); } private void postResultInfoEvent(FirmwareUpdateResult result, String message) { diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/link/ItemChannelLinkConfigDescriptionProvider.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/link/ItemChannelLinkConfigDescriptionProvider.java index bc0f4fd4ce0..4261f215504 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/link/ItemChannelLinkConfigDescriptionProvider.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/link/ItemChannelLinkConfigDescriptionProvider.java @@ -111,8 +111,8 @@ private List getOptions(ItemChannelLink link, Item item, Channe case STATE: return profileType instanceof StateProfileType && isSupportedItemType(profileType, item); case TRIGGER: - return profileType instanceof TriggerProfileType && isSupportedItemType(profileType, item) - && isSupportedChannelType((TriggerProfileType) profileType, channel); + return profileType instanceof TriggerProfileType tpt && isSupportedItemType(profileType, item) + && isSupportedChannelType(tpt, channel); default: throw new IllegalArgumentException("Unknown channel kind: " + channel.getKind()); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java index 83852688947..1213fad1c44 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfile.java @@ -84,14 +84,13 @@ public SystemHysteresisStateProfile(ProfileCallback callback, ProfileContext con private @Nullable QuantityType getParam(ProfileContext context, String param) { final Object paramValue = context.getConfiguration().get(param); logger.debug("Configuring profile with {} parameter '{}'", param, paramValue); - if (paramValue instanceof String) { + if (paramValue instanceof String string) { try { - return new QuantityType<>((String) paramValue); + return new QuantityType<>(string); } catch (IllegalArgumentException e) { logger.error("Cannot convert value '{}' of parameter {} into a valid QuantityType.", paramValue, param); } - } else if (paramValue instanceof BigDecimal) { - final BigDecimal value = (BigDecimal) paramValue; + } else if (paramValue instanceof BigDecimal value) { return QuantityType.valueOf(value.doubleValue(), AbstractUnit.ONE); } return null; @@ -111,8 +110,8 @@ public void onStateUpdateFromItem(State state) { public void onCommandFromHandler(Command command) { final Type mappedCommand = mapValue(command); logger.trace("Mapped command from '{}' to command '{}'.", command, mappedCommand); - if (mappedCommand instanceof Command) { - callback.sendCommand((Command) mappedCommand); + if (mappedCommand instanceof Command command1) { + callback.sendCommand(command1); } } @@ -125,14 +124,13 @@ public void onCommandFromItem(Command command) { public void onStateUpdateFromHandler(State state) { final Type mappedState = mapValue(state); logger.trace("Mapped state from '{}' to state '{}'.", state, mappedState); - if (mappedState instanceof State) { - callback.sendUpdate((State) mappedState); + if (mappedState instanceof State state1) { + callback.sendUpdate(state1); } } private Type mapValue(Type value) { - if (value instanceof QuantityType) { - final QuantityType qtState = (QuantityType) value; + if (value instanceof QuantityType qtState) { final QuantityType finalLower; final QuantityType finalUpper; if (Units.ONE.equals(lower.getUnit()) && Units.ONE.equals(upper.getUnit())) { @@ -156,9 +154,8 @@ private Type mapValue(Type value) { } } return previousType = mapValue(finalLower.doubleValue(), finalUpper.doubleValue(), qtState.doubleValue()); - } else if (value instanceof DecimalType) { - return previousType = mapValue(lower.doubleValue(), upper.doubleValue(), - ((DecimalType) value).doubleValue()); + } else if (value instanceof DecimalType type) { + return previousType = mapValue(lower.doubleValue(), upper.doubleValue(), type.doubleValue()); } return previousType; } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemOffsetProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemOffsetProfile.java index 364b0b4d2ea..bf92f3cb083 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemOffsetProfile.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemOffsetProfile.java @@ -55,16 +55,15 @@ public SystemOffsetProfile(ProfileCallback callback, ProfileContext context) { Object paramValue = context.getConfiguration().get(OFFSET_PARAM); logger.debug("Configuring profile with {} parameter '{}'", OFFSET_PARAM, paramValue); - if (paramValue instanceof String) { + if (paramValue instanceof String string) { try { - offset = new QuantityType<>((String) paramValue); + offset = new QuantityType<>(string); } catch (IllegalArgumentException e) { logger.error( "Cannot convert value '{}' of parameter '{}' into a valid offset of type QuantityType. Using offset 0 now.", paramValue, OFFSET_PARAM); } - } else if (paramValue instanceof BigDecimal) { - BigDecimal bd = (BigDecimal) paramValue; + } else if (paramValue instanceof BigDecimal bd) { offset = new QuantityType<>(bd.toString()); } else { logger.error( @@ -106,8 +105,7 @@ private Type applyOffset(Type state, boolean towardsItem) { QuantityType finalOffset = towardsItem ? offset : offset.negate(); Type result = UnDefType.UNDEF; - if (state instanceof QuantityType) { - QuantityType qtState = (QuantityType) state; + if (state instanceof QuantityType qtState) { try { if (Units.ONE.equals(finalOffset.getUnit()) && !Units.ONE.equals(qtState.getUnit())) { // allow offsets without unit -> implicitly assume its the same as the one from the state, but warn @@ -129,8 +127,7 @@ private Type applyOffset(Type state, boolean towardsItem) { } catch (UnconvertibleException e) { logger.warn("Cannot apply offset '{}' to state '{}' because types do not match.", finalOffset, qtState); } - } else if (state instanceof DecimalType && Units.ONE.equals(finalOffset.getUnit())) { - DecimalType decState = (DecimalType) state; + } else if (state instanceof DecimalType decState && Units.ONE.equals(finalOffset.getUnit())) { result = new DecimalType(decState.toBigDecimal().add(finalOffset.toBigDecimal())); } else { logger.warn( diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java index 8bed81cb0fb..5de09b33d59 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfile.java @@ -91,14 +91,13 @@ public SystemRangeStateProfile(ProfileCallback callback, ProfileContext context) private @Nullable QuantityType getParam(ProfileContext context, String param) { final Object paramValue = context.getConfiguration().get(param); logger.debug("Configuring profile with {} parameter '{}'", param, paramValue); - if (paramValue instanceof String) { + if (paramValue instanceof String string) { try { - return new QuantityType<>((String) paramValue); + return new QuantityType<>(string); } catch (IllegalArgumentException e) { logger.error("Cannot convert value '{}' of parameter {} into a valid QuantityType.", paramValue, param); } - } else if (paramValue instanceof BigDecimal) { - final BigDecimal value = (BigDecimal) paramValue; + } else if (paramValue instanceof BigDecimal value) { return QuantityType.valueOf(value.doubleValue(), AbstractUnit.ONE); } return null; @@ -118,8 +117,8 @@ public void onStateUpdateFromItem(State state) { public void onCommandFromHandler(Command command) { final Type mappedCommand = mapValue(command); logger.trace("Mapped command from '{}' to command '{}'.", command, mappedCommand); - if (mappedCommand instanceof Command) { - callback.sendCommand((Command) mappedCommand); + if (mappedCommand instanceof Command command1) { + callback.sendCommand(command1); } } @@ -132,14 +131,13 @@ public void onCommandFromItem(Command command) { public void onStateUpdateFromHandler(State state) { final Type mappedState = mapValue(state); logger.trace("Mapped state from '{}' to state '{}'.", state, mappedState); - if (mappedState instanceof State) { - callback.sendUpdate((State) mappedState); + if (mappedState instanceof State state1) { + callback.sendUpdate(state1); } } private Type mapValue(Type value) { - if (value instanceof QuantityType) { - final QuantityType qtState = (QuantityType) value; + if (value instanceof QuantityType qtState) { final QuantityType finalLower; final QuantityType finalUpper; if (Units.ONE.equals(lower.getUnit()) && Units.ONE.equals(upper.getUnit())) { @@ -163,9 +161,8 @@ private Type mapValue(Type value) { } } return previousType = mapValue(finalLower.doubleValue(), finalUpper.doubleValue(), qtState.doubleValue()); - } else if (value instanceof DecimalType) { - return previousType = mapValue(lower.doubleValue(), upper.doubleValue(), - ((DecimalType) value).doubleValue()); + } else if (value instanceof DecimalType type) { + return previousType = mapValue(lower.doubleValue(), upper.doubleValue(), type.doubleValue()); } return previousType; } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java index c62421e884a..58a808d2347 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java @@ -58,11 +58,9 @@ public TimestampOffsetProfile(ProfileCallback callback, ProfileContext context) Object offsetParam = context.getConfiguration().get(OFFSET_PARAM); logger.debug("Configuring profile with {} parameter '{}'", OFFSET_PARAM, offsetParam); - if (offsetParam instanceof Number) { - Number bd = (Number) offsetParam; + if (offsetParam instanceof Number bd) { offset = Duration.ofSeconds(bd.longValue()); - } else if (offsetParam instanceof String) { - String s = (String) offsetParam; + } else if (offsetParam instanceof String s) { offset = Duration.ofSeconds(Long.valueOf(s)); } else { logger.error( @@ -121,8 +119,8 @@ private Type applyOffsetAndTimezone(Type type, boolean towardsItem) { Duration finalOffset = towardsItem ? offset : offset.negated(); Type result = UnDefType.UNDEF; - if (type instanceof DateTimeType) { - ZonedDateTime zdt = ((DateTimeType) type).getZonedDateTime(); + if (type instanceof DateTimeType timeType) { + ZonedDateTime zdt = timeType.getZonedDateTime(); // apply offset if (!Duration.ZERO.equals(offset)) { diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java index 0ebe8949e6b..a56f104399b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java @@ -12,100 +12,23 @@ */ package org.openhab.core.thing.internal.update; -import java.net.URL; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Enumeration; -import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.openhab.core.thing.internal.update.dto.XmlAddChannel; -import org.openhab.core.thing.internal.update.dto.XmlInstructionSet; -import org.openhab.core.thing.internal.update.dto.XmlRemoveChannel; -import org.openhab.core.thing.internal.update.dto.XmlThingType; -import org.openhab.core.thing.internal.update.dto.XmlUpdateChannel; -import org.openhab.core.thing.internal.update.dto.XmlUpdateDescriptions; -import org.openhab.core.util.BundleResolver; -import org.osgi.framework.Bundle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link ThingUpdateInstructionReader} is used to read instructions for a given {@link ThingHandlerFactory} and - * * create a list of {@link ThingUpdateInstruction}s + * create a list of {@link ThingUpdateInstruction}s * * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class ThingUpdateInstructionReader { - private final Logger logger = LoggerFactory.getLogger(ThingUpdateInstructionReader.class); - private final BundleResolver bundleResolver; - - public ThingUpdateInstructionReader(BundleResolver bundleResolver) { - this.bundleResolver = bundleResolver; - } - - public Map> readForFactory(ThingHandlerFactory factory) { - Bundle bundle = bundleResolver.resolveBundle(factory.getClass()); - if (bundle == null) { - logger.error( - "Could not get bundle for '{}', thing type updates will fail. If this occurs outside of tests, it is a bug.", - factory.getClass()); - return Map.of(); - } - - Map> updateInstructions = new HashMap<>(); - Enumeration entries = bundle.findEntries("OH-INF/update", "*.xml", true); - if (entries != null) { - while (entries.hasMoreElements()) { - URL url = entries.nextElement(); - try { - JAXBContext context = JAXBContext.newInstance(XmlUpdateDescriptions.class); - Unmarshaller u = context.createUnmarshaller(); - XmlUpdateDescriptions updateDescriptions = (XmlUpdateDescriptions) u.unmarshal(url); - - for (XmlThingType thingType : updateDescriptions.getThingType()) { - ThingTypeUID thingTypeUID = new ThingTypeUID(thingType.getUid()); - UpdateInstructionKey key = new UpdateInstructionKey(factory, thingTypeUID); - List instructions = new ArrayList<>(); - List instructionSets = thingType.getInstructionSet().stream() - .sorted(Comparator.comparing(XmlInstructionSet::getTargetVersion)).toList(); - for (XmlInstructionSet instructionSet : instructionSets) { - int targetVersion = instructionSet.getTargetVersion(); - for (Object instruction : instructionSet.getInstructions()) { - if (instruction instanceof XmlAddChannel addChannelType) { - instructions.add(new UpdateChannelInstructionImpl(targetVersion, addChannelType)); - } else if (instruction instanceof XmlUpdateChannel updateChannelType) { - instructions - .add(new UpdateChannelInstructionImpl(targetVersion, updateChannelType)); - } else if (instruction instanceof XmlRemoveChannel removeChannelType) { - instructions - .add(new RemoveChannelInstructionImpl(targetVersion, removeChannelType)); - } else { - logger.warn("Instruction type '{}' is unknown.", instruction.getClass()); - } - } - } - updateInstructions.put(key, instructions); - } - logger.trace("Reading update instructions from '{}'", url.getPath()); - } catch (IllegalArgumentException | JAXBException e) { - logger.warn("Failed to parse update instructions from '{}':", url, e); - } - } - } - - return updateInstructions; - } +public interface ThingUpdateInstructionReader { + Map> readForFactory(ThingHandlerFactory factory); - public record UpdateInstructionKey(ThingHandlerFactory factory, ThingTypeUID thingTypeId) { + record UpdateInstructionKey(ThingHandlerFactory factory, ThingTypeUID thingTypeId) { } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReaderImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReaderImpl.java new file mode 100644 index 00000000000..8cb809bd527 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReaderImpl.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.update; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.internal.update.dto.XmlAddChannel; +import org.openhab.core.thing.internal.update.dto.XmlInstructionSet; +import org.openhab.core.thing.internal.update.dto.XmlRemoveChannel; +import org.openhab.core.thing.internal.update.dto.XmlThingType; +import org.openhab.core.thing.internal.update.dto.XmlUpdateChannel; +import org.openhab.core.thing.internal.update.dto.XmlUpdateDescriptions; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.util.BundleResolver; +import org.osgi.framework.Bundle; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ThingUpdateInstructionReaderImpl} is an implementation of {@link ThingUpdateInstructionReader} + * + * @author Jan N. Klug - Initial contribution + */ +@Component(service = ThingUpdateInstructionReader.class) +@NonNullByDefault +public class ThingUpdateInstructionReaderImpl implements ThingUpdateInstructionReader { + private final Logger logger = LoggerFactory.getLogger(ThingUpdateInstructionReaderImpl.class); + private final BundleResolver bundleResolver; + private final ChannelTypeRegistry channelTypeRegistry; + private final ConfigDescriptionRegistry configDescriptionRegistry; + + @Activate + public ThingUpdateInstructionReaderImpl(@Reference BundleResolver bundleResolver, + @Reference ChannelTypeRegistry channelTypeRegistry, + @Reference ConfigDescriptionRegistry configDescriptionRegistry) { + this.bundleResolver = bundleResolver; + this.channelTypeRegistry = channelTypeRegistry; + this.configDescriptionRegistry = configDescriptionRegistry; + } + + @Override + public Map> readForFactory(ThingHandlerFactory factory) { + Bundle bundle = bundleResolver.resolveBundle(factory.getClass()); + if (bundle == null) { + logger.error( + "Could not get bundle for '{}', thing type updates will fail. If this occurs outside of tests, it is a bug.", + factory.getClass()); + return Map.of(); + } + + Map> updateInstructions = new HashMap<>(); + Enumeration entries = bundle.findEntries("OH-INF/update", "*.xml", true); + if (entries != null) { + while (entries.hasMoreElements()) { + URL url = entries.nextElement(); + try { + JAXBContext context = JAXBContext.newInstance(XmlUpdateDescriptions.class); + Unmarshaller u = context.createUnmarshaller(); + XmlUpdateDescriptions updateDescriptions = (XmlUpdateDescriptions) u.unmarshal(url); + + for (XmlThingType thingType : updateDescriptions.getThingType()) { + ThingTypeUID thingTypeUID = new ThingTypeUID(thingType.getUid()); + UpdateInstructionKey key = new UpdateInstructionKey(factory, thingTypeUID); + List instructions = new ArrayList<>(); + List instructionSets = thingType.getInstructionSet().stream() + .sorted(Comparator.comparing(XmlInstructionSet::getTargetVersion)).toList(); + for (XmlInstructionSet instructionSet : instructionSets) { + int targetVersion = instructionSet.getTargetVersion(); + for (Object instruction : instructionSet.getInstructions()) { + if (instruction instanceof XmlAddChannel addChannelType) { + instructions.add(new UpdateChannelInstructionImpl(targetVersion, addChannelType, + channelTypeRegistry, configDescriptionRegistry)); + } else if (instruction instanceof XmlUpdateChannel updateChannelType) { + instructions.add(new UpdateChannelInstructionImpl(targetVersion, updateChannelType, + channelTypeRegistry, configDescriptionRegistry)); + } else if (instruction instanceof XmlRemoveChannel removeChannelType) { + instructions + .add(new RemoveChannelInstructionImpl(targetVersion, removeChannelType)); + } else { + logger.warn("Instruction type '{}' is unknown.", instruction.getClass()); + } + } + } + updateInstructions.put(key, instructions); + } + logger.trace("Reading update instructions from '{}'", url.getPath()); + } catch (IllegalArgumentException | JAXBException e) { + logger.warn("Failed to parse update instructions from '{}':", url, e); + } + } + } + + return updateInstructions; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java index 41ba71f5428..88c26aae26a 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java @@ -19,14 +19,20 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.ConfigDescriptionRegistry; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.internal.ThingFactoryHelper; import org.openhab.core.thing.internal.update.dto.XmlAddChannel; import org.openhab.core.thing.internal.update.dto.XmlUpdateChannel; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link UpdateChannelInstructionImpl} implements a {@link ThingUpdateInstruction} that updates a channel of a @@ -36,40 +42,49 @@ */ @NonNullByDefault public class UpdateChannelInstructionImpl implements ThingUpdateInstruction { + private final Logger logger = LoggerFactory.getLogger(UpdateChannelInstructionImpl.class); private final boolean removeOldChannel; private final int thingTypeVersion; private final boolean preserveConfig; private final List groupIds; private final String channelId; - private final String channelTypeUid; + private final ChannelTypeUID channelTypeUid; private final @Nullable String label; private final @Nullable String description; private final @Nullable List tags; + private final ChannelTypeRegistry channelTypeRegistry; + private final ConfigDescriptionRegistry configDescriptionRegistry; - UpdateChannelInstructionImpl(int thingTypeVersion, XmlUpdateChannel updateChannel) { + UpdateChannelInstructionImpl(int thingTypeVersion, XmlUpdateChannel updateChannel, + ChannelTypeRegistry channelTypeRegistry, ConfigDescriptionRegistry configDescriptionRegistry) { this.removeOldChannel = true; this.thingTypeVersion = thingTypeVersion; this.channelId = updateChannel.getId(); - this.channelTypeUid = updateChannel.getType(); + this.channelTypeUid = new ChannelTypeUID(updateChannel.getType()); String rawGroupIds = updateChannel.getGroupIds(); this.groupIds = rawGroupIds != null ? Arrays.asList(rawGroupIds.split(",")) : List.of(); this.label = updateChannel.getLabel(); this.description = updateChannel.getDescription(); this.tags = updateChannel.getTags(); this.preserveConfig = updateChannel.isPreserveConfiguration(); + this.channelTypeRegistry = channelTypeRegistry; + this.configDescriptionRegistry = configDescriptionRegistry; } - UpdateChannelInstructionImpl(int thingTypeVersion, XmlAddChannel addChannel) { + UpdateChannelInstructionImpl(int thingTypeVersion, XmlAddChannel addChannel, + ChannelTypeRegistry channelTypeRegistry, ConfigDescriptionRegistry configDescriptionRegistry) { this.removeOldChannel = false; this.thingTypeVersion = thingTypeVersion; this.channelId = addChannel.getId(); - this.channelTypeUid = addChannel.getType(); + this.channelTypeUid = new ChannelTypeUID(addChannel.getType()); String rawGroupIds = addChannel.getGroupIds(); this.groupIds = rawGroupIds != null ? Arrays.asList(rawGroupIds.split(",")) : List.of(); this.label = addChannel.getLabel(); this.description = addChannel.getDescription(); this.tags = addChannel.getTags(); this.preserveConfig = false; + this.channelTypeRegistry = channelTypeRegistry; + this.configDescriptionRegistry = configDescriptionRegistry; } @Override @@ -79,6 +94,7 @@ public int getThingTypeVersion() { @Override public void perform(Thing thing, ThingBuilder thingBuilder) { + logger.debug("Applying {} to thing '{}'", this, thing.getUID()); if (groupIds.isEmpty()) { doChannel(thing, thingBuilder, new ChannelUID(thing.getUID(), channelId)); } else { @@ -88,13 +104,19 @@ public void perform(Thing thing, ThingBuilder thingBuilder) { } private void doChannel(Thing thing, ThingBuilder thingBuilder, ChannelUID affectedChannelUid) { - if (removeOldChannel) { thingBuilder.withoutChannel(affectedChannelUid); } - ChannelBuilder channelBuilder = ChannelBuilder.create(affectedChannelUid) - .withType(new ChannelTypeUID(channelTypeUid)); + ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid); + if (channelType == null) { + logger.error("Failed to create channel '{}' because channel type '{}' could not be found.", + affectedChannelUid, channelTypeUid); + return; + } + + ChannelBuilder channelBuilder = ThingFactoryHelper.createChannelBuilder(affectedChannelUid, channelType, + configDescriptionRegistry); if (preserveConfig) { Channel oldChannel = thing.getChannel(affectedChannelUid); @@ -116,4 +138,12 @@ private void doChannel(Thing thing, ThingBuilder thingBuilder, ChannelUID affect thingBuilder.withChannel(channelBuilder.build()); } + + @Override + public String toString() { + return "UpdateChannelInstructionImpl{" + "removeOldChannel=" + removeOldChannel + ", thingTypeVersion=" + + thingTypeVersion + ", preserveConfig=" + preserveConfig + ", groupIds=" + groupIds + ", channelId='" + + channelId + "'" + ", channelTypeUid=" + channelTypeUid + ", label='" + label + "'" + ", description='" + + description + "'" + ", tags=" + tags + "}"; + } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/AbstractLink.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/AbstractLink.java index 70b0fd5919b..952a30edb07 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/AbstractLink.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/AbstractLink.java @@ -56,8 +56,7 @@ public AbstractLink(String itemName) { @Override public boolean equals(@Nullable Object obj) { - if (obj instanceof AbstractLink) { - AbstractLink link = (AbstractLink) obj; + if (obj instanceof AbstractLink link) { return getUID().equals(link.getUID()); } return false; diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/ItemChannelLink.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/ItemChannelLink.java index badcdc9cc9a..fb6ee1d2eaa 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/ItemChannelLink.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/ItemChannelLink.java @@ -62,8 +62,7 @@ public Configuration getConfiguration() { @Override public boolean equals(@Nullable Object obj) { - if (obj instanceof ItemChannelLink) { - ItemChannelLink link = (ItemChannelLink) obj; + if (obj instanceof ItemChannelLink link) { return super.equals(obj) && configuration.equals(link.getConfiguration()); } return false; diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/ProfileTypeRegistry.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/ProfileTypeRegistry.java index 2d53f84cabe..1cbff58443c 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/ProfileTypeRegistry.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/ProfileTypeRegistry.java @@ -32,7 +32,7 @@ public interface ProfileTypeRegistry { * * @return all profile types */ - public List getProfileTypes(); + List getProfileTypes(); /** * Get the available {@link ProfileType}s from all providers. @@ -40,5 +40,5 @@ public interface ProfileTypeRegistry { * @param locale the language to use (may be null) * @return all profile types */ - public List getProfileTypes(@Nullable Locale locale); + List getProfileTypes(@Nullable Locale locale); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/i18n/ProfileTypeI18nLocalizationService.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/i18n/ProfileTypeI18nLocalizationService.java index 74cefbe9b9b..76e8a15ea8a 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/i18n/ProfileTypeI18nLocalizationService.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/profiles/i18n/ProfileTypeI18nLocalizationService.java @@ -49,16 +49,14 @@ public ProfileType createLocalizedProfileType(Bundle bundle, ProfileType profile String defaultLabel = profileType.getLabel(); String label = profileI18nUtil.getProfileLabel(bundle, profileTypeUID, defaultLabel, locale); - if (profileType instanceof StateProfileType) { + if (profileType instanceof StateProfileType type) { return ProfileTypeBuilder.newState(profileTypeUID, label != null ? label : defaultLabel) .withSupportedItemTypes(profileType.getSupportedItemTypes()) - .withSupportedItemTypesOfChannel(((StateProfileType) profileType).getSupportedItemTypesOfChannel()) - .build(); - } else if (profileType instanceof TriggerProfileType) { + .withSupportedItemTypesOfChannel(type.getSupportedItemTypesOfChannel()).build(); + } else if (profileType instanceof TriggerProfileType type) { return ProfileTypeBuilder.newTrigger(profileTypeUID, label != null ? label : defaultLabel) .withSupportedItemTypes(profileType.getSupportedItemTypes()) - .withSupportedChannelTypeUIDs(((TriggerProfileType) profileType).getSupportedChannelTypeUIDs()) - .build(); + .withSupportedChannelTypeUIDs(type.getSupportedChannelTypeUIDs()).build(); } else { return profileType; } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AutoUpdatePolicy.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AutoUpdatePolicy.java index 4ecb759b570..085eceeba84 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AutoUpdatePolicy.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AutoUpdatePolicy.java @@ -46,12 +46,12 @@ public enum AutoUpdatePolicy { * Parses the input string into an {@link AutoUpdatePolicy}. * * @param input the input string - * @return the parsed AutoUpdatePolicy + * @return the parsed AutoUpdatePolicy or null if the input was null * @throws IllegalArgumentException if the input couldn't be parsed. */ - public static AutoUpdatePolicy parse(@Nullable String input) { + public static @Nullable AutoUpdatePolicy parse(@Nullable String input) { if (input == null) { - return DEFAULT; + return null; } for (AutoUpdatePolicy value : values()) { diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/util/ThingHelper.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/util/ThingHelper.java index 0fdfd8e122b..16a90ebeaf7 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/util/ThingHelper.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/util/ThingHelper.java @@ -12,10 +12,8 @@ */ package org.openhab.core.thing.util; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -79,24 +77,10 @@ public static boolean equals(Thing a, Thing b) { return false; } // channels - List channelsOfA = a.getChannels(); - List channelsOfB = b.getChannels(); - if (channelsOfA.size() != channelsOfB.size()) { - return false; - } - if (!toString(channelsOfA).equals(toString(channelsOfB))) { - return false; - } - return true; - } + Set channelsOfA = new HashSet<>(a.getChannels()); + Set channelsOfB = new HashSet<>(b.getChannels()); - private static String toString(List channels) { - List strings = new ArrayList<>(channels.size()); - for (Channel channel : channels) { - strings.add(channel.getUID().toString() + '#' + channel.getAcceptedItemType() + '#' + channel.getKind()); - } - Collections.sort(strings); - return String.join(",", strings); + return channelsOfA.equals(channelsOfB); } public static void addChannelsToThing(Thing thing, Collection channels) { @@ -227,9 +211,7 @@ public static Thing merge(Thing thing, ThingDTO updatedContents) { Thing mergedThing = builder.build(); // keep all child things in place on a merged bridge - if (mergedThing instanceof BridgeImpl && thing instanceof Bridge) { - Bridge bridge = (Bridge) thing; - BridgeImpl mergedBridge = (BridgeImpl) mergedThing; + if (mergedThing instanceof BridgeImpl mergedBridge && thing instanceof Bridge bridge) { for (Thing child : bridge.getThings()) { mergedBridge.addThing(child); } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/AbstractDescriptionTypeConverter.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/AbstractDescriptionTypeConverter.java index 56d794c1131..323b8d2b4c2 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/AbstractDescriptionTypeConverter.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/AbstractDescriptionTypeConverter.java @@ -125,8 +125,8 @@ protected String readLabel(NodeIterator nodeIterator) throws ConversionException Object nextNode = nodeIterator.next(); if (nextNode != null) { - if (nextNode instanceof ConfigDescription) { - return (ConfigDescription) nextNode; + if (nextNode instanceof ConfigDescription description) { + return description; } nodeIterator.revert(); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ChannelTypeConverter.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ChannelTypeConverter.java index 03e37f67eb4..d6a7b404bd7 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ChannelTypeConverter.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ChannelTypeConverter.java @@ -123,8 +123,8 @@ private boolean readBoolean(Map attributes, String attributeName Object nextNode = nodeIterator.next(); if (nextNode != null) { - if (nextNode instanceof StateDescription) { - return (StateDescription) nextNode; + if (nextNode instanceof StateDescription description) { + return description; } nodeIterator.revert(); @@ -137,8 +137,8 @@ private boolean readBoolean(Map attributes, String attributeName Object nextNode = nodeIterator.next(); if (nextNode != null) { - if (nextNode instanceof EventDescription) { - return (EventDescription) nextNode; + if (nextNode instanceof EventDescription description) { + return description; } nodeIterator.revert(); @@ -151,8 +151,8 @@ private boolean readBoolean(Map attributes, String attributeName Object nextNode = nodeIterator.next(); if (nextNode != null) { - if (nextNode instanceof CommandDescription) { - return (CommandDescription) nextNode; + if (nextNode instanceof CommandDescription description) { + return description; } nodeIterator.revert(); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingDescriptionList.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingDescriptionList.java index 8f96fe215ea..7827dc5df92 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingDescriptionList.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingDescriptionList.java @@ -27,6 +27,8 @@ @NonNullByDefault public class ThingDescriptionList extends ArrayList { + private static final long serialVersionUID = -1579556977347296301L; + @SuppressWarnings("unchecked") public ThingDescriptionList(Collection list) { super(list); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingTypeXmlProvider.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingTypeXmlProvider.java index c8fc4d64d41..fd74c108928 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingTypeXmlProvider.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/xml/internal/ThingTypeXmlProvider.java @@ -83,15 +83,12 @@ public ThingTypeXmlProvider(Bundle bundle, AbstractXmlConfigDescriptionProvider @Override public synchronized void addingObject(List types) { for (Object type : types) { - if (type instanceof ThingTypeXmlResult) { - ThingTypeXmlResult typeResult = (ThingTypeXmlResult) type; + if (type instanceof ThingTypeXmlResult typeResult) { addConfigDescription(typeResult.getConfigDescription()); thingTypeRefs.add(typeResult); - } else if (type instanceof ChannelGroupTypeXmlResult) { - ChannelGroupTypeXmlResult typeResult = (ChannelGroupTypeXmlResult) type; + } else if (type instanceof ChannelGroupTypeXmlResult typeResult) { channelGroupTypeRefs.add(typeResult); - } else if (type instanceof ChannelTypeXmlResult) { - ChannelTypeXmlResult typeResult = (ChannelTypeXmlResult) type; + } else if (type instanceof ChannelTypeXmlResult typeResult) { channelTypeRefs.add(typeResult); addConfigDescription(typeResult.getConfigDescription()); } else { diff --git a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/DefaultSystemChannels_el.properties b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/DefaultSystemChannels_el.properties index 334158715db..e39331c1002 100644 --- a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/DefaultSystemChannels_el.properties +++ b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/DefaultSystemChannels_el.properties @@ -1,11 +1,14 @@ channel-type.system.signal-strength.label = Ισχύς Σήματος +channel-type.system.signal-strength.description = Ισχύς σήματος με τιμές 0 (χειρότερη), 1, 2, 3 ή 4 (καλύτερη) channel-type.system.signal-strength.state.option.0 = κανένα σήμα channel-type.system.signal-strength.state.option.1 = αδύναμο channel-type.system.signal-strength.state.option.2 = μέτριο channel-type.system.signal-strength.state.option.3 = καλό channel-type.system.signal-strength.state.option.4 = εξαιρετικό channel-type.system.low-battery.label = Χαμηλή Μπαταρία +channel-type.system.low-battery.description = Προειδοποίηση χαμηλής στάθμης μπαταρίας με τιμές on (χαμηλή μπαταρία) ή off (επαρκής μπαταρία) channel-type.system.battery-level.label = Επίπεδο μπαταρίας +channel-type.system.battery-level.description = Επίπεδο μπαταρίας ως ποσοστό (0-100%) channel-type.system.trigger.label = Ενεργοποίηση channel-type.system.rawbutton.label = Σκέτο Κουμπί channel-type.system.button.label = Κουμπί @@ -45,3 +48,11 @@ channel-type.system.atmospheric-humidity.label = Ατμοσφαιρική Υγρ channel-type.system.atmospheric-humidity.description = Τρέχουσα ατμοσφαιρική σχετική υγρασία channel-type.system.barometric-pressure.label = Βαρομετρική Πίεση channel-type.system.barometric-pressure.description = Τρέχουσα βαρομετρική πίεση +channel-type.system.electric-power.label = Ηλεκτρική Ισχύς +channel-type.system.electric-power.description = Τρέχουσα ηλεκτρική ισχύς +channel-type.system.electric-current.label = Ηλεκτρική Ένταση +channel-type.system.electric-current.description = Τρέχουσα ηλεκτρική ένταση +channel-type.system.electric-voltage.label = Ηλεκτρική Τάση +channel-type.system.electric-voltage.description = Τρέχουσα ηλεκτρική τάση +channel-type.system.electrical-energy.label = Ηλεκτρική Ενέργεια +channel-type.system.electrical-energy.description = Τρέχουσα ηλεκτρική ενέργεια diff --git a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties index 08495c13951..9d3c7f09df1 100644 --- a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties +++ b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles_de.properties @@ -20,7 +20,7 @@ profile.config.system.range.inverted.description = Invertiert die resultierende profile-type.system.timestamp-change.label = Zeitstempel bei Änderung profile-type.system.timestamp-offset.label = Zeitstempel Versatz profile.config.system.timestamp-offset.offset.label = Versatz -profile.config.system.timestamp-offset.offset.description = Versatz (numerischer Wert), welcher auf den Zeitstempel addiert wird. Ein negativer Versatz wird vom Zeitstempel abgezogen. +profile.config.system.timestamp-offset.offset.description = Versatz (numerischer Wert in Sekunden), welcher auf den Zeitstempel addiert wird. Ein negativer Versatz wird vom Zeitstempel abgezogen. profile.config.system.timestamp-offset.timezone.label = Zeitzone profile.config.system.timestamp-offset.timezone.description = Die Zeitzone, in die der Zeitstempel verändert werden soll. profile-type.system.timestamp-trigger.label = Zeitstempel bei Auslöser diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/CommunicationManagerConversionTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/CommunicationManagerConversionTest.java new file mode 100644 index 00000000000..f191b1b19da --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/CommunicationManagerConversionTest.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.core.items.Item; +import org.openhab.core.library.items.CallItem; +import org.openhab.core.library.items.ColorItem; +import org.openhab.core.library.items.ContactItem; +import org.openhab.core.library.items.DateTimeItem; +import org.openhab.core.library.items.DimmerItem; +import org.openhab.core.library.items.ImageItem; +import org.openhab.core.library.items.LocationItem; +import org.openhab.core.library.items.PlayerItem; +import org.openhab.core.library.items.RollershutterItem; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.NextPreviousType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.RewindFastforwardType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.Type; +import org.openhab.core.types.UnDefType; + +/** + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class CommunicationManagerConversionTest { + // TODO: remove test - only to show CommunicationManager is too complex + + private static final List> itemTypes = List.of(CallItem.class, ColorItem.class, + ContactItem.class, DateTimeItem.class, DimmerItem.class, ImageItem.class, LocationItem.class, + PlayerItem.class, RollershutterItem.class, StringItem.class); + + private static final List> types = List.of(DateTimeType.class, DecimalType.class, + HSBType.class, IncreaseDecreaseType.class, NextPreviousType.class, OnOffType.class, OpenClosedType.class, + PercentType.class, PlayPauseType.class, PointType.class, QuantityType.class, RawType.class, + RewindFastforwardType.class, StringType.class, UpDownType.class, UnDefType.class); + + private static Stream arguments() + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + List arguments = new ArrayList<>(); + for (Class itemType : itemTypes) { + Item item = itemType.getDeclaredConstructor(String.class).newInstance("testItem"); + for (Class type : types) { + if (type.isEnum()) { + arguments.add(Arguments.of(item, type.getEnumConstants()[0])); + } else if (type == RawType.class) { + arguments.add(Arguments.of(item, new RawType(new byte[] {}, "mimeType"))); + } else { + arguments.add(Arguments.of(item, type.getDeclaredConstructor().newInstance())); + } + } + } + return arguments.stream(); + } + + @Disabled + @MethodSource("arguments") + @ParameterizedTest + public void testCommand(Item item, Type originalType) { + Type returnType = null; + + List> acceptedTypes = item.getAcceptedCommandTypes(); + if (acceptedTypes.contains(originalType.getClass())) { + returnType = originalType; + } else { + // Look for class hierarchy and convert appropriately + for (Class typeClass : acceptedTypes) { + if (!typeClass.isEnum() && typeClass.isAssignableFrom(originalType.getClass()) // + && State.class.isAssignableFrom(typeClass) && originalType instanceof State state) { + returnType = state.as((Class) typeClass); + } + } + } + + if (returnType != null && !returnType.getClass().equals(originalType.getClass())) { + fail("CommunicationManager did a conversion for target item " + item.getType() + " from " + + originalType.getClass() + " to " + returnType.getClass()); + } + } + + @MethodSource("arguments") + @ParameterizedTest + public void testState(Item item, Type originalType) { + Type returnType = null; + + List> acceptedTypes = item.getAcceptedDataTypes(); + if (acceptedTypes.contains(originalType.getClass())) { + returnType = originalType; + } else { + // Look for class hierarchy and convert appropriately + for (Class typeClass : acceptedTypes) { + if (!typeClass.isEnum() && typeClass.isAssignableFrom(originalType.getClass()) // + && State.class.isAssignableFrom(typeClass) && originalType instanceof State state) { + returnType = state.as((Class) typeClass); + + } + } + } + + if (returnType != null && !returnType.equals(originalType)) { + fail("CommunicationManager did a conversion for target item " + item.getType() + " from " + + originalType.getClass() + " to " + returnType.getClass()); + } + } +} diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProviderTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProviderTest.java new file mode 100644 index 00000000000..22ac0a2e56a --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProviderTest.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.AutoUpdatePolicy; +import org.openhab.core.thing.type.ChannelDefinition; +import org.openhab.core.thing.type.ChannelDefinitionBuilder; +import org.openhab.core.thing.type.ChannelGroupDefinition; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeBuilder; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.types.CommandDescriptionBuilder; +import org.openhab.core.types.StateDescriptionFragmentBuilder; + +/** + * The {@link AbstractStorageBasedTypeProviderTest} contains tests for the static mapping-methods + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AbstractStorageBasedTypeProviderTest { + + @Test + public void testStateChannelTypeProperlyMappedToEntityAndBack() { + ChannelTypeUID channelTypeUID = new ChannelTypeUID("TestBinding:testChannelType"); + + ChannelType expected = ChannelTypeBuilder.state(channelTypeUID, "testLabel", "Switch") + .withDescription("testDescription").withCategory("testCategory") + .withConfigDescriptionURI(URI.create("testBinding:testConfig")) + .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).isAdvanced(true).withTag("testTag") + .withCommandDescription(CommandDescriptionBuilder.create().build()) + .withStateDescriptionFragment(StateDescriptionFragmentBuilder.create().build()).build(); + AbstractStorageBasedTypeProvider.ChannelTypeEntity entity = AbstractStorageBasedTypeProvider + .mapToEntity(expected); + ChannelType actual = AbstractStorageBasedTypeProvider.mapFromEntity(entity); + + assertThat(actual.getUID(), is(expected.getUID())); + assertThat(actual.getKind(), is(expected.getKind())); + assertThat(actual.getLabel(), is(expected.getLabel())); + assertThat(actual.getDescription(), is(expected.getDescription())); + assertThat(actual.getConfigDescriptionURI(), is(expected.getConfigDescriptionURI())); + assertThat(actual.isAdvanced(), is(expected.isAdvanced())); + assertThat(actual.getAutoUpdatePolicy(), is(expected.getAutoUpdatePolicy())); + assertThat(actual.getCategory(), is(expected.getCategory())); + assertThat(actual.getEvent(), is(expected.getEvent())); + assertThat(actual.getCommandDescription(), is(expected.getCommandDescription())); + assertThat(actual.getState(), is(expected.getState())); + assertThat(actual.getItemType(), is(expected.getItemType())); + assertThat(actual.getTags(), hasItems(expected.getTags().toArray(String[]::new))); + } + + @Test + public void testChannelGroupTypeProperlyMappedToEntityAndBack() { + ChannelGroupTypeUID groupTypeUID = new ChannelGroupTypeUID("testBinding:testGroupType"); + + ChannelDefinition channelDefinition = new ChannelDefinitionBuilder("channelName", + new ChannelTypeUID("system:color")).withLabel("label").withDescription("description") + .withProperties(Map.of("key", "value")).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); + ChannelGroupType expected = ChannelGroupTypeBuilder.instance(groupTypeUID, "testLabel") + .withDescription("testDescription").withCategory("testCategory") + .withChannelDefinitions(List.of(channelDefinition)).build(); + + AbstractStorageBasedTypeProvider.ChannelGroupTypeEntity entity = AbstractStorageBasedTypeProvider + .mapToEntity(expected); + ChannelGroupType actual = AbstractStorageBasedTypeProvider.mapFromEntity(entity); + + assertThat(actual.getUID(), is(expected.getUID())); + assertThat(actual.getLabel(), is(expected.getLabel())); + assertThat(actual.getDescription(), is(expected.getDescription())); + assertThat(actual.getCategory(), is(expected.getCategory())); + List expectedChannelDefinitions = expected.getChannelDefinitions(); + List actualChannelDefinitions = actual.getChannelDefinitions(); + assertThat(actualChannelDefinitions.size(), is(expectedChannelDefinitions.size())); + for (ChannelDefinition expectedChannelDefinition : expectedChannelDefinitions) { + ChannelDefinition actualChannelDefinition = actualChannelDefinitions.stream() + .filter(d -> d.getId().equals(expectedChannelDefinition.getId())).findFirst().orElse(null); + assertThat(actualChannelDefinition, is(notNullValue())); + assertChannelDefinition(actualChannelDefinition, expectedChannelDefinition); + } + } + + @Test + public void testThingTypeProperlyMappedToEntityAndBack() { + ThingTypeUID thingTypeUID = new ThingTypeUID("testBinding:testThingType"); + + ChannelDefinition channelDefinition = new ChannelDefinitionBuilder("channelName", + new ChannelTypeUID("system:color")).withLabel("label").withDescription("description") + .withProperties(Map.of("key", "value")).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); + ChannelGroupDefinition channelGroupDefinition = new ChannelGroupDefinition("groupName", + new ChannelGroupTypeUID("testBinding:channelGroupType"), "label", "description"); + ThingType expected = ThingTypeBuilder.instance(thingTypeUID, "testLabel").withDescription("description") + .withCategory("category").withExtensibleChannelTypeIds(List.of("ch1", "ch2")) + .withConfigDescriptionURI(URI.create("testBinding:testConfig")) + .withChannelDefinitions(List.of(channelDefinition)) + .withChannelGroupDefinitions(List.of(channelGroupDefinition)).isListed(true) + .withProperties(Map.of("key", "value")).withSupportedBridgeTypeUIDs(List.of("bridge1", "bridge2")) + .build(); + + AbstractStorageBasedTypeProvider.ThingTypeEntity entity = AbstractStorageBasedTypeProvider + .mapToEntity(expected); + ThingType actual = AbstractStorageBasedTypeProvider.mapFromEntity(entity); + + assertThat(actual.getUID(), is(expected.getUID())); + assertThat(actual.getLabel(), is(expected.getLabel())); + assertThat(actual.getDescription(), is(expected.getDescription())); + assertThat(actual.getExtensibleChannelTypeIds(), + containsInAnyOrder(expected.getExtensibleChannelTypeIds().toArray(String[]::new))); + assertThat(actual.getSupportedBridgeTypeUIDs(), + containsInAnyOrder(expected.getSupportedBridgeTypeUIDs().toArray(String[]::new))); + assertThat(actual.getCategory(), is(expected.getCategory())); + assertThat(actual.isListed(), is(expected.isListed())); + assertThat(actual.getRepresentationProperty(), is(expected.getRepresentationProperty())); + assertThat(actual.getConfigDescriptionURI(), is(expected.getConfigDescriptionURI())); + assertMap(actual.getProperties(), expected.getProperties()); + List expectedChannelDefinitions = expected.getChannelDefinitions(); + List actualChannelDefinitions = actual.getChannelDefinitions(); + assertThat(actualChannelDefinitions.size(), is(expectedChannelDefinitions.size())); + for (ChannelDefinition expectedChannelDefinition : expectedChannelDefinitions) { + ChannelDefinition actualChannelDefinition = actualChannelDefinitions.stream() + .filter(d -> d.getId().equals(expectedChannelDefinition.getId())).findFirst().orElse(null); + assertThat(actualChannelDefinition, is(notNullValue())); + assertChannelDefinition(actualChannelDefinition, expectedChannelDefinition); + } + List expectedChannelGroupDefinitions = expected.getChannelGroupDefinitions(); + List actualChannelGroupDefinitions = actual.getChannelGroupDefinitions(); + assertThat(actualChannelGroupDefinitions.size(), is(expectedChannelGroupDefinitions.size())); + for (ChannelGroupDefinition expectedChannelGroupDefinition : expectedChannelGroupDefinitions) { + ChannelGroupDefinition actualChannelGroupDefinition = actualChannelGroupDefinitions.stream() + .filter(d -> d.getId().equals(expectedChannelGroupDefinition.getId())).findFirst().orElse(null); + assertThat(actualChannelGroupDefinition, is(notNullValue())); + assertChannelGroupDefinition(actualChannelGroupDefinition, expectedChannelGroupDefinition); + } + } + + private void assertChannelDefinition(ChannelDefinition actual, ChannelDefinition expected) { + assertThat(actual.getId(), is(expected.getId())); + assertThat(actual.getChannelTypeUID(), is(expected.getChannelTypeUID())); + assertThat(actual.getLabel(), is(expected.getLabel())); + assertThat(actual.getDescription(), is(expected.getDescription())); + assertThat(actual.getAutoUpdatePolicy(), is(expected.getAutoUpdatePolicy())); + assertMap(actual.getProperties(), expected.getProperties()); + } + + private void assertChannelGroupDefinition(ChannelGroupDefinition actual, ChannelGroupDefinition expected) { + assertThat(actual.getId(), is(expected.getId())); + assertThat(actual.getTypeUID(), is(expected.getTypeUID())); + assertThat(actual.getLabel(), is(expected.getLabel())); + assertThat(actual.getDescription(), is(expected.getDescription())); + } + + private void assertMap(Map actual, Map expected) { + assertThat(actual.size(), is(expected.size())); + for (Map.Entry entry : expected.entrySet()) { + assertThat(actual, hasEntry(entry.getKey(), entry.getValue())); + } + } +} diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java new file mode 100644 index 00000000000..d7847b63228 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/ChannelTransformationTest.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.transform.TransformationException; +import org.openhab.core.transform.TransformationHelper; +import org.openhab.core.transform.TransformationService; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * The {@link ChannelTransformationTest} contains tests for the {@link ChannelTransformation} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class ChannelTransformationTest { + private static final String T1_NAME = "TRANSFORM1"; + private static final String T1_PATTERN = "T1Pattern"; + private static final String T1_INPUT = "T1Input"; + private static final String T1_RESULT = "T1Result"; + + private static final String T2_NAME = "TRANSFORM2"; + private static final String T2_PATTERN = "T2Pattern"; + private static final String T2_INPUT = T1_RESULT; + private static final String T2_RESULT = "T2Result"; + + private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock; + private @Mock @NonNullByDefault({}) TransformationService transformationService2Mock; + + private @Mock @NonNullByDefault({}) BundleContext bundleContextMock; + private @Mock @NonNullByDefault({}) ServiceReference serviceRef1Mock; + private @Mock @NonNullByDefault({}) ServiceReference serviceRef2Mock; + + private @NonNullByDefault({}) TransformationHelper transformationHelper; + + @BeforeEach + public void init() throws TransformationException { + Mockito.when(transformationService1Mock.transform(eq(T1_PATTERN), eq(T1_INPUT))) + .thenAnswer(answer -> T1_RESULT); + Mockito.when(transformationService2Mock.transform(eq(T2_PATTERN), eq(T1_INPUT))) + .thenAnswer(answer -> T2_RESULT); + Mockito.when(transformationService2Mock.transform(eq(T2_PATTERN), eq(T2_INPUT))) + .thenAnswer(answer -> T2_RESULT); + + Mockito.when(serviceRef1Mock.getProperty(any())).thenReturn("TRANSFORM1"); + Mockito.when(serviceRef2Mock.getProperty(any())).thenReturn("TRANSFORM2"); + + Mockito.when(bundleContextMock.getService(serviceRef1Mock)).thenReturn(transformationService1Mock); + Mockito.when(bundleContextMock.getService(serviceRef2Mock)).thenReturn(transformationService2Mock); + + transformationHelper = new TransformationHelper(bundleContextMock); + transformationHelper.setTransformationService(serviceRef1Mock); + transformationHelper.setTransformationService(serviceRef2Mock); + } + + @AfterEach + public void tearDown() { + transformationHelper.deactivate(); + } + + @Test + public void testMissingTransformation() { + String pattern = "TRANSFORM:pattern"; + + ChannelTransformation transformation = new ChannelTransformation(pattern); + String result = transformation.apply(T1_INPUT).orElse(null); + + assertNull(result); + } + + @Test + public void testSingleTransformation() { + String pattern = T1_NAME + ":" + T1_PATTERN; + + ChannelTransformation transformation = new ChannelTransformation(pattern); + String result = transformation.apply(T1_INPUT).orElse(null); + + assertEquals(T1_RESULT, result); + } + + @Test + public void testInvalidFirstTransformation() { + String pattern = T1_NAME + "X:" + T1_PATTERN + "∩" + T2_NAME + ":" + T2_PATTERN; + + ChannelTransformation transformation = new ChannelTransformation(pattern); + String result = transformation.apply(T1_INPUT).orElse(null); + + assertNull(result); + } + + @Test + public void testInvalidSecondTransformation() { + String pattern = T1_NAME + ":" + T1_PATTERN + "∩" + T2_NAME + "X:" + T2_PATTERN; + + ChannelTransformation transformation = new ChannelTransformation(pattern); + String result = transformation.apply(T1_INPUT).orElse(null); + + assertNull(result); + } + + @Test + public void testDoubleTransformationWithoutSpaces() { + String pattern = T1_NAME + ":" + T1_PATTERN + "∩" + T2_NAME + ":" + T2_PATTERN; + + ChannelTransformation transformation = new ChannelTransformation(pattern); + String result = transformation.apply(T1_INPUT).orElse(null); + + assertEquals(T2_RESULT, result); + } + + @Test + public void testDoubleTransformationWithSpaces() { + String pattern = " " + T1_NAME + " : " + T1_PATTERN + " ∩ " + T2_NAME + " : " + T2_PATTERN + " "; + + ChannelTransformation transformation = new ChannelTransformation(pattern); + String result = transformation.apply(T1_INPUT).orElse(null); + + assertEquals(T2_RESULT, result); + } +} diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java new file mode 100644 index 00000000000..1d5e2e6b24e --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/generic/converter/ConverterTest.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding.generic.converter; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.binding.generic.ChannelHandlerContent; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link ConverterTest} is a test class for state converters + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class ConverterTest { + + private @Mock @NonNullByDefault({}) Consumer sendValueMock; + + private @Mock @NonNullByDefault({}) Consumer updateStateMock; + + private @Mock @NonNullByDefault({}) Consumer postCommandMock; + + @Test + public void numberItemConverter() { + NumberChannelHandler converter = new NumberChannelHandler(updateStateMock, postCommandMock, sendValueMock, + new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig()); + + // without unit + Assertions.assertEquals(Optional.of(new DecimalType(1234)), converter.toState("1234")); + + // unit in transformation result + Assertions.assertEquals(Optional.of(new QuantityType<>(100, SIUnits.CELSIUS)), converter.toState("100°C")); + + // no valid value + Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("W")); + Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("")); + } + + @Test + public void numberItemConverterWithUnit() { + ChannelValueConverterConfig channelConfig = new ChannelValueConverterConfig(); + channelConfig.unit = "W"; + NumberChannelHandler converter = new NumberChannelHandler(updateStateMock, postCommandMock, sendValueMock, + new ChannelTransformation(null), new ChannelTransformation(null), channelConfig); + + // without unit + Assertions.assertEquals(Optional.of(new QuantityType<>(500, Units.WATT)), converter.toState("500")); + + // no valid value + Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("foo")); + Assertions.assertEquals(Optional.of(UnDefType.UNDEF), converter.toState("")); + } + + @Test + public void stringTypeConverter() { + GenericChannelHandler converter = createConverter(StringType::new); + Assertions.assertEquals(Optional.of(new StringType("Test")), converter.toState("Test")); + } + + @Test + public void decimalTypeConverter() { + GenericChannelHandler converter = createConverter(DecimalType::new); + Assertions.assertEquals(Optional.of(new DecimalType(15.6)), converter.toState("15.6")); + } + + @Test + public void pointTypeConverter() { + GenericChannelHandler converter = createConverter(PointType::new); + Assertions.assertEquals( + Optional.of(new PointType(new DecimalType(51.1), new DecimalType(7.2), new DecimalType(100))), + converter.toState("51.1, 7.2, 100")); + } + + @Test + public void playerItemTypeConverter() { + ChannelValueConverterConfig cfg = new ChannelValueConverterConfig(); + cfg.playValue = "PLAY"; + ChannelHandlerContent content = new ChannelHandlerContent("PLAY".getBytes(StandardCharsets.UTF_8), "UTF-8", + null); + PlayerChannelHandler converter = new PlayerChannelHandler(updateStateMock, postCommandMock, sendValueMock, + new ChannelTransformation(null), new ChannelTransformation(null), cfg); + converter.process(content); + converter.process(content); + + Mockito.verify(postCommandMock).accept(PlayPauseType.PLAY); + Mockito.verify(updateStateMock, Mockito.never()).accept(ArgumentMatchers.any()); + } + + @Test + public void colorItemTypeRGBConverter() { + ChannelValueConverterConfig cfg = new ChannelValueConverterConfig(); + cfg.colorMode = ColorChannelHandler.ColorMode.RGB; + ChannelHandlerContent content = new ChannelHandlerContent("123,34,47".getBytes(StandardCharsets.UTF_8), "UTF-8", + null); + ColorChannelHandler converter = new ColorChannelHandler(updateStateMock, postCommandMock, sendValueMock, + new ChannelTransformation(null), new ChannelTransformation(null), cfg); + + converter.process(content); + Mockito.verify(updateStateMock).accept(HSBType.fromRGB(123, 34, 47)); + } + + @Test + public void colorItemTypeHSBConverter() { + ChannelValueConverterConfig cfg = new ChannelValueConverterConfig(); + cfg.colorMode = ColorChannelHandler.ColorMode.HSB; + ChannelHandlerContent content = new ChannelHandlerContent("123,34,47".getBytes(StandardCharsets.UTF_8), "UTF-8", + null); + ColorChannelHandler converter = new ColorChannelHandler(updateStateMock, postCommandMock, sendValueMock, + new ChannelTransformation(null), new ChannelTransformation(null), cfg); + + converter.process(content); + Mockito.verify(updateStateMock).accept(new HSBType("123,34,47")); + } + + @Test + public void rollerSHutterConverter() { + ChannelValueConverterConfig cfg = new ChannelValueConverterConfig(); + RollershutterChannelHandler converter = new RollershutterChannelHandler(updateStateMock, postCommandMock, + sendValueMock, new ChannelTransformation(null), new ChannelTransformation(null), cfg); + + // test 0 and 100 + ChannelHandlerContent content = new ChannelHandlerContent("0".getBytes(StandardCharsets.UTF_8), "UTF-8", null); + converter.process(content); + Mockito.verify(updateStateMock).accept(PercentType.ZERO); + content = new ChannelHandlerContent("100".getBytes(StandardCharsets.UTF_8), "UTF-8", null); + converter.process(content); + Mockito.verify(updateStateMock).accept(PercentType.HUNDRED); + + // test under/over-range (expect two times total for zero/100 + content = new ChannelHandlerContent("-1".getBytes(StandardCharsets.UTF_8), "UTF-8", null); + converter.process(content); + Mockito.verify(updateStateMock, Mockito.times(2)).accept(PercentType.ZERO); + content = new ChannelHandlerContent("105".getBytes(StandardCharsets.UTF_8), "UTF-8", null); + converter.process(content); + Mockito.verify(updateStateMock, Mockito.times(2)).accept(PercentType.HUNDRED); + + // test value + content = new ChannelHandlerContent("67".getBytes(StandardCharsets.UTF_8), "UTF-8", null); + converter.process(content); + Mockito.verify(updateStateMock).accept(new PercentType(67)); + } + + public GenericChannelHandler createConverter(Function fcn) { + return new GenericChannelHandler(fcn, updateStateMock, postCommandMock, sendValueMock, + new ChannelTransformation(null), new ChannelTransformation(null), new ChannelValueConverterConfig()); + } +} diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java index 861cd7728d1..bf635ce2c29 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java @@ -41,6 +41,7 @@ import org.openhab.core.thing.binding.ThingHandlerFactory; import org.openhab.core.thing.i18n.ThingStatusInfoI18nLocalizationService; import org.openhab.core.thing.internal.ThingTracker.ThingTrackerEvent; +import org.openhab.core.thing.internal.update.ThingUpdateInstructionReader; import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.openhab.core.thing.type.ChannelGroupTypeRegistry; import org.openhab.core.thing.type.ChannelTypeRegistry; @@ -72,6 +73,7 @@ public class ThingManagerImplTest extends JavaTest { private @Mock @NonNullByDefault({}) Thing thingMock; private @Mock @NonNullByDefault({}) ThingRegistryImpl thingRegistryMock; private @Mock @NonNullByDefault({}) BundleResolver bundleResolverMock; + private @Mock @NonNullByDefault({}) ThingUpdateInstructionReader thingUpdateInstructionReaderMock; private @Mock @NonNullByDefault({}) TranslationProvider translationProviderMock; private @Mock @NonNullByDefault({}) BundleContext bundleContextMock; private @Mock @NonNullByDefault({}) ThingType thingTypeMock; @@ -92,8 +94,8 @@ private ThingManagerImpl createThingManager() { return new ThingManagerImpl(channelGroupTypeRegistryMock, channelTypeRegistryMock, communicationManagerMock, configDescriptionRegistryMock, configDescriptionValidatorMock, eventPublisherMock, itemChannelLinkRegistryMock, readyServiceMock, safeCallerMock, storageServiceMock, thingRegistryMock, - thingStatusInfoI18nLocalizationService, thingTypeRegistryMock, bundleResolverMock, - translationProviderMock, bundleContextMock); + thingStatusInfoI18nLocalizationService, thingTypeRegistryMock, thingUpdateInstructionReaderMock, + bundleResolverMock, translationProviderMock, bundleContextMock); } @Test diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java new file mode 100644 index 00000000000..08207319d4a --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/binding/generic/converter/AbstractTransformingItemConverterTest.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.binding.generic.converter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.binding.generic.ChannelHandlerContent; +import org.openhab.core.thing.binding.generic.ChannelTransformation; +import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link AbstractTransformingItemConverterTest} is a test class for the + * {@link AbstractTransformingChannelHandler} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AbstractTransformingItemConverterTest { + + @Mock + private @NonNullByDefault({}) Consumer sendHttpValue; + + @Mock + private @NonNullByDefault({}) Consumer updateState; + + @Mock + private @NonNullByDefault({}) Consumer postCommand; + + private @NonNullByDefault({}) AutoCloseable closeable; + + @Spy + private ChannelTransformation stateChannelTransformation = new ChannelTransformation(null); + + @Spy + private ChannelTransformation commandChannelTransformation = new ChannelTransformation(null); + + @BeforeEach + public void init() { + closeable = MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void close() throws Exception { + closeable.close(); + } + + @Test + public void undefOnNullContentTest() { + TestChannelHandler realConverter = new TestChannelHandler(updateState, postCommand, sendHttpValue, + stateChannelTransformation, commandChannelTransformation, false); + TestChannelHandler converter = spy(realConverter); + + converter.process(null); + // make sure UNDEF is send as state update + verify(updateState, only()).accept(UnDefType.UNDEF); + verify(postCommand, never()).accept(any()); + verify(sendHttpValue, never()).accept(any()); + + // make sure no other processing applies + verify(converter, never()).toState(any()); + verify(converter, never()).toCommand(any()); + verify(converter, never()).toString(any()); + } + + @Test + public void commandIsPostedAsCommand() { + TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue, + stateChannelTransformation, commandChannelTransformation, true); + + converter.process(new ChannelHandlerContent("TEST".getBytes(StandardCharsets.UTF_8), "", null)); + + // check state transformation is applied + verify(stateChannelTransformation).apply(any()); + verify(commandChannelTransformation, never()).apply(any()); + + // check only postCommand is applied + verify(updateState, never()).accept(any()); + verify(postCommand, only()).accept(new StringType("TEST")); + verify(sendHttpValue, never()).accept(any()); + } + + @Test + public void updateIsPostedAsUpdate() { + TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue, + stateChannelTransformation, commandChannelTransformation, false); + + converter.process(new ChannelHandlerContent("TEST".getBytes(StandardCharsets.UTF_8), "", null)); + + // check state transformation is applied + verify(stateChannelTransformation).apply(any()); + verify(commandChannelTransformation, never()).apply(any()); + + // check only updateState is called + verify(updateState, only()).accept(new StringType("TEST")); + verify(postCommand, never()).accept(any()); + verify(sendHttpValue, never()).accept(any()); + } + + @Test + public void sendCommandSendsCommand() { + TestChannelHandler converter = new TestChannelHandler(updateState, postCommand, sendHttpValue, + stateChannelTransformation, commandChannelTransformation, false); + + converter.send(new StringType("TEST")); + + // check command transformation is applied + verify(stateChannelTransformation, never()).apply(any()); + verify(commandChannelTransformation).apply(any()); + + // check only sendHttpValue is applied + verify(updateState, never()).accept(any()); + verify(postCommand, never()).accept(any()); + verify(sendHttpValue, only()).accept("TEST"); + } + + private static class TestChannelHandler extends AbstractTransformingChannelHandler { + private boolean hasCommand; + + public TestChannelHandler(Consumer updateState, Consumer postCommand, + @Nullable Consumer sendValue, ChannelTransformation stateChannelTransformation, + ChannelTransformation commandChannelTransformation, boolean hasCommand) { + super(updateState, postCommand, sendValue, stateChannelTransformation, commandChannelTransformation, + new ChannelValueConverterConfig()); + this.hasCommand = hasCommand; + } + + @Override + protected @Nullable Command toCommand(String value) { + return hasCommand ? new StringType(value) : null; + } + + @Override + protected Optional toState(String value) { + return Optional.of(new StringType(value)); + } + + @Override + protected String toString(Command command) { + return command.toString(); + } + } +} diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfileTest.java index 21f3c796c3a..c5e33e9ab84 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfileTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemHysteresisStateProfileTest.java @@ -77,7 +77,7 @@ public ParameterSet(List sources, List results, this.commands = (List) sources; this.resultingCommands = new ArrayList<>(results.size()); results.forEach(result -> { - resultingCommands.add(result instanceof Command ? (Command) result : null); + resultingCommands.add(result instanceof Command c ? c : null); }); this.lower = lower; this.upper = upper; diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java index 564d5d84aa3..d211ae82281 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/SystemRangeStateProfileTest.java @@ -77,7 +77,7 @@ public ParameterSet(List sources, List results, this.commands = (List) sources; this.resultingCommands = new ArrayList<>(results.size()); results.forEach(result -> { - resultingCommands.add(result instanceof Command ? (Command) result : null); + resultingCommands.add(result instanceof Command c ? c : null); }); this.lower = lower; this.upper = upper; diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/ToggleProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/ToggleProfileTest.java index 3dae3df0b84..0ace19f08c0 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/ToggleProfileTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/ToggleProfileTest.java @@ -153,7 +153,7 @@ public void testWrongUserConfiguredEvent() { private void initializeContextMock(@Nullable String triggerEvent) { Map params = triggerEvent == null ? Collections.emptyMap() - : Collections.singletonMap(ToggleProfile.EVENT_PARAM, triggerEvent); + : Map.of(ToggleProfile.EVENT_PARAM, triggerEvent); when(contextMock.getConfiguration()).thenReturn(new Configuration(params)); } diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/util/ThingHelperTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/util/ThingHelperTest.java index 71934b6635a..3a5f96e0e85 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/util/ThingHelperTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/util/ThingHelperTest.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -142,4 +143,21 @@ public void assertThatNoDuplicateChannelsCanBeAdded() { ChannelBuilder.create(new ChannelUID(thingUID, "channel3"), "").build()) .collect(toList()))); } + + @Test + public void asserThatChannelsWithDifferentConfigurationAreDetectedAsDifferent() { + Thing thingA = ThingBuilder.create(THING_TYPE_UID, THING_UID) + .withChannels(ChannelBuilder.create(new ChannelUID("binding:type:thingId:channel1"), "itemType") + .withConfiguration(new Configuration(Map.of("key", "v1"))).build()) + .withConfiguration(new Configuration()).build(); + + assertTrue(ThingHelper.equals(thingA, thingA)); + + Thing thingB = ThingBuilder.create(THING_TYPE_UID, THING_UID) + .withChannels(ChannelBuilder.create(new ChannelUID("binding:type:thingId:channel1"), "itemType") + .withConfiguration(new Configuration(Map.of("key", "v2"))).build()) + .withConfiguration(new Configuration()).build(); + + assertFalse(ThingHelper.equals(thingA, thingB)); + } } diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/xml/internal/ThingDescriptionReaderTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/xml/internal/ThingDescriptionReaderTest.java index 492d94852aa..2efcae6af11 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/xml/internal/ThingDescriptionReaderTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/xml/internal/ThingDescriptionReaderTest.java @@ -47,12 +47,12 @@ public void readFromXML() throws Exception { List channelTypeXmlResults = new ArrayList<>(); for (Object type : types) { - if (type instanceof ThingTypeXmlResult) { - thingTypeXmlResults.add((ThingTypeXmlResult) type); - } else if (type instanceof ChannelGroupTypeXmlResult) { - channelGroupTypeXmlResults.add((ChannelGroupTypeXmlResult) type); - } else if (type instanceof ChannelTypeXmlResult) { - channelTypeXmlResults.add((ChannelTypeXmlResult) type); + if (type instanceof ThingTypeXmlResult result) { + thingTypeXmlResults.add(result); + } else if (type instanceof ChannelGroupTypeXmlResult result) { + channelGroupTypeXmlResults.add(result); + } else if (type instanceof ChannelTypeXmlResult result) { + channelTypeXmlResults.add(result); } } diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java index a5df5441d90..4fd1213ef8b 100644 --- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java @@ -67,7 +67,8 @@ public FileTransformationProvider( watchService.registerListener(this, transformationPath); // read initial contents try (Stream files = Files.walk(transformationPath)) { - files.filter(Files::isRegularFile).map(transformationPath::relativize).forEach(f -> processPath(CREATE, f)); + files.filter(Files::isRegularFile).map(transformationPath::relativize) + .forEach(f -> processWatchEvent(CREATE, f)); } catch (IOException e) { logger.warn("Could not list files in '{}', transformation configurations might be missing: {}", transformationPath, e.getMessage()); @@ -94,16 +95,18 @@ public Collection getAll() { return transformationConfigurations.values(); } - private void processPath(WatchService.Kind kind, Path path) { + @Override + public void processWatchEvent(WatchService.Kind kind, Path path) { Path finalPath = transformationPath.resolve(path); - if (kind == DELETE) { - Transformation oldElement = transformationConfigurations.remove(path); - if (oldElement != null) { - logger.trace("Removed configuration from file '{}", path); - listeners.forEach(listener -> listener.removed(this, oldElement)); - } - } else if (Files.isRegularFile(finalPath) && ((kind == CREATE) || (kind == MODIFY))) { - try { + try { + if (kind == DELETE) { + Transformation oldElement = transformationConfigurations.remove(path); + if (oldElement != null) { + logger.trace("Removed configuration from file '{}", path); + listeners.forEach(listener -> listener.removed(this, oldElement)); + } + } else if (Files.isRegularFile(finalPath) && !Files.isHidden(finalPath) + && ((kind == CREATE) || (kind == MODIFY))) { String fileName = path.getFileName().toString(); Matcher m = FILENAME_PATTERN.matcher(fileName); if (!m.matches()) { @@ -131,16 +134,11 @@ private void processPath(WatchService.Kind kind, Path path) { logger.trace("Updated new configuration from file '{}'", path); listeners.forEach(listener -> listener.updated(this, oldElement, newElement)); } - } catch (IOException e) { - logger.warn("Skipping {} event for '{}' - failed to read content: {}", kind, path, e.getMessage()); + } else { + logger.trace("Skipping {} event for '{}' - not a regular file", kind, path); } - } else { - logger.trace("Skipping {} event for '{}' - not a regular file", kind, path); + } catch (IOException e) { + logger.warn("Skipping {} event for '{}' - failed to process it: {}", kind, path, e.getMessage()); } } - - @Override - public void processWatchEvent(WatchService.Kind kind, Path path) { - processPath(kind, path); - } } diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java index 04eb909b43e..94e953d7253 100644 --- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationHelper.java @@ -12,25 +12,34 @@ */ package org.openhab.core.transform; -import java.util.Collection; import java.util.IllegalFormatException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.osgi.framework.BundleContext; -import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Kai Kreuzer - Initial contribution + * @author Jan N. Klug - Refactored to OSGi service */ +@Component(immediate = true) @NonNullByDefault public class TransformationHelper { + private static final Map SERVICES = new ConcurrentHashMap<>(); private static final Logger LOGGER = LoggerFactory.getLogger(TransformationHelper.class); @@ -40,6 +49,35 @@ public class TransformationHelper { protected static final Pattern EXTRACT_TRANSFORMFUNCTION_PATTERN = Pattern .compile("(.*?)\\((.*)\\)" + FUNCTION_VALUE_DELIMITER + "(.*)"); + private final BundleContext bundleContext; + + @Activate + public TransformationHelper(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @Deactivate + public void deactivate() { + SERVICES.clear(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void setTransformationService(ServiceReference ref) { + String key = (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME); + TransformationService service = bundleContext.getService(ref); + if (service != null) { + SERVICES.put(key, service); + LOGGER.debug("Added transformation service {}", key); + } + } + + public void unsetTransformationService(ServiceReference ref) { + String key = (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME); + if (SERVICES.remove(key) != null) { + LOGGER.debug("Removed transformation service {}", key); + } + } + /** * determines whether a pattern refers to a transformation service * @@ -50,52 +88,57 @@ public static boolean isTransform(String pattern) { return EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(pattern).matches(); } + public static @Nullable TransformationService getTransformationService(String serviceName) { + return SERVICES.get(serviceName); + } + /** - * Queries the OSGi service registry for a service that provides a transformation service of - * a given transformation type (e.g. REGEX, XSLT, etc.) + * Return the transformation service that provides a given transformation type (e.g. REGEX, XSLT, etc.) * * @param context the bundle context which can be null * @param transformationType the desired transformation type * @return a service instance or null, if none could be found + * + * @deprecated use {@link #getTransformationService(String)} instead */ + @Deprecated public static @Nullable TransformationService getTransformationService(@Nullable BundleContext context, String transformationType) { - if (context != null) { - String filter = "(openhab.transform=" + transformationType + ")"; - try { - Collection> refs = context - .getServiceReferences(TransformationService.class, filter); - if (refs != null && !refs.isEmpty()) { - return context.getService(refs.iterator().next()); - } else { - LOGGER.debug("Cannot get service reference for transformation service of type {}", - transformationType); - } - } catch (InvalidSyntaxException e) { - LOGGER.debug("Cannot get service reference for transformation service of type {}", transformationType, - e); - } - } - return null; + return getTransformationService(transformationType); } /** * Transforms a state string using transformation functions within a given pattern. * * @param context a valid bundle context, required for accessing the services - * @param stateDescPattern the pattern that contains the transformation instructions + * @param transformationString the pattern that contains the transformation instructions * @param state the state to be formatted before being passed into the transformation function * @return the result of the transformation. If no transformation was done, null is returned * @throws TransformationException if transformation service is not available or the transformation failed + * + * @deprecated Use {@link #transform(String, String)} instead */ - public static @Nullable String transform(BundleContext context, String stateDescPattern, String state) + @Deprecated + public static @Nullable String transform(BundleContext context, String transformationString, String state) throws TransformationException { - Matcher matcher = EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(stateDescPattern); + return transform(transformationString, state); + } + + /** + * Transforms a state string using transformation functions within a given pattern. + * + * @param transformationString the pattern that contains the transformation instructions + * @param state the state to be formatted before being passed into the transformation function + * @return the result of the transformation. If no transformation was done, null is returned + * @throws TransformationException if transformation service is not available or the transformation failed + */ + public static @Nullable String transform(String transformationString, String state) throws TransformationException { + Matcher matcher = EXTRACT_TRANSFORMFUNCTION_PATTERN.matcher(transformationString); if (matcher.find()) { String type = matcher.group(1); String pattern = matcher.group(2); String value = matcher.group(3); - TransformationService transformation = TransformationHelper.getTransformationService(context, type); + TransformationService transformation = SERVICES.get(type); if (transformation != null) { return transform(transformation, pattern, value, state); } else { @@ -125,6 +168,8 @@ public static boolean isTransform(String pattern) { return service.transform(function, value); } catch (IllegalFormatException e) { throw new TransformationException("Cannot format state '" + state + "' to format '" + format + "'", e); + } catch (RuntimeException e) { + throw new TransformationException("Transformation service threw an exception: " + e.getMessage(), e); } } } diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationService.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationService.java index 60be09cdbe7..f018a3a4aee 100644 --- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationService.java +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationService.java @@ -32,8 +32,10 @@ @NonNullByDefault public interface TransformationService { - public static final String TRANSFORM_FOLDER_NAME = "transform"; - public static final String TRANSFORM_PROFILE_SCOPE = "transform"; + String SERVICE_PROPERTY_NAME = "openhab.transform"; + String SERVICE_PROPERTY_LABEL = "openhab.transform.label"; + String TRANSFORM_FOLDER_NAME = "transform"; + String TRANSFORM_PROFILE_SCOPE = "transform"; /** * Transforms the input source by means of the given function and returns the transformed diff --git a/bundles/org.openhab.core.ui.icon/src/main/java/org/openhab/core/ui/icon/internal/IconServlet.java b/bundles/org.openhab.core.ui.icon/src/main/java/org/openhab/core/ui/icon/internal/IconServlet.java index 058296c0dbe..1cf34e79965 100644 --- a/bundles/org.openhab.core.ui.icon/src/main/java/org/openhab/core/ui/icon/internal/IconServlet.java +++ b/bundles/org.openhab.core.ui.icon/src/main/java/org/openhab/core/ui/icon/internal/IconServlet.java @@ -87,8 +87,8 @@ protected void activate(Map config) { @Modified protected void modified(Map config) { Object iconSetId = config.get("default"); - if (iconSetId instanceof String) { - defaultIconSetId = (String) iconSetId; + if (iconSetId instanceof String string) { + defaultIconSetId = string; } } @@ -106,7 +106,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se return; } - String state = getState(req); + String state = req.getParameter(PARAM_STATE); String iconSetId = getIconSetId(req); Format format = getFormat(req); @@ -173,8 +173,7 @@ private String substringBeforeLast(String str, String separator) { private String getCategory(HttpServletRequest req) { String category = substringAfterLast(req.getRequestURI(), "/"); - category = substringBeforeLast(category, "."); - return substringBeforeLast(category, "-"); + return substringBeforeLast(category, "."); } private Format getFormat(HttpServletRequest req) { @@ -206,22 +205,6 @@ private String getIconSetId(HttpServletRequest req) { } } - private @Nullable String getState(HttpServletRequest req) { - String state = req.getParameter(PARAM_STATE); - if (state != null) { - return state; - } else { - String filename = substringAfterLast(req.getRequestURI(), "/"); - state = substringAfterLast(filename, "-"); - state = substringBeforeLast(state, "."); - if (!state.isEmpty()) { - return state; - } else { - return null; - } - } - } - private @Nullable IconProvider getIconProvider(String category, String iconSetId, Format format) { IconProvider topProvider = null; int maxPrio = Integer.MIN_VALUE; diff --git a/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/AbstractResourceIconProviderTest.java b/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/AbstractResourceIconProviderTest.java index 132c286f23e..c86ec9284cc 100644 --- a/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/AbstractResourceIconProviderTest.java +++ b/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/AbstractResourceIconProviderTest.java @@ -57,6 +57,10 @@ public void setUp() { return new ByteArrayInputStream("x-30.png".getBytes()); case "x-y z.png": return new ByteArrayInputStream("x-y z.png".getBytes()); + case "a-bb-ccc-30.png": + return new ByteArrayInputStream("a-bb-ccc-30.png".getBytes()); + case "a-bb-ccc-y z.png": + return new ByteArrayInputStream("a-bb-ccc-y z.png".getBytes()); default: return null; } @@ -96,6 +100,7 @@ public Integer getPriority() { public void testScanningForState() throws IOException { try (InputStream is = provider.getIcon("x", "classic", "34", Format.PNG)) { assertNotNull(is); + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8), is("x-30.png")); } try (InputStream is = provider.getIcon("x", "classic", "25", Format.PNG)) { @@ -103,6 +108,18 @@ public void testScanningForState() throws IOException { } } + @Test + public void testScanningIconWithHyphensForState() throws IOException { + try (InputStream is = provider.getIcon("a-bb-ccc", "classic", "34", Format.PNG)) { + assertNotNull(is); + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8), is("a-bb-ccc-30.png")); + } + + try (InputStream is = provider.getIcon("a-bb-ccc", "classic", "25", Format.PNG)) { + assertNull(is); + } + } + @Test public void testWithQuantityTypeState() throws IOException { try (InputStream is = provider.getIcon("x", "classic", "34 °C", Format.PNG)) { @@ -110,10 +127,24 @@ public void testWithQuantityTypeState() throws IOException { } } + @Test + public void testIconWithHyphensWithQuantityTypeState() throws IOException { + try (InputStream is = provider.getIcon("a-bb-ccc", "classic", "34 °C", Format.PNG)) { + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8), is("a-bb-ccc-30.png")); + } + } + @Test public void testWithStringTypeState() throws IOException { try (InputStream is = provider.getIcon("x", "classic", "y z", Format.PNG)) { assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8), is("x-y z.png")); } } + + @Test + public void testIconWithHyphensWithStringTypeState() throws IOException { + try (InputStream is = provider.getIcon("a-bb-ccc", "classic", "y z", Format.PNG)) { + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8), is("a-bb-ccc-y z.png")); + } + } } diff --git a/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/internal/IconServletTest.java b/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/internal/IconServletTest.java index 6c80b7a94ea..84a6f711f6c 100644 --- a/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/internal/IconServletTest.java +++ b/bundles/org.openhab.core.ui.icon/src/test/java/org/openhab/core/ui/icon/internal/IconServletTest.java @@ -89,23 +89,6 @@ public void before() throws IOException { responseOutputStream.reset(); } - @Test - public void testOldUrlStyle() throws ServletException, IOException { - when(requestMock.getRequestURI()).thenReturn("/icon/y-34.png"); - - when(responseMock.getOutputStream()).thenReturn(responseOutputStream); - - when(provider1Mock.hasIcon("y", "classic", Format.PNG)).thenReturn(0); - when(provider1Mock.getIcon("y", "classic", "34", Format.PNG)) - .thenReturn(new ByteArrayInputStream("provider 1 icon: y classic 34 png".getBytes())); - - servlet.addIconProvider(provider1Mock); - servlet.doGet(requestMock, responseMock); - - assertEquals("provider 1 icon: y classic 34 png", responseOutputStream.getOutput()); - verify(responseMock, never()).sendError(anyInt()); - } - @Test public void testPriority() throws ServletException, IOException { when(requestMock.getRequestURI()).thenReturn("/icon/x"); diff --git a/bundles/org.openhab.core.ui/NOTICE b/bundles/org.openhab.core.ui/NOTICE index f4cf4cb3fa1..c9355858dd6 100644 --- a/bundles/org.openhab.core.ui/NOTICE +++ b/bundles/org.openhab.core.ui/NOTICE @@ -15,10 +15,10 @@ https://github.com/openhab/openhab-core == Third-party Content -xchart-2.6.1 +xchart-3.8.4 * License: Apache License, 2.0 * Project: http://knowm.org/open-source/xchart -* Source: https://github.com/timmolter/XChart/tree/xchart-2.6.1 +* Source: https://github.com/timmolter/XChart/tree/xchart-3.8.4 == Third-party license(s) diff --git a/bundles/org.openhab.core.ui/bnd.bnd b/bundles/org.openhab.core.ui/bnd.bnd index c13a3330e6a..d6f226152e2 100644 --- a/bundles/org.openhab.core.ui/bnd.bnd +++ b/bundles/org.openhab.core.ui/bnd.bnd @@ -4,4 +4,7 @@ Private-Package: \ org.knowm.xchart.* Import-Package: \ de.erichseifert.vectorgraphics2d.*;resolution:=optional,\ + de.rototor.pdfbox.*;resolution:=optional,\ + com.madgag.*;resolution:=optional,\ + org.apache.pdfbox.*;resolution:=optional,\ * \ No newline at end of file diff --git a/bundles/org.openhab.core.ui/pom.xml b/bundles/org.openhab.core.ui/pom.xml index 575615ef393..2eef0e13fbb 100644 --- a/bundles/org.openhab.core.ui/pom.xml +++ b/bundles/org.openhab.core.ui/pom.xml @@ -18,8 +18,7 @@ org.knowm.xchart xchart - - 3.1.0 + 3.8.4 org.openhab.core.bundles diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java index e9ab7768c2c..5ea642e32f4 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java @@ -106,9 +106,9 @@ default BufferedImage createChart(@Nullable String service, @Nullable String the * Provides a list of image types * */ - public enum ImageType { + enum ImageType { png, jpg, - gif; + gif } } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java index b432ddc52fd..23740561fca 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java @@ -30,21 +30,21 @@ public interface ChartTheme { * * @return theme name */ - public String getThemeName(); + String getThemeName(); /** * Background color, plot area. * * @return background color, plot area */ - public Color getPlotBackgroundColor(); + Color getPlotBackgroundColor(); /** * Color for the grid lines. * * @return color for the grid lines */ - public Color getPlotGridLinesColor(); + Color getPlotGridLinesColor(); /** * Return the width of the grid lines. @@ -52,7 +52,7 @@ public interface ChartTheme { * @param dpi DPI dots per inch to calculate the width * @return width of the grid lines */ - public double getPlotGridLinesWidth(int dpi); + double getPlotGridLinesWidth(int dpi); /** * Return the dash spacing for the grid lines. @@ -60,28 +60,28 @@ public interface ChartTheme { * @param dpi DPI dots per inch to calculate the width * @return dash spacing for the grid lines */ - public double getPlotGridLinesDash(int dpi); + double getPlotGridLinesDash(int dpi); /** * Background color, legend area. * * @return background color, legend area */ - public Color getLegendBackgroundColor(); + Color getLegendBackgroundColor(); /** * Background color, whole chart * * @return background color, whole chart */ - public Color getChartBackgroundColor(); + Color getChartBackgroundColor(); /** * Font color, legend and general use. * * @return */ - public Color getChartFontColor(); + Color getChartFontColor(); /** * Return a color for the given series number. @@ -89,7 +89,7 @@ public interface ChartTheme { * @param series series number * @return color for the given series numer */ - public Color getLineColor(int series); + Color getLineColor(int series); /** * Return the width of the series lines. @@ -97,14 +97,14 @@ public interface ChartTheme { * @param dpi DPI dots per inch to calculate the width * @return width of the series lines */ - public double getLineWidth(int dpi); + double getLineWidth(int dpi); /** * Color for the axis labels. * * @return */ - public Color getAxisTickLabelsColor(); + Color getAxisTickLabelsColor(); /** * Font for the axis labels. @@ -113,7 +113,7 @@ public interface ChartTheme { * @param dpi the DPI to calculate the font size * @return {@link Font} for the axis labels. */ - public Font getAxisTickLabelsFont(int dpi); + Font getAxisTickLabelsFont(int dpi); /** * Font for the legend text. @@ -122,7 +122,7 @@ public interface ChartTheme { * @param dpi the DPI to calculate the font size * @return {@link Font} for the legend text */ - public Font getLegendFont(int dpi); + Font getLegendFont(int dpi); /** * Padding of the chart. @@ -130,7 +130,7 @@ public interface ChartTheme { * @param dpi the DPI to calculate the padding * @return padding of the chart */ - public int getChartPadding(int dpi); + int getChartPadding(int dpi); /** * Length of the line markers in the legend, in px. @@ -138,5 +138,5 @@ public interface ChartTheme { * @param dpi the DPI to calculate the line length * @return length of the line markers in the legend, in px */ - public int getLegendSeriesLineLength(int dpi); + int getLegendSeriesLineLength(int dpi); } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java index b0090bbe8ad..5f89cb04859 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java @@ -32,7 +32,7 @@ import org.knowm.xchart.XYChart; import org.knowm.xchart.XYChartBuilder; import org.knowm.xchart.XYSeries; -import org.knowm.xchart.style.Styler; +import org.knowm.xchart.style.AxesChartStyler; import org.knowm.xchart.style.Styler.LegendPosition; import org.knowm.xchart.style.XYStyler; import org.knowm.xchart.style.markers.None; @@ -150,8 +150,7 @@ public BufferedImage createChart(@Nullable String serviceId, @Nullable String th : persistenceServiceRegistry.get(serviceId); // Did we find a service? - QueryablePersistenceService persistenceService = (service instanceof QueryablePersistenceService) - ? (QueryablePersistenceService) service + QueryablePersistenceService persistenceService = (service instanceof QueryablePersistenceService qps) ? qps : (QueryablePersistenceService) persistenceServiceRegistry.getAll() // .stream() // .filter(it -> it instanceof QueryablePersistenceService) // @@ -196,7 +195,7 @@ public BufferedImage createChart(@Nullable String serviceId, @Nullable String th styler.setYAxisDecimalPattern(yAxisDecimalPattern); } styler.setYAxisTickMarkSpacingHint(yAxisSpacing); - styler.setYAxisLabelAlignment(Styler.TextAlignment.Right); + styler.setYAxisLabelAlignment(AxesChartStyler.TextAlignment.Right); // chart styler.setChartBackgroundColor(chartTheme.getChartBackgroundColor()); styler.setChartFontColor(chartTheme.getChartFontColor()); @@ -231,8 +230,7 @@ public BufferedImage createChart(@Nullable String serviceId, @Nullable String th String[] groupNames = groups.split(","); for (String groupName : groupNames) { Item item = itemUIRegistry.getItem(groupName); - if (item instanceof GroupItem) { - GroupItem groupItem = (GroupItem) item; + if (item instanceof GroupItem groupItem) { for (Item member : groupItem.getMembers()) { if (addItem(chart, persistenceService, startTime, endTime, member, seriesCounter, chartTheme, dpi, legendPositionDecider)) { @@ -293,10 +291,10 @@ public BufferedImage createChart(@Nullable String serviceId, @Nullable String th } private double convertData(State state) { - if (state instanceof DecimalType) { - return ((DecimalType) state).doubleValue(); - } else if (state instanceof QuantityType) { - return ((QuantityType) state).doubleValue(); + if (state instanceof DecimalType type) { + return type.doubleValue(); + } else if (state instanceof QuantityType type) { + return type.doubleValue(); } else if (state instanceof OnOffType) { return state == OnOffType.OFF ? 0 : 1; } else if (state instanceof OpenClosedType) { diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java index 47d54996b8c..a9bee32fd9c 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java @@ -45,6 +45,7 @@ import org.openhab.core.model.sitemap.sitemap.impl.FrameImpl; import org.openhab.core.model.sitemap.sitemap.impl.GroupImpl; import org.openhab.core.model.sitemap.sitemap.impl.ImageImpl; +import org.openhab.core.model.sitemap.sitemap.impl.InputImpl; import org.openhab.core.model.sitemap.sitemap.impl.MappingImpl; import org.openhab.core.model.sitemap.sitemap.impl.MapviewImpl; import org.openhab.core.model.sitemap.sitemap.impl.SelectionImpl; @@ -235,6 +236,11 @@ protected Sitemap buildSitemap(RootUIComponent rootComponent) { addWidgetMappings(selectionWidget.getMappings(), component); widget = selectionWidget; break; + case "Input": + InputImpl inputWidget = (InputImpl) SitemapFactory.eINSTANCE.createInput(); + widget = inputWidget; + setWidgetPropertyFromComponentConfig(widget, component, "inputHint", SitemapPackage.INPUT__INPUT_HINT); + break; case "Setpoint": SetpointImpl setpointWidget = (SetpointImpl) SitemapFactory.eINSTANCE.createSetpoint(); widget = setpointWidget; @@ -263,8 +269,7 @@ protected Sitemap buildSitemap(RootUIComponent rootComponent) { setWidgetPropertyFromComponentConfig(widget, component, "icon", SitemapPackage.WIDGET__ICON); setWidgetPropertyFromComponentConfig(widget, component, "item", SitemapPackage.WIDGET__ITEM); - if (widget instanceof LinkableWidget) { - LinkableWidget linkableWidget = (LinkableWidget) widget; + if (widget instanceof LinkableWidget linkableWidget) { if (component.getSlots() != null && component.getSlots().containsKey("widgets")) { for (UIComponent childComponent : component.getSlot("widgets")) { Widget childWidget = buildWidget(childComponent); @@ -297,7 +302,7 @@ private void setWidgetPropertyFromComponentConfig(Widget widget, @Nullable UICom WidgetImpl widgetImpl = (WidgetImpl) widget; Object normalizedValue = ConfigUtil.normalizeType(value); if (widgetImpl.eGet(feature, false, false) instanceof Integer) { - normalizedValue = (normalizedValue instanceof BigDecimal) ? ((BigDecimal) normalizedValue).intValue() + normalizedValue = (normalizedValue instanceof BigDecimal bd) ? bd.intValue() : Integer.valueOf(normalizedValue.toString()); } else if (widgetImpl.eGet(feature, false, false) instanceof Boolean && !(normalizedValue instanceof Boolean)) { diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java index 672408e4fef..1862082e2e8 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java @@ -108,6 +108,7 @@ * @author Stefan Triller - Method to convert a state into something a sitemap entity can understand * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType * @author Laurent Garnier - new method getIconColor + * @author Mark Herwege - new method getFormatPattern(widget), clean pattern */ @NonNullByDefault @Component(immediate = true, configurationPid = "org.openhab.sitemap", // @@ -121,9 +122,9 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { protected static final Pattern EXTRACT_TRANSFORM_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\):(.*)"); /* RegEx to identify format patterns. See java.util.Formatter#formatSpecifier (without the '%' at the very end). */ - protected static final String IDENTIFY_FORMAT_PATTERN_PATTERN = "%((unit%)|((\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z])))"; + protected static final String IDENTIFY_FORMAT_PATTERN_PATTERN = "%(?:(unit%)|(?:(?:\\d+\\$)?(?:[-#+ 0,(<]*)?(?:\\d+)?(?:\\.\\d+)?(?:[tT])?(?:[a-zA-Z])))"; + private static final Pattern FORMAT_PATTERN = Pattern.compile("(?:^|[^%])" + IDENTIFY_FORMAT_PATTERN_PATTERN); - private static final Pattern LABEL_PATTERN = Pattern.compile(".*?\\[.*? (.*?)]"); private static final int MAX_BUTTONS = 4; private static final String DEFAULT_SORTING = "NONE"; @@ -331,10 +332,14 @@ private Switch createPlayerButtons() { String labelMappedOption = null; State state = null; StateDescription stateDescription = null; - String formatPattern = getFormatPattern(label); + String formatPattern = getFormatPattern(w); + + if (formatPattern != null && label.indexOf("[") < 0) { + label = label + " [" + formatPattern + "]"; + } // now insert the value, if the state is a string or decimal value and there is some formatting pattern defined - // in the label (i.e. it contains at least a %) + // in the label or state description (i.e. it contains at least a %) try { final Item item = getItem(itemName); @@ -348,13 +353,8 @@ private Switch createPlayerButtons() { // returned StateDescription. What is expected is the display of a value using the pattern // provided by the channel state description provider. stateDescription = item.getStateDescription(); - if (formatPattern == null && stateDescription != null && stateDescription.getPattern() != null) { - label = label + " [" + stateDescription.getPattern() + "]"; - } - String updatedPattern = getFormatPattern(label); - if (updatedPattern != null) { - formatPattern = updatedPattern; + if (formatPattern != null) { state = item.getState(); if (formatPattern.contains("%d")) { @@ -367,11 +367,11 @@ private Switch createPlayerButtons() { // for fraction digits in state we don't want to risk format exceptions, // so treat everything as floats: - formatPattern = formatPattern.replaceAll("%d", "%.0f"); + formatPattern = formatPattern.replace("%d", "%.0f"); } } } catch (ItemNotFoundException e) { - logger.error("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); + logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); } boolean considerTransform = false; @@ -410,8 +410,7 @@ private Switch createPlayerButtons() { if (formatPattern.contains(UnitUtils.UNIT_PLACEHOLDER)) { formatPattern = formatPattern.replaceAll(UnitUtils.UNIT_PLACEHOLDER, "").stripTrailing(); } - } else if (state instanceof QuantityType) { - QuantityType quantityState = (QuantityType) state; + } else if (state instanceof QuantityType quantityState) { // sanity convert current state to the item state description unit in case it was updated in the // meantime. The item state is still in the "original" unit while the state description will // display the new unit: @@ -425,10 +424,10 @@ private Switch createPlayerButtons() { quantityState = convertStateToWidgetUnit(quantityState, w); state = quantityState; } - } else if (state instanceof DateTimeType) { + } else if (state instanceof DateTimeType type) { // Translate a DateTimeType state to the local time zone try { - state = ((DateTimeType) state).toLocaleZone(); + state = type.toLocaleZone(); } catch (DateTimeException ignored) { } } @@ -456,7 +455,10 @@ private Switch createPlayerButtons() { } label = label.trim(); - label = label.substring(0, label.indexOf("[") + 1) + formatPattern + "]"; + int index = label.indexOf("["); + if (index >= 0) { + label = label.substring(0, index + 1) + formatPattern + "]"; + } } } @@ -464,7 +466,7 @@ private Switch createPlayerButtons() { } private QuantityType convertStateToWidgetUnit(QuantityType quantityState, Widget w) { - Unit widgetUnit = UnitUtils.parseUnit(getFormatPattern(w.getLabel())); + Unit widgetUnit = UnitUtils.parseUnit(getFormatPattern(w)); if (widgetUnit != null && !widgetUnit.equals(quantityState.getUnit())) { return Objects.requireNonNullElse(quantityState.toInvertibleUnit(widgetUnit), quantityState); } @@ -472,6 +474,56 @@ private QuantityType convertStateToWidgetUnit(QuantityType quantityState, return quantityState; } + @Override + public @Nullable String getFormatPattern(Widget w) { + String label = getLabelFromWidget(w); + String pattern = getFormatPattern(label); + String itemName = w.getItem(); + try { + Item item = null; + if (itemName != null && !itemName.isBlank()) { + item = getItem(itemName); + } + if (item != null && pattern == null) { + StateDescription stateDescription = item.getStateDescription(); + if (stateDescription != null) { + pattern = stateDescription.getPattern(); + } + } + + if (pattern == null) { + return null; + } + + // remove last part of pattern, after unit, if it exists, as this is not valid and creates problems with + // updates + if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) { + Matcher m = FORMAT_PATTERN.matcher(pattern); + int matcherEnd = 0; + if (m.find() && m.group(1) == null) { + matcherEnd = m.end(); + } + String unit = pattern.substring(matcherEnd).trim(); + String postfix = ""; + int unitEnd = unit.indexOf(" "); + if (unitEnd > -1) { + postfix = unit.substring(unitEnd + 1).trim(); + unit = unit.substring(0, unitEnd); + } + if (!postfix.isBlank()) { + logger.warn( + "Item '{}' with unit, nothing allowed after unit in label pattern '{}', dropping postfix", + itemName, pattern); + } + pattern = pattern.substring(0, matcherEnd) + (!unit.isBlank() ? " " + unit : ""); + } + } catch (ItemNotFoundException e) { + logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); + } + + return pattern; + } + private @Nullable String getFormatPattern(@Nullable String label) { if (label == null) { return null; @@ -609,7 +661,7 @@ private String transform(String label, boolean matchTransform, @Nullable String Item item = getItem(itemName); return convertState(w, item, item.getState()); } catch (ItemNotFoundException e) { - logger.error("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); + logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); } } return UnDefType.UNDEF; @@ -628,8 +680,8 @@ private String transform(String label, boolean matchTransform, @Nullable String State returnState = null; State itemState = i.getState(); - if (itemState instanceof QuantityType) { - itemState = convertStateToWidgetUnit((QuantityType) itemState, w); + if (itemState instanceof QuantityType quantityTypeState) { + itemState = convertStateToWidgetUnit(quantityTypeState, w); } if (w instanceof Switch && i instanceof RollershutterItem) { @@ -638,11 +690,10 @@ private String transform(String label, boolean matchTransform, @Nullable String } else if (w instanceof Slider) { if (i.getAcceptedDataTypes().contains(PercentType.class)) { returnState = itemState.as(PercentType.class); - } else { + } else if (!(itemState instanceof QuantityType)) { returnState = itemState.as(DecimalType.class); } - } else if (w instanceof Switch) { - Switch sw = (Switch) w; + } else if (w instanceof Switch sw) { if (sw.getMappings().isEmpty()) { returnState = itemState.as(OnOffType.class); } @@ -698,8 +749,8 @@ public EList getChildren(Sitemap sitemap) { @Override public EList getChildren(LinkableWidget w) { EList widgets; - if (w instanceof Group && w.getChildren().isEmpty()) { - widgets = getDynamicGroupChildren((Group) w); + if (w instanceof Group group && w.getChildren().isEmpty()) { + widgets = getDynamicGroupChildren(group); } else { widgets = w.getChildren(); } @@ -761,8 +812,7 @@ private EList getDynamicGroupChildren(Group group) { try { if (itemName != null) { Item item = getItem(itemName); - if (item instanceof GroupItem) { - GroupItem groupItem = (GroupItem) item; + if (item instanceof GroupItem groupItem) { List members = new ArrayList<>(groupItem.getMembers()); switch (groupMembersSorting) { case "LABEL": @@ -991,8 +1041,8 @@ private boolean matchStateToValue(State state, String value, @Nullable String ma try { double compareDoubleValue = Double.parseDouble(unquotedValue); double stateDoubleValue; - if (state instanceof DecimalType) { - stateDoubleValue = ((DecimalType) state).doubleValue(); + if (state instanceof DecimalType type) { + stateDoubleValue = type.doubleValue(); } else { stateDoubleValue = ((QuantityType) state).doubleValue(); } @@ -1032,8 +1082,8 @@ private boolean matchStateToValue(State state, String value, @Nullable String ma } catch (NumberFormatException e) { logger.debug("matchStateToValue: Decimal format exception: ", e); } - } else if (state instanceof DateTimeType) { - ZonedDateTime val = ((DateTimeType) state).getZonedDateTime(); + } else if (state instanceof DateTimeType type) { + ZonedDateTime val = type.getZonedDateTime(); ZonedDateTime now = ZonedDateTime.now(); long secsDif = ChronoUnit.SECONDS.between(val, now); @@ -1211,7 +1261,7 @@ public boolean getVisiblity(Widget w) { try { item = itemRegistry.getItem(itemName); } catch (ItemNotFoundException e) { - logger.error("Cannot retrieve visibility item {} for widget {}", rule.getItem(), + logger.warn("Cannot retrieve visibility item {} for widget {}", rule.getItem(), w.eClass().getInstanceTypeName()); // Default to visible! @@ -1323,25 +1373,29 @@ public void removeRegistryHook(RegistryHook hook) { @Override public @Nullable String getUnitForWidget(Widget w) { - try { - Item item = getItem(w.getItem()); + String itemName = w.getItem(); + if (itemName != null) { + try { + Item item = getItem(itemName); - // we require the item to define a dimension, otherwise no unit will be reported to the UIs. - if (item instanceof NumberItem && ((NumberItem) item).getDimension() != null) { - if (w.getLabel() == null) { - // if no Label was assigned to the Widget we fallback to the items unit - return ((NumberItem) item).getUnitSymbol(); - } + // we require the item to define a dimension, otherwise no unit will be reported to the UIs. + if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) { + String pattern = getFormatPattern(w); + if (pattern == null || pattern.isBlank()) { + // if no Label was assigned to the Widget we fallback to the items unit + return numberItem.getUnitSymbol(); + } - String unit = getUnitFromLabel(w.getLabel()); - if (!UnitUtils.UNIT_PLACEHOLDER.equals(unit)) { - return unit; - } + String unit = getUnitFromPattern(pattern); + if (!UnitUtils.UNIT_PLACEHOLDER.equals(unit)) { + return unit; + } - return ((NumberItem) item).getUnitSymbol(); + return numberItem.getUnitSymbol(); + } + } catch (ItemNotFoundException e) { + logger.warn("Failed to retrieve item during widget rendering, item does not exist: {}", e.getMessage()); } - } catch (ItemNotFoundException e) { - logger.debug("Failed to retrieve item during widget rendering: {}", e.getMessage()); } return ""; @@ -1357,14 +1411,13 @@ public void removeRegistryHook(RegistryHook hook) { return state; } - private @Nullable String getUnitFromLabel(@Nullable String label) { - if (label == null || label.isBlank()) { + private @Nullable String getUnitFromPattern(@Nullable String format) { + if (format == null || format.isBlank()) { return null; } - Matcher m = LABEL_PATTERN.matcher(label); - if (m.matches()) { - return m.group(1); - } - return null; + int index = format.lastIndexOf(" "); + String unit = index > 0 ? format.substring(index + 1) : null; + unit = UnitUtils.UNIT_PERCENT_FORMAT_STRING.equals(unit) ? "%" : unit; + return unit; } } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/proxy/ProxyServletService.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/proxy/ProxyServletService.java index 9cd464dba0a..e1fbc1d2952 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/proxy/ProxyServletService.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/proxy/ProxyServletService.java @@ -243,10 +243,10 @@ URI uriFromRequest(HttpServletRequest request) { } String uriString = null; - if (widget instanceof Image) { - uriString = ((Image) widget).getUrl(); - } else if (widget instanceof Video) { - uriString = ((Video) widget).getUrl(); + if (widget instanceof Image image) { + uriString = image.getUrl(); + } else if (widget instanceof Video video) { + uriString = video.getUrl(); } else { throw new ProxyServletException(HttpServletResponse.SC_FORBIDDEN, String.format("Widget type '%s' is not supported!", widget.getClass().getName())); diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIProvider.java index 5273cab8991..9303ac87c96 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIProvider.java @@ -32,7 +32,8 @@ public interface ItemUIProvider { * @param itemName the name of the item to return the icon for * @return the name of the category to use or null if undefined. */ - public @Nullable String getCategory(String itemName); + @Nullable + String getCategory(String itemName); /** * Returns the label text to be used for an item in the UI. @@ -40,7 +41,8 @@ public interface ItemUIProvider { * @param item the name of the item to return the label text for * @return the label text to be used in the UI or null if undefined. */ - public @Nullable String getLabel(String itemName); + @Nullable + String getLabel(String itemName); /** * Provides a default widget for a given item (class). This is used whenever @@ -52,7 +54,8 @@ public interface ItemUIProvider { * @return a widget implementation that can be used for the given item or null, if no default is available for the * type */ - public @Nullable Widget getDefaultWidget(@Nullable Class itemType, String itemName); + @Nullable + Widget getDefaultWidget(@Nullable Class itemType, String itemName); /** *

@@ -66,5 +69,6 @@ public interface ItemUIProvider { * @param itemName the item name to get the widget for * @return a widget to use for the given item or null if sitemap should not be overridden. */ - public @Nullable Widget getWidget(String itemName); + @Nullable + Widget getWidget(String itemName); } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java index 998a11d3c4c..a979543d4cf 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java @@ -34,6 +34,7 @@ * @author Kai Kreuzer - Initial contribution * @author Chris Jackson - Initial contribution * @author Laurent Garnier - new method getIconColor + * @author Mark Herwege - new method getFormatPattern */ @NonNullByDefault public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider { @@ -130,6 +131,15 @@ public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider { @Nullable EObject getParent(Widget w); + /** + * Gets the format pattern for the widget value, retrieved from widget label, item label or item state description + * + * @param w Widget + * @return String with the format pattern + */ + @Nullable + String getFormatPattern(Widget w); + /** * Gets the label color for the widget. Checks conditional statements to * find the color based on the item value diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/tiles/TileProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/tiles/TileProvider.java index 8e45677e281..911a06f12fb 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/tiles/TileProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/tiles/TileProvider.java @@ -23,5 +23,5 @@ */ @NonNullByDefault public interface TileProvider { - public Stream getTiles(); + Stream getTiles(); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSListener.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSListener.java index 2b9fd5835b3..cf752e4bc05 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSListener.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSListener.java @@ -31,5 +31,5 @@ public interface KSListener { * * @param ksEvent The {@link KSEvent} fired by the {@link KSService} */ - public void ksEventReceived(KSEvent ksEvent); + void ksEventReceived(KSEvent ksEvent); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSService.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSService.java index 61a2dc930e2..1e220f390a8 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSService.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSService.java @@ -34,7 +34,7 @@ public interface KSService { * * @return an id that identifies this service */ - public String getId(); + String getId(); /** * Returns a localized human readable label that can be used within UIs. @@ -42,21 +42,21 @@ public interface KSService { * @param locale the locale to provide the label for * @return a localized string to be used in UIs */ - public String getLabel(@Nullable Locale locale); + String getLabel(@Nullable Locale locale); /** * Obtain the Locales available from this KSService * * @return The Locales available from this service */ - public Set getSupportedLocales(); + Set getSupportedLocales(); /** * Obtain the audio formats supported by this KSService * * @return The audio formats supported by this service */ - public Set getSupportedFormats(); + Set getSupportedFormats(); /** * This method starts the process of keyword spotting @@ -84,6 +84,6 @@ public interface KSService { * @return A {@link KSServiceHandle} used to abort keyword spotting * @throws A {@link KSException} if any parameter is invalid or a problem occurs */ - public KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword) + KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword) throws KSException; } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSServiceHandle.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSServiceHandle.java index 289fa590a14..4a8e7f58ab8 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSServiceHandle.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/KSServiceHandle.java @@ -24,5 +24,5 @@ public interface KSServiceHandle { /** * Aborts keyword spotting in the associated {@link KSService} */ - public void abort(); + void abort(); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTListener.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTListener.java index 4234bcd887f..967041d9e8c 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTListener.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTListener.java @@ -32,5 +32,5 @@ public interface STTListener { * * @param sttEvent The {@link STTEvent} fired by the {@link STTService} */ - public void sttEventReceived(STTEvent sttEvent); + void sttEventReceived(STTEvent sttEvent); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTService.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTService.java index 02241e69f98..5f494e10603 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTService.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTService.java @@ -33,7 +33,7 @@ public interface STTService { * * @return an id that identifies this service */ - public String getId(); + String getId(); /** * Returns a localized human readable label that can be used within UIs. @@ -41,21 +41,21 @@ public interface STTService { * @param locale the locale to provide the label for * @return a localized string to be used in UIs */ - public String getLabel(@Nullable Locale locale); + String getLabel(@Nullable Locale locale); /** * Obtain the Locales available from this STTService * * @return The Locales available from this service */ - public Set getSupportedLocales(); + Set getSupportedLocales(); /** * Obtain the audio formats supported by this STTService * * @return The audio formats supported by this service */ - public Set getSupportedFormats(); + Set getSupportedFormats(); /** * This method starts the process of speech recognition. @@ -85,6 +85,6 @@ public interface STTService { * @return A {@link STTServiceHandle} used to abort recognition * @throws A {@link SSTException} if any parameter is invalid or a STT problem occurs */ - public STTServiceHandle recognize(STTListener sttListener, AudioStream audioStream, Locale locale, - Set grammars) throws STTException; + STTServiceHandle recognize(STTListener sttListener, AudioStream audioStream, Locale locale, Set grammars) + throws STTException; } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTServiceHandle.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTServiceHandle.java index 11aacbd6758..58773fe53ba 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTServiceHandle.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/STTServiceHandle.java @@ -24,5 +24,5 @@ public interface STTServiceHandle { /** * Aborts recognition in the associated {@link STTService} */ - public void abort(); + void abort(); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/Voice.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/Voice.java index 86851c493d4..c64677aeacc 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/Voice.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/Voice.java @@ -30,19 +30,19 @@ public interface Voice { * * @return A String uniquely identifying the voice. */ - public String getUID(); + String getUID(); /** * The voice label, usually used for GUIs * * @return The voice label, may not be globally unique */ - public String getLabel(); + String getLabel(); /** * Locale of the voice * * @return Locale of the voice */ - public Locale getLocale(); + Locale getLocale(); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java index bfa9c2a5bd5..d34aa774b13 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java @@ -14,12 +14,10 @@ import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.audio.AudioSink; import org.openhab.core.audio.AudioSource; import org.openhab.core.library.types.PercentType; import org.openhab.core.voice.text.HumanLanguageInterpreter; @@ -138,71 +136,9 @@ public interface VoiceManager { DialogContext getLastDialogContext(); /** - * Starts an infinite dialog sequence using all default services: keyword spotting on the default audio source, - * audio source listening to retrieve a question or a command (default Speech to Text service), interpretation and - * handling of the command, and finally playback of the answer on the default audio sink (default Text to Speech - * service). - * - * Only one dialog can be started for the default audio source. - * - * @throws IllegalStateException if required services are not all available or the default locale is not supported - * by all these services or a dialog is already started for the default audio source - */ - @Deprecated - void startDialog() throws IllegalStateException; - - /** - * Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve - * a question or a command (Speech to Text service), interpretation and handling of the command, and finally - * playback of the answer on the audio sink (Text to Speech service). - * - * Only one dialog can be started for an audio source. - * - * @param ks the keyword spotting service to use or null to use the default service - * @param stt the speech-to-text service to use or null to use the default service - * @param tts the text-to-speech service to use or null to use the default service - * @param hli the human language text interpreter to use or null to use the default service - * @param source the audio source to use or null to use the default source - * @param sink the audio sink to use or null to use the default sink - * @param locale the locale to use or null to use the default locale - * @param keyword the keyword to use during keyword spotting or null to use the default keyword - * @param listeningItem the item to switch ON while listening to a question - * @throws IllegalStateException if required services are not all available or the provided locale is not supported - * by all these services or a dialog is already started for this audio source - */ - @Deprecated - void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts, - @Nullable HumanLanguageInterpreter hli, @Nullable AudioSource source, @Nullable AudioSink sink, - @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem) - throws IllegalStateException; - - /** - * Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve - * a question or a command (Speech to Text service), interpretation and handling of the command, and finally - * playback of the answer on the audio sink (Text to Speech service). - * - * Only one dialog can be started for an audio source. - * - * @param ks the keyword spotting service to use or null to use the default service - * @param stt the speech-to-text service to use or null to use the default service - * @param tts the text-to-speech service to use or null to use the default service - * @param voice the voice to use or null to use the default voice or any voice provided by the text-to-speech - * service matching the locale - * @param hlis list of human language text interpreters to use, they are executed in order until the first - * successful response, or empty to use the default service - * @param source the audio source to use or null to use the default source - * @param sink the audio sink to use or null to use the default sink - * @param locale the locale to use or null to use the default locale - * @param keyword the keyword to use during keyword spotting or null to use the default keyword - * @param listeningItem the item to switch ON while listening to a question - * @throws IllegalStateException if required services are not all available or the provided locale is not supported - * by all these services or a dialog is already started for this audio source + * Returns a list with the contexts of all running dialogs. */ - @Deprecated - void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts, @Nullable Voice voice, - List hlis, @Nullable AudioSource source, @Nullable AudioSink sink, - @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem) - throws IllegalStateException; + List getDialogsContexts(); /** * Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve @@ -233,44 +169,6 @@ void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTS */ void stopDialog(DialogContext context) throws IllegalStateException; - /** - * Executes a simple dialog sequence without keyword spotting using all default services: default audio source - * listening to retrieve a question or a command (default Speech to Text service), interpretation and handling of - * the command, and finally playback of the answer on the default audio sink (default Text to Speech service). - * - * Only possible if no dialog processor is already started for the default audio source. - * - * @throws IllegalStateException if required services are not all available or the provided default locale is not - * supported by all these services or a dialog is already started for the default audio source - */ - @Deprecated - void listenAndAnswer() throws IllegalStateException; - - /** - * Executes a simple dialog sequence without keyword spotting: audio source listening to retrieve a question or a - * command (Speech to Text service), interpretation and handling of the command, and finally playback of the - * answer on the audio sink (Text to Speech service). - * - * Only possible if no dialog processor is already started for the audio source. - * - * @param stt the speech-to-text service to use or null to use the default service - * @param tts the text-to-speech service to use or null to use the default service - * @param voice the voice to use or null to use the default voice or any voice provided by the text-to-speech - * service matching the locale - * @param hlis list of human language text interpreters to use, they are executed in order until the first - * successful response, or empty to use the default service - * @param source the audio source to use or null to use the default source - * @param sink the audio sink to use or null to use the default sink - * @param locale the locale to use or null to use the default locale - * @param listeningItem the item to switch ON while listening to a question - * @throws IllegalStateException if required services are not all available or the provided locale is not supported - * by all these services or a dialog is already started for this audio source - */ - @Deprecated - void listenAndAnswer(@Nullable STTService stt, @Nullable TTSService tts, @Nullable Voice voice, - List hlis, @Nullable AudioSource source, @Nullable AudioSink sink, - @Nullable Locale locale, @Nullable String listeningItem) throws IllegalStateException; - /** * Executes a simple dialog sequence without keyword spotting: audio source listening to retrieve a question or a * command (Speech to Text service), interpretation and handling of the command, and finally playback of the @@ -290,7 +188,6 @@ void listenAndAnswer(@Nullable STTService stt, @Nullable TTSService tts, @Nullab * Only one registration can be done for an audio source. * * @param registration with the desired services ids and options for the dialog - * * @throws IllegalStateException if there is another registration for the same source */ void registerDialog(DialogRegistration registration) throws IllegalStateException; diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java index 7fd5a8bfa24..f23b962140f 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java @@ -223,6 +223,13 @@ public void stop() { eventListener.onDialogStopped(dialogContext); } + /** + * Returns the dialog context used to start this processor. + */ + public DialogContext getContext() { + return dialogContext; + } + /** * Indicates if voice recognition is running. */ @@ -294,9 +301,8 @@ public void ksEventReceived(KSEvent ksEvent) { } catch (IllegalStateException e) { logger.warn("{}", e.getMessage()); } - } else if (ksEvent instanceof KSErrorEvent) { + } else if (ksEvent instanceof KSErrorEvent kse) { logger.debug("KSErrorEvent event received"); - KSErrorEvent kse = (KSErrorEvent) ksEvent; String text = i18nProvider.getText(bundle, "error.ks-error", null, dialogContext.locale()); say(text == null ? kse.getMessage() : text.replace("{0}", kse.getMessage())); } @@ -305,10 +311,9 @@ public void ksEventReceived(KSEvent ksEvent) { @Override public synchronized void sttEventReceived(STTEvent sttEvent) { - if (sttEvent instanceof SpeechRecognitionEvent) { + if (sttEvent instanceof SpeechRecognitionEvent sre) { logger.debug("SpeechRecognitionEvent event received"); if (!isSTTServerAborting) { - SpeechRecognitionEvent sre = (SpeechRecognitionEvent) sttEvent; String question = sre.getTranscript(); logger.debug("Text recognized: {}", question); toggleProcessing(false); @@ -335,12 +340,11 @@ public synchronized void sttEventReceived(STTEvent sttEvent) { } else if (sttEvent instanceof RecognitionStopEvent) { logger.debug("RecognitionStopEvent event received"); toggleProcessing(false); - } else if (sttEvent instanceof SpeechRecognitionErrorEvent) { + } else if (sttEvent instanceof SpeechRecognitionErrorEvent sre) { logger.debug("SpeechRecognitionErrorEvent event received"); if (!isSTTServerAborting) { abortSTT(); toggleProcessing(false); - SpeechRecognitionErrorEvent sre = (SpeechRecognitionErrorEvent) sttEvent; String text = i18nProvider.getText(bundle, "error.stt-error", null, dialogContext.locale()); say(text == null ? sre.getMessage() : text.replace("{0}", sre.getMessage())); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java index c29b0091b57..c4eb1ff0941 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -33,6 +34,7 @@ import org.openhab.core.items.ItemNotUniqueException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.voice.DialogContext; +import org.openhab.core.voice.DialogRegistration; import org.openhab.core.voice.KSService; import org.openhab.core.voice.STTService; import org.openhab.core.voice.TTSService; @@ -60,7 +62,11 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio private static final String SUBCMD_VOICES = "voices"; private static final String SUBCMD_START_DIALOG = "startdialog"; private static final String SUBCMD_STOP_DIALOG = "stopdialog"; + private static final String SUBCMD_REGISTER_DIALOG = "registerdialog"; + private static final String SUBCMD_UNREGISTER_DIALOG = "unregisterdialog"; private static final String SUBCMD_LISTEN_ANSWER = "listenandanswer"; + private static final String SUBCMD_DIALOGS = "dialogs"; + private static final String SUBCMD_DIALOG_REGS = "dialogregs"; private static final String SUBCMD_INTERPRETERS = "interpreters"; private static final String SUBCMD_KEYWORD_SPOTTERS = "keywordspotters"; private static final String SUBCMD_STT_SERVICES = "sttservices"; @@ -87,13 +93,21 @@ public List getUsages() { return List.of(buildCommandUsage(SUBCMD_SAY + " ", "speaks a text"), buildCommandUsage(SUBCMD_INTERPRET + " ", "interprets a human language command"), buildCommandUsage(SUBCMD_VOICES, "lists available voices of the TTS services"), + buildCommandUsage(SUBCMD_DIALOGS, "lists the running dialog and their audio/voice services"), + buildCommandUsage(SUBCMD_DIALOG_REGS, + "lists the existing dialog registrations and their selected audio/voice services"), + buildCommandUsage(SUBCMD_REGISTER_DIALOG + + " [--source ] [--sink ] [--hlis ] [--tts [--voice ]] [--stt ] [--ks ks [--keyword ]] [--listening-item ]", + "register a new dialog processing using the default services or the services identified with provided arguments, it will be persisted and keep running whenever is possible."), + buildCommandUsage(SUBCMD_UNREGISTER_DIALOG + " [source]", + "unregister the dialog processing for the default audio source or the audio source identified with provided argument, stopping it if started"), buildCommandUsage(SUBCMD_START_DIALOG - + " [--source ] [--sink ] [--interpreters ] [--tts [--voice ]] [--stt ] [--ks ks [--keyword ]] [--listening-item ]", + + " [--source ] [--sink ] [--hlis ] [--tts [--voice ]] [--stt ] [--ks ks [--keyword ]] [--listening-item ]", "start a new dialog processing using the default services or the services identified with provided arguments"), buildCommandUsage(SUBCMD_STOP_DIALOG + " []", "stop the dialog processing for the default audio source or the audio source identified with provided argument"), buildCommandUsage(SUBCMD_LISTEN_ANSWER - + " [--source ] [--sink ] [--interpreters ] [--tts [--voice ]] [--stt ] [--listening-item ]", + + " [--source ] [--sink ] [--hlis ] [--tts [--voice ]] [--stt ] [--listening-item ]", "Execute a simple dialog sequence without keyword spotting using the default services or the services identified with provided arguments"), buildCommandUsage(SUBCMD_INTERPRETERS, "lists the interpreters"), buildCommandUsage(SUBCMD_KEYWORD_SPOTTERS, "lists the keyword spotters"), @@ -135,10 +149,41 @@ public void execute(String[] args, Console console) { } return; } + case SUBCMD_REGISTER_DIALOG -> { + DialogRegistration dialogRegistration; + try { + dialogRegistration = parseDialogRegistration(args); + } catch (IllegalStateException e) { + console.println(Objects.requireNonNullElse(e.getMessage(), + "An error occurred while parsing the dialog options")); + break; + } + try { + voiceManager.registerDialog(dialogRegistration); + } catch (IllegalStateException e) { + console.println(Objects.requireNonNullElse(e.getMessage(), + "An error occurred while registering the dialog")); + } + return; + } + case SUBCMD_UNREGISTER_DIALOG -> { + try { + var sourceId = args.length < 2 ? audioManager.getSourceId() : args[1]; + if (sourceId == null) { + console.println("No source provided nor default source available"); + break; + } + voiceManager.unregisterDialog(sourceId); + } catch (IllegalStateException e) { + console.println(Objects.requireNonNullElse(e.getMessage(), + "An error occurred while stopping the dialog")); + } + return; + } case SUBCMD_START_DIALOG -> { DialogContext.Builder dialogContextBuilder; try { - dialogContextBuilder = parseDialogParameters(args); + dialogContextBuilder = parseDialogContext(args); } catch (IllegalStateException e) { console.println(Objects.requireNonNullElse(e.getMessage(), "An error occurred while parsing the dialog options")); @@ -164,7 +209,7 @@ public void execute(String[] args, Console console) { case SUBCMD_LISTEN_ANSWER -> { DialogContext.Builder dialogContextBuilder; try { - dialogContextBuilder = parseDialogParameters(args); + dialogContextBuilder = parseDialogContext(args); } catch (IllegalStateException e) { console.println(Objects.requireNonNullElse(e.getMessage(), "An error occurred while parsing the dialog options")); @@ -178,6 +223,14 @@ public void execute(String[] args, Console console) { } return; } + case SUBCMD_DIALOGS -> { + listDialogs(console); + return; + } + case SUBCMD_DIALOG_REGS -> { + listDialogRegistrations(console); + return; + } case SUBCMD_INTERPRETERS -> { listInterpreters(console); return; @@ -252,6 +305,42 @@ private void say(String[] args, Console console) { voiceManager.say(msg.toString()); } + private void listDialogRegistrations(Console console) { + Collection registrations = voiceManager.getDialogRegistrations(); + if (!registrations.isEmpty()) { + registrations.stream().sorted(comparing(dr -> dr.sourceId)).forEach(dr -> { + console.println( + String.format(" Source: %s - Sink: %s (STT: %s, TTS: %s, HLIs: %s, KS: %s, Keyword: %s)", + dr.sourceId, dr.sinkId, getOrDefault(dr.sttId), getOrDefault(dr.ttsId), + dr.hliIds.isEmpty() ? getOrDefault(null) : String.join("->", dr.hliIds), + getOrDefault(dr.ksId), getOrDefault(dr.keyword))); + }); + } else { + console.println("No dialog registrations."); + } + } + + private String getOrDefault(@Nullable String value) { + return value != null && !value.isBlank() ? value : "**Default**"; + } + + private void listDialogs(Console console) { + Collection dialogContexts = voiceManager.getDialogsContexts(); + if (!dialogContexts.isEmpty()) { + dialogContexts.stream().sorted(comparing(s -> s.source().getId())).forEach(c -> { + var ks = c.ks(); + String ksText = ks != null ? String.format(", KS: %s, Keyword: %s", ks.getId(), c.keyword()) : ""; + console.println( + String.format(" Source: %s - Sink: %s (STT: %s, TTS: %s, HLIs: %s%s)", c.source().getId(), + c.sink().getId(), c.stt().getId(), c.tts().getId(), c.hlis().stream() + .map(HumanLanguageInterpreter::getId).collect(Collectors.joining("->")), + ksText)); + }); + } else { + console.println("No running dialogs."); + } + } + private void listInterpreters(Console console) { Collection interpreters = voiceManager.getHLIs(); if (!interpreters.isEmpty()) { @@ -314,11 +403,7 @@ private void listTTSs(Console console) { .orElse(null); } - private DialogContext.Builder parseDialogParameters(String[] args) { - var dialogContextBuilder = voiceManager.getDialogContextBuilder(); - if (args.length < 2) { - return dialogContextBuilder; - } + private HashMap parseDialogParameters(String[] args) { var parameters = new HashMap(); for (int i = 1; i < args.length; i++) { var arg = args[i].trim(); @@ -333,6 +418,15 @@ private DialogContext.Builder parseDialogParameters(String[] args) { throw new IllegalStateException("Argument name should start by -- " + arg); } } + return parameters; + } + + private DialogContext.Builder parseDialogContext(String[] args) { + var dialogContextBuilder = voiceManager.getDialogContextBuilder(); + if (args.length < 2) { + return dialogContextBuilder; + } + var parameters = parseDialogParameters(args); String sourceId = parameters.remove("source"); if (sourceId != null) { var source = audioManager.getSource(sourceId); @@ -363,4 +457,40 @@ private DialogContext.Builder parseDialogParameters(String[] args) { } return dialogContextBuilder; } + + private DialogRegistration parseDialogRegistration(String[] args) { + var parameters = parseDialogParameters(args); + @Nullable + String sourceId = parameters.remove("source"); + if (sourceId == null) { + sourceId = audioManager.getSourceId(); + } + if (sourceId == null) { + throw new IllegalStateException("A source is required if the default is not configured"); + } + @Nullable + String sinkId = parameters.remove("sink"); + if (sinkId == null) { + sinkId = audioManager.getSinkId(); + } + if (sinkId == null) { + throw new IllegalStateException("A sink is required if the default is not configured"); + } + var dr = new DialogRegistration(sourceId, sinkId); + dr.ksId = parameters.remove("ks"); + dr.keyword = parameters.remove("keyword"); + dr.sttId = parameters.remove("stt"); + dr.ttsId = parameters.remove("tts"); + dr.voiceId = parameters.remove("voice"); + dr.listeningItem = parameters.remove("listening-item"); + String hliIds = parameters.remove("hlis"); + if (hliIds != null) { + dr.hliIds = Arrays.stream(hliIds.split(",")).map(String::trim).collect(Collectors.toList()); + } + if (!parameters.isEmpty()) { + throw new IllegalStateException( + "Argument " + parameters.keySet().stream().findAny().orElse("") + " is not supported"); + } + return dr; + } } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java index 105eb6e7d75..c70a2d84c22 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java @@ -12,7 +12,6 @@ */ package org.openhab.core.voice.internal; -import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -41,8 +40,6 @@ import org.openhab.core.audio.AudioSink; import org.openhab.core.audio.AudioSource; import org.openhab.core.audio.AudioStream; -import org.openhab.core.audio.UnsupportedAudioFormatException; -import org.openhab.core.audio.UnsupportedAudioStreamException; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.config.core.ConfigOptionProvider; import org.openhab.core.config.core.ConfigurableService; @@ -272,39 +269,12 @@ public void say(String text, @Nullable String voiceId, @Nullable String sinkId, throw new TTSException( "Failed playing audio stream '" + audioStream + "' as audio sink doesn't support it"); } - - 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); - } 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); - } - } - } - } catch (TTSException | UnsupportedAudioFormatException | UnsupportedAudioStreamException e) { + Runnable restoreVolume = audioManager.handleVolumeCommand(volume, sink); + sink.processAndComplete(audioStream).exceptionally(exception -> { + logger.warn("Error playing '{}': {}", audioStream, exception.getMessage(), exception); + return null; + }).thenRun(restoreVolume); + } catch (TTSException e) { if (logger.isDebugEnabled()) { logger.debug("Error saying '{}': {}", text, e.getMessage(), e); } else { @@ -526,64 +496,13 @@ public DialogContext.Builder getDialogContextBuilder() { } @Override - public @Nullable DialogContext getLastDialogContext() { - return lastDialogContext; - } - - @Override - @Deprecated - public void startDialog() throws IllegalStateException { - startDialog(null, null, null, null, List.of(), null, null, null, this.keyword, this.listeningItem); + public List getDialogsContexts() { + return dialogProcessors.values().stream().map(DialogProcessor::getContext).collect(Collectors.toList()); } @Override - @Deprecated - public void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts, - @Nullable HumanLanguageInterpreter hli, @Nullable AudioSource source, @Nullable AudioSink sink, - @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem) - throws IllegalStateException { - startDialog(ks, stt, tts, null, hli == null ? List.of() : List.of(hli), source, sink, locale, keyword, - listeningItem); - } - - @Override - @Deprecated - public void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts, - @Nullable Voice voice, List hlis, @Nullable AudioSource source, - @Nullable AudioSink sink, @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem) - throws IllegalStateException { - var builder = getDialogContextBuilder(); - if (ks != null) { - builder.withKS(ks); - } - if (keyword != null) { - builder.withKeyword(keyword); - } - if (stt != null) { - builder.withSTT(stt); - } - if (tts != null) { - builder.withTTS(tts); - } - if (voice != null) { - builder.withVoice(voice); - } - if (!hlis.isEmpty()) { - builder.withHLIs(hlis); - } - if (source != null) { - builder.withSource(source); - } - if (sink != null) { - builder.withSink(sink); - } - if (locale != null) { - builder.withLocale(locale); - } - if (listeningItem != null) { - builder.withListeningItem(listeningItem); - } - startDialog(builder.build()); + public @Nullable DialogContext getLastDialogContext() { + return lastDialogContext; } @Override @@ -645,42 +564,6 @@ public void stopDialog(DialogContext context) throws IllegalStateException { stopDialog(context.source()); } - @Override - @Deprecated - public void listenAndAnswer() throws IllegalStateException { - listenAndAnswer(null, null, null, List.of(), null, null, null, null); - } - - @Override - @Deprecated - public void listenAndAnswer(@Nullable STTService stt, @Nullable TTSService tts, @Nullable Voice voice, - List hlis, @Nullable AudioSource source, @Nullable AudioSink sink, - @Nullable Locale locale, @Nullable String listeningItem) throws IllegalStateException { - var builder = getDialogContextBuilder(); - if (stt != null) { - builder.withSTT(stt); - } - if (tts != null) { - builder.withTTS(tts); - } - if (!hlis.isEmpty()) { - builder.withHLIs(hlis); - } - if (source != null) { - builder.withSource(source); - } - if (sink != null) { - builder.withSink(sink); - } - if (locale != null) { - builder.withLocale(locale); - } - if (listeningItem != null) { - builder.withListeningItem(listeningItem); - } - listenAndAnswer(builder.build()); - } - @Override public void listenAndAnswer(DialogContext context) throws IllegalStateException { Bundle b = bundle; diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/AudioStreamFromCache.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/AudioStreamFromCache.java index 1cc79ea458a..360769944e3 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/AudioStreamFromCache.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/AudioStreamFromCache.java @@ -32,10 +32,12 @@ public class AudioStreamFromCache extends FixedLengthAudioStream { private InputStreamCacheWrapper inputStream; private AudioFormat audioFormat; + private String key; - public AudioStreamFromCache(InputStreamCacheWrapper inputStream, AudioFormatInfo audioFormat) { + public AudioStreamFromCache(InputStreamCacheWrapper inputStream, AudioFormatInfo audioFormat, String key) { this.inputStream = inputStream; this.audioFormat = audioFormat.toAudioFormat(); + this.key = key; } @Override @@ -101,4 +103,9 @@ public synchronized void reset() throws IOException { public boolean markSupported() { return inputStream.markSupported(); } + + @Override + public @Nullable String getId() { + return key; + } } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImpl.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImpl.java index 0ec33b9883d..ace6df0dce6 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImpl.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImpl.java @@ -99,7 +99,6 @@ protected void modified(Map config) { @Override public AudioStream get(CachedTTSService tts, String text, Voice voice, AudioFormat requestedFormat) throws TTSException { - LRUMediaCache lruMediaCacheLocal = lruMediaCache; if (!enableCacheTTS || lruMediaCacheLocal == null) { return tts.synthesizeForCache(text, voice, requestedFormat); @@ -115,14 +114,14 @@ public AudioStream get(CachedTTSService tts, String text, Voice voice, AudioForm return new LRUMediaCacheEntry(key, audioInputStream, new AudioFormatInfo(audioInputStream.getFormat())); } catch (TTSException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } }); - } catch (RuntimeException re) { - if (re.getCause() != null && re.getCause() instanceof TTSException ttse) { + } catch (IllegalStateException ise) { + if (ise.getCause() != null && ise.getCause() instanceof TTSException ttse) { throw ttse; } else { - throw re; + throw ise; } } @@ -136,7 +135,7 @@ public AudioStream get(CachedTTSService tts, String text, Voice voice, AudioForm // we are sure that the cache is used, and so we can use an AudioStream // implementation that use convenient methods for some client, like getClonedStream() // or mark /reset - return new AudioStreamFromCache(inputStreamCacheWrapper, metadata); + return new AudioStreamFromCache(inputStreamCacheWrapper, metadata, key); } else { // the cache is not used, we can use the original response AudioStream return (AudioStream) fileAndMetadata.getInputStream(); diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ASTNode.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ASTNode.java index 38a84b16e77..fe131534cf8 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ASTNode.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ASTNode.java @@ -66,7 +66,7 @@ public ASTNode findNode(String name) { * @return the value of this node as {@code String[]} */ public String[] getValueAsStringArray() { - Object[] objs = value instanceof Object[] ? (Object[]) value : new Object[] { value }; + Object[] objs = value instanceof Object[] os ? os : new Object[] { value }; String[] result = new String[objs.length]; for (int i = 0; i < objs.length; i++) { result[i] = objs[i] == null ? "" : ("" + objs[i]); diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/AbstractRuleBasedInterpreter.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/AbstractRuleBasedInterpreter.java index e95881e8a00..e942a7863a2 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/AbstractRuleBasedInterpreter.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/AbstractRuleBasedInterpreter.java @@ -190,8 +190,8 @@ private void addItem(Locale locale, Map>> target, Set()); } list.add(nt); - if (item instanceof GroupItem) { - for (Item member : ((GroupItem) item).getMembers()) { + if (item instanceof GroupItem groupItem) { + for (Item member : groupItem.getMembers()) { addItem(locale, target, nt, member); } } @@ -308,10 +308,10 @@ public InterpretationResult interpretAST(ResourceBundle language, ASTNode node) Object tag = cmdNode.getTag(); Object value = cmdNode.getValue(); Command command; - if (tag instanceof Command) { - command = (Command) tag; - } else if (value instanceof Number) { - command = new DecimalType(((Number) value).longValue()); + if (tag instanceof Command command1) { + command = command1; + } else if (value instanceof Number number) { + command = new DecimalType(number.longValue()); } else { command = new StringType(cmdNode.getValueAsString()); } @@ -335,8 +335,8 @@ public InterpretationResult interpretAST(ResourceBundle language, ASTNode node) * @return resulting expression */ protected @Nullable Expression exp(@Nullable Object obj) { - if (obj instanceof Expression) { - return (Expression) obj; + if (obj instanceof Expression expression) { + return expression; } else { return obj == null ? null : new ExpressionMatch(obj.toString()); } @@ -501,9 +501,8 @@ protected String executeSingle(ResourceBundle language, String[] labelFragments, throw new InterpretationException(language.getString(MULTIPLE_OBJECTS)); } else { Item item = items.get(0); - if (command instanceof State) { + if (command instanceof State newState) { try { - State newState = (State) command; State oldState = item.getStateAs(newState.getClass()); if (newState.equals(oldState)) { String template = language.getString(STATE_ALREADY_SINGULAR); @@ -669,8 +668,7 @@ private int addExpression(Expression exp) { private int addExportedExpression(Expression exp) { shared.add(exp); exported.add(exp); - int id = addExpression(exp); - return id; + return addExpression(exp); } private Expression unwrapLet(Expression expression) { @@ -717,16 +715,16 @@ private void emitUse(Expression expression) { private void emitExpression(Expression expression) { Expression unwrappedExpression = unwrapLet(expression); - if (unwrappedExpression instanceof ExpressionMatch) { - emitMatchExpression((ExpressionMatch) unwrappedExpression); - } else if (unwrappedExpression instanceof ExpressionSequence) { - emitSequenceExpression((ExpressionSequence) unwrappedExpression); - } else if (unwrappedExpression instanceof ExpressionAlternatives) { - emitAlternativesExpression((ExpressionAlternatives) unwrappedExpression); - } else if (unwrappedExpression instanceof ExpressionCardinality) { - emitCardinalExpression((ExpressionCardinality) unwrappedExpression); - } else if (unwrappedExpression instanceof ExpressionIdentifier) { - emitItemIdentifierExpression((ExpressionIdentifier) unwrappedExpression); + if (unwrappedExpression instanceof ExpressionMatch match) { + emitMatchExpression(match); + } else if (unwrappedExpression instanceof ExpressionSequence sequence) { + emitSequenceExpression(sequence); + } else if (unwrappedExpression instanceof ExpressionAlternatives alternatives) { + emitAlternativesExpression(alternatives); + } else if (unwrappedExpression instanceof ExpressionCardinality cardinality) { + emitCardinalExpression(cardinality); + } else if (unwrappedExpression instanceof ExpressionIdentifier identifier) { + emitItemIdentifierExpression(identifier); } } @@ -816,8 +814,8 @@ String getGrammar() { addExportedExpression(e); } for (Expression e : ids.keySet()) { - if (e instanceof ExpressionIdentifier) { - Expression stopper = ((ExpressionIdentifier) e).getStopper(); + if (e instanceof ExpressionIdentifier identifier) { + Expression stopper = identifier.getStopper(); if (stopper != null) { identifierExcludes.addAll(stopper.getFirsts(language)); } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionAlternatives.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionAlternatives.java index 5b0cd5521fd..72f3a02d61f 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionAlternatives.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionAlternatives.java @@ -13,7 +13,6 @@ package org.openhab.core.voice.text; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.ResourceBundle; import java.util.Set; @@ -34,8 +33,7 @@ final class ExpressionAlternatives extends Expression { */ public ExpressionAlternatives(Expression... subExpressions) { super(); - this.subExpressions = Collections - .unmodifiableList(Arrays.asList(Arrays.copyOf(subExpressions, subExpressions.length))); + this.subExpressions = List.of(Arrays.copyOf(subExpressions, subExpressions.length)); } @Override diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionCardinality.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionCardinality.java index ffce6d8716a..78661fdb002 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionCardinality.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionCardinality.java @@ -13,8 +13,6 @@ package org.openhab.core.voice.text; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.ResourceBundle; import java.util.Set; @@ -70,7 +68,7 @@ ASTNode parse(ResourceBundle language, TokenList tokenList) { @Override List getChildExpressions() { - return Collections.unmodifiableList(Arrays.asList(subExpression)); + return List.of(subExpression); } @Override diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionSequence.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionSequence.java index 0f810d9d754..4ccddd94aaa 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionSequence.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/ExpressionSequence.java @@ -13,7 +13,6 @@ package org.openhab.core.voice.text; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.ResourceBundle; import java.util.Set; @@ -34,8 +33,7 @@ public final class ExpressionSequence extends Expression { */ public ExpressionSequence(Expression... subExpressions) { super(); - this.subExpressions = Collections - .unmodifiableList(Arrays.asList(Arrays.copyOf(subExpressions, subExpressions.length))); + this.subExpressions = List.of(Arrays.copyOf(subExpressions, subExpressions.length)); } @Override diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/HumanLanguageInterpreter.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/HumanLanguageInterpreter.java index 607a320a7d0..9a5364d1ffd 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/HumanLanguageInterpreter.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/text/HumanLanguageInterpreter.java @@ -31,7 +31,7 @@ public interface HumanLanguageInterpreter { * * @return an id that identifies this service */ - public String getId(); + String getId(); /** * Returns a localized human readable label that can be used within UIs. @@ -39,7 +39,7 @@ public interface HumanLanguageInterpreter { * @param locale the locale to provide the label for * @return a localized string to be used in UIs */ - public String getLabel(@Nullable Locale locale); + String getLabel(@Nullable Locale locale); /** * Interprets a human language text fragment of a given {@link Locale} diff --git a/bundles/org.openhab.core.voice/src/main/resources/LanguageSupport_pt.properties b/bundles/org.openhab.core.voice/src/main/resources/LanguageSupport_pt.properties index 1ba5c43c48c..05bdbe461a3 100644 --- a/bundles/org.openhab.core.voice/src/main/resources/LanguageSupport_pt.properties +++ b/bundles/org.openhab.core.voice/src/main/resources/LanguageSupport_pt.properties @@ -1,21 +1,21 @@ ok = Ok. -sorry = Desculpa, eu não entendi. -error = Desculpa. Ocorreu um erro inesperado. +sorry = Desculpe, não percebi. +error = Desculpe. Ocorreu um erro inesperado. multiple_objects = Há mais de um objeto com um nome semelhante. no_objects = Não há nenhum objeto com esse nome. -command_not_accepted = O objeto referenciado não aceita o comando . -state_already_singular = O objeto já está . -state_already_plural = Todos objetos já estão . +command_not_accepted = O objeto indicado não aceita o comando . +state_already_singular = O objeto já é ou está . +state_already_plural = Todos os objetos já são ou estão . state_current = neste estado state_on = ligado state_off = desligado -state_up = para cima -state_down = para baixo -state_play = reproduzindo -state_pause = pausado +state_up = acima +state_down = baixo +state_play = ativo +state_pause = parado state_rewind = retrocedendo -state_fastforward = avançando rápido -state_open = aberta -state_closed = fechada +state_fastforward = avanço rápido +state_open = aberto +state_closed = fechado state_undef = indefinido state_null = não definido diff --git a/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_el.properties b/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_el.properties index 9b999a64761..918ece1a1a7 100644 --- a/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_el.properties +++ b/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_el.properties @@ -1,20 +1,24 @@ -system.config.voice.defaultTTS.label = Προεπιλεγμένο Κείμενο-Σε-Ομιλία -system.config.voice.defaultTTS.description = Η προεπιλεγμένη υπηρεσία κειμένου-σε-ομιλία (TTS) που θα χρησιμοποιηθεί αν δεν οριστεί κάποια άλλη. -system.config.voice.defaultSTT.label = Προεπιλεγμένη Ομιλία-Σε-Κείμενο -system.config.voice.defaultSTT.description = Η προεπιλεγμένη υπηρεσία ομιλίας-σε-κείμενο (STT) που θα χρησιμοποιηθεί αν δεν έχει καθοριστεί άλλη υπηρεσία. -system.config.voice.defaultVoice.label = Προεπιλεγμένη Φωνή -system.config.voice.defaultVoice.description = Η προεπιλεγμένη φωνή που θα χρησιμοποιηθεί αν δεν έχει καθοριστεί συγκεκριμένη υπηρεσία TTS ή φωνή. system.config.voice.defaultHLI.label = Προεπιλεγμένος Διερμηνέας Ανθρώπινης Γλώσσας system.config.voice.defaultHLI.description = Ο προεπιλεγμένος διερμηνέας ανθρώπινης γλώσσας που θα χρησιμοποιηθεί εάν δεν έχει καθοριστεί άλλος. system.config.voice.defaultKS.label = Προεπιλεγμένος Εντοπισμός Λέξεων-Κλειδιών system.config.voice.defaultKS.description = Η προεπιλεγμένη υπηρεσία εντοπισμού λέξεων-κλειδιών για χρήση εάν δεν έχει καθοριστεί άλλη. +system.config.voice.defaultSTT.label = Προεπιλεγμένη Ομιλία-Σε-Κείμενο +system.config.voice.defaultSTT.description = Η προεπιλεγμένη υπηρεσία ομιλίας-σε-κείμενο (STT) που θα χρησιμοποιηθεί αν δεν έχει καθοριστεί άλλη υπηρεσία. +system.config.voice.defaultTTS.label = Προεπιλεγμένο Κείμενο-Σε-Ομιλία +system.config.voice.defaultTTS.description = Η προεπιλεγμένη υπηρεσία κειμένου-σε-ομιλία (TTS) που θα χρησιμοποιηθεί αν δεν οριστεί κάποια άλλη. +system.config.voice.defaultVoice.label = Προεπιλεγμένη Φωνή +system.config.voice.defaultVoice.description = Η προεπιλεγμένη φωνή που θα χρησιμοποιηθεί αν δεν έχει καθοριστεί συγκεκριμένη υπηρεσία TTS ή φωνή. system.config.voice.keyword.label = Μαγική Λέξη system.config.voice.keyword.description = Η μαγική λέξη που θα αναγνωρίζεται και θα σηματοδοτεί την έναρξη του διαλόγου. system.config.voice.listeningItem.label = Διακόπτης Ακρόασης system.config.voice.listeningItem.description = Αν συμπληρωθεί, αυτό το αντικείμενο θα είναι ενεργό κατά την περίοδο κατά την οποία ο επεξεργαστής διαλόγου έχει εντοπίσει τη λέξη-κλειδί και ακούει για εντολές. - -service.system.voice.label = Φωνή +system.config.voice.listeningMelody.label = Μελωδία ειδοποίησης +system.config.voice.listeningMelody.description = Μια μελωδία που θα παιχτεί για να ειδοποιήσει το χρήστη όταν πρόκειται να ξεκινήσει η επεξεργασία διαλόγου. Αφήστε το κενό για να το απενεργοποιήσετε. (Χρησιμοποιήστε διαχωρισμένη λίστα σημειώσεων. Παράδειγμα\: "A O\:100 A'\:50") +system.config.voice.listeningMelody.option.Bb = Bb +system.config.voice.listeningMelody.option.F\# = F\# +system.config.voice.listeningMelody.option.E = E error.ks-error = Παρουσιάστηκε σφάλμα κατά τον εντοπισμό λέξεων-κλειδιών, {0} error.stt-error = Παρουσιάστηκε σφάλμα κατά την αναγνώριση κειμένου, {0} error.stt-exception = Σφάλμα κατά την αναγνώριση, {0} +service.system.voice.label = Φωνή diff --git a/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_hu.properties b/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_hu.properties index ccb7093c46e..b85dc48ff72 100644 --- a/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_hu.properties +++ b/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice_hu.properties @@ -21,4 +21,4 @@ system.config.voice.listeningMelody.option.E = E error.ks-error = Hiba történt a kulcsszavak észlelésekor, {0} error.stt-error = Hiba történt a szöveg felismerésekor, {0} error.stt-exception = Hiba felismerés közben, {0} -service.system.voice.label = Hang +service.system.voice.label = Beszéd diff --git a/bundles/org.openhab.core.voice/src/test/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImplTest.java b/bundles/org.openhab.core.voice/src/test/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImplTest.java index ec336fe04b7..09c00e7b773 100644 --- a/bundles/org.openhab.core.voice/src/test/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImplTest.java +++ b/bundles/org.openhab.core.voice/src/test/java/org/openhab/core/voice/internal/cache/TTSLRUCacheImplTest.java @@ -71,8 +71,7 @@ private TTSLRUCacheImpl createTTSCache(long size) throws IOException { Map config = new HashMap<>(); config.put(TTSLRUCacheImpl.CONFIG_CACHE_SIZE_TTS, size); config.put(TTSLRUCacheImpl.CONFIG_ENABLE_CACHE_TTS, true); - TTSLRUCacheImpl voiceLRUCache = new TTSLRUCacheImpl(storageService, config); - return voiceLRUCache; + return new TTSLRUCacheImpl(storageService, config); } @Test @@ -116,7 +115,6 @@ public void getCacheMissAndTwoHitAndTTsIsCalledOnlyOnce() throws TTSException, I @Test public void loadTTSResultsFromCacheDirectory() throws IOException, TTSException { - // prepare cache directory Path cacheDirectory = tempDir.resolve("cache").resolve(TTSLRUCacheImpl.VOICE_TTS_CACHE_PID); Files.createDirectories(cacheDirectory); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/User.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/User.java index 756ba6af77d..b83a7e2c763 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/User.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/User.java @@ -32,5 +32,5 @@ public interface User extends Principal, Identifiable { * @see Role * @return role attributed to the user */ - public Set getRoles(); + Set getRoles(); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/UserRegistry.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/UserRegistry.java index cd8b5c92bbe..0a19d071986 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/UserRegistry.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/UserRegistry.java @@ -37,7 +37,7 @@ public interface UserRegistry extends Registry, AuthenticationProv * @param roles the roles attributed to the new user * @return the new registered {@link User} instance */ - public User register(String username, String password, Set roles); + User register(String username, String password, Set roles); /** * Change the password for a {@link User} in this registry. The implementation receives the new password and is @@ -46,7 +46,7 @@ public interface UserRegistry extends Registry, AuthenticationProv * @param username the username of the existing user * @param newPassword the new password */ - public void changePassword(User user, String newPassword); + void changePassword(User user, String newPassword); /** * Adds a new session to the user profile @@ -54,7 +54,7 @@ public interface UserRegistry extends Registry, AuthenticationProv * @param user the user * @param session the session to add */ - public void addUserSession(User user, UserSession session); + void addUserSession(User user, UserSession session); /** * Removes the specified session from the user profile @@ -62,14 +62,14 @@ public interface UserRegistry extends Registry, AuthenticationProv * @param user the user * @param session the session to remove */ - public void removeUserSession(User user, UserSession session); + void removeUserSession(User user, UserSession session); /** * Clears all sessions from the user profile * * @param user the user */ - public void clearSessions(User user); + void clearSessions(User user); /** * Adds a new API token to the user profile. The implementation is responsible for storing the token in a secure way @@ -80,7 +80,7 @@ public interface UserRegistry extends Registry, AuthenticationProv * @param scope the scope this API token will be valid for * @return the string that can be used as a Bearer token to match the new API token */ - public String addUserApiToken(User user, String name, String scope); + String addUserApiToken(User user, String name, String scope); /** * Removes the specified API token from the user profile @@ -88,5 +88,5 @@ public interface UserRegistry extends Registry, AuthenticationProv * @param user the user * @param apiToken the API token */ - public void removeUserApiToken(User user, UserApiToken apiToken); + void removeUserApiToken(User user, UserApiToken apiToken); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthClientService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthClientService.java index 2f509a614e9..b278343643d 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthClientService.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthClientService.java @@ -17,11 +17,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import com.google.gson.JsonDeserializer; +import com.google.gson.GsonBuilder; /** * This is the service factory to produce an OAuth2 service client that authenticates using OAUTH2. - * This is a service factory pattern; the OAuthe2 service client is not shared between bundles. + * This is a service factory pattern; the OAuth2 service client is not shared between bundles. * *

* The basic uses of this OAuthClient are as follows: @@ -291,10 +291,10 @@ AccessTokenResponse getAccessTokenByImplicit(@Nullable String redirectURI, @Null boolean removeAccessTokenRefreshListener(AccessTokenRefreshListener listener); /** - * Adds a personalized deserializer to a given oauth service. + * Adds a custom GsonBuilder to be used with the OAuth service instance. * - * @param deserializeClass the deserializer class that should be used to deserialize AccessTokenResponse - * @return the oauth service + * @param gsonBuilder the custom GsonBuilder instance + * @return the OAuth service */ - > OAuthClientService withDeserializer(Class deserializerClass); + OAuthClientService withGsonBuilder(GsonBuilder gsonBuilder); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthFactory.java index a4b62501e98..b2b962b78c1 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/auth/client/oauth2/OAuthFactory.java @@ -26,12 +26,12 @@ public interface OAuthFactory { /** - * Creates a new oauth service. Use this method only once to obtain a handle and store + * Creates a new OAuth service. Use this method only once to obtain a handle and store * this handle for further in a persistent storage container. * - * @param handle the handle to the oauth service - * @param tokenUrl the token url of the oauth provider. This is used for getting access token. - * @param authorizationUrl the authorization url of the oauth provider. This is used purely for generating + * @param handle the handle to the OAuth service + * @param tokenUrl the token url of the OAuth provider. This is used for getting access token. + * @param authorizationUrl the authorization url of the OAuth provider. This is used purely for generating * authorization code/ url. * @param clientId the client id * @param clientSecret the client secret (optional) @@ -39,23 +39,23 @@ public interface OAuthFactory { * @param supportsBasicAuth whether the OAuth provider supports basic authorization or the client id and client * secret should be passed as form params. true - use http basic authentication, false - do not use http * basic authentication, null - unknown (default to do not use) - * @return the oauth service + * @return the OAuth service */ OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nullable String authorizationUrl, String clientId, @Nullable String clientSecret, @Nullable String scope, @Nullable Boolean supportsBasicAuth); /** - * Gets the oauth service for a given handle + * Gets the OAuth service for a given handle * - * @param handle the handle to the oauth service - * @return the oauth service or null if it doesn't exist + * @param handle the handle to the OAuth service + * @return the OAuth service or null if it doesn't exist */ @Nullable OAuthClientService getOAuthClientService(String handle); /** - * Unget an oauth service, this unget/unregister the service, and frees the resources. + * Unget an OAuth service, this unget/unregister the service, and frees the resources. * The existing tokens/ configurations (persisted parameters) are still saved * in the store. It will internally call {@code OAuthClientService#close()}. * @@ -64,7 +64,7 @@ OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nul * If OAuth service is closed directly, without using {@code #ungetOAuthService(String)}, * then a small residual footprint is left in the cache. * - * @param handle the handle to the oauth service + * @param handle the handle to the OAuth service */ void ungetOAuthService(String handle); @@ -72,7 +72,7 @@ OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nul * This method is for unget/unregister the service, * then DELETE access token, configuration data from the store * - * @param handle + * @param handle the handle to the OAuth service */ void deleteServiceAndAccessToken(String handle); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/ExpiringCacheMap.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/ExpiringCacheMap.java index 1b8474c224f..499d637bb69 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/ExpiringCacheMap.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/ExpiringCacheMap.java @@ -231,7 +231,7 @@ public synchronized void invalidate(K key) { * Invalidates all values in the cache. */ public synchronized void invalidateAll() { - items.values().forEach(item -> item.invalidateValue()); + items.values().forEach(ExpiringCache::invalidateValue); } /** diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/InputStreamCacheWrapper.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/InputStreamCacheWrapper.java index 1797e028db3..5cb13d60ffd 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/InputStreamCacheWrapper.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/InputStreamCacheWrapper.java @@ -37,6 +37,7 @@ public class InputStreamCacheWrapper extends InputStream { private LRUMediaCacheEntry cacheEntry; private int offset = 0; + private int markedOffset = 0; /*** * Construct a transparent InputStream wrapper around data from the cache. @@ -113,4 +114,19 @@ public long length() { public InputStream getClonedStream() throws IOException { return cacheEntry.getInputStream(); } + + @Override + public synchronized void mark(int readlimit) { + markedOffset = offset; + } + + @Override + public synchronized void reset() throws IOException { + offset = markedOffset; + } + + @Override + public boolean markSupported() { + return true; + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCache.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCache.java index d1c0b10f652..da1ce32b974 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCache.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCache.java @@ -20,6 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; @@ -30,7 +31,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; -import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -112,21 +113,19 @@ public LRUMediaCache(@Reference StorageService storageService, long maxCacheSize } private void cleanCacheDirectory() throws IOException { - try { - List<@Nullable Path> filesInCacheFolder = Files.list(cacheFolder).collect(Collectors.toList()); + try (Stream files = Files.list(cacheFolder)) { + List filesInCacheFolder = new ArrayList<>(files.toList()); // 1 delete empty files - Iterator<@Nullable Path> fileDeleterIterator = filesInCacheFolder.iterator(); + Iterator fileDeleterIterator = filesInCacheFolder.iterator(); while (fileDeleterIterator.hasNext()) { Path path = fileDeleterIterator.next(); - if (path != null) { - File file = path.toFile(); - if (file.length() == 0) { - file.delete(); - String fileName = path.getFileName().toString(); - storage.remove(fileName); - fileDeleterIterator.remove(); - } + File file = path.toFile(); + if (file.length() == 0) { + file.delete(); + String fileName = path.getFileName().toString(); + storage.remove(fileName); + fileDeleterIterator.remove(); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCacheEntry.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCacheEntry.java index c88350d3b03..8bf60773cc8 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCacheEntry.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/cache/lru/LRUMediaCacheEntry.java @@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.Disposable; import org.openhab.core.storage.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -166,7 +167,6 @@ protected String getKey() { * @throws IOException */ public InputStream getInputStream() throws IOException { - File localFile = file; if (localFile == null) { // the cache entry is not tied to the disk. The cache is not ready or not to be used. InputStream inputStreamLocal = inputStream; @@ -234,6 +234,9 @@ protected void closeStreamClient() throws IOException { if (inputStreamLocal != null) { inputStreamLocal.close(); } + if (inputStreamLocal instanceof Disposable disposableStream) { + disposableStream.dispose(); + } } } } finally { diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Switch.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/Disposable.java similarity index 58% rename from bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Switch.java rename to bundles/org.openhab.core/src/main/java/org/openhab/core/common/Disposable.java index 0fbb27945f0..b2b4fd9db0d 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Switch.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/Disposable.java @@ -10,17 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.semantics.model.point; +package org.openhab.core.common; + +import java.io.IOException; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.TagInfo; /** - * This class defines a Switch. + * For resource needing a callback when they are not needed anymore. * - * @author Generated from generateTagClasses.groovy - Initial contribution + * @author Gwendal Roulleau - Initial contribution */ @NonNullByDefault -@TagInfo(id = "Point_Control_Switch", label = "Switch", synonyms = "", description = "") -public interface Switch extends Control { +@FunctionalInterface +public interface Disposable { + void dispose() throws IOException; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java index 8df3497ddad..94c72c6c1ac 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java @@ -96,9 +96,9 @@ protected void modified(Map properties) { if (config == null) { configs.remove(poolName); } - if (config instanceof String) { + if (config instanceof String string) { try { - Integer poolSize = Integer.valueOf((String) config); + Integer poolSize = Integer.valueOf(string); configs.put(poolName, poolSize); ThreadPoolExecutor pool = (ThreadPoolExecutor) pools.get(poolName); if (pool instanceof ScheduledThreadPoolExecutor) { @@ -142,8 +142,8 @@ public static ScheduledExecutorService getScheduledPool(String poolName) { } } } - if (pool instanceof ScheduledExecutorService) { - return new UnstoppableScheduledExecutorService(poolName, (ScheduledExecutorService) pool); + if (pool instanceof ScheduledExecutorService service) { + return new UnstoppableScheduledExecutorService(poolName, service); } else { throw new IllegalArgumentException("Pool " + poolName + " is not a scheduled pool!"); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractProvider.java index bdcc76d0ee1..9d076df8927 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractProvider.java @@ -36,7 +36,7 @@ public abstract class AbstractProvider<@NonNull E> implements Provider { private enum EventType { ADDED, REMOVED, - UPDATED; + UPDATED } protected final Logger logger = LoggerFactory.getLogger(AbstractProvider.class); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractRegistry.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractRegistry.java index 777e6c01099..6938670a993 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractRegistry.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/AbstractRegistry.java @@ -63,7 +63,7 @@ public abstract class AbstractRegistry<@NonNull E extends Identifiable, @NonN private enum EventType { ADDED, REMOVED, - UPDATED; + UPDATED } private final Logger logger = LoggerFactory.getLogger(AbstractRegistry.class); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/Registry.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/Registry.java index efcef464e26..e69176dc3f3 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/Registry.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/Registry.java @@ -59,7 +59,8 @@ public interface Registry<@NonNull E extends Identifiable, @NonNull K> { * @param key key of the element * @return element or null if no element was found */ - public @Nullable E get(K key); + @Nullable + E get(K key); /** * Removes a {@link RegistryChangeListener} from the registry. @@ -75,7 +76,7 @@ public interface Registry<@NonNull E extends Identifiable, @NonNull K> { * @return the added element or newly created object of the same type * @throws IllegalStateException if no ManagedProvider is available */ - public E add(E element); + E add(E element); /** * Updates the given element at the according {@link ManagedProvider}. @@ -85,7 +86,8 @@ public interface Registry<@NonNull E extends Identifiable, @NonNull K> { * exists * @throws IllegalStateException if no ManagedProvider is available */ - public @Nullable E update(E element); + @Nullable + E update(E element); /** * Removes the given element from the according {@link ManagedProvider}. @@ -95,5 +97,6 @@ public interface Registry<@NonNull E extends Identifiable, @NonNull K> { * key exists * @throws IllegalStateException if no ManagedProvider is available */ - public @Nullable E remove(K key); + @Nullable + E remove(K key); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java index f54ecebd4f7..2a2d90b2e2a 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/events/AbstractEventFactory.java @@ -22,8 +22,7 @@ /** * The {@link AbstractEventFactory} defines an abstract implementation of the {@link EventFactory} interface. Subclasses * must implement the abstract method {@link #createEventByType(String, String, String, String)} in order to create - * event - * instances based on the event type. + * event instances based on the event type. * * @author Stefan Bußweiler - Initial contribution */ diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/events/EventSubscriber.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/events/EventSubscriber.java index 2bf1bc74d6b..bc6bd1fc009 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/events/EventSubscriber.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/events/EventSubscriber.java @@ -30,7 +30,7 @@ public interface EventSubscriber { * The constant {@link #ALL_EVENT_TYPES} must be returned by the {@link #getSubscribedEventTypes()} method, if the * event subscriber should subscribe to all event types. */ - public static String ALL_EVENT_TYPES = "ALL"; + static String ALL_EVENT_TYPES = "ALL"; /** * Gets the event types to which the event subscriber is subscribed to. diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/LocalizedKey.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/LocalizedKey.java index 2903d8eca59..f7bc6ce0f5c 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/LocalizedKey.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/LocalizedKey.java @@ -65,9 +65,6 @@ public boolean equals(@Nullable Object obj) { if (!Objects.equals(key, other.key)) { return false; } - if (!Objects.equals(locale, other.locale)) { - return false; - } - return true; + return Objects.equals(locale, other.locale); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/UnitProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/UnitProvider.java index 59f02f882d7..c1077ecb058 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/UnitProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/i18n/UnitProvider.java @@ -12,12 +12,13 @@ */ package org.openhab.core.i18n; +import java.util.Collection; + import javax.measure.Quantity; import javax.measure.Unit; import javax.measure.spi.SystemOfUnits; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; /** * Provides {@link Unit}s and the current {@link SystemOfUnits}. @@ -34,9 +35,10 @@ public interface UnitProvider { * @param dimension The {@link Quantity}, called dimension here, defines the base unit for the retrieved unit. E.g. * call {@code getUnit(javax.measure.quantity.Temperature.class)} to retrieve the temperature unit * according to the current {@link SystemOfUnits}. - * @return The {@link Unit} matching the given {@link Quantity}, {@code null} otherwise. + * @return The {@link Unit} matching the given {@link Quantity} + * @throws IllegalArgumentException when the dimension is unknown */ - > @Nullable Unit getUnit(@Nullable Class dimension); + > Unit getUnit(Class dimension) throws IllegalArgumentException; /** * Returns the {@link SystemOfUnits} which is currently set, must not be null. @@ -44,4 +46,6 @@ public interface UnitProvider { * @return the {@link SystemOfUnits} which is currently set, must not be null. */ SystemOfUnits getMeasurementSystem(); + + Collection>> getAllDimensions(); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/auth/UserRegistryImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/auth/UserRegistryImpl.java index f53abe707da..60dacd37638 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/auth/UserRegistryImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/auth/UserRegistryImpl.java @@ -133,8 +133,7 @@ private Optional hash(String password, String salt, int iterations) { @Override public Authentication authenticate(Credentials credentials) throws AuthenticationException { - if (credentials instanceof UsernamePasswordCredentials) { - UsernamePasswordCredentials usernamePasswordCreds = (UsernamePasswordCredentials) credentials; + if (credentials instanceof UsernamePasswordCredentials usernamePasswordCreds) { User user = get(usernamePasswordCreds.getUsername()); if (user == null) { throw new AuthenticationException("User not found: " + usernamePasswordCreds.getUsername()); @@ -148,8 +147,7 @@ public Authentication authenticate(Credentials credentials) throws Authenticatio } return new Authentication(managedUser.getName(), managedUser.getRoles().stream().toArray(String[]::new)); - } else if (credentials instanceof UserApiTokenCredentials) { - UserApiTokenCredentials apiTokenCreds = (UserApiTokenCredentials) credentials; + } else if (credentials instanceof UserApiTokenCredentials apiTokenCreds) { String[] apiTokenParts = apiTokenCreds.getApiToken().split("\\."); if (apiTokenParts.length != 3 || !APITOKEN_PREFIX.equals(apiTokenParts[0])) { throw new AuthenticationException("Invalid API token format"); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/AbstractInvocationHandler.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/AbstractInvocationHandler.java index 83c10c1e7cd..84ab4478db9 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/AbstractInvocationHandler.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/AbstractInvocationHandler.java @@ -88,10 +88,10 @@ Runnable getTimeoutHandler() { void handleExecutionException(Method method, ExecutionException e) { Throwable cause = e.getCause(); - if (cause instanceof DuplicateExecutionException) { - handleDuplicate(method, (DuplicateExecutionException) cause); - } else if (cause instanceof InvocationTargetException) { - handleException(method, (InvocationTargetException) cause); + if (cause instanceof DuplicateExecutionException exception) { + handleDuplicate(method, exception); + } else if (cause instanceof InvocationTargetException exception) { + handleException(method, exception); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/WrappedScheduledExecutorService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/WrappedScheduledExecutorService.java index 4ed0b059755..77766cde174 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/WrappedScheduledExecutorService.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/common/WrappedScheduledExecutorService.java @@ -43,9 +43,7 @@ public WrappedScheduledExecutorService(int corePoolSize, ThreadFactory threadFac protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); Throwable actualThrowable = t; - if (actualThrowable == null && r instanceof Future) { - Future f = (Future) r; - + if (actualThrowable == null && r instanceof Future f) { // The Future is the wrapper task around our scheduled Runnable. This is only "done" if an Exception // occurred, the Task was completed, or aborted. A periodic Task (scheduleWithFixedDelay etc.) is NEVER // "done" unless there was an Exception because the outer Task is always rescheduled. diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java index 24958732c19..aa8cbd624e3 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java @@ -13,14 +13,18 @@ package org.openhab.core.internal.events; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -47,9 +51,7 @@ public class EventHandler implements AutoCloseable { private final Map> typedEventSubscribers; private final Map typedEventFactories; - private final ScheduledExecutorService watcher = Executors - .newSingleThreadScheduledExecutor(new NamedThreadFactory("eventwatcher")); - private final ExecutorService executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("eventexecutor")); + private final Map, ExecutorRecord> executors = new HashMap<>(); /** * Create a new event handler. @@ -63,10 +65,19 @@ public EventHandler(final Map> typedEventSubscriber this.typedEventFactories = typedEventFactories; } + private synchronized ExecutorRecord createExecutorRecord(Class subscriber) { + return new ExecutorRecord( + Executors.newSingleThreadExecutor(new NamedThreadFactory("eventexecutor-" + executors.size())), + Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("eventwatcher-" + executors.size())), + new AtomicInteger()); + } + @Override public void close() { - watcher.shutdownNow(); - executor.shutdownNow(); + executors.values().forEach(r -> { + r.executor.shutdownNow(); + r.watcher.shutdownNow(); + }); } public void handleEvent(org.osgi.service.event.Event osgiEvent) { @@ -75,11 +86,9 @@ public void handleEvent(org.osgi.service.event.Event osgiEvent) { Object topicObj = osgiEvent.getProperty("topic"); Object sourceObj = osgiEvent.getProperty("source"); - if (typeObj instanceof String && payloadObj instanceof String && topicObj instanceof String) { - String typeStr = (String) typeObj; - String payloadStr = (String) payloadObj; - String topicStr = (String) topicObj; - String sourceStr = (sourceObj instanceof String) ? (String) sourceObj : null; + if (typeObj instanceof String typeStr && payloadObj instanceof String payloadStr + && topicObj instanceof String topicStr) { + String sourceStr = (sourceObj instanceof String s) ? s : null; if (!typeStr.isEmpty() && !payloadStr.isEmpty() && !topicStr.isEmpty()) { handleEvent(typeStr, payloadStr, topicStr, sourceStr); } @@ -142,8 +151,16 @@ private synchronized void dispatchEvent(final Set eventSubscrib EventFilter filter = eventSubscriber.getEventFilter(); if (filter == null || filter.apply(event)) { logger.trace("Delegate event to subscriber ({}).", eventSubscriber.getClass()); - executor.submit(() -> { - ScheduledFuture logTimeout = watcher.schedule( + ExecutorRecord executorRecord = Objects.requireNonNull( + executors.computeIfAbsent(eventSubscriber.getClass(), this::createExecutorRecord)); + int queueSize = executorRecord.count().incrementAndGet(); + if (queueSize > 1000) { + logger.warn( + "The queue for a subscriber of type '{}' exceeds 1000 elements. System may be unstable.", + eventSubscriber.getClass()); + } + CompletableFuture.runAsync(() -> { + ScheduledFuture logTimeout = executorRecord.watcher().schedule( () -> logger.warn("Dispatching event to subscriber '{}' takes more than {}ms.", eventSubscriber, EVENTSUBSCRIBER_EVENTHANDLING_MAX_MS), EVENTSUBSCRIBER_EVENTHANDLING_MAX_MS, TimeUnit.MILLISECONDS); @@ -154,10 +171,13 @@ private synchronized void dispatchEvent(final Set eventSubscrib EventSubscriber.class.getName(), ex.getMessage(), ex); } logTimeout.cancel(false); - }); + }, executorRecord.executor()).thenRun(executorRecord.count::decrementAndGet); } else { logger.trace("Skip event subscriber ({}) because of its filter.", eventSubscriber.getClass()); } } } + + private record ExecutorRecord(ExecutorService executor, ScheduledExecutorService watcher, AtomicInteger count) { + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/OSGiEventPublisher.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/OSGiEventPublisher.java index d3cb35dc164..aebbabbe9a0 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/OSGiEventPublisher.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/OSGiEventPublisher.java @@ -60,7 +60,6 @@ private void postAsOSGiEvent(final EventAdmin eventAdmin, final Event event) thr properties.put("source", source); } eventAdmin.postEvent(new org.osgi.service.event.Event("openhab", properties)); - } catch (Exception e) { throw new IllegalStateException("Cannot post the event via the event bus. Error message: " + e.getMessage(), e); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java index 80151468a3d..abe76195a22 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java @@ -17,11 +17,13 @@ import java.text.MessageFormat; import java.time.DateTimeException; import java.time.ZoneId; +import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.ResourceBundle; +import java.util.Set; import javax.measure.Quantity; import javax.measure.Unit; @@ -73,6 +75,7 @@ import org.openhab.core.library.dimension.Density; import org.openhab.core.library.dimension.ElectricConductivity; import org.openhab.core.library.dimension.Intensity; +import org.openhab.core.library.dimension.RadiationSpecificActivity; import org.openhab.core.library.dimension.VolumetricFlowRate; import org.openhab.core.library.types.PointType; import org.openhab.core.library.unit.ImperialUnits; @@ -142,12 +145,12 @@ public class I18nProviderImpl // UnitProvider static final String MEASUREMENT_SYSTEM = "measurementSystem"; private @Nullable SystemOfUnits measurementSystem; - private final Map>, Map>>> dimensionMap = new HashMap<>(); + private static final Map>, Map>>> DIMENSION_MAP = getDimensionMap(); @Activate @SuppressWarnings("unchecked") public I18nProviderImpl(ComponentContext componentContext) { - initDimensionMap(); + getDimensionMap(); modified((Map) componentContext.getProperties()); this.resourceBundleTracker = new ResourceBundleTracker(componentContext.getBundleContext(), this); @@ -187,16 +190,12 @@ private void setMeasurementSystem(@Nullable String measurementSystem) { final SystemOfUnits newMeasurementSystem; switch (ms) { - case SIUnits.MEASUREMENT_SYSTEM_NAME: - newMeasurementSystem = SIUnits.getInstance(); - break; - case ImperialUnits.MEASUREMENT_SYSTEM_NAME: - newMeasurementSystem = ImperialUnits.getInstance(); - break; - default: + case SIUnits.MEASUREMENT_SYSTEM_NAME -> newMeasurementSystem = SIUnits.getInstance(); + case ImperialUnits.MEASUREMENT_SYSTEM_NAME -> newMeasurementSystem = ImperialUnits.getInstance(); + default -> { logger.debug("Error setting measurement system for value '{}'.", measurementSystem); newMeasurementSystem = null; - break; + } } this.measurementSystem = newMeasurementSystem; @@ -358,12 +357,14 @@ public Locale getLocale() { @Override @SuppressWarnings("unchecked") - public > @Nullable Unit getUnit(@Nullable Class dimension) { - Map>> map = dimensionMap.get(dimension); + public > Unit getUnit(Class dimension) { + Map>> map = DIMENSION_MAP.get(dimension); if (map == null) { - return null; + throw new IllegalArgumentException("Dimension " + dimension.getName() + " is unknown. This is a bug."); } - return (Unit) map.get(getMeasurementSystem()); + Unit unit = (Unit) map.get(getMeasurementSystem()); + assert unit != null; + return unit; } @Override @@ -380,54 +381,68 @@ public SystemOfUnits getMeasurementSystem() { return SIUnits.getInstance(); } - private void initDimensionMap() { - addDefaultUnit(Acceleration.class, Units.METRE_PER_SQUARE_SECOND); - addDefaultUnit(AmountOfSubstance.class, Units.MOLE); - addDefaultUnit(Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE); - addDefaultUnit(Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT); - addDefaultUnit(ArealDensity.class, Units.DOBSON_UNIT); - addDefaultUnit(CatalyticActivity.class, Units.KATAL); - addDefaultUnit(DataAmount.class, Units.BYTE); - addDefaultUnit(DataTransferRate.class, Units.MEGABIT_PER_SECOND); - addDefaultUnit(Density.class, Units.KILOGRAM_PER_CUBICMETRE); - addDefaultUnit(Dimensionless.class, Units.ONE); - addDefaultUnit(ElectricCapacitance.class, Units.FARAD); - addDefaultUnit(ElectricCharge.class, Units.COULOMB); - addDefaultUnit(ElectricConductance.class, Units.SIEMENS); - addDefaultUnit(ElectricConductivity.class, Units.SIEMENS_PER_METRE); - addDefaultUnit(ElectricCurrent.class, Units.AMPERE); - addDefaultUnit(ElectricInductance.class, Units.HENRY); - addDefaultUnit(ElectricPotential.class, Units.VOLT); - addDefaultUnit(ElectricResistance.class, Units.OHM); - addDefaultUnit(Energy.class, Units.KILOWATT_HOUR); - addDefaultUnit(Force.class, Units.NEWTON); - addDefaultUnit(Frequency.class, Units.HERTZ); - addDefaultUnit(Illuminance.class, Units.LUX); - addDefaultUnit(Intensity.class, Units.IRRADIANCE); - addDefaultUnit(Length.class, SIUnits.METRE, ImperialUnits.FOOT); - addDefaultUnit(LuminousFlux.class, Units.LUMEN); - addDefaultUnit(LuminousIntensity.class, Units.CANDELA); - addDefaultUnit(MagneticFlux.class, Units.WEBER); - addDefaultUnit(MagneticFluxDensity.class, Units.TESLA); - addDefaultUnit(Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND); - addDefaultUnit(Power.class, Units.WATT); - addDefaultUnit(Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY); - addDefaultUnit(RadiationDoseAbsorbed.class, Units.GRAY); - addDefaultUnit(RadiationDoseEffective.class, Units.SIEVERT); - addDefaultUnit(Radioactivity.class, Units.BECQUEREL); - addDefaultUnit(SolidAngle.class, Units.STERADIAN); - addDefaultUnit(Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR); - addDefaultUnit(Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT); - addDefaultUnit(Time.class, Units.SECOND); - addDefaultUnit(Volume.class, SIUnits.CUBIC_METRE, ImperialUnits.GALLON_LIQUID_US); - addDefaultUnit(VolumetricFlowRate.class, Units.LITRE_PER_MINUTE, ImperialUnits.GALLON_PER_MINUTE); + @Override + public Collection>> getAllDimensions() { + return Set.copyOf(getDimensionMap().keySet()); + } + + public static Map>, Map>>> getDimensionMap() { + Map>, Map>>> dimensionMap = new HashMap<>(); + + addDefaultUnit(dimensionMap, Acceleration.class, Units.METRE_PER_SQUARE_SECOND); + addDefaultUnit(dimensionMap, AmountOfSubstance.class, Units.MOLE); + addDefaultUnit(dimensionMap, Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE); + addDefaultUnit(dimensionMap, Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT); + addDefaultUnit(dimensionMap, ArealDensity.class, Units.DOBSON_UNIT); + addDefaultUnit(dimensionMap, CatalyticActivity.class, Units.KATAL); + addDefaultUnit(dimensionMap, DataAmount.class, Units.BYTE); + addDefaultUnit(dimensionMap, DataTransferRate.class, Units.MEGABIT_PER_SECOND); + addDefaultUnit(dimensionMap, Density.class, Units.KILOGRAM_PER_CUBICMETRE); + addDefaultUnit(dimensionMap, Dimensionless.class, Units.ONE); + addDefaultUnit(dimensionMap, ElectricCapacitance.class, Units.FARAD); + addDefaultUnit(dimensionMap, ElectricCharge.class, Units.COULOMB); + addDefaultUnit(dimensionMap, ElectricConductance.class, Units.SIEMENS); + addDefaultUnit(dimensionMap, ElectricConductivity.class, Units.SIEMENS_PER_METRE); + addDefaultUnit(dimensionMap, ElectricCurrent.class, Units.AMPERE); + addDefaultUnit(dimensionMap, ElectricInductance.class, Units.HENRY); + addDefaultUnit(dimensionMap, ElectricPotential.class, Units.VOLT); + addDefaultUnit(dimensionMap, ElectricResistance.class, Units.OHM); + addDefaultUnit(dimensionMap, Energy.class, Units.KILOWATT_HOUR); + addDefaultUnit(dimensionMap, Force.class, Units.NEWTON); + addDefaultUnit(dimensionMap, Frequency.class, Units.HERTZ); + addDefaultUnit(dimensionMap, Illuminance.class, Units.LUX); + addDefaultUnit(dimensionMap, Intensity.class, Units.IRRADIANCE); + addDefaultUnit(dimensionMap, Length.class, SIUnits.METRE, ImperialUnits.INCH); + addDefaultUnit(dimensionMap, LuminousFlux.class, Units.LUMEN); + addDefaultUnit(dimensionMap, LuminousIntensity.class, Units.CANDELA); + addDefaultUnit(dimensionMap, MagneticFlux.class, Units.WEBER); + addDefaultUnit(dimensionMap, MagneticFluxDensity.class, Units.TESLA); + addDefaultUnit(dimensionMap, Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND); + addDefaultUnit(dimensionMap, Power.class, Units.WATT); + addDefaultUnit(dimensionMap, Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY); + addDefaultUnit(dimensionMap, RadiationDoseAbsorbed.class, Units.GRAY); + addDefaultUnit(dimensionMap, RadiationDoseEffective.class, Units.SIEVERT); + addDefaultUnit(dimensionMap, Radioactivity.class, Units.BECQUEREL); + addDefaultUnit(dimensionMap, SolidAngle.class, Units.STERADIAN); + addDefaultUnit(dimensionMap, RadiationSpecificActivity.class, Units.BECQUEREL_PER_CUBIC_METRE); + addDefaultUnit(dimensionMap, Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR); + addDefaultUnit(dimensionMap, Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT); + addDefaultUnit(dimensionMap, Time.class, Units.SECOND); + addDefaultUnit(dimensionMap, Volume.class, SIUnits.CUBIC_METRE, ImperialUnits.GALLON_LIQUID_US); + addDefaultUnit(dimensionMap, VolumetricFlowRate.class, Units.LITRE_PER_MINUTE, ImperialUnits.GALLON_PER_MINUTE); + + return dimensionMap; } - private > void addDefaultUnit(Class dimension, Unit siUnit, Unit imperialUnit) { + private static > void addDefaultUnit( + Map>, Map>>> dimensionMap, + Class dimension, Unit siUnit, Unit imperialUnit) { dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), siUnit, ImperialUnits.getInstance(), imperialUnit)); } - private > void addDefaultUnit(Class dimension, Unit unit) { + private static > void addDefaultUnit( + Map>, Map>>> dimensionMap, + Class dimension, Unit unit) { dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), unit, ImperialUnits.getInstance(), unit)); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java index f83a8eef1f4..5be2b606881 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java @@ -242,18 +242,15 @@ public void receive(Event event) { return; } - if (event instanceof ItemStateEvent) { - ItemStateEvent isEvent = (ItemStateEvent) event; + if (event instanceof ItemStateEvent isEvent) { if (!expireConfig.ignoreStateUpdates) { processEvent(itemName, isEvent.getItemState(), expireConfig, event.getClass()); } - } else if (event instanceof ItemCommandEvent) { - ItemCommandEvent icEvent = (ItemCommandEvent) event; + } else if (event instanceof ItemCommandEvent icEvent) { if (!expireConfig.ignoreCommands) { processEvent(itemName, icEvent.getItemCommand(), expireConfig, event.getClass()); } - } else if (event instanceof ItemStateChangedEvent) { - ItemStateChangedEvent icEvent = (ItemStateChangedEvent) event; + } else if (event instanceof ItemStateChangedEvent icEvent) { processEvent(itemName, icEvent.getItemState(), expireConfig, event.getClass()); } } @@ -386,10 +383,10 @@ public ExpireConfig(Item item, String configString, Map configur private boolean getBooleanConfigValue(Map configuration, String configKey) { boolean configValue; Object configValueObject = configuration.get(configKey); - if (configValueObject instanceof String) { - configValue = Boolean.parseBoolean((String) configValueObject); - } else if (configValueObject instanceof Boolean) { - configValue = (Boolean) configValueObject; + if (configValueObject instanceof String string) { + configValue = Boolean.parseBoolean(string); + } else if (configValueObject instanceof Boolean boolean1) { + configValue = boolean1; } else { configValue = false; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemBuilderImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemBuilderImpl.java index 93586f06208..be6c0fabf28 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemBuilderImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemBuilderImpl.java @@ -55,9 +55,9 @@ public ItemBuilderImpl(Set itemFactories, Item item) { this.groups = item.getGroupNames(); this.label = item.getLabel(); this.tags = item.getTags(); - if (item instanceof GroupItem) { - this.baseItem = ((GroupItem) item).getBaseItem(); - this.groupFunction = ((GroupItem) item).getFunction(); + if (item instanceof GroupItem groupItem) { + this.baseItem = groupItem.getBaseItem(); + this.groupFunction = groupItem.getFunction(); } } @@ -129,8 +129,7 @@ public Item build() { if (item == null) { throw new IllegalStateException("No item factory could handle type " + itemType); } - if (item instanceof ActiveItem) { - ActiveItem activeItem = (ActiveItem) item; + if (item instanceof ActiveItem activeItem) { activeItem.setLabel(label); activeItem.setCategory(category); activeItem.addGroupNames(groups); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemRegistryImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemRegistryImpl.java index 7d9adb1b42e..daeba98f4c8 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemRegistryImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemRegistryImpl.java @@ -23,8 +23,8 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.registry.AbstractRegistry; import org.openhab.core.common.registry.Provider; +import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.events.EventPublisher; -import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; @@ -35,6 +35,8 @@ import org.openhab.core.items.ItemStateConverter; import org.openhab.core.items.ItemUtil; import org.openhab.core.items.ManagedItemProvider; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataAwareItem; import org.openhab.core.items.MetadataRegistry; import org.openhab.core.items.RegistryHook; import org.openhab.core.items.events.ItemEventFactory; @@ -55,14 +57,15 @@ * This is the main implementing class of the {@link ItemRegistry} interface. It * keeps track of all declared items of all item providers and keeps their * current state in memory. This is the central point where states are kept and - * thus it is a core part for all stateful services. + * thus is a core part for all stateful services. * * @author Kai Kreuzer - Initial contribution * @author Stefan Bußweiler - Migration to new event mechanism */ @NonNullByDefault @Component(immediate = true) -public class ItemRegistryImpl extends AbstractRegistry implements ItemRegistry { +public class ItemRegistryImpl extends AbstractRegistry + implements ItemRegistry, RegistryChangeListener { private final Logger logger = LoggerFactory.getLogger(ItemRegistryImpl.class); @@ -70,7 +73,7 @@ public class ItemRegistryImpl extends AbstractRegistry groupItemNames) { for (String groupName : groupItemNames) { if (groupName != null) { try { - Item groupItem = getItem(groupName); - if (groupItem instanceof GroupItem) { - ((GroupItem) groupItem).addMember(item); + if (getItem(groupName) instanceof GroupItem groupItem) { + groupItem.addMember(item); } } catch (ItemNotFoundException e) { // the group might not yet be registered, let's ignore this @@ -161,9 +170,8 @@ private void replaceInGroupItems(Item oldItem, Item newItem, List groupI for (String groupName : groupItemNames) { if (groupName != null) { try { - Item groupItem = getItem(groupName); - if (groupItem instanceof GroupItem) { - ((GroupItem) groupItem).replaceMember(oldItem, newItem); + if (getItem(groupName) instanceof GroupItem groupItem) { + groupItem.replaceMember(oldItem, newItem); } } catch (ItemNotFoundException e) { // the group might not yet be registered, let's ignore this @@ -185,9 +193,9 @@ private void initializeItem(Item item) throws IllegalArgumentException { injectServices(item); - if (item instanceof GroupItem) { + if (item instanceof GroupItem groupItem) { // fill group with its members - addMembersToGroupItem((GroupItem) item); + addMembersToGroupItem(groupItem); } // add the item to all relevant groups @@ -195,14 +203,16 @@ private void initializeItem(Item item) throws IllegalArgumentException { } private void injectServices(Item item) { - if (item instanceof GenericItem) { - GenericItem genericItem = (GenericItem) item; + if (item instanceof GenericItem genericItem) { genericItem.setEventPublisher(getEventPublisher()); genericItem.setStateDescriptionService(stateDescriptionService); genericItem.setCommandDescriptionService(commandDescriptionService); - genericItem.setUnitProvider(unitProvider); genericItem.setItemStateConverter(itemStateConverter); } + if (item instanceof MetadataAwareItem metadataAwareItem) { + metadataRegistry.stream().filter(m -> m.getUID().getItemName().equals(item.getName())) + .forEach(metadataAwareItem::addedMetadata); + } } private void addMembersToGroupItem(GroupItem groupItem) { @@ -217,9 +227,8 @@ private void removeFromGroupItems(Item item, List groupItemNames) { for (String groupName : groupItemNames) { if (groupName != null) { try { - Item groupItem = getItem(groupName); - if (groupItem instanceof GroupItem) { - ((GroupItem) groupItem).removeMember(item); + if (getItem(groupName) instanceof GroupItem groupItem) { + groupItem.removeMember(item); } } catch (ItemNotFoundException e) { // the group might not yet be registered, let's ignore this @@ -235,16 +244,16 @@ protected void onAddElement(Item element) throws IllegalArgumentException { @Override protected void onRemoveElement(Item element) { - if (element instanceof GenericItem) { - ((GenericItem) element).dispose(); + if (element instanceof GenericItem genericItem) { + genericItem.dispose(); } removeFromGroupItems(element, element.getGroupNames()); } @Override protected void beforeUpdateElement(Item existingElement) { - if (existingElement instanceof GenericItem) { - ((GenericItem) existingElement).dispose(); + if (existingElement instanceof GenericItem genericItem) { + genericItem.dispose(); } } @@ -253,13 +262,13 @@ protected void onUpdateElement(Item oldItem, Item item) { // don't use #initialize and retain order of items in groups: List oldNames = oldItem.getGroupNames(); List newNames = item.getGroupNames(); - List commonNames = oldNames.stream().filter(name -> newNames.contains(name)).collect(toList()); + List commonNames = oldNames.stream().filter(newNames::contains).collect(toList()); removeFromGroupItems(oldItem, oldNames.stream().filter(name -> !commonNames.contains(name)).collect(toList())); replaceInGroupItems(oldItem, item, commonNames); addToGroupItems(item, newNames.stream().filter(name -> !commonNames.contains(name)).collect(toList())); - if (item instanceof GroupItem) { - addMembersToGroupItem((GroupItem) item); + if (item instanceof GroupItem groupItem) { + addMembersToGroupItem(groupItem); } injectServices(item); } @@ -292,21 +301,6 @@ protected void unsetReadyService(ReadyService readyService) { super.unsetReadyService(readyService); } - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) - public void setUnitProvider(UnitProvider unitProvider) { - this.unitProvider = unitProvider; - for (Item item : getItems()) { - ((GenericItem) item).setUnitProvider(unitProvider); - } - } - - public void unsetUnitProvider(UnitProvider unitProvider) { - this.unitProvider = null; - for (Item item : getItems()) { - ((GenericItem) item).setUnitProvider(null); - } - } - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) protected void setItemStateConverter(ItemStateConverter itemStateConverter) { this.itemStateConverter = itemStateConverter; @@ -376,20 +370,20 @@ public Collection getItemsByTagAndType(String type, String... tags) { @Override protected void notifyListenersAboutAddedElement(Item element) { - super.notifyListenersAboutAddedElement(element); postEvent(ItemEventFactory.createAddedEvent(element)); + super.notifyListenersAboutAddedElement(element); } @Override protected void notifyListenersAboutRemovedElement(Item element) { - super.notifyListenersAboutRemovedElement(element); postEvent(ItemEventFactory.createRemovedEvent(element)); + super.notifyListenersAboutRemovedElement(element); } @Override protected void notifyListenersAboutUpdatedElement(Item oldElement, Item element) { - super.notifyListenersAboutUpdatedElement(oldElement, element); postEvent(ItemEventFactory.createUpdateEvent(element, oldElement)); + super.notifyListenersAboutUpdatedElement(oldElement, element); } @Override @@ -443,17 +437,6 @@ public void removeRegistryHook(RegistryHook hook) { registryHooks.remove(hook); } - @Activate - protected void activate(final ComponentContext componentContext) { - super.activate(componentContext.getBundleContext()); - } - - @Override - @Deactivate - protected void deactivate() { - super.deactivate(); - } - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) public void setStateDescriptionService(StateDescriptionService stateDescriptionService) { this.stateDescriptionService = stateDescriptionService; @@ -496,4 +479,31 @@ protected void setManagedProvider(ManagedItemProvider provider) { protected void unsetManagedProvider(ManagedItemProvider provider) { super.unsetManagedProvider(provider); } + + @Override + public void added(Metadata element) { + String itemName = element.getUID().getItemName(); + Item item = get(itemName); + if (item instanceof MetadataAwareItem metadataAwareItem) { + metadataAwareItem.addedMetadata(element); + } + } + + @Override + public void removed(Metadata element) { + String itemName = element.getUID().getItemName(); + Item item = get(itemName); + if (item instanceof MetadataAwareItem metadataAwareItem) { + metadataAwareItem.removedMetadata(element); + } + } + + @Override + public void updated(Metadata oldElement, Metadata element) { + String itemName = element.getUID().getItemName(); + Item item = get(itemName); + if (item instanceof MetadataAwareItem metadataAwareItem) { + metadataAwareItem.updatedMetadata(oldElement, element); + } + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemStateConverterImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemStateConverterImpl.java index 7a32c527018..4a47fdf3eb1 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemStateConverterImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemStateConverterImpl.java @@ -70,11 +70,8 @@ public State convertToAcceptedState(@Nullable State state, @Nullable Item item) } } - if (item instanceof NumberItem && state instanceof QuantityType) { - NumberItem numberItem = (NumberItem) item; + if (item instanceof NumberItem numberItem && state instanceof QuantityType quantityState) { if (numberItem.getDimension() != null) { - QuantityType quantityState = (QuantityType) state; - // in case the item does define a unit it takes precedence over all other conversions: Unit itemUnit = parseItemUnit(numberItem); if (itemUnit != null) { @@ -94,8 +91,6 @@ public State convertToAcceptedState(@Nullable State state, @Nullable Item item) && UnitUtils.isDifferentMeasurementSystem(conversionUnit, quantityState.getUnit())) { return convertOrUndef(quantityState, conversionUnit); } - - return state; } else { State convertedState = state.as(DecimalType.class); if (convertedState != null) { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java index 2ee6dea8c7d..3b5db162036 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ItemUpdater.java @@ -89,8 +89,7 @@ protected void receiveUpdate(ItemStateEvent updateEvent) { protected void receiveCommand(ItemCommandEvent commandEvent) { try { Item item = itemRegistry.getItem(commandEvent.getItemName()); - if (item instanceof GroupItem) { - GroupItem groupItem = (GroupItem) item; + if (item instanceof GroupItem groupItem) { groupItem.send(commandEvent.getItemCommand()); } } catch (ItemNotFoundException e) { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java index b2f2478b13b..cba4baa9608 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java @@ -63,8 +63,8 @@ public MetadataCommandDescriptionProvider(final @Reference MetadataRegistry meta if (metadata.getConfiguration().containsKey("options")) { Stream.of(metadata.getConfiguration().get("options").toString().split(",")).forEach(o -> { if (o.contains("=")) { - commandDescription.addCommandOption( - new CommandOption(o.split("=")[0].trim(), o.split("=")[1].trim())); + var pair = parseValueLabelPair(o.trim()); + commandDescription.addCommandOption(new CommandOption(pair[0], pair[1])); } else { commandDescription.addCommandOption(new CommandOption(o.trim(), null)); } @@ -80,4 +80,19 @@ public MetadataCommandDescriptionProvider(final @Reference MetadataRegistry meta return null; } + + public static String[] parseValueLabelPair(String text) { + String value; + String label; + if (text.startsWith("\"") && text.contains("\"=\"") && text.endsWith("\"")) { + String[] parts = text.split("\"=\""); + value = parts[0].substring(1); + label = parts[1].substring(0, parts[1].length() - 1); + } else { + String[] parts = text.split("="); + value = parts[0]; + label = parts[1]; + } + return new String[] { value.trim(), label.trim() }; + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java index 705a198765c..a61bd846e64 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java @@ -12,6 +12,8 @@ */ package org.openhab.core.internal.items; +import static org.openhab.core.internal.items.MetadataCommandDescriptionProvider.parseValueLabelPair; + import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; @@ -103,9 +105,12 @@ public MetadataStateDescriptionFragmentProvider(final @Reference MetadataRegistr if (metadata.getConfiguration().containsKey("options")) { List stateOptions = Stream .of(metadata.getConfiguration().get("options").toString().split(",")).map(o -> { - return (o.contains("=")) - ? new StateOption(o.split("=")[0].trim(), o.split("=")[1].trim()) - : new StateOption(o.trim(), null); + if (o.contains("=")) { + var pair = parseValueLabelPair(o.trim()); + return new StateOption(pair[0], pair[1]); + } else { + return new StateOption(o.trim(), null); + } }).collect(Collectors.toList()); builder.withOptions(stateOptions); } @@ -122,14 +127,14 @@ public MetadataStateDescriptionFragmentProvider(final @Reference MetadataRegistr private BigDecimal getBigDecimal(Object value) { BigDecimal ret = null; if (value != null) { - if (value instanceof BigDecimal) { - ret = (BigDecimal) value; - } else if (value instanceof String) { - ret = new BigDecimal((String) value); - } else if (value instanceof BigInteger) { - ret = new BigDecimal((BigInteger) value); - } else if (value instanceof Number) { - ret = new BigDecimal(((Number) value).doubleValue()); + if (value instanceof BigDecimal decimal) { + ret = decimal; + } else if (value instanceof String string) { + ret = new BigDecimal(string); + } else if (value instanceof BigInteger integer) { + ret = new BigDecimal(integer); + } else if (value instanceof Number number) { + ret = new BigDecimal(number.doubleValue()); } else { throw new ClassCastException("Not possible to coerce [" + value + "] from class " + value.getClass() + " into a BigDecimal."); @@ -141,10 +146,10 @@ private BigDecimal getBigDecimal(Object value) { private Boolean getBoolean(Object value) { Boolean ret = null; if (value != null) { - if (value instanceof Boolean) { - ret = (Boolean) value; - } else if (value instanceof String) { - ret = Boolean.parseBoolean((String) value); + if (value instanceof Boolean boolean1) { + ret = boolean1; + } else if (value instanceof String string) { + ret = Boolean.parseBoolean(string); } else { throw new ClassCastException( "Not possible to coerce [" + value + "] from class " + value.getClass() + " into a Boolean."); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/CronSchedulerImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/CronSchedulerImpl.java index 96742e301be..baa37bd5361 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/CronSchedulerImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/CronSchedulerImpl.java @@ -80,10 +80,10 @@ void addSchedule(CronJob cronJob, Map map) { final Object scheduleConfig = map.get(CronJob.CRON); String[] schedules = null; - if (scheduleConfig instanceof String[]) { - schedules = (String[]) scheduleConfig; - } else if (scheduleConfig instanceof String) { - schedules = new String[] { (String) scheduleConfig }; + if (scheduleConfig instanceof String[] strings) { + schedules = strings; + } else if (scheduleConfig instanceof String string) { + schedules = new String[] { string }; } if (schedules == null || schedules.length == 0) { logger.info("No schedules in map with key '" + CronJob.CRON + "'. Nothing scheduled"); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/SchedulerImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/SchedulerImpl.java index d4e91bb3b0d..361be34786c 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/SchedulerImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/scheduler/SchedulerImpl.java @@ -176,9 +176,7 @@ private void schedule(ScheduledCompletableFutureRecurring recurringSchedu ZonedDateTime.from(newTime)); deferred.thenAccept(v -> { - if (temporalAdjuster instanceof SchedulerTemporalAdjuster) { - final SchedulerTemporalAdjuster schedulerTemporalAdjuster = (SchedulerTemporalAdjuster) temporalAdjuster; - + if (temporalAdjuster instanceof SchedulerTemporalAdjuster schedulerTemporalAdjuster) { if (!schedulerTemporalAdjuster.isDone(newTime)) { schedule(recurringSchedule, runnable, identifier, temporalAdjuster); return; diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/ReadyServiceImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/ReadyServiceImpl.java index d70d7ae4ce0..1b8e993c9b0 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/ReadyServiceImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/ReadyServiceImpl.java @@ -72,11 +72,8 @@ public void unmarkReady(ReadyMarker readyMarker) { } private void notifyTrackers(ReadyMarker readyMarker, Consumer action) { - trackers.entrySet().stream().filter(entry -> { - return entry.getValue().apply(readyMarker); - }).map(entry -> { - return entry.getKey(); - }).forEach(action); + trackers.entrySet().stream().filter(entry -> entry.getValue().apply(readyMarker)).map(Map.Entry::getKey) + .forEach(action); } @Override @@ -95,7 +92,7 @@ public void registerTracker(ReadyTracker readyTracker, ReadyMarkerFilter filter) try { if (!trackers.containsKey(readyTracker)) { trackers.put(readyTracker, filter); - notifyTracker(readyTracker, marker -> readyTracker.onReadyMarkerAdded(marker)); + notifyTracker(readyTracker, readyTracker::onReadyMarkerAdded); } } catch (RuntimeException e) { logger.error("Registering tracker '{}' failed!", readyTracker, e); @@ -109,7 +106,7 @@ public void unregisterTracker(ReadyTracker readyTracker) { rwlTrackers.writeLock().lock(); try { if (trackers.containsKey(readyTracker)) { - notifyTracker(readyTracker, marker -> readyTracker.onReadyMarkerRemoved(marker)); + notifyTracker(readyTracker, readyTracker::onReadyMarkerRemoved); } trackers.remove(readyTracker); } finally { @@ -119,6 +116,6 @@ public void unregisterTracker(ReadyTracker readyTracker) { private void notifyTracker(ReadyTracker readyTracker, Consumer action) { ReadyMarkerFilter f = trackers.get(readyTracker); - markers.stream().filter(marker -> f.apply(marker)).forEach(action); + markers.stream().filter(f::apply).forEach(action); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java index e9aaa5b1856..c77010e37d9 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java @@ -21,6 +21,7 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -48,6 +49,7 @@ import io.methvin.watcher.DirectoryChangeEvent; import io.methvin.watcher.DirectoryChangeListener; import io.methvin.watcher.DirectoryWatcher; +import io.methvin.watcher.hashing.FileHash; /** * The {@link WatchServiceImpl} is the implementation of the {@link WatchService} @@ -70,6 +72,7 @@ public class WatchServiceImpl implements WatchService, DirectoryChangeListener { private final List dirPathListeners = new CopyOnWriteArrayList<>(); private final List subDirPathListeners = new CopyOnWriteArrayList<>(); + private final Map hashCache = new ConcurrentHashMap<>(); private final ExecutorService executor; private final ScheduledExecutorService scheduler; @@ -81,7 +84,7 @@ public class WatchServiceImpl implements WatchService, DirectoryChangeListener { private @Nullable ServiceRegistration reg; private final Map> scheduledEvents = new HashMap<>(); - private final Map> scheduledEventKinds = new ConcurrentHashMap<>(); + private final Map> scheduledEventKinds = new ConcurrentHashMap<>(); @Activate public WatchServiceImpl(WatchServiceConfiguration config, BundleContext bundleContext) throws IOException { @@ -170,6 +173,8 @@ private void closeWatcherAndUnregister() throws IOException { } this.reg = null; } + + hashCache.clear(); } @Override @@ -210,6 +215,7 @@ public void unregisterListener(WatchEventListener watchEventListener) { @Override public void onEvent(@Nullable DirectoryChangeEvent directoryChangeEvent) throws IOException { + logger.trace("onEvent {}", directoryChangeEvent); if (directoryChangeEvent == null || directoryChangeEvent.isDirectory() || directoryChangeEvent.eventType() == DirectoryChangeEvent.EventType.OVERFLOW) { // exit early, we are neither interested in directory events nor in OVERFLOW events @@ -217,12 +223,6 @@ public void onEvent(@Nullable DirectoryChangeEvent directoryChangeEvent) throws } Path path = directoryChangeEvent.path(); - Kind kind = switch (directoryChangeEvent.eventType()) { - case CREATE -> Kind.CREATE; - case MODIFY -> Kind.MODIFY; - case DELETE -> Kind.DELETE; - case OVERFLOW -> Kind.OVERFLOW; - }; synchronized (scheduledEvents) { ScheduledFuture future = scheduledEvents.remove(path); @@ -230,39 +230,46 @@ public void onEvent(@Nullable DirectoryChangeEvent directoryChangeEvent) throws future.cancel(true); } future = scheduler.schedule(() -> notifyListeners(path), PROCESSING_TIME, TimeUnit.MILLISECONDS); - scheduledEventKinds.computeIfAbsent(path, k -> new CopyOnWriteArrayList<>()).add(kind); + scheduledEventKinds.computeIfAbsent(path, k -> new CopyOnWriteArrayList<>()).add(directoryChangeEvent); scheduledEvents.put(path, future); - } } private void notifyListeners(Path path) { - List kinds = scheduledEventKinds.remove(path); - if (kinds == null || kinds.isEmpty()) { + List events = scheduledEventKinds.remove(path); + if (events == null || events.isEmpty()) { logger.debug("Tried to notify listeners of change events for '{}', but the event list is empty.", path); return; } - if (kinds.size() == 1) { - // we have only one event - doNotify(path, kinds.get(0)); - return; - } - - Kind firstElement = kinds.get(0); - Kind lastElement = kinds.get(kinds.size() - 1); + DirectoryChangeEvent firstElement = events.get(0); + DirectoryChangeEvent lastElement = events.get(events.size() - 1); // determine final event - if (lastElement == Kind.DELETE) { - if (firstElement == Kind.CREATE) { - logger.debug("Discarding events for '{}' because file was immediately deleted bafter creation", path); + if (lastElement.eventType() == DirectoryChangeEvent.EventType.DELETE) { + if (firstElement.eventType() == DirectoryChangeEvent.EventType.CREATE) { + logger.debug("Discarding events for '{}' because file was immediately deleted after creation", path); return; } + hashCache.remove(lastElement.path()); doNotify(path, Kind.DELETE); - } else if (firstElement == Kind.CREATE) { + } else if (firstElement.eventType() == DirectoryChangeEvent.EventType.CREATE) { + if (lastElement.hash() == null) { + logger.warn("Detected invalid event (hash must not be null for CREATE/MODIFY): {}", lastElement); + return; + } + hashCache.put(lastElement.path(), lastElement.hash()); doNotify(path, Kind.CREATE); } else { - doNotify(path, Kind.MODIFY); + if (lastElement.hash() == null) { + logger.warn("Detected invalid event (hash must not be null for CREATE/MODIFY): {}", lastElement); + return; + } + FileHash oldHash = hashCache.put(lastElement.path(), lastElement.hash()); + if (!Objects.equals(oldHash, lastElement.hash())) { + // only notify if hashes are different, otherwise the file content did not chnge + doNotify(path, Kind.MODIFY); + } } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ActiveItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ActiveItem.java index 22a11daea8c..5901f206065 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ActiveItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ActiveItem.java @@ -50,59 +50,59 @@ public interface ActiveItem extends Item { * * @param tag a tag that is to be added to item's tags. */ - public void addTag(String tag); + void addTag(String tag); /** * Adds tags to the item. * * @param tags tags that are to be added to item's tags. */ - public void addTags(String... tags); + void addTags(String... tags); /** * Adds tags to the item. * * @param tags tags that are to be added to item's tags. */ - public void addTags(Collection tags); + void addTags(Collection tags); /** * Removes a tag from the item. * * @param tag a tag that is to be removed from item's tags. */ - public void removeTag(String tag); + void removeTag(String tag); /** * Clears all tags of this item. */ - public void removeAllTags(); + void removeAllTags(); /** * Removes the according item from a group. * * @param groupItemName name of the group (must not be null) */ - public abstract void removeGroupName(String groupItemName); + void removeGroupName(String groupItemName); /** * Assigns the according item to a group. * * @param groupItemName name of the group (must not be null) */ - public abstract void addGroupName(String groupItemName); + void addGroupName(String groupItemName); /** * Assigns the according item to the given groups. * * @param groupItemNames names of the groups (must not be null) */ - public abstract void addGroupNames(String... groupItemNames); + void addGroupNames(String... groupItemNames); /** * Assigns the according item to the given groups. * * @param groupItemNames names of the groups (must not be null) */ - public abstract void addGroupNames(List groupItemNames); + void addGroupNames(List groupItemNames); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java index 5e7c3366b01..f7ab33b0311 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GenericItem.java @@ -29,7 +29,6 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.events.EventPublisher; -import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.service.CommandDescriptionService; import org.openhab.core.service.StateDescriptionService; @@ -83,8 +82,6 @@ public abstract class GenericItem implements ActiveItem { private @Nullable CommandDescriptionService commandDescriptionService; - protected @Nullable UnitProvider unitProvider; - protected @Nullable ItemStateConverter itemStateConverter; public GenericItem(String type, String name) { @@ -176,7 +173,6 @@ public void dispose() { this.eventPublisher = null; this.stateDescriptionService = null; this.commandDescriptionService = null; - this.unitProvider = null; this.itemStateConverter = null; } @@ -192,10 +188,6 @@ public void setCommandDescriptionService(@Nullable CommandDescriptionService com this.commandDescriptionService = commandDescriptionService; } - public void setUnitProvider(@Nullable UnitProvider unitProvider) { - this.unitProvider = unitProvider; - } - public void setItemStateConverter(@Nullable ItemStateConverter itemStateConverter) { this.itemStateConverter = itemStateConverter; } @@ -231,14 +223,23 @@ protected final void applyState(State state) { State oldState = this.state; this.state = state; notifyListeners(oldState, state); + sendStateUpdatedEvent(state); if (!oldState.equals(state)) { sendStateChangedEvent(state, oldState); } } + private void sendStateUpdatedEvent(State newState) { + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1.post(ItemEventFactory.createStateUpdatedEvent(this.name, newState, null)); + } + } + private void sendStateChangedEvent(State newState, State oldState) { - if (eventPublisher != null) { - eventPublisher.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1.post(ItemEventFactory.createStateChangedEvent(this.name, newState, oldState)); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java index 2125ed7ac7f..72d9e3ea497 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/GroupItem.java @@ -25,7 +25,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.events.EventPublisher; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.service.CommandDescriptionService; import org.openhab.core.service.StateDescriptionService; @@ -39,7 +39,7 @@ * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault -public class GroupItem extends GenericItem implements StateChangeListener { +public class GroupItem extends GenericItem implements StateChangeListener, MetadataAwareItem { public static final String TYPE = "Group"; @@ -138,8 +138,8 @@ private void collectMembers(Collection allMembers, Collection member continue; } allMembers.add(member); - if (member instanceof GroupItem) { - collectMembers(allMembers, ((GroupItem) member).members); + if (member instanceof GroupItem item) { + collectMembers(allMembers, item.members); } } } @@ -171,22 +171,20 @@ public void addMember(Item item) { // in case membership is constructed programmatically this sanitizes // the group names on the item: - if (added && item instanceof GenericItem) { - ((GenericItem) item).addGroupName(getName()); + if (added && item instanceof GenericItem genericItem) { + genericItem.addGroupName(getName()); } registerStateListener(item); } private void registerStateListener(Item item) { - if (item instanceof GenericItem) { - GenericItem genericItem = (GenericItem) item; + if (item instanceof GenericItem genericItem) { genericItem.addStateChangeListener(this); } } private void unregisterStateListener(Item old) { - if (old instanceof GenericItem) { - GenericItem genericItem = (GenericItem) old; + if (old instanceof GenericItem genericItem) { genericItem.removeStateChangeListener(this); } } @@ -308,9 +306,9 @@ protected void internalSend(Command command) { newState = function.getStateAs(getStateMembers(getMembers()), typeClass); } - if (newState == null && baseItem != null && baseItem instanceof GenericItem) { + if (newState == null && baseItem != null && baseItem instanceof GenericItem item) { // we use the transformation method from the base item - ((GenericItem) baseItem).setState(state); + item.setState(state); newState = baseItem.getStateAs(typeClass); } if (newState == null) { @@ -371,6 +369,7 @@ public void stateUpdated(Item item, State state) { State calculatedState = function.calculate(getStateMembers(getMembers())); newState = itemStateConverter.convertToAcceptedState(calculatedState, baseItem); setState(newState); + sendGroupStateUpdatedEvent(item.getName(), newState); } if (!oldState.equals(newState)) { sendGroupStateChangedEvent(item.getName(), newState, oldState); @@ -380,8 +379,8 @@ public void stateUpdated(Item item, State state) { @Override public void setState(State state) { State oldState = this.state; - if (baseItem != null && baseItem instanceof GenericItem) { - ((GenericItem) baseItem).setState(state); + if (baseItem != null && baseItem instanceof GenericItem item) { + item.setState(state); this.state = baseItem.getState(); } else { this.state = state; @@ -392,30 +391,30 @@ public void setState(State state) { @Override public void setStateDescriptionService(@Nullable StateDescriptionService stateDescriptionService) { super.setStateDescriptionService(stateDescriptionService); - if (baseItem instanceof GenericItem) { - ((GenericItem) baseItem).setStateDescriptionService(stateDescriptionService); + if (baseItem instanceof GenericItem item) { + item.setStateDescriptionService(stateDescriptionService); } } @Override public void setCommandDescriptionService(@Nullable CommandDescriptionService commandDescriptionService) { super.setCommandDescriptionService(commandDescriptionService); - if (baseItem instanceof GenericItem) { - ((GenericItem) baseItem).setCommandDescriptionService(commandDescriptionService); + if (baseItem instanceof GenericItem item) { + item.setCommandDescriptionService(commandDescriptionService); } } - @Override - public void setUnitProvider(@Nullable UnitProvider unitProvider) { - super.setUnitProvider(unitProvider); - if (baseItem instanceof GenericItem) { - ((GenericItem) baseItem).setUnitProvider(unitProvider); + private void sendGroupStateUpdatedEvent(String memberName, State state) { + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1.post(ItemEventFactory.createGroupStateUpdatedEvent(getName(), memberName, state, null)); } } private void sendGroupStateChangedEvent(String memberName, State newState, State oldState) { - if (eventPublisher != null) { - eventPublisher + EventPublisher eventPublisher1 = this.eventPublisher; + if (eventPublisher1 != null) { + eventPublisher1 .post(ItemEventFactory.createGroupStateChangedEvent(getName(), memberName, newState, oldState)); } } @@ -449,4 +448,25 @@ private boolean isGroupItem(Item item) { private boolean hasOwnState(GroupItem item) { return item.getFunction() != null && item.getBaseItem() != null; } + + @Override + public void addedMetadata(Metadata metadata) { + if (baseItem instanceof MetadataAwareItem metadataAwareItem) { + metadataAwareItem.addedMetadata(metadata); + } + } + + @Override + public void updatedMetadata(Metadata oldMetadata, Metadata newMetadata) { + if (baseItem instanceof MetadataAwareItem metadataAwareItem) { + metadataAwareItem.updatedMetadata(oldMetadata, newMetadata); + } + } + + @Override + public void removedMetadata(Metadata metadata) { + if (baseItem instanceof MetadataAwareItem metadataAwareItem) { + metadataAwareItem.removedMetadata(metadata); + } + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java index 541f89f75e3..aba02c363bb 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Item.java @@ -46,7 +46,7 @@ public interface Item extends Identifiable { * * @return the current state */ - public State getState(); + State getState(); /** * returns the current state of the item as a specific type @@ -54,21 +54,21 @@ public interface Item extends Identifiable { * @return the current state in the requested type or * null, if state cannot be provided as the requested type */ - public @Nullable T getStateAs(Class typeClass); + @Nullable T getStateAs(Class typeClass); /** * returns the name of the item * * @return the name of the item */ - public String getName(); + String getName(); /** * returns the item type as defined by {@link ItemFactory}s * * @return the item type */ - public String getType(); + String getType(); /** *

@@ -86,7 +86,7 @@ public interface Item extends Identifiable { * * @return a list of data types that can be used to update the item state */ - public List> getAcceptedDataTypes(); + List> getAcceptedDataTypes(); /** *

@@ -99,28 +99,29 @@ public interface Item extends Identifiable { * * @return a list of all command types that can be used for this item */ - public List> getAcceptedCommandTypes(); + List> getAcceptedCommandTypes(); /** * Returns a list of the names of the groups this item belongs to. * * @return list of item group names */ - public List getGroupNames(); + List getGroupNames(); /** * Returns a set of tags. If the item is not tagged, an empty set is returned. * * @return set of tags. */ - public Set getTags(); + Set getTags(); /** * Returns the label of the item or null if no label is set. * * @return item label or null */ - public @Nullable String getLabel(); + @Nullable + String getLabel(); /** * Returns true if the item's tags contains the specific tag, otherwise false. @@ -128,14 +129,15 @@ public interface Item extends Identifiable { * @param tag a tag whose presence in the item's tags is to be tested. * @return true if the item's tags contains the specific tag, otherwise false. */ - public boolean hasTag(String tag); + boolean hasTag(String tag); /** * Returns the category of the item or null if no category is set. * * @return category or null */ - public @Nullable String getCategory(); + @Nullable + String getCategory(); /** * Returns the first provided state description (uses the default locale). @@ -143,7 +145,8 @@ public interface Item extends Identifiable { * * @return state description (can be null) */ - public @Nullable StateDescription getStateDescription(); + @Nullable + StateDescription getStateDescription(); /** * Returns the first provided state description for a given locale. @@ -152,7 +155,8 @@ public interface Item extends Identifiable { * @param locale locale (can be null) * @return state description (can be null) */ - public @Nullable StateDescription getStateDescription(@Nullable Locale locale); + @Nullable + StateDescription getStateDescription(@Nullable Locale locale); /** * Returns the {@link CommandDescription} for this item. In case no dedicated {@link CommandDescription} is @@ -161,7 +165,7 @@ public interface Item extends Identifiable { * * @return the {@link CommandDescription} for the default locale (can be null). */ - public default @Nullable CommandDescription getCommandDescription() { + default @Nullable CommandDescription getCommandDescription() { return getCommandDescription(null); } @@ -173,5 +177,6 @@ public interface Item extends Identifiable { * @param locale locale (can be null) * @return command description (can be null) */ - public @Nullable CommandDescription getCommandDescription(@Nullable Locale locale); + @Nullable + CommandDescription getCommandDescription(@Nullable Locale locale); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ItemRegistry.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ItemRegistry.java index fafc3c7ac3d..86847a6f2a6 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ItemRegistry.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ItemRegistry.java @@ -38,7 +38,7 @@ public interface ItemRegistry extends Registry { * @return the uniquely identified item * @throws ItemNotFoundException if no item matches the input */ - public Item getItem(String name) throws ItemNotFoundException; + Item getItem(String name) throws ItemNotFoundException; /** * This method retrieves a single item from the registry. @@ -49,14 +49,14 @@ public interface ItemRegistry extends Registry { * @throws ItemNotFoundException if no item matches the input * @throws ItemNotUniqueException if multiply items match the input */ - public Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException; + Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException; /** * This method retrieves all items that are currently available in the registry * * @return a collection of all available items */ - public Collection getItems(); + Collection getItems(); /** * This method retrieves all items with the given type @@ -64,14 +64,14 @@ public interface ItemRegistry extends Registry { * @param type item type as defined by {@link ItemFactory}s * @return a collection of all items of the given type */ - public Collection getItemsOfType(String type); + Collection getItemsOfType(String type); /** * This method retrieves all items that match a given search pattern * * @return a collection of all items matching the search pattern */ - public Collection getItems(String pattern); + Collection getItems(String pattern); /** * Returns list of items which contains all of the given tags. @@ -79,7 +79,7 @@ public interface ItemRegistry extends Registry { * @param tags array of tags to be present on the returned items. * @return list of items which contains all of the given tags. */ - public Collection getItemsByTag(String... tags); + Collection getItemsByTag(String... tags); /** * Returns list of items with a certain type containing all of the given tags. @@ -88,7 +88,7 @@ public interface ItemRegistry extends Registry { * @param tags array of tags to be present on the returned items. * @return list of items which contains all of the given tags. */ - public Collection getItemsByTagAndType(String type, String... tags); + Collection getItemsByTagAndType(String type, String... tags); /** * Returns list of items which contains all of the given tags. @@ -98,12 +98,13 @@ public interface ItemRegistry extends Registry { * @return list of items which contains all of the given tags, which is * filtered by the given type filter. */ - public Collection getItemsByTag(Class typeFilter, String... tags); + Collection getItemsByTag(Class typeFilter, String... tags); /** * @see ManagedItemProvider#remove(String, boolean) */ - public @Nullable Item remove(String itemName, boolean recursive); + @Nullable + Item remove(String itemName, boolean recursive); /** * Add a hook to be informed before adding/after removing items. diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ManagedItemProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ManagedItemProvider.java index 59268718282..2a54f1e9b14 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ManagedItemProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/ManagedItemProvider.java @@ -86,8 +86,8 @@ public ManagedItemProvider(final @Reference StorageService storageService, @Override public @Nullable Item remove(String key) { Item item = get(key); - if (item instanceof GroupItem) { - removeGroupNameFromMembers((GroupItem) item); + if (item instanceof GroupItem groupItem) { + removeGroupNameFromMembers(groupItem); } return super.remove(key); @@ -102,8 +102,8 @@ public ManagedItemProvider(final @Reference StorageService storageService, */ public @Nullable Item remove(String itemName, boolean recursive) { Item item = get(itemName); - if (recursive && item instanceof GroupItem) { - for (String member : getMemberNamesRecursively((GroupItem) item, getAll())) { + if (recursive && item instanceof GroupItem groupItem) { + for (String member : getMemberNamesRecursively(groupItem, getAll())) { remove(member); } } @@ -128,8 +128,8 @@ private List getMemberNamesRecursively(GroupItem groupItem, Collection getMembers(GroupItem groupItem, Collection allItems) { private @Nullable Item createItem(String itemType, String itemName) { try { - Item item = itemBuilderFactory.newItemBuilder(itemType, itemName).build(); - return item; + return itemBuilderFactory.newItemBuilder(itemType, itemName).build(); } catch (IllegalStateException e) { logger.debug("Couldn't create item '{}' of type '{}'", itemName, itemType); return null; @@ -159,8 +158,8 @@ private Set getMembers(GroupItem groupItem, Collection allItems) { private void removeGroupNameFromMembers(GroupItem groupItem) { Set members = getMembers(groupItem, getAll()); for (Item member : members) { - if (member instanceof GenericItem) { - ((GenericItem) member).removeGroupName(groupItem.getUID()); + if (member instanceof GenericItem item) { + item.removeGroupName(groupItem.getUID()); update(member); } } @@ -187,9 +186,9 @@ protected void addItemFactory(ItemFactory itemFactory) { String itemName = entry.getKey(); PersistedItem persistedItem = entry.getValue(); Item item = itemFactory.createItem(persistedItem.itemType, itemName); - if (item != null && item instanceof GenericItem) { + if (item != null && item instanceof GenericItem genericItem) { iterator.remove(); - configureItem(persistedItem, (GenericItem) item); + configureItem(persistedItem, genericItem); notifyListenersAboutAddedElement(item); } else { logger.debug("The added item factory '{}' still could not instantiate item '{}'.", itemFactory, @@ -237,8 +236,8 @@ protected String keyToString(String key) { item = createItem(persistedItem.itemType, itemName); } - if (item != null && item instanceof GenericItem) { - configureItem(persistedItem, (GenericItem) item); + if (item != null && item instanceof GenericItem genericItem) { + configureItem(persistedItem, genericItem); } if (item == null) { @@ -283,8 +282,7 @@ protected PersistedItem toPersistableElement(Item item) { PersistedItem persistedItem = new PersistedItem( item instanceof GroupItem ? GroupItem.TYPE : toItemFactoryName(item)); - if (item instanceof GroupItem) { - GroupItem groupItem = (GroupItem) item; + if (item instanceof GroupItem groupItem) { String baseItemType = null; Item baseItem = groupItem.getBaseItem(); if (baseItem != null) { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Metadata.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Metadata.java index b31cddeb81a..068c1177905 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Metadata.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/Metadata.java @@ -95,10 +95,7 @@ public boolean equals(@Nullable Object obj) { return false; } Metadata other = (Metadata) obj; - if (!key.equals(other.key)) { - return false; - } - return true; + return key.equals(other.key); } @Override diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataAwareItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataAwareItem.java new file mode 100644 index 00000000000..dba3323d3c4 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataAwareItem.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.items; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MetadataAwareItem} is an interface that can be implemented by {@link Item}s that need to be notified of + * metadata changes. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface MetadataAwareItem { + + /** + * Can be implemented by subclasses to be informed about added metadata + * + * @param metadata the added {@link Metadata} object for this {@link Item} + */ + void addedMetadata(Metadata metadata); + + /** + * Can be implemented by subclasses to be informed about updated metadata + * + * @param oldMetadata the old {@link Metadata} object for this {@link Item} + * @param newMetadata the new {@link Metadata} object for this {@link Item} + * + */ + void updatedMetadata(Metadata oldMetadata, Metadata newMetadata); + + /** + * Can be implemented by subclasses to be informed about removed metadata + * + * @param metadata the removed {@link Metadata} object for this {@link Item} + */ + void removedMetadata(Metadata metadata); +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataRegistry.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataRegistry.java index d6734c41c54..5f5d098611e 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataRegistry.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/MetadataRegistry.java @@ -28,7 +28,7 @@ @NonNullByDefault public interface MetadataRegistry extends Registry { - public static final String INTERNAL_NAMESPACE_PREFIX = "_"; + static final String INTERNAL_NAMESPACE_PREFIX = "_"; /** * Determines whether the given namespace is internal. @@ -43,7 +43,7 @@ public interface MetadataRegistry extends Registry { * * @param itemname the name of the item for which the namespaces should be searched. */ - public Collection getAllNamespaces(String itemname); + Collection getAllNamespaces(String itemname); /** * Remove all metadata of a given item diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java index 98dc0f2c2cc..e910f5c20f0 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/StateChangeListener.java @@ -34,7 +34,7 @@ public interface StateChangeListener { * @param oldState the previous state * @param newState the new state */ - public void stateChanged(Item item, State oldState, State newState); + void stateChanged(Item item, State oldState, State newState); /** * This method is called, if a state was updated, but has not changed @@ -42,5 +42,5 @@ public interface StateChangeListener { * @param item the item whose state was updated * @param state the current state, same before and after the update */ - public void stateUpdated(Item item, State state); + void stateUpdated(Item item, State state); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/dto/ItemDTOMapper.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/dto/ItemDTOMapper.java index ed65f881c28..fff5703f16c 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/dto/ItemDTOMapper.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/dto/ItemDTOMapper.java @@ -60,8 +60,7 @@ public class ItemDTOMapper { if (itemDTO.type != null) { ItemBuilder builder = itemBuilderFactory.newItemBuilder(itemDTO.type, itemDTO.name); - if (itemDTO instanceof GroupItemDTO && GroupItem.TYPE.equals(itemDTO.type)) { - GroupItemDTO groupItemDTO = (GroupItemDTO) itemDTO; + if (itemDTO instanceof GroupItemDTO groupItemDTO && GroupItem.TYPE.equals(itemDTO.type)) { Item baseItem = null; if (groupItemDTO.groupType != null && !groupItemDTO.groupType.isEmpty()) { baseItem = itemBuilderFactory.newItemBuilder(groupItemDTO.groupType, itemDTO.name).build(); @@ -107,8 +106,7 @@ public static ItemDTO map(Item item) { } private static void fillProperties(ItemDTO itemDTO, Item item) { - if (item instanceof GroupItem) { - GroupItem groupItem = (GroupItem) item; + if (item instanceof GroupItem groupItem) { GroupItemDTO groupItemDTO = (GroupItemDTO) itemDTO; Item baseItem = groupItem.getBaseItem(); if (baseItem != null) { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java index d0b1a57f84d..e9f323fc991 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/AbstractItemEventSubscriber.java @@ -40,10 +40,10 @@ public Set getSubscribedEventTypes() { @Override public void receive(Event event) { - if (event instanceof ItemStateEvent) { - receiveUpdate((ItemStateEvent) event); - } else if (event instanceof ItemCommandEvent) { - receiveCommand((ItemCommandEvent) event); + if (event instanceof ItemStateEvent stateEvent) { + receiveUpdate(stateEvent); + } else if (event instanceof ItemCommandEvent commandEvent) { + receiveCommand(commandEvent); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java index 171aae7ff81..0545c5f1573 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupItemStateChangedEvent.java @@ -17,7 +17,8 @@ /** * {@link GroupItemStateChangedEvent}s can be used to deliver group item state changes through the openHAB event bus. In - * contrast to the {@link GroupItemStateEvent} the {@link GroupItemStateChangedEvent} is only sent if the state changed. + * contrast to the {@link GroupStateUpdatedEvent} the {@link GroupItemStateChangedEvent} is only sent if the state + * changed. * State events must be created with the {@link ItemEventFactory}. * * @author Christoph Knauf - Initial contribution diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java new file mode 100644 index 00000000000..9a1d8d773c7 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/GroupStateUpdatedEvent.java @@ -0,0 +1,58 @@ +/** + * 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.items.events; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; + +/** + * {@link GroupStateUpdatedEvent}s can be used to deliver group item state updates through the openHAB event bus. + * In contrast to the {@link GroupItemStateChangedEvent} it is always sent. + * State events must be created with the {@link ItemEventFactory}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class GroupStateUpdatedEvent extends ItemStateUpdatedEvent { + + /** + * The group item state changed event type. + */ + public static final String TYPE = GroupStateUpdatedEvent.class.getSimpleName(); + + private final String memberName; + + protected GroupStateUpdatedEvent(String topic, String payload, String itemName, String memberName, + State newItemState, @Nullable String source) { + super(topic, payload, itemName, newItemState, source); + this.memberName = memberName; + } + + /** + * @return the name of the changed group member + */ + public String getMemberName() { + return this.memberName; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String toString() { + return String.format("Group '%s' updated to %s through %s", itemName, itemState, memberName); + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java index b380be7270c..5f4bdbc460d 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemEventFactory.java @@ -50,10 +50,14 @@ public class ItemEventFactory extends AbstractEventFactory { private static final String ITEM_STATE_EVENT_TOPIC = "openhab/items/{itemName}/state"; + private static final String ITEM_STATE_UPDATED_EVENT_TOPIC = "openhab/items/{itemName}/stateupdated"; + private static final String ITEM_STATE_PREDICTED_EVENT_TOPIC = "openhab/items/{itemName}/statepredicted"; private static final String ITEM_STATE_CHANGED_EVENT_TOPIC = "openhab/items/{itemName}/statechanged"; + private static final String GROUP_STATE_EVENT_TOPIC = "openhab/items/{itemName}/{memberName}/stateupdated"; + private static final String GROUPITEM_STATE_CHANGED_EVENT_TOPIC = "openhab/items/{itemName}/{memberName}/statechanged"; private static final String ITEM_ADDED_EVENT_TOPIC = "openhab/items/{itemName}/added"; @@ -67,8 +71,8 @@ public class ItemEventFactory extends AbstractEventFactory { */ public ItemEventFactory() { super(Set.of(ItemCommandEvent.TYPE, ItemStateEvent.TYPE, ItemStatePredictedEvent.TYPE, - ItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemUpdatedEvent.TYPE, ItemRemovedEvent.TYPE, - GroupItemStateChangedEvent.TYPE)); + ItemStateUpdatedEvent.TYPE, ItemStateChangedEvent.TYPE, ItemAddedEvent.TYPE, ItemUpdatedEvent.TYPE, + ItemRemovedEvent.TYPE, GroupStateUpdatedEvent.TYPE, GroupItemStateChangedEvent.TYPE)); } @Override @@ -80,6 +84,8 @@ protected Event createEventByType(String eventType, String topic, String payload return createStateEvent(topic, payload, source); } else if (ItemStatePredictedEvent.TYPE.equals(eventType)) { return createStatePredictedEvent(topic, payload); + } else if (ItemStateUpdatedEvent.TYPE.equals(eventType)) { + return createStateUpdatedEvent(topic, payload); } else if (ItemStateChangedEvent.TYPE.equals(eventType)) { return createStateChangedEvent(topic, payload); } else if (ItemAddedEvent.TYPE.equals(eventType)) { @@ -88,12 +94,22 @@ protected Event createEventByType(String eventType, String topic, String payload return createUpdatedEvent(topic, payload); } else if (ItemRemovedEvent.TYPE.equals(eventType)) { return createRemovedEvent(topic, payload); + } else if (GroupStateUpdatedEvent.TYPE.equals(eventType)) { + return createGroupStateUpdatedEvent(topic, payload); } else if (GroupItemStateChangedEvent.TYPE.equals(eventType)) { return createGroupStateChangedEvent(topic, payload); } throw new IllegalArgumentException("The event type '" + eventType + "' is not supported by this factory."); } + private Event createGroupStateUpdatedEvent(String topic, String payload) { + String itemName = getItemName(topic); + String memberName = getMemberName(topic); + ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); + State state = getState(bean.getType(), bean.getValue()); + return new GroupStateUpdatedEvent(topic, payload, itemName, memberName, state, null); + } + private Event createGroupStateChangedEvent(String topic, String payload) { String itemName = getItemName(topic); String memberName = getMemberName(topic); @@ -124,6 +140,13 @@ private Event createStatePredictedEvent(String topic, String payload) { return new ItemStatePredictedEvent(topic, payload, itemName, state, bean.isConfirmation()); } + private Event createStateUpdatedEvent(String topic, String payload) { + String itemName = getItemName(topic); + ItemEventPayloadBean bean = deserializePayload(payload, ItemEventPayloadBean.class); + State state = getState(bean.getType(), bean.getValue()); + return new ItemStateUpdatedEvent(topic, payload, itemName, state, null); + } + private Event createStateChangedEvent(String topic, String payload) { String itemName = getItemName(topic); ItemStateChangedEventPayloadBean bean = deserializePayload(payload, ItemStateChangedEventPayloadBean.class); @@ -268,6 +291,54 @@ public static ItemEvent createStateEvent(String itemName, State state) { return createStateEvent(itemName, state, null); } + /** + * Creates an item state updated event. + * + * @param itemName the name of the item to report the state update for + * @param state the new state + * @return the created item state update event + * @throws IllegalArgumentException if itemName or state is null + */ + public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state) { + return createStateUpdatedEvent(itemName, state, null); + } + + /** + * Creates an item state updated event. + * + * @param itemName the name of the item to report the state update for + * @param state the new state + * @param source the name of the source identifying the sender (can be null) + * @return the created item state update event + * @throws IllegalArgumentException if itemName or state is null + */ + public static ItemStateUpdatedEvent createStateUpdatedEvent(String itemName, State state, @Nullable String source) { + assertValidArguments(itemName, state, "state"); + String topic = buildTopic(ITEM_STATE_UPDATED_EVENT_TOPIC, itemName); + ItemEventPayloadBean bean = new ItemEventPayloadBean(getStateType(state), state.toFullString()); + String payload = serializePayload(bean); + return new ItemStateUpdatedEvent(topic, payload, itemName, state, source); + } + + /** + * Creates a group item state updated event. + * + * @param groupName the name of the group to report the state update for + * @param member the name of the item that updated the group state + * @param state the new state + * @param source the name of the source identifying the sender (can be null) + * @return the created group item state update event + * @throws IllegalArgumentException if groupName or state is null + */ + public static GroupStateUpdatedEvent createGroupStateUpdatedEvent(String groupName, String member, State state, + @Nullable String source) { + assertValidArguments(groupName, member, state, "state"); + String topic = buildGroupTopic(GROUP_STATE_EVENT_TOPIC, groupName, member); + ItemEventPayloadBean bean = new ItemEventPayloadBean(getStateType(state), state.toFullString()); + String payload = serializePayload(bean); + return new GroupStateUpdatedEvent(topic, payload, groupName, member, state, source); + } + /** * Creates an item state predicted event. * diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java index 80865370ca5..90d6eb326a4 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateEvent.java @@ -30,7 +30,7 @@ public class ItemStateEvent extends ItemEvent { */ public static final String TYPE = ItemStateEvent.class.getSimpleName(); - private final State itemState; + protected final State itemState; /** * Constructs a new item state event. @@ -62,6 +62,6 @@ public State getItemState() { @Override public String toString() { - return String.format("Item '%s' updated to %s", itemName, itemState); + return String.format("Item '%s' shall update to %s", itemName, itemState); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java new file mode 100644 index 00000000000..0b582c282c4 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/items/events/ItemStateUpdatedEvent.java @@ -0,0 +1,68 @@ +/** + * 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.items.events; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; + +/** + * {@link ItemStateUpdatedEvent}s can be used to report item status updates through the openHAB event bus. + * State update events must be created with the {@link ItemEventFactory}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ItemStateUpdatedEvent extends ItemEvent { + + /** + * The item state event type. + */ + public static final String TYPE = ItemStateUpdatedEvent.class.getSimpleName(); + + protected final State itemState; + + /** + * Constructs a new item state event. + * + * @param topic the topic + * @param payload the payload + * @param itemName the item name + * @param itemState the item state + * @param source the source, can be null + */ + protected ItemStateUpdatedEvent(String topic, String payload, String itemName, State itemState, + @Nullable String source) { + super(topic, payload, itemName, source); + this.itemState = itemState; + } + + @Override + public String getType() { + return TYPE; + } + + /** + * Gets the item state. + * + * @return the item state + */ + public State getItemState() { + return itemState; + } + + @Override + public String toString() { + return String.format("Item '%s' updated to %s", itemName, itemState); + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java index 63390bcc7e4..8bef6bd054b 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java @@ -14,6 +14,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.ItemFactory; import org.openhab.core.items.ItemUtil; @@ -29,7 +30,9 @@ import org.openhab.core.library.items.RollershutterItem; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.items.SwitchItem; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; /** * {@link CoreItemFactory}-Implementation for the core ItemTypes @@ -54,6 +57,12 @@ public class CoreItemFactory implements ItemFactory { public static final String ROLLERSHUTTER = "Rollershutter"; public static final String STRING = "String"; public static final String SWITCH = "Switch"; + private final UnitProvider unitProvider; + + @Activate + public CoreItemFactory(final @Reference UnitProvider unitProvider) { + this.unitProvider = unitProvider; + } @Override public @Nullable GenericItem createItem(@Nullable String itemTypeName, String itemName) { @@ -62,34 +71,21 @@ public class CoreItemFactory implements ItemFactory { } String itemType = ItemUtil.getMainItemType(itemTypeName); - switch (itemType) { - case CALL: - return new CallItem(itemName); - case COLOR: - return new ColorItem(itemName); - case CONTACT: - return new ContactItem(itemName); - case DATETIME: - return new DateTimeItem(itemName); - case DIMMER: - return new DimmerItem(itemName); - case IMAGE: - return new ImageItem(itemName); - case LOCATION: - return new LocationItem(itemName); - case NUMBER: - return new NumberItem(itemTypeName, itemName); - case PLAYER: - return new PlayerItem(itemName); - case ROLLERSHUTTER: - return new RollershutterItem(itemName); - case STRING: - return new StringItem(itemName); - case SWITCH: - return new SwitchItem(itemName); - default: - return null; - } + return switch (itemType) { + case CALL -> new CallItem(itemName); + case COLOR -> new ColorItem(itemName); + case CONTACT -> new ContactItem(itemName); + case DATETIME -> new DateTimeItem(itemName); + case DIMMER -> new DimmerItem(itemName); + case IMAGE -> new ImageItem(itemName); + case LOCATION -> new LocationItem(itemName); + case NUMBER -> new NumberItem(itemTypeName, itemName, unitProvider); + case PLAYER -> new PlayerItem(itemName); + case ROLLERSHUTTER -> new RollershutterItem(itemName); + case STRING -> new StringItem(itemName); + case SWITCH -> new SwitchItem(itemName); + default -> null; + }; } @Override diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/RadiationSpecificActivity.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/RadiationSpecificActivity.java new file mode 100644 index 00000000000..2fd554e7372 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/RadiationSpecificActivity.java @@ -0,0 +1,26 @@ +/** + * 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.library.dimension; + +import javax.measure.Quantity; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Define Specific activity type (basic unit is Bq/m^3) + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface RadiationSpecificActivity extends Quantity { +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/ColorItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/ColorItem.java index 3e53d4b030c..45afe755309 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/ColorItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/ColorItem.java @@ -63,19 +63,19 @@ public void setState(State state) { if (isAcceptedState(ACCEPTED_DATA_TYPES, state)) { State currentState = this.state; - if (currentState instanceof HSBType) { - DecimalType hue = ((HSBType) currentState).getHue(); - PercentType saturation = ((HSBType) currentState).getSaturation(); + if (currentState instanceof HSBType hsbType) { + DecimalType hue = hsbType.getHue(); + PercentType saturation = hsbType.getSaturation(); // we map ON/OFF values to dark/bright, so that the hue and saturation values are not changed if (state == OnOffType.OFF) { applyState(new HSBType(hue, saturation, PercentType.ZERO)); } else if (state == OnOffType.ON) { applyState(new HSBType(hue, saturation, PercentType.HUNDRED)); - } else if (state instanceof PercentType && !(state instanceof HSBType)) { - applyState(new HSBType(hue, saturation, (PercentType) state)); - } else if (state instanceof DecimalType && !(state instanceof HSBType)) { + } else if (state instanceof PercentType percentType && !(state instanceof HSBType)) { + applyState(new HSBType(hue, saturation, percentType)); + } else if (state instanceof DecimalType decimalType && !(state instanceof HSBType)) { applyState(new HSBType(hue, saturation, - new PercentType(((DecimalType) state).toBigDecimal().multiply(BigDecimal.valueOf(100))))); + new PercentType(decimalType.toBigDecimal().multiply(BigDecimal.valueOf(100))))); } else { applyState(state); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/LocationItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/LocationItem.java index 341e1dd42aa..3d8f691e7fe 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/LocationItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/LocationItem.java @@ -65,9 +65,8 @@ public List> getAcceptedCommandTypes() { * @return distance between the two points in meters */ public DecimalType distanceFrom(@Nullable LocationItem awayItem) { - if (awayItem != null && awayItem.state instanceof PointType && this.state instanceof PointType) { - PointType thisPoint = (PointType) this.state; - PointType awayPoint = (PointType) awayItem.state; + if (awayItem != null && awayItem.state instanceof PointType awayPoint + && this.state instanceof PointType thisPoint) { return thisPoint.distanceFrom(awayPoint); } return new DecimalType(-1); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/NumberItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/NumberItem.java index 619d9a6f4eb..43727408989 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/NumberItem.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/NumberItem.java @@ -21,11 +21,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.ItemUtil; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataAwareItem; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -33,6 +37,8 @@ import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.UnDefType; import org.openhab.core.types.util.UnitUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A NumberItem has a decimal value and is usually used for all kinds @@ -43,26 +49,40 @@ * @author Kai Kreuzer - Initial contribution */ @NonNullByDefault -public class NumberItem extends GenericItem { - +public class NumberItem extends GenericItem implements MetadataAwareItem { + public static final String UNIT_METADATA_NAMESPACE = "unit"; private static final List> ACCEPTED_DATA_TYPES = List.of(DecimalType.class, QuantityType.class, UnDefType.class); private static final List> ACCEPTED_COMMAND_TYPES = List.of(DecimalType.class, QuantityType.class, RefreshType.class); - @Nullable - private Class> dimension; + private final Logger logger = LoggerFactory.getLogger(NumberItem.class); + + private final @Nullable Class> dimension; + private Unit unit = Units.ONE; + private final @Nullable UnitProvider unitProvider; public NumberItem(String name) { - this(CoreItemFactory.NUMBER, name); + this(CoreItemFactory.NUMBER, name, null); } - public NumberItem(String type, String name) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + public NumberItem(String type, String name, @Nullable UnitProvider unitProvider) { super(type, name); + this.unitProvider = unitProvider; String itemTypeExtension = ItemUtil.getItemTypeExtension(getType()); if (itemTypeExtension != null) { dimension = UnitUtils.parseDimension(itemTypeExtension); + if (dimension == null) { + throw new IllegalArgumentException("The given dimension " + itemTypeExtension + " is unknown."); + } else if (unitProvider == null) { + throw new IllegalArgumentException("A unit provider is required for items with a dimension."); + } + this.unit = unitProvider.getUnit((Class) dimension); + logger.trace("Item '{}' now has unit '{}'", name, unit); + } else { + dimension = null; } } @@ -85,7 +105,12 @@ public void send(QuantityType command) { DecimalType strippedCommand = new DecimalType(command.toBigDecimal()); internalSend(strippedCommand); } else { - internalSend(command); + if (command.getUnit().isCompatible(unit) || command.getUnit().inverse().isCompatible(unit)) { + internalSend(command); + } else { + logger.warn("Command '{}' to item '{}' was rejected because it is incompatible with the item unit '{}'", + command, name, unit); + } } } @@ -114,41 +139,34 @@ public void send(QuantityType command) { @Override public void setState(State state) { - // QuantityType update to a NumberItem without, strip unit - if (state instanceof QuantityType && dimension == null) { - DecimalType plainState = new DecimalType(((QuantityType) state).toBigDecimal()); - super.setState(plainState); - return; - } - - // DecimalType update for a NumberItem with dimension, convert to QuantityType: - if (state instanceof DecimalType && dimension != null) { - Unit unit = getUnit(dimension, false); - if (unit != null) { - super.setState(new QuantityType<>(((DecimalType) state).doubleValue(), unit)); - return; - } - } - - // QuantityType update, check unit and convert if necessary: - if (state instanceof QuantityType) { - Unit itemUnit = getUnit(dimension, true); - Unit stateUnit = ((QuantityType) state).getUnit(); - if (itemUnit != null && (!stateUnit.getSystemUnit().equals(itemUnit.getSystemUnit()) - || UnitUtils.isDifferentMeasurementSystem(itemUnit, stateUnit))) { - QuantityType convertedState = ((QuantityType) state).toInvertibleUnit(itemUnit); + if (state instanceof QuantityType quantityType) { + if (dimension == null) { + // QuantityType update to a NumberItem without unit, strip unit + DecimalType plainState = new DecimalType(quantityType.toBigDecimal()); + super.applyState(plainState); + } else { + // QuantityType update to a NumberItem with unit, convert to item unit (if possible) + Unit stateUnit = quantityType.getUnit(); + State convertedState = (stateUnit.isCompatible(unit) || stateUnit.inverse().isCompatible(unit)) + ? quantityType.toInvertibleUnit(unit) + : null; if (convertedState != null) { - super.setState(convertedState); - return; + super.applyState(convertedState); + } else { + logger.warn("Failed to update item '{}' because '{}' could not be converted to the item unit '{}'", + name, state, unit); } - - // the state could not be converted to an accepted unit. - return; } - } - - if (isAcceptedState(ACCEPTED_DATA_TYPES, state)) { - super.setState(state); + } else if (state instanceof DecimalType decimalType) { + if (dimension == null) { + // DecimalType update to NumberItem with unit + super.applyState(decimalType); + } else { + // DecimalType update for a NumberItem with dimension, convert to QuantityType + super.applyState(new QuantityType<>(decimalType.doubleValue(), unit)); + } + } else if (state instanceof UnDefType) { + super.applyState(state); } else { logSetTypeError(state); } @@ -160,83 +178,54 @@ public void setState(State state) { * @return the optional unit symbol for this {@link NumberItem}. */ public @Nullable String getUnitSymbol() { - Unit unit = getUnit(dimension, true); - return unit != null ? unit.toString() : null; + return (dimension != null) ? unit.toString() : null; } /** - * Derive the unit for this item by the following priority: + * Get the unit for this item, either: + * *

    - *
  • the unit parsed from the state description
  • - *
  • no unit if state description contains %unit%
  • - *
  • the default system unit from the item's dimension
  • + *
  • the unit retrieved from the unit namespace in the item's metadata
  • + *
  • the default system unit for the item's dimension
  • *
* * @return the {@link Unit} for this item if available, {@code null} otherwise. */ public @Nullable Unit> getUnit() { - return getUnit(dimension, true); + return (dimension != null) ? unit : null; } - /** - * Try to convert a {@link DecimalType} into a new {@link QuantityType}. The unit for the new - * type is derived either from the state description (which might also give a hint on items w/o dimension) or from - * the system default unit of the given dimension. - * - * @param originalType the source {@link DecimalType}. - * @param dimension the dimension to which the new {@link QuantityType} should adhere. - * @return the new {@link QuantityType} from the given originalType, {@code null} if a unit could not be calculated. - */ - public @Nullable QuantityType toQuantityType(DecimalType originalType, - @Nullable Class> dimension) { - Unit> itemUnit = getUnit(dimension, false); - if (itemUnit != null) { - return new QuantityType<>(originalType.toBigDecimal(), itemUnit); + @Override + public void addedMetadata(Metadata metadata) { + if (dimension != null && UNIT_METADATA_NAMESPACE.equals(metadata.getUID().getNamespace())) { + Unit unit = UnitUtils.parseUnit(metadata.getValue()); + if (unit == null) { + logger.warn("Unit '{}' could not be parsed to a known unit. Keeping old unit '{}' for item '{}'.", + metadata.getValue(), this.unit, name); + return; + } + if (!unit.isCompatible(this.unit) && !unit.inverse().isCompatible(this.unit)) { + logger.warn("Unit '{}' could not be parsed to a known unit. Keeping old unit '{}' for item '{}'.", + metadata.getValue(), this.unit, name); + return; + } + this.unit = unit; + logger.trace("Item '{}' now has unit '{}'", name, unit); } + } - return null; + @Override + public void updatedMetadata(Metadata oldMetadata, Metadata newMetadata) { + addedMetadata(newMetadata); } - /** - * Derive the unit for this item by the following priority: - *
    - *
  • the unit parsed from the state description
  • - *
  • the unit from the value if hasUnit = true and state description has unit - * %unit%
  • - *
  • the default system unit from the (optional) dimension parameter
  • - *
- * - * @param dimension the (optional) dimension - * @param hasUnit if the value has a unit - * @return the {@link Unit} for this item if available, {@code null} otherwise. - */ + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - private @Nullable Unit> getUnit(@Nullable Class> dimension, - boolean hasUnit) { - if (dimension == null) { - // if it is a plain number without dimension, we do not have a unit. - return null; + public void removedMetadata(Metadata metadata) { + if (dimension != null && UNIT_METADATA_NAMESPACE.equals(metadata.getUID().getNamespace())) { + assert unitProvider != null; + unit = unitProvider.getUnit((Class) dimension); + logger.trace("Item '{}' now has unit '{}'", name, unit); } - StateDescription stateDescription = getStateDescription(); - if (stateDescription != null) { - String pattern = stateDescription.getPattern(); - if (pattern != null) { - if (hasUnit && pattern.contains(UnitUtils.UNIT_PLACEHOLDER)) { - // use provided unit if present - return null; - } - Unit stateDescriptionUnit = UnitUtils.parseUnit(pattern); - if (stateDescriptionUnit != null) { - return stateDescriptionUnit; - } - } - } - - if (unitProvider != null) { - // explicit cast to Class as JDK compiler complains - return unitProvider.getUnit((Class) dimension); - } - - return null; } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DecimalType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DecimalType.java index d427968dd94..137a0f15006 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DecimalType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DecimalType.java @@ -51,10 +51,10 @@ public DecimalType() { * @param value a number */ public DecimalType(Number value) { - if (value instanceof QuantityType) { - this.value = ((QuantityType) value).toBigDecimal(); - } else if (value instanceof HSBType) { - this.value = ((HSBType) value).toBigDecimal(); + if (value instanceof QuantityType type) { + this.value = type.toBigDecimal(); + } else if (value instanceof HSBType type) { + this.value = type.toBigDecimal(); } else { this.value = new BigDecimal(value.toString()); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/HSBType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/HSBType.java index c2360f861c2..5744ae67b10 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/HSBType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/HSBType.java @@ -34,6 +34,7 @@ * * @author Kai Kreuzer - Initial contribution * @author Chris Jackson - Added fromRGB + * @author Andrew Fiddian-Green - closeTo (copied from binding) */ @NonNullByDefault public class HSBType extends PercentType implements ComplexType, State, Command { @@ -85,7 +86,7 @@ public HSBType(DecimalType h, PercentType s, PercentType b) { * @param value a stringified HSBType value in the format "hue,saturation,brightness" */ public HSBType(String value) { - List constituents = Arrays.stream(value.split(",")).map(in -> in.trim()).collect(Collectors.toList()); + List constituents = Arrays.stream(value.split(",")).map(String::trim).collect(Collectors.toList()); if (constituents.size() == 3) { this.hue = new BigDecimal(constituents.get(0)); this.saturation = new BigDecimal(constituents.get(1)); @@ -113,50 +114,22 @@ public static HSBType valueOf(String value) { } /** - * Create HSB from RGB + * Create HSB from RGB. + * + * See also {@link ColorUtil#rgbToHsb(int[])}. * * @param r red 0-255 * @param g green 0-255 * @param b blue 0-255 + * @throws IllegalArgumentException when color values exceed allowed range */ - public static HSBType fromRGB(int r, int g, int b) { - float tmpHue, tmpSaturation, tmpBrightness; - int max = (r > g) ? r : g; - if (b > max) { - max = b; - } - int min = (r < g) ? r : g; - if (b < min) { - min = b; - } - tmpBrightness = max / 2.55f; - tmpSaturation = (max != 0 ? ((float) (max - min)) / ((float) max) : 0) * 100; - if (tmpSaturation == 0) { - tmpHue = 0; - } else { - float red = ((float) (max - r)) / ((float) (max - min)); - float green = ((float) (max - g)) / ((float) (max - min)); - float blue = ((float) (max - b)) / ((float) (max - min)); - if (r == max) { - tmpHue = blue - green; - } else if (g == max) { - tmpHue = 2.0f + red - blue; - } else { - tmpHue = 4.0f + green - red; - } - tmpHue = tmpHue / 6.0f * 360; - if (tmpHue < 0) { - tmpHue = tmpHue + 360.0f; - } - } - - return new HSBType(new DecimalType((int) tmpHue), new PercentType((int) tmpSaturation), - new PercentType((int) tmpBrightness)); + public static HSBType fromRGB(int r, int g, int b) throws IllegalArgumentException { + return ColorUtil.rgbToHsb(new int[] { r, g, b }); } /** - * @deprecated Use {@link ColorUtil#xyToHsv(double[])} or {@link ColorUtil#xyToHsv(double[], ColorUtil.Gamut)} - * instead + * @deprecated Use {@link ColorUtil#xyToHsb(double[])} or {@link ColorUtil#xyToHsb(double[], ColorUtil.Gamut)} + * instead. * * Returns a HSBType object representing the provided xy color values in CIE XY color model. * Conversion from CIE XY color model to sRGB using D65 reference white @@ -164,11 +137,11 @@ public static HSBType fromRGB(int r, int g, int b) { * * @param x, y color information 0.0 - 1.0 * @return new HSBType object representing the given CIE XY color, full brightness - * + * @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range */ @Deprecated - public static HSBType fromXY(float x, float y) { - return ColorUtil.xyToHsv(new double[] { x, y }); + public static HSBType fromXY(float x, float y) throws IllegalArgumentException { + return ColorUtil.xyToHsb(new double[] { x, y }); } @Override @@ -192,29 +165,36 @@ public PercentType getBrightness() { return new PercentType(value); } + /** @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */ + @Deprecated public PercentType getRed() { return toRGB()[0]; } + /** @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */ + @Deprecated public PercentType getGreen() { return toRGB()[1]; } + /** @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */ + @Deprecated public PercentType getBlue() { return toRGB()[2]; } /** - * Returns the RGB value representing the color in the default sRGB - * color model. - * (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). + * @deprecated Use {@link ColorUtil#hsbTosRgb(HSBType)} instead. + * + * Returns the RGB value representing the color in the default sRGB + * color model. + * (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). * * @return the RGB value of the color in the default sRGB color model */ + @Deprecated public int getRGB() { - PercentType[] rgb = toRGB(); - return ((0xFF) << 24) | ((convertPercentToByte(rgb[0]) & 0xFF) << 16) - | ((convertPercentToByte(rgb[1]) & 0xFF) << 8) | ((convertPercentToByte(rgb[2]) & 0xFF) << 0); + return ColorUtil.hsbTosRgb(this); } @Override @@ -266,58 +246,10 @@ public boolean equals(@Nullable Object obj) { && getBrightness().equals(other.getBrightness()); } + /* @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */ + @Deprecated public PercentType[] toRGB() { - PercentType red = null; - PercentType green = null; - PercentType blue = null; - - BigDecimal h = hue.divide(BIG_DECIMAL_HUNDRED, 10, RoundingMode.HALF_UP); - BigDecimal s = saturation.divide(BIG_DECIMAL_HUNDRED); - - int hInt = h.multiply(BigDecimal.valueOf(5)).divide(BigDecimal.valueOf(3), 10, RoundingMode.HALF_UP).intValue(); - BigDecimal f = h.multiply(BigDecimal.valueOf(5)).divide(BigDecimal.valueOf(3), 10, RoundingMode.HALF_UP) - .remainder(BigDecimal.ONE); - PercentType a = new PercentType(value.multiply(BigDecimal.ONE.subtract(s))); - PercentType b = new PercentType(value.multiply(BigDecimal.ONE.subtract(s.multiply(f)))); - PercentType c = new PercentType( - value.multiply(BigDecimal.ONE.subtract((BigDecimal.ONE.subtract(f)).multiply(s)))); - - switch (hInt) { - case 0: - case 6: - red = getBrightness(); - green = c; - blue = a; - break; - case 1: - red = b; - green = getBrightness(); - blue = a; - break; - case 2: - red = a; - green = getBrightness(); - blue = c; - break; - case 3: - red = a; - green = b; - blue = getBrightness(); - break; - case 4: - red = c; - green = a; - blue = getBrightness(); - break; - case 5: - red = getBrightness(); - green = a; - blue = b; - break; - default: - throw new IllegalArgumentException("Could not convert to RGB."); - } - return new PercentType[] { red, green, blue }; + return ColorUtil.hsbToRgbPercent(this); } /** @@ -334,7 +266,7 @@ public PercentType[] toXY() { } private int convertPercentToByte(PercentType percent) { - return percent.value.multiply(BigDecimal.valueOf(255)).divide(BIG_DECIMAL_HUNDRED, 2, RoundingMode.HALF_UP) + return percent.value.multiply(BigDecimal.valueOf(255)).divide(BIG_DECIMAL_HUNDRED, 0, RoundingMode.HALF_UP) .intValue(); } @@ -352,4 +284,21 @@ private int convertPercentToByte(PercentType percent) { return defaultConversion(target); } } + + /** + * Helper method for checking if two HSBType colors are close to each other. A maximum deviation is specifid in + * percent. + * + * @param other an HSBType containing the other color. + * @param maxPercentage the maximum allowed difference in percent (range 0.0..1.0). + * @throws IllegalArgumentException if percentage is out of range. + */ + public boolean closeTo(HSBType other, double maxPercentage) throws IllegalArgumentException { + if (maxPercentage <= 0.0 || maxPercentage > 1.0) { + throw new IllegalArgumentException("'maxPercentage' out of bounds, allowed range 0..1"); + } + double[] exp = ColorUtil.hsbToXY(this); + double[] act = ColorUtil.hsbToXY(other); + return ((Math.abs(exp[0] - act[0]) < maxPercentage) && (Math.abs(exp[1] - act[1]) < maxPercentage)); + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PointType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PointType.java index 23af517628d..40db3682d20 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PointType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PointType.java @@ -85,7 +85,7 @@ public PointType(StringType latitude, StringType longitude, StringType altitude) public PointType(String value) { if (!value.isEmpty()) { - List elements = Arrays.stream(value.split(",")).map(in -> in.trim()).collect(Collectors.toList()); + List elements = Arrays.stream(value.split(",")).map(String::trim).collect(Collectors.toList()); if (elements.size() >= 2) { canonicalize(new DecimalType(elements.get(0)), new DecimalType(elements.get(1))); if (elements.size() == 3) { @@ -264,10 +264,7 @@ public boolean equals(@Nullable Object obj) { return false; } PointType other = (PointType) obj; - if (!getLatitude().equals(other.getLatitude()) || !getLongitude().equals(other.getLongitude()) - || !getAltitude().equals(other.getAltitude())) { - return false; - } - return true; + return !(!getLatitude().equals(other.getLatitude()) || !getLongitude().equals(other.getLongitude()) + || !getAltitude().equals(other.getAltitude())); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java index bfb209026e4..7f983f2e9b5 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java @@ -298,7 +298,9 @@ public Dimension getDimension() { * @return the new {@link QuantityType} in the given {@link Unit} or {@code null} in case of an erro. */ public @Nullable QuantityType toInvertibleUnit(Unit targetUnit) { - if (!targetUnit.equals(getUnit()) && getUnit().inverse().isCompatible(targetUnit)) { + // only invert if unit is not equal and inverse is compatible and targetUnit is not ONE + if (!targetUnit.equals(getUnit()) && !targetUnit.isCompatible(AbstractUnit.ONE) + && getUnit().inverse().isCompatible(targetUnit)) { return inverse().toUnit(targetUnit); } return toUnit(targetUnit); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java index 8443ec5abff..4a143e2c71f 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java @@ -59,10 +59,10 @@ public State[] getParameters() { } protected boolean isSameDimension(@Nullable Item item) { - if (item instanceof GroupItem) { - return isSameDimension(((GroupItem) item).getBaseItem()); + if (item instanceof GroupItem groupItem) { + return isSameDimension(groupItem.getBaseItem()); } - return item instanceof NumberItem && dimension.equals(((NumberItem) item).getDimension()); + return item instanceof NumberItem ni && dimension.equals(ni.getDimension()); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/RawType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/RawType.java index 3fc3ff54cbe..7e646a272e6 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/RawType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/RawType.java @@ -101,9 +101,6 @@ public boolean equals(@Nullable Object obj) { if (!mimeType.equals(other.mimeType)) { return false; } - if (!Arrays.equals(bytes, other.bytes)) { - return false; - } - return true; + return Arrays.equals(bytes, other.bytes); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/Units.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/Units.java index 3ce1cbaebc6..ce910fe11a7 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/Units.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/Units.java @@ -55,6 +55,7 @@ import org.openhab.core.library.dimension.Density; import org.openhab.core.library.dimension.ElectricConductivity; import org.openhab.core.library.dimension.Intensity; +import org.openhab.core.library.dimension.RadiationSpecificActivity; import org.openhab.core.library.dimension.VolumetricFlowRate; import si.uom.NonSI; @@ -139,6 +140,8 @@ public final class Units extends CustomUnits { new ProductUnit<>(VOLT_AMPERE.multiply(tech.units.indriya.unit.Units.HOUR)), Energy.class); public static final Unit NEWTON = addUnit(tech.units.indriya.unit.Units.NEWTON); public static final Unit HERTZ = addUnit(tech.units.indriya.unit.Units.HERTZ); + public static final Unit RPM = addUnit( + new ProductUnit<>(AbstractUnit.ONE.divide(tech.units.indriya.unit.Units.MINUTE))); public static final Unit IRRADIANCE = addUnit( new ProductUnit<>(tech.units.indriya.unit.Units.WATT.divide(tech.units.indriya.unit.Units.SQUARE_METRE))); public static final Unit MICROWATT_PER_SQUARE_CENTIMETRE = addUnit( @@ -158,8 +161,14 @@ public final class Units extends CustomUnits { MultiplyConverter.ofRational(BigInteger.valueOf(100000), BigInteger.ONE))); public static final Unit MILLIBAR = addUnit(MetricPrefix.MILLI(BAR)); public static final Unit BECQUEREL = addUnit(tech.units.indriya.unit.Units.BECQUEREL); - public static final Unit BECQUEREL_PER_CUBIC_METRE = addUnit(new ProductUnit<>( + public static final Unit CURIE = addUnit(NonSI.CURIE); + public static final Unit MILLI_CURIE = addUnit(MetricPrefix.MILLI(CURIE)); + public static final Unit MICRO_CURIE = addUnit(MetricPrefix.MICRO(CURIE)); + public static final Unit NANO_CURIE = addUnit(MetricPrefix.NANO(CURIE)); + public static final Unit PICO_CURIE = addUnit(MetricPrefix.PICO(CURIE)); + public static final Unit BECQUEREL_PER_CUBIC_METRE = addUnit(new ProductUnit<>( tech.units.indriya.unit.Units.BECQUEREL.divide(tech.units.indriya.unit.Units.CUBIC_METRE))); + public static final Unit GRAY = addUnit(tech.units.indriya.unit.Units.GRAY); public static final Unit SIEVERT = addUnit(tech.units.indriya.unit.Units.SIEVERT); public static final Unit MILLIMETRE_PER_HOUR = addUnit( @@ -232,6 +241,11 @@ public final class Units extends CustomUnits { SimpleUnitFormat.getInstance().label(BIT_PER_SECOND, "bit/s"); SimpleUnitFormat.getInstance().label(BYTE, "B"); SimpleUnitFormat.getInstance().alias(BYTE, "o"); + SimpleUnitFormat.getInstance().label(CURIE, "Ci"); + SimpleUnitFormat.getInstance().label(MILLI_CURIE, "mCi"); + SimpleUnitFormat.getInstance().label(MICRO_CURIE, "µCi"); + SimpleUnitFormat.getInstance().label(NANO_CURIE, "nCi"); + SimpleUnitFormat.getInstance().label(PICO_CURIE, "pCi"); SimpleUnitFormat.getInstance().label(CUBICMETRE_PER_DAY, "m³/d"); SimpleUnitFormat.getInstance().label(CUBICMETRE_PER_HOUR, "m³/h"); SimpleUnitFormat.getInstance().label(CUBICMETRE_PER_MINUTE, "m³/min"); @@ -276,6 +290,7 @@ public final class Units extends CustomUnits { SimpleUnitFormat.getInstance().label(PEBIBYTE, "PiB"); SimpleUnitFormat.getInstance().alias(PEBIBYTE, "Pio"); SimpleUnitFormat.getInstance().label(PETABIT, "Pbit"); + SimpleUnitFormat.getInstance().label(RPM, "rpm"); SimpleUnitFormat.getInstance().label(STANDARD_GRAVITY, "gₙ"); SimpleUnitFormat.getInstance().label(SIEMENS_PER_METRE, "S/m"); SimpleUnitFormat.getInstance().label(TERABYTE, "TB"); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetUtil.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetUtil.java index d30775e55de..b213d6718f7 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetUtil.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetUtil.java @@ -287,7 +287,7 @@ public static List getAllBroadcastAddresses() { if (primaryIp != null) { try { Short prefix = getAllInterfaceAddresses().stream() - .filter(a -> a.getAddress().getHostAddress().equals(primaryIp)).map(a -> a.getPrefix()) + .filter(a -> a.getAddress().getHostAddress().equals(primaryIp)).map(CidrAddress::getPrefix) .findFirst().get().shortValue(); broadcastAddress = getIpv4NetBroadcastAddress(primaryIp, prefix); } catch (IllegalArgumentException ex) { @@ -467,7 +467,7 @@ public static String getIpv4NetBroadcastAddress(String ipAddressString, short pr String ipv4AddressOnInterface = addr.getHostAddress(); String subnetStringOnInterface = getIpv4NetAddress(ipv4AddressOnInterface, - ifAddr.getNetworkPrefixLength()) + "/" + String.valueOf(ifAddr.getNetworkPrefixLength()); + ifAddr.getNetworkPrefixLength()) + "/" + ifAddr.getNetworkPrefixLength(); String configuredSubnetString = getIpv4NetAddress(ipAddress, Short.parseShort(subnetMask)) + "/" + subnetMask; @@ -493,7 +493,7 @@ public static String getIpv4NetBroadcastAddress(String ipAddressString, short pr */ public static boolean isValidIPConfig(String ipAddress) { if (ipAddress.contains("/")) { - String parts[] = ipAddress.split("/"); + String[] parts = ipAddress.split("/"); boolean ipMatches = IPV4_PATTERN.matcher(parts[0]).matches(); int netMask = Integer.parseInt(parts[1]); @@ -518,7 +518,7 @@ private void scheduleToPollNetworkInterface(int intervalInSeconds) { } networkInterfacePollFuture = scheduledExecutorService.scheduleWithFixedDelay( - () -> this.pollAndNotifyNetworkInterfaceAddress(), 1, intervalInSeconds, TimeUnit.SECONDS); + this::pollAndNotifyNetworkInterfaceAddress, 1, intervalInSeconds, TimeUnit.SECONDS); } private void pollAndNotifyNetworkInterfaceAddress() { @@ -587,11 +587,11 @@ private boolean getConfigParameter(Map parameters, String parame if (value == null) { return defaultValue; } - if (value instanceof Boolean) { - return (Boolean) value; + if (value instanceof Boolean boolean1) { + return boolean1; } - if (value instanceof String) { - return Boolean.valueOf((String) value); + if (value instanceof String string) { + return Boolean.valueOf(string); } else { return defaultValue; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetworkAddressService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetworkAddressService.java index 9266b13a8c5..335f510dd91 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetworkAddressService.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/net/NetworkAddressService.java @@ -62,12 +62,12 @@ public interface NetworkAddressService { * * @param listener The listener */ - public void addNetworkAddressChangeListener(NetworkAddressChangeListener listener); + void addNetworkAddressChangeListener(NetworkAddressChangeListener listener); /** * Removes a {@link NetworkAddressChangeListener} so that it is no longer notified about changes. * * @param listener The listener */ - public void removeNetworkAddressChangeListener(NetworkAddressChangeListener listener); + void removeNetworkAddressChangeListener(NetworkAddressChangeListener listener); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/scheduler/CronJob.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/scheduler/CronJob.java index 5e9eaaee749..023b4d2012d 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/scheduler/CronJob.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/scheduler/CronJob.java @@ -34,5 +34,5 @@ public interface CronJob { * @param data The data for the job * @throws Exception Exception thrown */ - public void run(Map data) throws Exception; + void run(Map data) throws Exception; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/StartLevelService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/StartLevelService.java index a824a67bfb7..e35dc37d3af 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/StartLevelService.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/StartLevelService.java @@ -58,7 +58,7 @@ * 10 - OSGi application start level has been reached, i.e. bundles are activated. * 20 - Model entities (items, things, links, persist config) have been loaded, both from db as well as files. * 30 - Item states have been restored from persistence service, where applicable. - * 40 - Rules are loaded and parsed, both from db as well as dsl and script files. + * 40 - Rules from db, dsl and script files are loaded and parsed, script engine factories are available. * 50 - Rule engine has executed all "system started" rules and is active. * 70 - User interface is up and running. * 80 - All things have been initialized. @@ -117,9 +117,7 @@ protected void activate(Map configuration) { if (openHABStartLevel >= 10) { for (Integer level : new TreeSet<>(startlevels.keySet())) { - if (openHABStartLevel >= level) { - continue; - } else { + if (openHABStartLevel < level) { boolean reached = isStartLevelReached(startlevels.get(level)); if (reached) { setStartLevel(level); @@ -171,7 +169,7 @@ private void handleOSGiStartlevel() { protected void modified(Map configuration) { // clean up slmarker.clear(); - trackers.values().forEach(t -> readyService.unregisterTracker(t)); + trackers.values().forEach(readyService::unregisterTracker); trackers.clear(); // set up trackers and markers @@ -180,7 +178,7 @@ protected void modified(Map configuration) { .forEach(sl -> slmarker.put(sl, new ReadyMarker(STARTLEVEL_MARKER_TYPE, Integer.toString(sl)))); slmarker.put(STARTLEVEL_COMPLETE, new ReadyMarker(STARTLEVEL_MARKER_TYPE, Integer.toString(STARTLEVEL_COMPLETE))); - startlevels.values().stream().forEach(ms -> ms.forEach(e -> registerTracker(e))); + startlevels.values().stream().forEach(ms -> ms.forEach(this::registerTracker)); } private void registerTracker(ReadyMarker e) { @@ -204,7 +202,7 @@ public void onReadyMarkerAdded(ReadyMarker readyMarker) { private Map> parseConfig(Map configuration) { return configuration.entrySet().stream() // - .filter(e -> hasIntegerKey(e)) // + .filter(this::hasIntegerKey) // .map(e -> new AbstractMap.SimpleEntry<>(Integer.valueOf(e.getKey()), markerSet(e.getValue()))) // .sorted(Map.Entry.> comparingByKey().reversed()) // .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); @@ -212,8 +210,8 @@ private Map> parseConfig(Map configura private Set markerSet(Object value) { Set markerSet = new HashSet<>(); - if (value instanceof String) { - String[] segments = ((String) value).split(","); + if (value instanceof String string) { + String[] segments = string.split(","); for (String segment : segments) { if (segment.contains(":")) { String[] markerParts = segment.strip().split(":"); @@ -238,7 +236,7 @@ private boolean hasIntegerKey(Entry entry) { @Deactivate protected void deactivate() { slmarker.clear(); - trackers.values().forEach(t -> readyService.unregisterTracker(t)); + trackers.values().forEach(readyService::unregisterTracker); ScheduledFuture job = this.job; if (job != null) { job.cancel(true); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/CommandDescriptionBuilder.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/CommandDescriptionBuilder.java index ed9be11400a..965e5ed2887 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/CommandDescriptionBuilder.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/CommandDescriptionBuilder.java @@ -48,7 +48,7 @@ public static CommandDescriptionBuilder create() { */ public CommandDescription build() { CommandDescriptionImpl commandDescription = new CommandDescriptionImpl(); - commandOptions.forEach(co -> commandDescription.addCommandOption(co)); + commandOptions.forEach(commandDescription::addCommandOption); return commandDescription; } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/ComplexType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/ComplexType.java index e685b6eefc8..9d0318cd9e4 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/ComplexType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/ComplexType.java @@ -30,5 +30,5 @@ public interface ComplexType extends Type { * * @return all constituents with their names */ - public SortedMap getConstituents(); + SortedMap getConstituents(); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java index b26c0c77731..3c312039ee1 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java @@ -76,8 +76,8 @@ public class UnitUtils { * @return the {@link Class} instance of the interface or {@code null} if the given dimension is blank. * @throws IllegalArgumentException in case no class instance could be parsed from the given dimension. */ - public static @Nullable Class> parseDimension(String dimension) { - if (dimension.isBlank()) { + public static @Nullable Class> parseDimension(@Nullable String dimension) { + if (dimension == null || dimension.isBlank()) { return null; } @@ -116,8 +116,8 @@ public class UnitUtils { for (Field field : system.getDeclaredFields()) { if (field.getType().isAssignableFrom(Unit.class) && Modifier.isStatic(field.getModifiers())) { Type genericType = field.getGenericType(); - if (genericType instanceof ParameterizedType) { - Type typeParam = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (genericType instanceof ParameterizedType type) { + Type typeParam = type.getActualTypeArguments()[0]; if (typeParam instanceof WildcardType) { continue; } @@ -149,7 +149,7 @@ public class UnitUtils { * label). In the latter case, the unit is expected to be the last part of the pattern separated by " " (e.g. "%.2f * °C" for °C). * - * @param stringWithUnit the string to extract the unit symbol from + * @param pattern the string to extract the unit symbol from * @return the unit symbol extracted from the string or {@code null} if no unit could be parsed * */ @@ -188,14 +188,12 @@ public static boolean isDifferentMeasurementSystem(Unit> t || (siUnits.contains(thatUnit) && usUnits.contains(thisUnit)); if (!differentSystems) { - if (thisUnit instanceof TransformedUnit - && isMetricConversion(((TransformedUnit) thisUnit).getConverter())) { - return isDifferentMeasurementSystem(((TransformedUnit) thisUnit).getParentUnit(), thatUnit); + if (thisUnit instanceof TransformedUnit unit && isMetricConversion(unit.getConverter())) { + return isDifferentMeasurementSystem(unit.getParentUnit(), thatUnit); } - if (thatUnit instanceof TransformedUnit - && isMetricConversion(((TransformedUnit) thatUnit).getConverter())) { - return isDifferentMeasurementSystem(thisUnit, ((TransformedUnit) thatUnit).getParentUnit()); + if (thatUnit instanceof TransformedUnit unit && isMetricConversion(unit.getConverter())) { + return isDifferentMeasurementSystem(thisUnit, unit.getParentUnit()); } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java index 7c1e7386eb8..23295f0e38a 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java @@ -12,23 +12,42 @@ */ package org.openhab.core.util; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.PercentType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link ColorUtil} is responsible for converting HSB to CIE + * The {@link ColorUtil} is responsible for converting different color formats. * - * The class is based work from Erik Baauw for the Homebridge - * project + * The implementation of HSB/CIE conversion is based work from Erik Baauw for the + * Homebridge + * project. * * @author Jan N. Klug - Initial contribution + * @author Holger Friedrich - Transfer RGB color conversion from HSBType, improve RGB conversion, restructuring + * @author Chris Jackson - Added fromRGB (moved from HSBType) */ @NonNullByDefault public class ColorUtil { private static final Logger LOGGER = LoggerFactory.getLogger(ColorUtil.class); + private static final MathContext COLOR_MATH_CONTEXT = new MathContext(5, RoundingMode.HALF_UP); + private static final BigDecimal BIG_DECIMAL_360 = BigDecimal.valueOf(360); + private static final BigDecimal BIG_DECIMAL_255 = BigDecimal.valueOf(255); + private static final BigDecimal BIG_DECIMAL_240 = BigDecimal.valueOf(240); + private static final BigDecimal BIG_DECIMAL_120 = BigDecimal.valueOf(120); + private static final BigDecimal BIG_DECIMAL_100 = BigDecimal.valueOf(100); + private static final BigDecimal BIG_DECIMAL_60 = BigDecimal.valueOf(60); + private static final BigDecimal BIG_DECIMAL_5 = BigDecimal.valueOf(5); + private static final BigDecimal BIG_DECIMAL_3 = BigDecimal.valueOf(3); + private static final BigDecimal BIG_DECIMAL_2_POINT_55 = new BigDecimal("2.55"); + public static final Gamut DEFAULT_GAMUT = new Gamut(new double[] { 0.9961, 0.0001 }, new double[] { 0, 0.9961 }, new double[] { 0, 0.0001 }); @@ -37,36 +56,138 @@ private ColorUtil() { } /** - * Transform sRGB based {@link HSBType} to + * Transform HSV based {@link HSBType} to + * sRGB. + * + * This function does rounding to integer valued components. It is the preferred way of doing HSB to RGB conversion. + * + * See also: {@link #hsbToRgbPercent(HSBType)}, {@link #hsbTosRgb(HSBType)} + * + * @param hsb an {@link HSBType} value. + * @return array of three int with the RGB values in the range 0 to 255. + */ + public static int[] hsbToRgb(HSBType hsb) { + final PercentType[] rgbPercent = hsbToRgbPercent(hsb); + return new int[] { convertColorPercentToByte(rgbPercent[0]), convertColorPercentToByte(rgbPercent[1]), + convertColorPercentToByte(rgbPercent[2]) }; + } + + /** + * Transform HSV based {@link HSBType} to + * sRGB. + * + * This function does not round the components. For conversion to integer values in the range 0 to 255 use + * {@link #hsbToRgb(HSBType)}. + * + * See also: {@link #hsbToRgb(HSBType)}, {@link #hsbTosRgb(HSBType)} + * + * @param hsb an {@link HSBType} value. + * @return array of three {@link PercentType} with the RGB values in the range 0 to 100 percent. + */ + public static PercentType[] hsbToRgbPercent(HSBType hsb) { + PercentType red = null; + PercentType green = null; + PercentType blue = null; + + final BigDecimal h = hsb.getHue().toBigDecimal().divide(BIG_DECIMAL_100, 10, RoundingMode.HALF_UP); + final BigDecimal s = hsb.getSaturation().toBigDecimal().divide(BIG_DECIMAL_100); + + int hInt = h.multiply(BIG_DECIMAL_5).divide(BIG_DECIMAL_3, 0, RoundingMode.DOWN).intValue(); + final BigDecimal f = h.multiply(BIG_DECIMAL_5).divide(BIG_DECIMAL_3, 10, RoundingMode.HALF_UP) + .remainder(BigDecimal.ONE); + final BigDecimal value = hsb.getBrightness().toBigDecimal(); + + PercentType a = new PercentType(value.multiply(BigDecimal.ONE.subtract(s))); + PercentType b = new PercentType(value.multiply(BigDecimal.ONE.subtract(s.multiply(f)))); + PercentType c = new PercentType( + value.multiply(BigDecimal.ONE.subtract((BigDecimal.ONE.subtract(f)).multiply(s)))); + + switch (hInt) { + case 0: + case 6: + red = hsb.getBrightness(); + green = c; + blue = a; + break; + case 1: + red = b; + green = hsb.getBrightness(); + blue = a; + break; + case 2: + red = a; + green = hsb.getBrightness(); + blue = c; + break; + case 3: + red = a; + green = b; + blue = hsb.getBrightness(); + break; + case 4: + red = c; + green = a; + blue = hsb.getBrightness(); + break; + case 5: + red = hsb.getBrightness(); + green = a; + blue = b; + break; + default: + throw new IllegalArgumentException("Could not convert to RGB."); + } + return new PercentType[] { red, green, blue }; + } + + /** + * Transform HSV based {@link HSBType} + * to the RGB value representing the color in the default + * sRGB color model. + * (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). + * + * See also: {@link #hsbToRgb(HSBType)}, {@link #hsbToRgbPercent(HSBType)} + * + * @param hsb an {@link HSBType} value. + * @return the RGB value of the color in the default sRGB color model. + */ + public static int hsbTosRgb(HSBType hsb) { + final int[] rgb = hsbToRgb(hsb); + return (0xFF << 24) | ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | ((rgb[2] & 0xFF) << 0); + } + + /** + * Transform HSV based {@link HSBType} to * CIE 1931 `xy` format. * * See Hue * developerportal. * - * @param hsbType a {@link HSBType} value - * @return double array with the closest matching CIE 1931 colour, x, y between 0.0000 and 1.0000. + * @param hsb an {@link HSBType} value. + * @return array of three double with the closest matching CIE 1931 x,y,Y in the range 0.0000 to 1.0000 */ - public static double[] hsbToXY(HSBType hsbType) { - return hsbToXY(hsbType, DEFAULT_GAMUT); + public static double[] hsbToXY(HSBType hsb) { + return hsbToXY(hsb, DEFAULT_GAMUT); } /** - * Transform sRGB based {@link HSBType} to + * Transform HSV based {@link HSBType} to * CIE 1931 `xy` format. * * See Hue * developer portal. * - * @param hsbType a {@link HSBType} value - * @param gamut the gamut supported by the light. - * @return double array with the closest matching CIE 1931 colour, x, y, Y between 0.0000 and 1.0000. + * @param hsb an {@link HSBType} value. + * @param gamut the color Gamut supported by the light. + * @return array of three double with the closest matching CIE 1931 x,y,Y in the range 0.0000 to 1.0000 */ - public static double[] hsbToXY(HSBType hsbType, Gamut gamut) { - double r = inverseCompand(hsbType.getRed().doubleValue() / PercentType.HUNDRED.doubleValue()); - double g = inverseCompand(hsbType.getGreen().doubleValue() / PercentType.HUNDRED.doubleValue()); - double b = inverseCompand(hsbType.getBlue().doubleValue() / PercentType.HUNDRED.doubleValue()); + public static double[] hsbToXY(HSBType hsb, Gamut gamut) { + PercentType[] rgb = hsbToRgbPercent(hsb); + double r = inverseCompand(rgb[0].doubleValue() / PercentType.HUNDRED.doubleValue()); + double g = inverseCompand(rgb[1].doubleValue() / PercentType.HUNDRED.doubleValue()); + double b = inverseCompand(rgb[2].doubleValue() / PercentType.HUNDRED.doubleValue()); double X = r * 0.664511 + g * 0.154324 + b * 0.162028; double Y = r * 0.283881 + g * 0.668433 + b * 0.047685; @@ -79,43 +200,115 @@ public static double[] hsbToXY(HSBType hsbType, Gamut gamut) { double[] xyY = new double[] { ((int) (q.x * 10000.0)) / 10000.0, ((int) (q.y * 10000.0)) / 10000.0, ((int) (Y * 10000.0)) / 10000.0 }; - LOGGER.trace("HSV: {} - RGB: {} - XYZ: {} {} {} - xyY: {}", hsbType, hsbType.toRGB(), X, Y, Z, xyY); - + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("HSB: {} - RGB: {} - XYZ: {} {} {} - xyY: {}", hsb, ColorUtil.hsbToRgbPercent(hsb), X, Y, Z, + xyY); + } return xyY; } + /** + * Transform sRGB color format to + * HSV based {@link HSBType}. + * + * @param rgb array of three int with the RGB values in the range 0 to 255. + * @return the corresponding {@link HSBType}. + * @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range. + */ + public static HSBType rgbToHsb(int[] rgb) throws IllegalArgumentException { + if (rgb.length != 3 || !inByteRange(rgb[0]) || !inByteRange(rgb[1]) || !inByteRange(rgb[2])) { + throw new IllegalArgumentException("RGB array only allows values between 0 and 255"); + } + return rgbToHsb(new PercentType[] { convertByteToColorPercent(rgb[0]), convertByteToColorPercent(rgb[1]), + convertByteToColorPercent(rgb[2]) }); + } + + /** + * Transform sRGB color format to + * HSV based {@link HSBType}. + * + * @param rgb array of three {@link PercentType] with the RGB values in the range 0 to 100 percent. + * @return the corresponding {@link HSBType}. + * @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range. + */ + public static HSBType rgbToHsb(PercentType[] rgb) throws IllegalArgumentException { + if (rgb.length != 3) { + throw new IllegalArgumentException("RGB array needs exactly three values!"); + } + + BigDecimal r = rgb[0].toBigDecimal(); + BigDecimal g = rgb[1].toBigDecimal(); + BigDecimal b = rgb[2].toBigDecimal(); + + BigDecimal max = r.max(g).max(b); + BigDecimal min = r.min(g).min(b); + BigDecimal span = max.subtract(min); + + if (max.compareTo(BigDecimal.ZERO) == 0) { // all values are 0, return black + return new HSBType(); + } else if (span.compareTo(BigDecimal.ZERO) == 0) { // all values are equal, return dimmed white + return new HSBType(new DecimalType(), new PercentType(), new PercentType(max)); + } + + PercentType saturation = new PercentType(span.divide(max, COLOR_MATH_CONTEXT).multiply(BIG_DECIMAL_100)); + PercentType brightness = new PercentType(max); + + BigDecimal scale = span.divide(BIG_DECIMAL_60, COLOR_MATH_CONTEXT); + + BigDecimal redAngle = max.subtract(r).divide(scale, COLOR_MATH_CONTEXT); + BigDecimal greenAngle = max.subtract(g).divide(scale, COLOR_MATH_CONTEXT); + BigDecimal blueAngle = max.subtract(b).divide(scale, COLOR_MATH_CONTEXT); + + BigDecimal hue; + if (r.compareTo(max) == 0) { + hue = blueAngle.subtract(greenAngle); + } else if (g.compareTo(max) == 0) { + hue = BIG_DECIMAL_120.add(redAngle).subtract(blueAngle); + } else { + hue = BIG_DECIMAL_240.add(greenAngle).subtract(redAngle); + } + if (hue.compareTo(BigDecimal.ZERO) < 0) { + hue = hue.add(BIG_DECIMAL_360); + } else if (hue.compareTo(BIG_DECIMAL_360) > 0) { + hue = hue.subtract(BIG_DECIMAL_360); + } + + return new HSBType(new DecimalType(hue), saturation, brightness); + } + /** * Transform CIE 1931 `xy` format to - * sRGB based {@link HSBType}. + * HSV based {@link HSBType}. * * See Hue * developer portal. * - * @param xy the CIE 1931 xy colour, x,y between 0.0000 and 1.0000. + * @param xy array of double with CIE 1931 x,y[,Y] in the range 0.0000 to 1.0000 Y value is optional. * @return the corresponding {@link HSBType}. + * @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range. */ - public static HSBType xyToHsv(double[] xy) { - return xyToHsv(xy, DEFAULT_GAMUT); + public static HSBType xyToHsb(double[] xy) throws IllegalArgumentException { + return xyToHsb(xy, DEFAULT_GAMUT); } /** * Transform CIE 1931 `xy` format to - * sRGB based {@link HSBType}. + * HSV based {@link HSBType}. * * See Hue * developer portal. * - * @param xy the CIE 1931 xy colour, x,y[,Y] between 0.0000 and 1.0000. Y value is optional. - * @param gamut the gamut supported by the light. + * @param xy array of double with CIE 1931 x,y[,Y] in the range 0.0000 to 1.0000 Y value is optional. + * @param gamut the color Gamut supported by the light. * @return the corresponding {@link HSBType}. * @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range */ - public static HSBType xyToHsv(double[] xy, Gamut gamut) { + public static HSBType xyToHsb(double[] xy, Gamut gamut) throws IllegalArgumentException { if (xy.length < 2 || xy.length > 3 || !inRange(xy[0]) || !inRange(xy[1]) || (xy.length == 3 && !inRange(xy[2]))) { - throw new IllegalArgumentException("xy array only allowes two or three values between 0.0 and 1.0."); + throw new IllegalArgumentException("xy array only allows two or three values between 0.0 and 1.0."); } Point p = gamut.closest(new Point(xy[0], xy[1])); double x = p.x; @@ -156,11 +349,10 @@ public static HSBType xyToHsv(double[] xy, Gamut gamut) { b /= max; } - HSBType hsb = HSBType.fromRGB((int) Math.round(255.0 * r), (int) Math.round(255.0 * g), - (int) Math.round(255.0 * b)); - LOGGER.trace("xy: {} - XYZ: {} {} {} - RGB: {} {} {} - HSB: {} ", xy, X, Y, Z, r, g, b, hsb); + LOGGER.trace("xy: {} - XYZ: {} {} {} - RGB: {} {} {}", xy, X, Y, Z, r, g, b); - return hsb; + return rgbToHsb(new PercentType[] { convertDoubleToColorPercent(r), convertDoubleToColorPercent(g), + convertDoubleToColorPercent(b) }); } /** @@ -295,7 +487,24 @@ public Point closest(Point p) { } } + private static boolean inByteRange(int val) { + return val >= 0 && val <= 255; + } + private static boolean inRange(double val) { return val >= 0.0 && val <= 1.0; } + + private static int convertColorPercentToByte(PercentType percent) { + return percent.toBigDecimal().multiply(BIG_DECIMAL_255).divide(BIG_DECIMAL_100, 0, RoundingMode.HALF_UP) + .intValue(); + } + + private static PercentType convertByteToColorPercent(int b) { + return new PercentType(new BigDecimal(b).divide(BIG_DECIMAL_2_POINT_55, COLOR_MATH_CONTEXT)); + } + + private static PercentType convertDoubleToColorPercent(double d) { + return new PercentType(new BigDecimal(d).multiply(BIG_DECIMAL_100, COLOR_MATH_CONTEXT)); + } } diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fi.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fi.properties index b97a02d1559..12e78a8e06a 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fi.properties +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_fi.properties @@ -1,6 +1,6 @@ system.config.addons.includeIncompatible.label = Sisällytä (mahdollisesti) yhteensopimattomat lisäosat system.config.addons.includeIncompatible.description = Jotkin lisäpalvelut voivat tarjota lisäosia, joissa yhteensopivuutta nykyisen järjestelmän kanssa ei ole odotettavissa. Tämän valinnan käyttöönotto sisällyttää nämä merkinnät saatavilla olevien lisäosien luetteloon. system.config.addons.remote.label = Käytä etäversiovarantoa -system.config.addons.remote.description = Määrittää, pitäisikö openHAB\:n käyttää lisäosien asennusta varten etätietovarantoa. +system.config.addons.remote.description = Määrittää, pitäisikö openHAB:n käyttää lisäosien asennusta varten etätietovarantoa. service.system.addons.label = Lisäosien hallinta diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_ro.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_ro.properties new file mode 100644 index 00000000000..df96365a81a --- /dev/null +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons_ro.properties @@ -0,0 +1,6 @@ +system.config.addons.includeIncompatible.label = Includeți suplimentele (potential) incompatibile +system.config.addons.includeIncompatible.description = Unele servicii suplimentare pot oferi suplimente în cazul în care nu se așteaptă compatibilitatea cu sistemul care rulează în prezent. Activarea acestei opțiuni va include aceste intrări în lista suplimentelor disponibile. +system.config.addons.remote.label = Accesare repozitoriu de la distanță +system.config.addons.remote.description = Definește dacă openHAB trebuie să acceseze depozitul de la distanță pentru instalarea suplimentelor. + +service.system.addons.label = Gestionare suplimente diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_el.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_el.properties index 037536bc483..37d95af1a68 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_el.properties +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_el.properties @@ -1,7 +1,7 @@ system.config.i18n.language.label = Γλώσσα system.config.i18n.language.description = Η προεπιλεγμένη γλώσσα που θα χρησιμοποιηθεί. Αν δεν καθοριστεί, χρησιμοποιείται η προεπιλεγμένη γλώσσα συστήματος. -system.config.i18n.script.label = Δέσμη ενεργειών -system.config.i18n.script.description = Η δέσμη ενεργειών που θα πρέπει να χρησιμοποιηθεί. +system.config.i18n.script.label = Σενάριο +system.config.i18n.script.description = Το σενάριο ενεργειών που θα πρέπει να χρησιμοποιηθεί. system.config.i18n.region.label = Χώρα / Περιοχή system.config.i18n.region.description = Η περιοχή που θα πρέπει να χρησιμοποιηθεί. system.config.i18n.variant.label = Παραλλαγή diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_ro.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_ro.properties new file mode 100644 index 00000000000..54fcef20f3d --- /dev/null +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/i18n_ro.properties @@ -0,0 +1,18 @@ +system.config.i18n.language.label = Limbă +system.config.i18n.language.description = Limba implicită care trebuie utilizată. Dacă nu este specificată, se utilizează setarea implicită a sistemului. +system.config.i18n.script.label = Script +system.config.i18n.script.description = Scriptul care ar trebui utilizat. +system.config.i18n.region.label = Țara / Regiunea +system.config.i18n.region.description = Regiunea care ar trebui utilizată. +system.config.i18n.variant.label = Variantă +system.config.i18n.variant.description = O variație a Localului. Orice valoare arbitrară folosită pentru a indica o variație a unui local. +system.config.i18n.timezone.label = Fus Orar +system.config.i18n.timezone.description = Fusul orar poate fi setat din interfața utilizatorului. Fusul orar de bază este cel implicit. +system.config.i18n.location.label = Locatie +system.config.i18n.location.description = Locația acestei instalări.
Coordonează ca <latitude>,<longitude>[<altitude>]
Exemplu\: "52.5200066,13.4049540" (Berlin) +system.config.i18n.measurementSystem.label = Sistem de măsurare +system.config.i18n.measurementSystem.description = Sistemul de măsurare este utilizat pentru conversia unităților. +system.config.i18n.measurementSystem.option.SI = Metric +system.config.i18n.measurementSystem.option.US = Imperial (SUA) + +service.system.i18n.label = Setări regionale diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/network_ro.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/network_ro.properties new file mode 100644 index 00000000000..4eff329d268 --- /dev/null +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/network_ro.properties @@ -0,0 +1,10 @@ +system.config.network.primaryAddress.label = Adresa Principala +system.config.network.primaryAddress.description = Un subnet (e.g. 192.168.1.0/24). +system.config.network.broadcastAddress.label = Adresă difuzare +system.config.network.broadcastAddress.description = O adresă de difuzare (ex. 192.168.1.255). +system.config.network.useOnlyOneAddress.label = Adresă IP unică per interfață +system.config.network.useOnlyOneAddress.description = Utilizați o singură adresă IP pentru fiecare interfață și familie. +system.config.network.useIPv6.label = Utilizare IPv6 +system.config.network.useIPv6.description = Utilizaţi adrese IPv6 dacă sunt disponibile. + +service.system.network.label = Setări rețea diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/ByteArrayFileCacheTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/ByteArrayFileCacheTest.java index d10eefd6130..a4e764ae3fe 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/ByteArrayFileCacheTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/ByteArrayFileCacheTest.java @@ -218,7 +218,7 @@ public void clearExpiredIfExpired() { } private static File createTempTxtFile() throws IOException { - final File file = File.createTempFile("doorbell", "txt"); + final File file = Files.createTempFile("doorbell", "txt").toFile(); file.deleteOnExit(); return file; } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheEntryTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheEntryTest.java index 32cad08eb1b..619ee0c0923 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheEntryTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheEntryTest.java @@ -26,6 +26,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apache.commons.lang3.mutable.Mutable; +import org.apache.commons.lang3.mutable.MutableObject; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; @@ -74,9 +76,8 @@ public Storage getStorage(String name) { } private LRUMediaCache createCache(long size) throws IOException { - LRUMediaCache voiceLRUCache = new LRUMediaCache(storageService, size, - "lrucachetest.pid", this.getClass().getClassLoader()); - return voiceLRUCache; + return new LRUMediaCache(storageService, size, "lrucachetest.pid", + this.getClass().getClassLoader()); } public static class FakeStream extends InputStream { @@ -192,10 +193,21 @@ public void loadTwoThreadsAtTheSameTimeFromTheSameSupplierTest() throws IOExcept InputStream actualAudioStream2 = lruMediaCacheEntry.getInputStream(); // read bytes from the two stream concurrently + Mutable<@Nullable IOException> exceptionCatched = new MutableObject<>(); List parallelAudioStreamList = Arrays.asList(actualAudioStream1, actualAudioStream2); - List bytesResultList = parallelAudioStreamList.parallelStream().map(stream -> readSafe(stream)) - .collect(Collectors.toList()); + List bytesResultList = parallelAudioStreamList.parallelStream().map(stream -> { + try { + return stream.readAllBytes(); + } catch (IOException e) { + exceptionCatched.setValue(e); + return new byte[0]; + } + }).collect(Collectors.toList()); + IOException possibleException = exceptionCatched.getValue(); + if (possibleException != null) { + throw possibleException; + } assertArrayEquals(randomData, bytesResultList.get(0)); assertArrayEquals(randomData, bytesResultList.get(1)); @@ -209,14 +221,6 @@ public void loadTwoThreadsAtTheSameTimeFromTheSameSupplierTest() throws IOExcept verifyNoMoreInteractions(ttsServiceMock); } - private byte[] readSafe(InputStream InputStream) { - try { - return InputStream.readAllBytes(); - } catch (IOException e) { - return new byte[0]; - } - } - private byte[] getRandomData(int length) { Random random = new Random(); byte[] randomBytes = new byte[length]; @@ -251,7 +255,6 @@ public void streamAndMetadataTest() throws IOException { @Test public void getTotalSizeByForcingReadAllTest() throws IOException { - LRUMediaCache lruMediaCache = createCache(1000); // init simulated data stream diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheTest.java index 20690ec38a8..90a9effc65f 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/cache/lru/LRUMediaCacheTest.java @@ -75,9 +75,8 @@ public Storage getStorage(String name) { } private LRUMediaCache createCache(long size) throws IOException { - LRUMediaCache voiceLRUCache = new LRUMediaCache(storageService, size, - "lrucachetest.pid", this.getClass().getClassLoader()); - return voiceLRUCache; + return new LRUMediaCache(storageService, size, "lrucachetest.pid", + this.getClass().getClassLoader()); } /** diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/auth/UserRegistryImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/auth/UserRegistryImplTest.java index aab0c0981ad..320a4fae6b5 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/auth/UserRegistryImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/auth/UserRegistryImplTest.java @@ -118,11 +118,11 @@ public void testApiTokens() throws Exception { registry.authenticate(new UserApiTokenCredentials(token2)); registry.authenticate(new UserApiTokenCredentials(token3)); registry.removeUserApiToken(user, - user.getApiTokens().stream().filter(t -> t.getName().equals("token1")).findAny().get()); + user.getApiTokens().stream().filter(t -> "token1".equals(t.getName())).findAny().get()); registry.removeUserApiToken(user, - user.getApiTokens().stream().filter(t -> t.getName().equals("token2")).findAny().get()); + user.getApiTokens().stream().filter(t -> "token2".equals(t.getName())).findAny().get()); registry.removeUserApiToken(user, - user.getApiTokens().stream().filter(t -> t.getName().equals("token3")).findAny().get()); + user.getApiTokens().stream().filter(t -> "token3".equals(t.getName())).findAny().get()); assertEquals(user.getApiTokens().size(), 0); } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/TestUnitProvider.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/TestUnitProvider.java new file mode 100644 index 00000000000..066e822f786 --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/TestUnitProvider.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.i18n; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import javax.measure.Quantity; +import javax.measure.Unit; +import javax.measure.spi.SystemOfUnits; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.library.unit.SIUnits; + +/** + * The {@link TestUnitProvider} implements a {@link UnitProvider} for testing purposes + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class TestUnitProvider implements UnitProvider { + + private final Map>, Map>>> dimensionMap = I18nProviderImpl + .getDimensionMap(); + + @Override + @SuppressWarnings("unchecked") + public > Unit getUnit(Class dimension) { + Unit unit = (Unit) dimensionMap.getOrDefault(dimension, Map.of()).get(SIUnits.getInstance()); + assert unit != null; + return unit; + } + + @Override + public SystemOfUnits getMeasurementSystem() { + return SIUnits.getInstance(); + } + + @Override + public Collection>> getAllDimensions() { + return Set.of(); + } +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java index aa057792c80..e7c8bb083b3 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ExpireManagerTest.java @@ -30,6 +30,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.internal.items.ExpireManager.ExpireConfig; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; @@ -47,6 +48,8 @@ import org.openhab.core.library.types.StringType; import org.openhab.core.types.UnDefType; +import tech.units.indriya.unit.Units; + /** * The {@link ExpireManagerTest} tests the {@link ExpireManager}. * @@ -341,7 +344,9 @@ void testExpireConfig() { // expected as state is invalid } - testItem = new NumberItem("Number:Temperature", ITEMNAME); + UnitProvider unitProviderMock = mock(UnitProvider.class); + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(Units.CELSIUS); + testItem = new NumberItem("Number:Temperature", ITEMNAME, unitProviderMock); cfg = new ExpireManager.ExpireConfig(testItem, "1h,15 °C", Map.of()); assertEquals(Duration.ofHours(1), cfg.duration); assertEquals(new QuantityType("15 °C"), cfg.expireState); diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java index 926e7649b2c..9bf79f3a3b8 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java @@ -97,14 +97,14 @@ public void testEmptyConfig() throws Exception { public void testOptions() throws Exception { MetadataKey metadataKey = new MetadataKey("commandDescription", ITEM_NAME); Map metadataConfig = new HashMap<>(); - metadataConfig.put("options", "OPTION1,OPTION2 , 3 =Option 3 "); + metadataConfig.put("options", "OPTION1,OPTION2 , 3 =Option 3 , \" 4=4 \"=\"Option 4 \""); Metadata metadata = new Metadata(metadataKey, "N/A", metadataConfig); metadataRegistryMock.added(managedProviderMock, metadata); CommandDescription commandDescription = commandDescriptionProvider.getCommandDescription(ITEM_NAME, null); assertNotNull(commandDescription); assertNotNull(commandDescription.getCommandOptions()); - assertEquals(3, commandDescription.getCommandOptions().size()); + assertEquals(4, commandDescription.getCommandOptions().size()); Iterator it = commandDescription.getCommandOptions().iterator(); CommandOption commandOption = it.next(); @@ -116,5 +116,8 @@ public void testOptions() throws Exception { commandOption = it.next(); assertEquals("3", commandOption.getCommand()); assertEquals("Option 3", commandOption.getLabel()); + commandOption = it.next(); + assertEquals("4=4", commandOption.getCommand()); + assertEquals("Option 4", commandOption.getLabel()); } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java index 0891736188a..21ed971171a 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java @@ -96,7 +96,7 @@ public void testFragment() throws Exception { metadataConfig.put("max", "34"); metadataConfig.put("step", 3); metadataConfig.put("readOnly", "true"); - metadataConfig.put("options", "OPTION1,OPTION2 , 3 =Option 3 "); + metadataConfig.put("options", "OPTION1,OPTION2 , 3 =Option 3 ,\"4=4\"=\" Option=4 \" "); Metadata metadata = new Metadata(metadataKey, "N/A", metadataConfig); metadataRegistryMock.added(managedProviderMock, metadata); @@ -119,5 +119,8 @@ public void testFragment() throws Exception { stateOption = it.next(); assertEquals("3", stateOption.getValue()); assertEquals("Option 3", stateOption.getLabel()); + stateOption = it.next(); + assertEquals("4=4", stateOption.getValue()); + assertEquals("Option=4", stateOption.getLabel()); } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/scheduler/SchedulerImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/scheduler/SchedulerImplTest.java index b0b430f6c11..c2b5808d69b 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/scheduler/SchedulerImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/scheduler/SchedulerImplTest.java @@ -77,7 +77,7 @@ public void testAfterCancelled() throws InterruptedException, InvocationTargetEx assertTrue(after.isCancelled(), "Scheduled job cancelled before timeout"); Thread.sleep(200); assertFalse(check.get(), "Callable method should not been called"); - assertThrows(CancellationException.class, () -> after.get()); + assertThrows(CancellationException.class, after::get); } @Test @@ -144,9 +144,7 @@ public void testBeforeCancelled() throws InterruptedException, InvocationTargetE ScheduledCompletableFuture before = scheduler.before(d, Duration.ofMillis(100)); before.cancel(true); assertTrue(before.getPromise().isCompletedExceptionally(), "Scheduled job cancelled before timeout"); - assertThrows(CancellationException.class, () -> { - before.get(); - }); + assertThrows(CancellationException.class, before::get); } @Test @@ -322,7 +320,7 @@ protected long currentTimeMillis() { } }; final AtomicReference> reference = new AtomicReference<>(); - final ScheduledCompletableFuture future = scheduler.schedule(() -> counter.incrementAndGet(), + final ScheduledCompletableFuture future = scheduler.schedule(counter::incrementAndGet, temporalAdjuster); reference.set(future); future.get(); diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java index ef1c6ee07cb..af0f9e2914d 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java @@ -13,12 +13,8 @@ package org.openhab.core.internal.service; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -26,19 +22,18 @@ import java.nio.file.Path; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.JavaTest; -import org.openhab.core.OpenHAB; -import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.service.WatchService; import org.openhab.core.service.WatchService.Kind; import org.osgi.framework.BundleContext; @@ -53,46 +48,37 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class WatchServiceImplTest extends JavaTest { private static final String SUB_DIR_PATH_NAME = "subDir"; - private static final String TEST_FILE_NANE = "testFile"; + private static final String TEST_FILE_NAME = "testFile"; - private @NonNullByDefault({}) String systemConfDirProperty; - - private @NonNullByDefault({}) WatchServiceImpl.WatchServiceConfiguration configurationMock; + public @Mock @NonNullByDefault({}) WatchServiceImpl.WatchServiceConfiguration configurationMock; + public @Mock @NonNullByDefault({}) BundleContext bundleContextMock; private @NonNullByDefault({}) WatchServiceImpl watchService; - private @NonNullByDefault({}) Path rootPath; - private @NonNullByDefault({}) Path subDirPath; + private @NonNullByDefault({}) @TempDir Path rootPath; private @NonNullByDefault({}) TestWatchEventListener listener; @BeforeEach public void setup() throws IOException { - // store property so we can restore later - systemConfDirProperty = System.getProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT); - - rootPath = Files.createDirectories(Path.of("target", "test-watcher")); - subDirPath = Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME)); - ExecutorService ex = ThreadPoolManager.getScheduledPool("file-processing"); - System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, rootPath.toString()); - when(configurationMock.name()).thenReturn("unnamed"); - when(configurationMock.path()).thenReturn(""); + when(configurationMock.path()).thenReturn(rootPath.toString()); - watchService = new WatchServiceImpl(configurationMock, mock(BundleContext.class)); + watchService = new WatchServiceImpl(configurationMock, bundleContextMock); listener = new TestWatchEventListener(); + + verify(bundleContextMock, timeout(5000)).registerService(eq(WatchService.class), eq(watchService), any()); } @AfterEach public void tearDown() throws IOException { watchService.deactivate(); - System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, systemConfDirProperty); } @Test - private void testFileInWatchedDir() throws IOException, InterruptedException { - watchService.registerListener(listener, Path.of(""), false); + public void testFileInWatchedDir() throws IOException, InterruptedException { + watchService.registerListener(listener, rootPath, false); - Path testFile = rootPath.resolve(TEST_FILE_NANE); - Path relativeTestFilePath = Path.of(TEST_FILE_NANE); + Path testFile = rootPath.resolve(TEST_FILE_NAME); + Path relativeTestFilePath = Path.of(TEST_FILE_NAME); Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); assertEvent(relativeTestFilePath, Kind.CREATE); @@ -108,12 +94,14 @@ private void testFileInWatchedDir() throws IOException, InterruptedException { } @Test - private void testFileInWatchedSubDir() throws IOException, InterruptedException { + public void testFileInWatchedSubDir() throws IOException, InterruptedException { + Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME)); + // listener is listening to root and sub-dir - watchService.registerListener(listener, Path.of(""), false); + watchService.registerListener(listener, rootPath, true); - Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NANE); - Path relativeTestFilePath = Path.of(SUB_DIR_PATH_NAME, TEST_FILE_NANE); + Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NAME); + Path relativeTestFilePath = Path.of(SUB_DIR_PATH_NAME, TEST_FILE_NAME); Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); assertEvent(relativeTestFilePath, Kind.CREATE); @@ -129,12 +117,14 @@ private void testFileInWatchedSubDir() throws IOException, InterruptedException } @Test - private void testFileInWatchedSubDir2() throws IOException, InterruptedException { + public void testFileInWatchedSubDir2() throws IOException, InterruptedException { + Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME)); + // listener is only listening to sub-dir of root watchService.registerListener(listener, Path.of(SUB_DIR_PATH_NAME), false); - Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NANE); - Path relativeTestFilePath = Path.of(TEST_FILE_NANE); + Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NAME); + Path relativeTestFilePath = Path.of(TEST_FILE_NAME); Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); assertEvent(relativeTestFilePath, Kind.CREATE); @@ -150,10 +140,12 @@ private void testFileInWatchedSubDir2() throws IOException, InterruptedException } @Test - private void testFileInUnwatchedSubDir() throws IOException, InterruptedException { - watchService.registerListener(listener, Path.of(""), false); + public void testFileInUnwatchedSubDir() throws IOException, InterruptedException { + Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME)); + + watchService.registerListener(listener, rootPath, false); - Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NANE); + Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NAME); Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); assertNoEvent(); @@ -169,14 +161,14 @@ private void testFileInUnwatchedSubDir() throws IOException, InterruptedExceptio } @Test - private void testNewSubDirAlsoWatched() throws IOException, InterruptedException { - watchService.registerListener(listener, Path.of(""), false); + public void testNewSubDirAlsoWatched() throws IOException, InterruptedException { + watchService.registerListener(listener, rootPath, true); Path subDirSubDir = Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME).resolve(SUB_DIR_PATH_NAME)); assertNoEvent(); - Path testFile = subDirSubDir.resolve(TEST_FILE_NANE); - Path relativeTestFilePath = testFile.relativize(rootPath); + Path testFile = subDirSubDir.resolve(TEST_FILE_NAME); + Path relativeTestFilePath = rootPath.relativize(testFile); Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); assertEvent(relativeTestFilePath, Kind.CREATE); @@ -209,7 +201,7 @@ private void assertEvent(Path path, Kind kind) throws InterruptedException { listener.events.clear(); } - private class TestWatchEventListener implements WatchService.WatchEventListener { + private static class TestWatchEventListener implements WatchService.WatchEventListener { List events = new CopyOnWriteArrayList<>(); @Override diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java index 370b86025ab..96aebcedc5a 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GenericItemTest.java @@ -26,8 +26,9 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.openhab.core.events.EventPublisher; -import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.items.events.ItemEvent; import org.openhab.core.items.events.ItemStateChangedEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.RawType; @@ -58,24 +59,51 @@ public void testItemPostsEventsCorrectly() { item.setEventPublisher(publisher); State oldState = item.getState(); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired item.setState(new RawType(new byte[0], RawType.DEFAULT_MIME_TYPE)); - ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStateChangedEvent.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemEvent.class); - verify(publisher, times(1)).post(captor.capture()); + verify(publisher, times(2)).post(captor.capture()); - ItemStateChangedEvent change = captor.getValue(); + List events = captor.getAllValues(); + assertEquals(2, events.size()); + // first event should be updated event + assertInstanceOf(ItemStateUpdatedEvent.class, events.get(0)); + ItemStateUpdatedEvent updated = (ItemStateUpdatedEvent) events.get(0); + assertEquals(item.getName(), updated.getItemName()); + assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); + assertEquals(item.getState(), updated.getItemState()); + assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); + + // second event should be changed event + assertInstanceOf(ItemStateChangedEvent.class, events.get(1)); + ItemStateChangedEvent change = (ItemStateChangedEvent) events.get(1); assertEquals(item.getName(), change.getItemName()); assertEquals("openhab/items/member1/statechanged", change.getTopic()); assertEquals(oldState, change.getOldItemState()); assertEquals(item.getState(), change.getItemState()); assertEquals(ItemStateChangedEvent.TYPE, change.getType()); - // State doesn't change -> no event is fired + // reset invocations and captor + clearInvocations(publisher); + captor = ArgumentCaptor.forClass(ItemEvent.class); + + // State doesn't change -> only update event is fired item.setState(item.getState()); - verifyNoMoreInteractions(publisher); + verify(publisher).post(captor.capture()); + + events = captor.getAllValues(); + assertEquals(1, events.size()); // two before and one additional + + // event should be updated event + assertInstanceOf(ItemStateUpdatedEvent.class, events.get(0)); + updated = (ItemStateUpdatedEvent) events.get(0); + assertEquals(item.getName(), updated.getItemName()); + assertEquals("openhab/items/member1/stateupdated", updated.getTopic()); + assertEquals(item.getState(), updated.getItemState()); + assertEquals(ItemStateUpdatedEvent.TYPE, updated.getType()); } @Test @@ -131,7 +159,6 @@ public void testDispose() { item.setEventPublisher(mock(EventPublisher.class)); item.setItemStateConverter(mock(ItemStateConverter.class)); item.setStateDescriptionService(null); - item.setUnitProvider(mock(UnitProvider.class)); item.addStateChangeListener(mock(StateChangeListener.class)); @@ -141,7 +168,6 @@ public void testDispose() { assertNull(item.itemStateConverter); // can not be tested as stateDescriptionProviders is private in GenericItem // assertThat(item.stateDescriptionProviders, is(nullValue())); - assertNull(item.unitProvider); assertEquals(0, item.listeners.size()); } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GroupItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GroupItemTest.java new file mode 100644 index 00000000000..8a683c0144a --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/items/GroupItemTest.java @@ -0,0 +1,56 @@ +/** + * 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.items; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.library.items.NumberItem; + +/** + * The {@link GroupItemTest} contains tests for {@link GroupItem} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class GroupItemTest { + private static final String ITEM_NAME = "test"; + + private @Mock @NonNullByDefault({}) NumberItem baseItemMock; + + @Test + public void testMetadataIsPropagatedToBaseItem() { + GroupItem groupItem = new GroupItem(ITEM_NAME, baseItemMock, new GroupFunction.Equality()); + + Metadata metadata = new Metadata(new MetadataKey("foo", ITEM_NAME), "foo", null); + Metadata updatedMetadata = new Metadata(new MetadataKey("foo", ITEM_NAME), "bar", null); + + groupItem.addedMetadata(metadata); + verify(baseItemMock).addedMetadata(eq(metadata)); + + groupItem.updatedMetadata(metadata, updatedMetadata); + verify(baseItemMock).updatedMetadata(eq(metadata), eq(updatedMetadata)); + + groupItem.removedMetadata(updatedMetadata); + verify(baseItemMock).removedMetadata(eq(updatedMetadata)); + } +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/CoreItemFactoryTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/CoreItemFactoryTest.java index f802e462633..d5481fbceb9 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/CoreItemFactoryTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/CoreItemFactoryTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Mockito.when; import java.util.List; @@ -22,18 +23,30 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.library.items.NumberItem; +import tech.units.indriya.unit.Units; + /** * @author Henning Treu - Initial contribution */ @NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class CoreItemFactoryTest { + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; + @Test public void shouldCreateItems() { - CoreItemFactory coreItemFactory = new CoreItemFactory(); + CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock); List itemTypeNames = List.of(coreItemFactory.getSupportedItemTypes()); for (String itemTypeName : itemTypeNames) { GenericItem item = coreItemFactory.createItem(itemTypeName, itemTypeName.toLowerCase()); @@ -45,7 +58,8 @@ public void shouldCreateItems() { @Test public void createNumberItemWithDimension() { - CoreItemFactory coreItemFactory = new CoreItemFactory(); + CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock); + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(Units.CELSIUS); NumberItem numberItem = (NumberItem) coreItemFactory.createItem(CoreItemFactory.NUMBER + ":Temperature", "myNumberItem"); @@ -54,7 +68,7 @@ public void createNumberItemWithDimension() { @Test public void shouldReturnNullForUnsupportedItemTypeName() { - CoreItemFactory coreItemFactory = new CoreItemFactory(); + CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock); GenericItem item = coreItemFactory.createItem("NoValidItemTypeName", "IWantMyItem"); assertThat(item, is(nullValue())); diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/DimmerItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/DimmerItemTest.java index 18d67e8d7ca..a3052cfc701 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/DimmerItemTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/DimmerItemTest.java @@ -41,8 +41,7 @@ private static DimmerItem createDimmerItem(final State state) { private static BigDecimal getState(final Item item, Class typeClass) { final State state = item.getStateAs(typeClass); final String str = state.toString(); - final BigDecimal result = new BigDecimal(str); - return result; + return new BigDecimal(str); } @Test diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java index 147c85ba65f..c69f3e95d1c 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/NumberItemTest.java @@ -14,10 +14,12 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; -import javax.measure.quantity.Energy; +import java.util.Objects; + +import javax.measure.Unit; +import javax.measure.quantity.Mass; import javax.measure.quantity.Temperature; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -31,12 +33,15 @@ import org.mockito.quality.Strictness; import org.openhab.core.events.EventPublisher; import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.internal.i18n.TestUnitProvider; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; import org.openhab.core.items.events.ItemCommandEvent; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.MetricPrefix; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; import org.openhab.core.service.StateDescriptionService; @@ -57,91 +62,106 @@ public class NumberItemTest { private static final String ITEM_NAME = "test"; private @Mock @NonNullByDefault({}) StateDescriptionService stateDescriptionServiceMock; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; + private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock; + + private final UnitProvider unitProvider = new TestUnitProvider(); @BeforeEach + @SuppressWarnings("unchecked") public void setup() { when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)) .thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.1f " + UnitUtils.UNIT_PLACEHOLDER) .build().toStateDescription()); + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); } + /* + * State handling + */ @Test - public void setDecimalType() { + public void testUndefType() { NumberItem item = new NumberItem(ITEM_NAME); - State decimal = new DecimalType("23"); - item.setState(decimal); - assertEquals(decimal, item.getState()); + StateUtil.testUndefStates(item); } @Test - public void setPercentType() { + public void testAcceptedStates() { NumberItem item = new NumberItem(ITEM_NAME); - State percent = new PercentType(50); - item.setState(percent); - assertEquals(percent, item.getState()); + StateUtil.testAcceptedStates(item); } @Test - public void setHSBType() { + public void testSetDecimalTypeToPlainItem() { NumberItem item = new NumberItem(ITEM_NAME); - State hsb = new HSBType("5,23,42"); - item.setState(hsb); - assertEquals(hsb, item.getState()); + State decimal = new DecimalType("23"); + item.setState(decimal); + assertThat(item.getState(), is(decimal)); } @Test - public void testUndefType() { - NumberItem item = new NumberItem(ITEM_NAME); - StateUtil.testUndefStates(item); + public void testSetDecimalTypeToDimensionItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + State decimal = new DecimalType("23"); + item.setState(decimal); + assertThat(item.getState(), is(new QuantityType<>("23 °C"))); } @Test - public void testAcceptedStates() { + public void testSetQuantityTypeToPlainItem() { NumberItem item = new NumberItem(ITEM_NAME); - StateUtil.testAcceptedStates(item); + State quantity = new QuantityType<>("23 °C"); + item.setState(quantity); + assertThat(item.getState(), is(new DecimalType("23"))); } @Test - public void testSetQuantityTypeAccepted() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - item.setState(new QuantityType<>("20 °C")); - - assertThat(item.getState(), is(new QuantityType<>("20 °C"))); + public void testSetValidQuantityTypeWithSameUnitToDimensionItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + State quantity = new QuantityType<>("23 °C"); + item.setState(quantity); + assertThat(item.getState(), is(quantity)); } @Test - public void testSetQuantityOnPlainNumberStripsUnit() { - NumberItem item = new NumberItem(ITEM_NAME); - item.setState(new QuantityType<>("20 °C")); - - assertThat(item.getState(), is(new DecimalType("20"))); + public void testSetValidQuantityTypeWithDifferentUnitToDimensionItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + QuantityType quantity = new QuantityType<>("23 K"); + item.setState(quantity); + assertThat(item.getState(), + is(quantity.toUnit(Objects.requireNonNull(unitProvider.getUnit(Temperature.class))))); } @Test - public void testSetQuantityTypeConverted() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - item.setState(new QuantityType<>(68, ImperialUnits.FAHRENHEIT)); - - assertThat(item.getState(), is(new QuantityType<>("20 °C"))); + public void testSetInvalidQuantityTypeToDimensionItemIsRejected() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + QuantityType quantity = new QuantityType<>("23 N"); + item.setState(quantity); + assertThat(item.getState(), is(UnDefType.NULL)); } @Test - public void testSetQuantityTypeUnconverted() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - UnitProvider unitProvider = mock(UnitProvider.class); - when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - item.setUnitProvider(unitProvider); - item.setState(new QuantityType<>("10 A")); // should not be accepted as valid state + public void testSetPercentType() { + NumberItem item = new NumberItem(ITEM_NAME); + State percent = new PercentType(50); + item.setState(percent); + assertThat(item.getState(), is(percent)); + } - assertThat(item.getState(), is(UnDefType.NULL)); + @Test + public void testSetHSBType() { + NumberItem item = new NumberItem(ITEM_NAME); + State hsb = new HSBType("5,23,42"); + item.setState(hsb); + assertThat(item.getState(), is(hsb)); } + /* + * Command handling + */ @Test - public void testCommandUnitIsPassedForDimensionItem() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - UnitProvider unitProvider = mock(UnitProvider.class); - when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - item.setUnitProvider(unitProvider); + public void testValidCommandUnitIsPassedForDimensionItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); EventPublisher eventPublisher = mock(EventPublisher.class); item.setEventPublisher(eventPublisher); @@ -156,134 +176,128 @@ public void testCommandUnitIsPassedForDimensionItem() { } @Test - public void testCommandUnitIsStrippedForDimensionlessItem() { - NumberItem item = new NumberItem("Number", ITEM_NAME); + public void testValidCommandDifferentUnitIsPassedForDimensionItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); EventPublisher eventPublisher = mock(EventPublisher.class); item.setEventPublisher(eventPublisher); - item.send(new QuantityType<>("15 °C")); + QuantityType command = new QuantityType<>("15 K"); + item.send(command); ArgumentCaptor captor = ArgumentCaptor.forClass(ItemCommandEvent.class); verify(eventPublisher).post(captor.capture()); ItemCommandEvent event = captor.getValue(); - assertThat(event.getItemCommand(), is(new DecimalType("15"))); + assertThat(event.getItemCommand(), is(command)); } - @SuppressWarnings("null") @Test - public void testStripUnitPlaceholderFromPlainNumberItem() { - NumberItem item = new NumberItem("Number", ITEM_NAME); - item.setStateDescriptionService(stateDescriptionServiceMock); + public void testInvalidCommandUnitIsRejectedForDimensionItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + EventPublisher eventPublisher = mock(EventPublisher.class); + item.setEventPublisher(eventPublisher); - assertThat(item.getStateDescription().getPattern(), is("%.1f")); + QuantityType command = new QuantityType<>("15 N"); + item.send(command); + + verify(eventPublisher, never()).post(any()); } - @SuppressWarnings("null") @Test - public void testLeaveUnitPlaceholderOnDimensionNumberItem() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - item.setStateDescriptionService(stateDescriptionServiceMock); + public void testCommandUnitIsStrippedForDimensionlessItem() { + NumberItem item = new NumberItem(ITEM_NAME); + EventPublisher eventPublisher = mock(EventPublisher.class); + item.setEventPublisher(eventPublisher); - assertThat(item.getStateDescription().getPattern(), is("%.1f " + UnitUtils.UNIT_PLACEHOLDER)); + item.send(new QuantityType<>("15 °C")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemCommandEvent.class); + verify(eventPublisher).post(captor.capture()); + + ItemCommandEvent event = captor.getValue(); + assertThat(event.getItemCommand(), is(new DecimalType("15"))); } + /* + * + State description handling + */ @SuppressWarnings("null") @Test - public void testMiredToKelvin() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn( - StateDescriptionFragmentBuilder.create().withPattern("%.0f K").build().toStateDescription()); + public void testStripUnitPlaceholderInStateDescriptionFromPlainNumberItem() { + NumberItem item = new NumberItem(ITEM_NAME); item.setStateDescriptionService(stateDescriptionServiceMock); - item.setState(new QuantityType<>("370 mired")); - assertThat(item.getState().format("%.0f K"), is("2703 K")); + assertThat(item.getStateDescription().getPattern(), is("%.1f")); } @SuppressWarnings("null") @Test - public void testKelvinToMired() { - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn( - StateDescriptionFragmentBuilder.create().withPattern("%.0f mired").build().toStateDescription()); + public void testLeaveUnitPlaceholderInStateDescriptionOnDimensionNumberItem() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); item.setStateDescriptionService(stateDescriptionServiceMock); - item.setState(new QuantityType<>("2700 K")); - assertThat(item.getState().format("%.0f mired"), is("370 mired")); + assertThat(item.getStateDescription().getPattern(), is("%.1f " + UnitUtils.UNIT_PLACEHOLDER)); } + /* + * Unit / metadata handling + */ @Test - void testStateDescriptionUnitUsedWhenStateDescriptionPresent() { - UnitProvider unitProviderMock = mock(UnitProvider.class); - when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn( - StateDescriptionFragmentBuilder.create().withPattern("%.0f °F").build().toStateDescription()); - - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - item.setStateDescriptionService(stateDescriptionServiceMock); - item.setUnitProvider(unitProviderMock); - - assertThat(item.getUnit(), is(ImperialUnits.FAHRENHEIT)); - - item.setState(new QuantityType<>("429 °F")); - assertThat(item.getState(), is(new QuantityType<>("429 °F"))); - - item.setState(new QuantityType<>("165 °C")); - assertThat(item.getState(), is(new QuantityType<>("329 °F"))); + void testSystemDefaultUnitIsUsedWithoutMetadata() { + final NumberItem item = new NumberItem("Number:Mass", ITEM_NAME, unitProvider); + assertThat(item.getUnit(), is(unitProvider.getUnit(Mass.class))); } @Test - void testPreservedWhenStateDescriptionContainsWildCard() { - UnitProvider unitProviderMock = mock(UnitProvider.class); - when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)) - .thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.0f " + UnitUtils.UNIT_PLACEHOLDER) - .build().toStateDescription()); - - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - item.setStateDescriptionService(stateDescriptionServiceMock); - item.setUnitProvider(unitProviderMock); + void testMetadataUnitLifecycleIsObserved() { + final NumberItem item = new NumberItem("Number:Mass", ITEM_NAME, unitProvider); - assertThat(item.getUnit(), is(nullValue())); + Metadata initialMetadata = getUnitMetadata(MetricPrefix.MEGA(SIUnits.GRAM)); + item.addedMetadata(initialMetadata); + assertThat(item.getUnit(), is(MetricPrefix.MEGA(SIUnits.GRAM))); - item.setState(new QuantityType<>("329 °F")); - assertThat(item.getState(), is(new QuantityType<>("329 °F"))); + Metadata updatedMetadata = getUnitMetadata(MetricPrefix.MILLI(SIUnits.GRAM)); + item.updatedMetadata(initialMetadata, updatedMetadata); + assertThat(item.getUnit(), is(MetricPrefix.MILLI(SIUnits.GRAM))); - item.setState(new QuantityType<>("100 °C")); - assertThat(item.getState(), is(new QuantityType<>("100 °C"))); + item.removedMetadata(updatedMetadata); + assertThat(item.getUnit(), is(unitProvider.getUnit(Mass.class))); } @Test - void testDefaultUnitUsedWhenStateDescriptionEmpty() { - UnitProvider unitProviderMock = mock(UnitProvider.class); - when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - - NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); - item.setUnitProvider(unitProviderMock); + void testInvalidMetadataUnitIsRejected() { + final NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + item.addedMetadata(getUnitMetadata(MetricPrefix.MEGA(SIUnits.GRAM))); + assertThat(item.getUnit(), is(unitProvider.getUnit(Temperature.class))); + } - assertThat(item.getUnit(), is(SIUnits.CELSIUS)); + /* + * Other tests + */ - item.setState(new QuantityType<>("329 °F")); - assertThat(item.getState(), is(new QuantityType<>("165 °C"))); + @SuppressWarnings("null") + @Test + public void testMiredToKelvin() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + item.addedMetadata(getUnitMetadata(Units.KELVIN)); + item.setState(new QuantityType<>("370 mired")); - item.setState(new QuantityType<>("100 °C")); - assertThat(item.getState(), is(new QuantityType<>("100 °C"))); + assertThat(item.getState().format("%.0f K"), is("2703 K")); } + @SuppressWarnings("null") @Test - void testNoUnitWhenUnitPlaceholderUsed() { - final UnitProvider unitProviderMock = mock(UnitProvider.class); - when(unitProviderMock.getUnit(Energy.class)).thenReturn(Units.JOULE); - - final NumberItem item = new NumberItem("Number:Energy", ITEM_NAME); - item.setUnitProvider(unitProviderMock); + public void testKelvinToMired() { + NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider); + item.addedMetadata(getUnitMetadata(Units.MIRED)); - assertThat(item.getUnit(), is(Units.JOULE)); + item.setState(new QuantityType<>("2700 K")); - item.setStateDescriptionService(stateDescriptionServiceMock); - item.setState(new QuantityType<>("329 kWh")); + assertThat(item.getState().format("%.0f mired"), is("370 mired")); + } - assertThat(item.getState(), is(new QuantityType<>("329 kWh"))); - assertThat(item.getUnit(), is(nullValue())); + private Metadata getUnitMetadata(Unit unit) { + MetadataKey key = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, ITEM_NAME); + return new Metadata(key, unit.toString(), null); } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/HSBTypeTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/HSBTypeTest.java index b90aa505bb8..b10dbf4f24a 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/HSBTypeTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/HSBTypeTest.java @@ -16,9 +16,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; -import java.math.BigDecimal; -import java.math.RoundingMode; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; @@ -46,7 +43,7 @@ public void testFormat() { HSBType hsb = new HSBType("316,69,47"); assertEquals("color 316,69,47", hsb.format("color %hsb%")); - assertEquals("color 119,37,97", hsb.format("color %rgb%")); + assertEquals("color 120,37,98", hsb.format("color %rgb%")); assertEquals("color 316,69,47", hsb.format("color %s")); } @@ -63,17 +60,13 @@ public void testHsbToRgbConversion() { compareHsbToRgbValues("300,100,40", 102, 0, 102); } - private int convertPercentToByte(PercentType percent) { - return percent.value.multiply(BigDecimal.valueOf(255)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP) - .intValue(); - } - private void compareHsbToRgbValues(String hsbValues, int red, int green, int blue) { HSBType hsb = new HSBType(hsbValues); + HSBType hsbRgb = HSBType.fromRGB(red, green, blue); - assertEquals(red, convertPercentToByte(hsb.getRed())); - assertEquals(green, convertPercentToByte(hsb.getGreen())); - assertEquals(blue, convertPercentToByte(hsb.getBlue())); + assertEquals(hsb.getHue().doubleValue(), hsbRgb.getHue().doubleValue(), 0.5); + assertEquals(hsb.getSaturation().doubleValue(), hsbRgb.getSaturation().doubleValue(), 0.5); + assertEquals(hsb.getBrightness().doubleValue(), hsbRgb.getBrightness().doubleValue(), 0.5); } @Test @@ -85,17 +78,17 @@ public void testRgbToHsbConversion() { compareRgbToHsbValues("240,100,100", 0, 0, 255); // blue compareRgbToHsbValues("60,60,60", 153, 153, 61); // green compareRgbToHsbValues("300,100,40", 102, 0, 102); - compareRgbToHsbValues("228,37,61", 99, 110, 158); // blueish - compareRgbToHsbValues("316,68,46", 119, 37, 97); // purple + compareRgbToHsbValues("229,37,62", 99, 110, 158); // blueish + compareRgbToHsbValues("316,69,47", 119, 37, 97); // purple } private void compareRgbToHsbValues(String hsbValues, int red, int green, int blue) { HSBType hsb = new HSBType(hsbValues); HSBType hsbRgb = HSBType.fromRGB(red, green, blue); - assertEquals(hsb.getHue(), hsbRgb.getHue()); - assertEquals(hsb.getSaturation(), hsbRgb.getSaturation()); - assertEquals(hsb.getBrightness(), hsbRgb.getBrightness()); + assertEquals(hsb.getHue().doubleValue(), hsbRgb.getHue().doubleValue(), 0.5); + assertEquals(hsb.getSaturation().doubleValue(), hsbRgb.getSaturation().doubleValue(), 0.5); + assertEquals(hsb.getBrightness().doubleValue(), hsbRgb.getBrightness().doubleValue(), 0.5); } @Test @@ -198,4 +191,19 @@ public void testConstructorWithIllegalSaturationValue() { public void testConstructorWithIllegalBrightnessValue() { assertThrows(IllegalArgumentException.class, () -> new HSBType("5,85,151")); } + + @Test + public void testCloseTo() { + HSBType hsb1 = new HSBType("5,85,11"); + HSBType hsb2 = new HSBType("4,84,12"); + HSBType hsb3 = new HSBType("1,8,99"); + + assertThrows(IllegalArgumentException.class, () -> hsb1.closeTo(hsb2, 0.0)); + assertThrows(IllegalArgumentException.class, () -> hsb1.closeTo(hsb2, 1.1)); + assertDoesNotThrow(() -> hsb1.closeTo(hsb2, 0.1)); + + assertTrue(hsb1.closeTo(hsb2, 0.01)); + assertTrue(!hsb1.closeTo(hsb3, 0.01)); + assertTrue(hsb1.closeTo(hsb3, 0.5)); + } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java index 83ebffdde25..ed93f1305d5 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java @@ -32,6 +32,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.internal.i18n.TestUnitProvider; import org.openhab.core.items.GroupFunction; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; @@ -39,6 +40,7 @@ import org.openhab.core.library.items.NumberItem; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.osgi.service.component.ComponentContext; /** * @author Henning Treu - Initial contribution @@ -47,7 +49,8 @@ @NonNullByDefault public class QuantityTypeArithmeticGroupFunctionTest { - private @NonNullByDefault({}) @Mock UnitProvider unitProvider; + private @Mock @NonNullByDefault({}) ComponentContext componentContext; + private final UnitProvider unitProvider = new TestUnitProvider(); /** * Locales having a different decimal and grouping separators to test string parsing and generation. @@ -313,16 +316,14 @@ public void testSumFunctionQuantityTypeWithGroups(Locale locale) { } private NumberItem createNumberItem(String name, Class> dimension, State state) { - NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name); - item.setUnitProvider(unitProvider); + NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, unitProvider); item.setState(state); return item; } private GroupItem createGroupItem(String name, Class> dimension, State state) { GroupItem item = new GroupItem(name, - new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name)); - item.setUnitProvider(unitProvider); + new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, unitProvider)); item.setState(state); return item; } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/UnitsTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/UnitsTest.java index 2cbde582bcd..68bdd23b414 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/UnitsTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/UnitsTest.java @@ -387,6 +387,20 @@ public void testConductivity() { assertThat(converted.toString(), anyOf(is("10000 \u00B5S/cm"), is("10000 \u03BCS/cm"))); } + @Test + public void testSpecificActivity() { + QuantityType radon = QuantityType.valueOf("37 kBq/m³"); + QuantityType converted = radon.toUnit("nCi/l"); + assertThat(converted.doubleValue(), is(closeTo(1.00, DEFAULT_ERROR))); + } + + @Test + public void testRpm() { + QuantityType oneHertz = QuantityType.valueOf("60 rpm"); + QuantityType converted = oneHertz.toUnit("Hz"); + assertThat(converted.doubleValue(), is(closeTo(1.00, DEFAULT_ERROR))); + } + private static class QuantityEquals extends IsEqual> { private Quantity quantity; @@ -397,9 +411,7 @@ public QuantityEquals(Quantity quantity) { @Override public boolean matches(@Nullable Object actualValue) { - if (actualValue instanceof Quantity) { - Quantity other = (Quantity) actualValue; - + if (actualValue instanceof Quantity other) { if (!other.getUnit().isCompatible(quantity.getUnit())) { return false; } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java index a371464a130..21acc54d127 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java @@ -13,49 +13,48 @@ package org.openhab.core.util; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThan; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.MethodSource; import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.util.ColorUtil.Gamut; /** * The {@link ColorUtilTest} is a test class for the color conversion * * @author Jan N. Klug - Initial contribution + * @author Holger Friedrich - Parameterized tests for RGB and HSB conversion */ @NonNullByDefault public class ColorUtilTest { - private static Stream colors() { - return Stream.of(HSBType.BLACK, HSBType.BLUE, HSBType.GREEN, HSBType.RED, HSBType.WHITE, - HSBType.fromRGB(127, 94, 19)).map(Arguments::of); - } - - private static Stream invalids() { - return Stream.of(new double[] { 0.0 }, new double[] { -1.0, 0.5 }, new double[] { 1.5, 0.5 }, - new double[] { 0.5, -1.0 }, new double[] { 0.5, 1.5 }, new double[] { 0.5, 0.5, -1.0 }, - new double[] { 0.5, 0.5, 1.5 }, new double[] { 0.0, 1.0, 0.0, 1.0 }).map(Arguments::of); - } @ParameterizedTest @MethodSource("colors") public void inversionTest(HSBType hsb) { - HSBType hsb2 = ColorUtil.xyToHsv(ColorUtil.hsbToXY(hsb)); + HSBType hsb2 = ColorUtil.xyToHsb(ColorUtil.hsbToXY(hsb)); double deltaHue = Math.abs(hsb.getHue().doubleValue() - hsb2.getHue().doubleValue()); - deltaHue = deltaHue > 180.0 ? Math.abs(deltaHue - 360) : deltaHue; // if deltaHue > 180, the "other direction" - // is shorter + // if deltaHue > 180, the "other direction" is shorter + deltaHue = deltaHue > 180.0 ? Math.abs(deltaHue - 360) : deltaHue; double deltaSat = Math.abs(hsb.getSaturation().doubleValue() - hsb2.getSaturation().doubleValue()); double deltaBri = Math.abs(hsb.getBrightness().doubleValue() - hsb2.getBrightness().doubleValue()); - - assertThat(deltaHue, is(lessThan(5.0))); + // hue is meaningless when saturation is zero + if (hsb.getSaturation().doubleValue() > 0) { + assertThat(deltaHue, is(lessThan(5.0))); + } assertThat(deltaSat, is(lessThanOrEqualTo(1.0))); assertThat(deltaBri, is(lessThanOrEqualTo(1.0))); } @@ -63,6 +62,169 @@ public void inversionTest(HSBType hsb) { @ParameterizedTest @MethodSource("invalids") public void invalidXyValues(double[] xy) { - assertThrows(IllegalArgumentException.class, () -> ColorUtil.xyToHsv(xy)); + assertThrows(IllegalArgumentException.class, () -> ColorUtil.xyToHsb(xy)); + } + + @Test + public void testConversionToXY() { + HSBType hsb = new HSBType("220,90,50"); + PercentType[] xy = hsb.toXY(); + assertEquals(14.65, xy[0].doubleValue(), 0.01); + assertEquals(11.56, xy[1].doubleValue(), 0.01); + } + + // test RGB -> HSB -> RGB conversion for different values, including the ones known to cause rounding error + @ParameterizedTest + @ArgumentsSource(RgbValueProvider.class) + public void testConversionRgbToHsbToRgb(int[] rgb) { + HSBType hsb = ColorUtil.rgbToHsb(rgb); + Assertions.assertNotNull(hsb); + + final int[] convertedRgb = ColorUtil.hsbToRgb(hsb); + assertRgbEquals(rgb, convertedRgb); + } + + @ParameterizedTest + @ArgumentsSource(HsbRgbProvider.class) + public void testConversionHsbToRgb(int[] hsb, int[] rgb) { + final String hsbString = hsb[0] + ", " + hsb[1] + ", " + hsb[2]; + final HSBType hsbType = new HSBType(hsbString); + + final int[] converted = ColorUtil.hsbToRgb(hsbType); + assertRgbEquals(rgb, converted); + } + + @ParameterizedTest + @ArgumentsSource(HsbRgbProvider.class) + public void testConversionRgbToRgb(int[] hsb, int[] rgb) { + final HSBType hsbType = ColorUtil.rgbToHsb(rgb); + + final int[] rgbConverted = ColorUtil.hsbToRgb(hsbType); + assertRgbEquals(rgb, rgbConverted); + } + + @ParameterizedTest + @ArgumentsSource(HsbRgbProvider.class) + public void testConversionRgbToHsb(int[] hsb, int[] rgb) { + HSBType hsbType = ColorUtil.rgbToHsb(rgb); + + final String expected = hsb[0] + ", " + hsb[1] + ", " + hsb[2]; + + // compare in HSB space, threshold 1% difference + assertTrue(hsbType.closeTo(new HSBType(expected), 0.01)); + } + + private void xyToXY(double[] xy, Gamut gamut) { + assertTrue(xy.length > 1); + HSBType hsb = ColorUtil.xyToHsb(xy, gamut); + double[] xy2 = ColorUtil.hsbToXY(hsb, gamut); + assertTrue(xy2.length > 1); + for (int i = 0; i < xy.length; i++) { + assertEquals(xy[i], xy2[i], 0.02); + } + } + + /** + * Test XY -> RGB -> HSB - RGB - XY round trips. + * Use ColorUtil fine precision methods for conversions. + * Test on Hue standard Gamuts 'A', 'B', and 'C'. + */ + @Test + public void testXyHsbRoundTrips() { + Gamut[] gamuts = new Gamut[] { + new Gamut(new double[] { 0.704, 0.296 }, new double[] { 0.2151, 0.7106 }, new double[] { 0.138, 0.08 }), + new Gamut(new double[] { 0.675, 0.322 }, new double[] { 0.409, 0.518 }, new double[] { 0.167, 0.04 }), + new Gamut(new double[] { 0.6915, 0.3038 }, new double[] { 0.17, 0.7 }, new double[] { 0.1532, 0.0475 }) // + }; + for (Gamut g : gamuts) { + xyToXY(g.r(), g); + xyToXY(g.g(), g); + xyToXY(g.b(), g); + xyToXY(new double[] { (g.r()[0] + g.g()[0]) / 2f, (g.r()[1] + g.g()[1]) / 2f }, g); + xyToXY(new double[] { (g.g()[0] + g.b()[0]) / 2f, (g.g()[1] + g.b()[1]) / 2f }, g); + xyToXY(new double[] { (g.b()[0] + g.r()[0]) / 2f, (g.b()[1] + g.r()[1]) / 2f }, g); + xyToXY(new double[] { (g.r()[0] + g.g()[0] + g.b()[0]) / 3f, (g.r()[1] + g.g()[1] + g.b()[1]) / 3f }, g); + xyToXY(ColorUtil.hsbToXY(HSBType.WHITE), g); + } + } + + /* Providers for parameterized tests */ + + private static Stream colors() { + return Stream + .of(HSBType.BLACK, HSBType.BLUE, HSBType.GREEN, HSBType.RED, HSBType.WHITE, + ColorUtil.rgbToHsb(new int[] { 127, 94, 19 }), new HSBType("0,0.1,0"), new HSBType("0,0.1,100")) + .map(Arguments::of); + } + + private static Stream invalids() { + return Stream.of(new double[] { 0.0 }, new double[] { -1.0, 0.5 }, new double[] { 1.5, 0.5 }, + new double[] { 0.5, -1.0 }, new double[] { 0.5, 1.5 }, new double[] { 0.5, 0.5, -1.0 }, + new double[] { 0.5, 0.5, 1.5 }, new double[] { 0.0, 1.0, 0.0, 1.0 }).map(Arguments::of); + } + + /* + * return a stream of well known HSB - RGB pairs + */ + static class HsbRgbProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(@Nullable ExtensionContext context) throws Exception { + return Stream.of(Arguments.of(new int[] { 0, 0, 0 }, new int[] { 0, 0, 0 }), + Arguments.of(new int[] { 0, 0, 100 }, new int[] { 255, 255, 255 }), + Arguments.of(new int[] { 0, 100, 100 }, new int[] { 255, 0, 0 }), + Arguments.of(new int[] { 120, 100, 100 }, new int[] { 0, 255, 0 }), + Arguments.of(new int[] { 240, 100, 100 }, new int[] { 0, 0, 255 }), + Arguments.of(new int[] { 60, 100, 100 }, new int[] { 255, 255, 0 }), + Arguments.of(new int[] { 180, 100, 100 }, new int[] { 0, 255, 255 }), + Arguments.of(new int[] { 300, 100, 100 }, new int[] { 255, 0, 255 }), + Arguments.of(new int[] { 0, 0, 75 }, new int[] { 191, 191, 191 }), + Arguments.of(new int[] { 0, 0, 50 }, new int[] { 128, 128, 128 }), + Arguments.of(new int[] { 0, 100, 50 }, new int[] { 128, 0, 0 }), + Arguments.of(new int[] { 60, 100, 50 }, new int[] { 128, 128, 0 }), + Arguments.of(new int[] { 120, 100, 50 }, new int[] { 0, 128, 0 }), + Arguments.of(new int[] { 300, 100, 50 }, new int[] { 128, 0, 128 }), + Arguments.of(new int[] { 180, 100, 50 }, new int[] { 0, 128, 128 }), + Arguments.of(new int[] { 240, 100, 50 }, new int[] { 0, 0, 128 })); + } + } + + /* + * Return a stream RGB values. + */ + static class RgbValueProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(@Nullable ExtensionContext context) throws Exception { + return Stream.of(Arguments.of(new int[] { 0, 0, 0 }), Arguments.of(new int[] { 255, 255, 255 }), + Arguments.of(new int[] { 255, 0, 0 }), Arguments.of(new int[] { 0, 255, 0 }), + Arguments.of(new int[] { 0, 0, 255 }), Arguments.of(new int[] { 255, 255, 0 }), + Arguments.of(new int[] { 255, 0, 255 }), Arguments.of(new int[] { 0, 255, 255 }), + Arguments.of(new int[] { 191, 191, 191 }), Arguments.of(new int[] { 128, 128, 128 }), + Arguments.of(new int[] { 128, 0, 0 }), Arguments.of(new int[] { 128, 128, 0 }), + Arguments.of(new int[] { 0, 128, 0 }), Arguments.of(new int[] { 128, 0, 128 }), + Arguments.of(new int[] { 0, 128, 128 }), Arguments.of(new int[] { 0, 0, 128 }), + Arguments.of(new int[] { 0, 132, 255 }), Arguments.of(new int[] { 1, 131, 254 }), + Arguments.of(new int[] { 2, 130, 253 }), Arguments.of(new int[] { 3, 129, 252 }), + Arguments.of(new int[] { 4, 128, 251 }), Arguments.of(new int[] { 5, 127, 250 })); + } + } + + /* Helper functions */ + + /** + * Helper method for checking if expected and actual RGB color parameters (int[3], 0..255) match. + * + * When the test fails, both colors are printed. + * + * @param expected an HSBType containing the expected color. + * @param actual an HSBType containing the actual color. + */ + private void assertRgbEquals(final int[] expected, final int[] actual) { + if (expected[0] != actual[0] || expected[1] != actual[1] || expected[2] != actual[2]) { + // only proceed if both RGB colors are not idential, + // just prepare readable string compare and let it fail + final String expectedS = expected[0] + ", " + expected[1] + ", " + expected[2]; + final String actualS = actual[0] + ", " + actual[1] + ", " + actual[2]; + assertEquals(expectedS, actualS); + } } } diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 1b3d69887ef..615a415ab7b 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -54,6 +54,7 @@ mvn:org.openhab.core.bundles/org.openhab.core.id/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.persistence/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.semantics/${project.version} + openhab.tp-asm mvn:org.openhab.core.bundles/org.openhab.core.thing/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.transform/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.audio/${project.version} @@ -180,6 +181,8 @@ openhab-core-base + openhab-core-io-rest-auth + mvn:org.eclipse.jetty.websocket/websocket-servlet/${jetty.version} mvn:org.eclipse.jetty.websocket/websocket-server/${jetty.version} mvn:org.openhab.core.bundles/org.openhab.core.io.websocket/${project.version} diff --git a/features/karaf/openhab-tp/src/main/feature/feature.xml b/features/karaf/openhab-tp/src/main/feature/feature.xml index f64dc3dd573..3e38044c9ca 100644 --- a/features/karaf/openhab-tp/src/main/feature/feature.xml +++ b/features/karaf/openhab-tp/src/main/feature/feature.xml @@ -103,12 +103,12 @@ - openhab.tp;feature=asm;version=9.2.0 - mvn:org.ow2.asm/asm/9.2 - mvn:org.ow2.asm/asm-analysis/9.2 - mvn:org.ow2.asm/asm-commons/9.2 - mvn:org.ow2.asm/asm-util/9.2 - mvn:org.ow2.asm/asm-tree/9.2 + openhab.tp;feature=asm;version=9.4.0 + mvn:org.ow2.asm/asm/9.4 + mvn:org.ow2.asm/asm-analysis/9.4 + mvn:org.ow2.asm/asm-commons/9.4 + mvn:org.ow2.asm/asm-util/9.4 + mvn:org.ow2.asm/asm-tree/9.4 @@ -120,7 +120,7 @@ mvn:org.osgi/org.osgi.util.function/1.1.0 mvn:org.osgi/org.osgi.util.promise/1.1.1 mvn:org.osgi/org.osgi.service.jaxrs/1.0.0 - mvn:org.apache.aries.spifly/org.apache.aries.spifly.dynamic.bundle/1.3.4 + mvn:org.apache.aries.spifly/org.apache.aries.spifly.dynamic.bundle/1.3.6 mvn:org.apache.aries.component-dsl/org.apache.aries.component-dsl.component-dsl/1.2.2 mvn:org.apache.aries.jax.rs/org.apache.aries.jax.rs.whiteboard/2.0.0 @@ -155,23 +155,23 @@ - openhab.tp;feature=netty;version=4.1.72.Final - mvn:io.netty/netty-buffer/4.1.72.Final - mvn:io.netty/netty-common/4.1.72.Final - mvn:io.netty/netty-codec/4.1.72.Final - mvn:io.netty/netty-codec-http/4.1.72.Final - mvn:io.netty/netty-codec-http2/4.1.72.Final - mvn:io.netty/netty-codec-mqtt/4.1.72.Final - mvn:io.netty/netty-codec-socks/4.1.72.Final - mvn:io.netty/netty-handler/4.1.72.Final - mvn:io.netty/netty-handler-proxy/4.1.72.Final - mvn:io.netty/netty-resolver/4.1.72.Final - mvn:io.netty/netty-transport/4.1.72.Final - mvn:io.netty/netty-transport-native-epoll/4.1.72.Final - mvn:io.netty/netty-transport-native-kqueue/4.1.72.Final - mvn:io.netty/netty-transport-native-unix-common/4.1.72.Final - mvn:io.netty/netty-transport-classes-epoll/4.1.72.Final - mvn:io.netty/netty-tcnative-classes/2.0.46.Final + openhab.tp;feature=netty;version=4.1.92.Final + mvn:io.netty/netty-buffer/4.1.92.Final + mvn:io.netty/netty-common/4.1.92.Final + mvn:io.netty/netty-codec/4.1.92.Final + mvn:io.netty/netty-codec-http/4.1.92.Final + mvn:io.netty/netty-codec-http2/4.1.92.Final + mvn:io.netty/netty-codec-mqtt/4.1.92.Final + mvn:io.netty/netty-codec-socks/4.1.92.Final + mvn:io.netty/netty-handler/4.1.92.Final + mvn:io.netty/netty-handler-proxy/4.1.92.Final + mvn:io.netty/netty-resolver/4.1.92.Final + mvn:io.netty/netty-transport/4.1.92.Final + mvn:io.netty/netty-transport-native-epoll/4.1.92.Final + mvn:io.netty/netty-transport-native-kqueue/4.1.92.Final + mvn:io.netty/netty-transport-native-unix-common/4.1.92.Final + mvn:io.netty/netty-transport-classes-epoll/4.1.92.Final + mvn:io.netty/netty-tcnative-classes/2.0.60.Final @@ -202,9 +202,9 @@ - openhab.tp;feature=jose4j;version=0.7.7 + openhab.tp;feature=jose4j;version=0.9.3 mvn:org.apache.servicemix.specs/org.apache.servicemix.specs.activation-api-1.2.1/1.2.1_3 - mvn:org.bitbucket.b_c/jose4j/0.7.7 + mvn:org.bitbucket.b_c/jose4j/0.9.3 diff --git a/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java b/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java index 0778f76f369..1531c2ab4b8 100644 --- a/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java +++ b/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java @@ -29,6 +29,8 @@ import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionParameter; import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.config.core.FilterCriteria; +import org.openhab.core.config.core.ParameterOption; import org.openhab.core.test.java.JavaOSGiTest; /** @@ -82,14 +84,16 @@ public void assertThatConfigWithOptionsAndFilterAreProperlyRead() throws Excepti ConfigDescriptionParameter listParameter = parameters.stream().filter(p -> "list".equals(p.getName())) .findFirst().get(); assertThat(listParameter, is(notNullValue())); - assertThat(listParameter.getOptions().stream().map(p -> p.toString()).collect(Collectors.joining(", ")), is( - "ParameterOption [value=\"key1\", label=\"label1\"], ParameterOption [value=\"key2\", label=\"label2\"]")); + assertThat( + listParameter.getOptions().stream().map(ParameterOption::toString) + .collect(Collectors.joining(", ")), + is("ParameterOption [value=\"key1\", label=\"label1\"], ParameterOption [value=\"key2\", label=\"label2\"]")); ConfigDescriptionParameter lightParameter = parameters.stream() .filter(p -> "color-alarming-light".equals(p.getName())).findFirst().get(); assertThat(lightParameter, is(notNullValue())); assertThat( - lightParameter.getFilterCriteria().stream().map(p -> p.toString()) + lightParameter.getFilterCriteria().stream().map(FilterCriteria::toString) .collect(Collectors.joining(", ")), is("FilterCriteria [name=\"tags\", value=\"alarm, light\"], FilterCriteria [name=\"type\", value=\"color\"], FilterCriteria [name=\"binding-id\", value=\"hue\"]")); }); diff --git a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml index 0fec4bf2ffc..4d351403227 100644 --- a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml +++ b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml @@ -7,6 +7,7 @@ hue Binding The hue Binding integrates the Philips hue system. It allows to control hue lights. + local diff --git a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/acmeweather.bundle/OH-INF/addon/addon.xml b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/acmeweather.bundle/OH-INF/addon/addon.xml index 1fd540c81aa..543cf1ccc69 100644 --- a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/acmeweather.bundle/OH-INF/addon/addon.xml +++ b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/acmeweather.bundle/OH-INF/addon/addon.xml @@ -7,5 +7,6 @@ ACME Weather Binding The ACME Weather Binding requests the ACME Weather Service to show the current temperature, humidity and pressure. + cloud diff --git a/itests/org.openhab.core.auth.oauth2client.tests/src/main/java/org/openhab/core/auth/oauth2client/test/internal/AbstractTestAgent.java b/itests/org.openhab.core.auth.oauth2client.tests/src/main/java/org/openhab/core/auth/oauth2client/test/internal/AbstractTestAgent.java index 54d1be91f17..c8a586673ce 100644 --- a/itests/org.openhab.core.auth.oauth2client.tests/src/main/java/org/openhab/core/auth/oauth2client/test/internal/AbstractTestAgent.java +++ b/itests/org.openhab.core.auth.oauth2client.tests/src/main/java/org/openhab/core/auth/oauth2client/test/internal/AbstractTestAgent.java @@ -65,11 +65,10 @@ private static String getProperty(Map properties, String propKey if (obj == null) { return ""; } - if (obj instanceof String) { - return (String) obj; + if (obj instanceof String string) { + return string; } - if (obj instanceof String[]) { - String[] strArr = (String[]) obj; + if (obj instanceof String[] strArr) { if (strArr.length >= 1) { return strArr[0]; } else { @@ -141,22 +140,19 @@ public AccessTokenResponse testGetAccessTokenByAuthorizationCode(String code) @Override public AccessTokenResponse testGetCachedAccessToken() throws OAuthException, IOException, OAuthResponseException { logger.debug("test getCachedAccessToken"); - AccessTokenResponse oldRefreshedToken = oauthClientService.getAccessTokenResponse(); - return oldRefreshedToken; + return oauthClientService.getAccessTokenResponse(); } @Override public AccessTokenResponse testRefreshToken() throws OAuthException, IOException, OAuthResponseException { logger.debug("test RefreshToken"); - AccessTokenResponse newRefreshedToken = oauthClientService.refreshToken(); - return newRefreshedToken; + return oauthClientService.refreshToken(); } @Override public String testGetAuthorizationUrl(String state) throws OAuthException { logger.debug("test getAuthorizationUrl {}", state); - String authorizationURL = oauthClientService.getAuthorizationUrl(redirectUri, scope, state); - return authorizationURL; + return oauthClientService.getAuthorizationUrl(redirectUri, scope, state); } @Override diff --git a/itests/org.openhab.core.automation.integration.tests/itest.bndrun b/itests/org.openhab.core.automation.integration.tests/itest.bndrun index 41f22a29f5e..9323415d07c 100644 --- a/itests/org.openhab.core.automation.integration.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.integration.tests/itest.bndrun @@ -61,4 +61,10 @@ Fragment-Host: org.openhab.core.automation junit-jupiter-engine;version='[5.9.2,5.9.3)',\ junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ - junit-platform-launcher;version='[1.9.2,1.9.3)' + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ + org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ + org.objenesis;version='[3.3.0,3.3.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationJsonTest.java b/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationJsonTest.java index 33362852a65..14fcf8d0c68 100644 --- a/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationJsonTest.java +++ b/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationJsonTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import java.util.Collection; import java.util.Optional; @@ -36,6 +37,7 @@ import org.openhab.core.automation.Trigger; import org.openhab.core.automation.events.RuleStatusInfoEvent; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.type.ActionType; import org.openhab.core.automation.type.Input; import org.openhab.core.automation.type.ModuleTypeRegistry; @@ -46,6 +48,7 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemProvider; @@ -55,6 +58,7 @@ import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.storage.StorageService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.storage.VolatileStorageService; @@ -90,7 +94,12 @@ public class AutomationIntegrationJsonTest extends JavaOSGiTest { public void before() { logger.info("@Before.begin"); - getService(ItemRegistry.class); + eventPublisher = getService(EventPublisher.class); + itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); ItemProvider itemProvider = new ItemProvider() { @@ -136,8 +145,6 @@ public void receive(Event e) { StorageService storageService = getService(StorageService.class); managedRuleProvider = getService(ManagedRuleProvider.class); - eventPublisher = getService(EventPublisher.class); - itemRegistry = getService(ItemRegistry.class); ruleRegistry = getService(RuleRegistry.class); ruleManager = getService(RuleManager.class); moduleTypeRegistry = getService(ModuleTypeRegistry.class); diff --git a/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java b/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java index d8bc15abec4..1632e368bb0 100644 --- a/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java +++ b/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java @@ -16,6 +16,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import java.util.Collection; import java.util.Collections; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -49,6 +51,7 @@ import org.openhab.core.automation.events.RuleStatusInfoEvent; import org.openhab.core.automation.events.RuleUpdatedEvent; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.template.RuleTemplate; import org.openhab.core.automation.template.RuleTemplateProvider; import org.openhab.core.automation.template.Template; @@ -67,6 +70,7 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemProvider; @@ -76,6 +80,7 @@ import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.storage.StorageService; import org.openhab.core.test.java.JavaOSGiTest; import org.slf4j.Logger; @@ -106,7 +111,13 @@ public class AutomationIntegrationTest extends JavaOSGiTest { public void before() { logger.info("@Before.begin"); - getService(ItemRegistry.class); + eventPublisher = getService(EventPublisher.class); + itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + Objects.requireNonNull(eventPublisher), Objects.requireNonNull(itemRegistry), + mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); ItemProvider itemProvider = new ItemProvider() { @Override @@ -148,8 +159,6 @@ public void removeProviderChangeListener(ProviderChangeListener listener) registerVolatileStorageService(); StorageService storageService = getService(StorageService.class); - eventPublisher = getService(EventPublisher.class); - itemRegistry = getService(ItemRegistry.class); ruleRegistry = getService(RuleRegistry.class); ruleEngine = getService(RuleManager.class); managedRuleProvider = getService(ManagedRuleProvider.class); diff --git a/itests/org.openhab.core.automation.module.core.tests/itest.bndrun b/itests/org.openhab.core.automation.module.core.tests/itest.bndrun index f86cb861a69..f87b5a71009 100644 --- a/itests/org.openhab.core.automation.module.core.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.module.core.tests/itest.bndrun @@ -61,4 +61,10 @@ Fragment-Host: org.openhab.core.automation junit-jupiter-engine;version='[5.9.2,5.9.3)',\ junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ - junit-platform-launcher;version='[1.9.2,1.9.3)' + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ + org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ + org.objenesis;version='[3.3.0,3.3.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RunRuleModuleTest.java b/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RunRuleModuleTest.java index 3ba71f090e0..b6e0dac1487 100644 --- a/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RunRuleModuleTest.java +++ b/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RunRuleModuleTest.java @@ -14,6 +14,7 @@ import static java.util.Map.entry; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.Collection; @@ -32,6 +33,7 @@ import org.openhab.core.automation.RuleRegistry; import org.openhab.core.automation.RuleStatus; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.util.ModuleBuilder; import org.openhab.core.automation.util.RuleBuilder; import org.openhab.core.common.registry.ProviderChangeListener; @@ -39,6 +41,7 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemProvider; @@ -48,6 +51,7 @@ import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.storage.VolatileStorageService; import org.osgi.framework.ServiceReference; @@ -68,6 +72,13 @@ public class RunRuleModuleTest extends JavaOSGiTest { @BeforeEach public void before() { + EventPublisher eventPublisher = getService(EventPublisher.class); + ItemRegistry itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); + registerService(new ItemProvider() { @Override public void addProviderChangeListener(final ProviderChangeListener listener) { @@ -99,7 +110,7 @@ private Rule createSceneRule() { final Configuration sceneRuleAction3Config = new Configuration( Map.ofEntries(entry("itemName", "switch3"), entry("command", "ON"))); - final Rule sceneRule = RuleBuilder.create("exampleSceneRule").withActions( + return RuleBuilder.create("exampleSceneRule").withActions( ModuleBuilder.createAction().withId("sceneItemPostCommandAction1").withTypeUID("core.ItemCommandAction") .withConfiguration(sceneRuleAction1Config).build(), ModuleBuilder.createAction().withId("sceneItemPostCommandAction2").withTypeUID("core.ItemCommandAction") @@ -107,8 +118,6 @@ private Rule createSceneRule() { ModuleBuilder.createAction().withId("sceneItemPostCommandAction3").withTypeUID("core.ItemCommandAction") .withConfiguration(sceneRuleAction3Config).build()) .withName("Example Scene").build(); - - return sceneRule; } private Rule createOuterRule() { @@ -120,14 +129,12 @@ private Rule createOuterRule() { final Configuration outerRuleActionConfig = new Configuration(Map.of("ruleUIDs", ruleUIDs)); - final Rule outerRule = RuleBuilder.create("sceneActivationRule") + return RuleBuilder.create("sceneActivationRule") .withTriggers(ModuleBuilder.createTrigger().withId("ItemStateChangeTrigger2") .withTypeUID("core.GenericEventTrigger").withConfiguration(outerRuleTriggerConfig).build()) .withActions(ModuleBuilder.createAction().withId("RunRuleAction1").withTypeUID("core.RunRuleAction") .withConfiguration(outerRuleActionConfig).build()) .withName("scene activator").build(); - - return outerRule; } @Test diff --git a/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RuntimeRuleTest.java b/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RuntimeRuleTest.java index c7fddef1b7f..22d0e6a8f39 100644 --- a/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RuntimeRuleTest.java +++ b/itests/org.openhab.core.automation.module.core.tests/src/main/java/org/openhab/core/automation/internal/module/RuntimeRuleTest.java @@ -14,6 +14,7 @@ import static java.util.Map.entry; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; import java.util.Collection; import java.util.Collections; @@ -38,6 +39,7 @@ import org.openhab.core.automation.RuleStatusDetail; import org.openhab.core.automation.events.RuleStatusInfoEvent; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.internal.module.handler.CompareConditionHandler; import org.openhab.core.automation.type.ModuleTypeRegistry; import org.openhab.core.automation.util.ModuleBuilder; @@ -47,6 +49,7 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemProvider; @@ -56,6 +59,7 @@ import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.storage.VolatileStorageService; import org.openhab.core.types.Command; @@ -78,6 +82,13 @@ public class RuntimeRuleTest extends JavaOSGiTest { @BeforeEach public void before() { + EventPublisher eventPublisher = getService(EventPublisher.class); + ItemRegistry itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); + registerService(new ItemProvider() { @Override public void addProviderChangeListener(final ProviderChangeListener listener) { diff --git a/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun b/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun index c938385357a..5e6f415c499 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun @@ -61,4 +61,10 @@ Fragment-Host: org.openhab.core.automation junit-jupiter-engine;version='[5.9.2,5.9.3)',\ junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ - junit-platform-launcher;version='[1.9.2,1.9.3)' + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ + org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ + org.objenesis;version='[3.3.0,3.3.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java index 2d04b921f3f..19c7b4b6d12 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/BasicConditionHandlerTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.Collection; @@ -38,6 +39,7 @@ import org.openhab.core.automation.RuleStatusInfo; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.internal.module.handler.ItemCommandActionHandler; import org.openhab.core.automation.internal.module.handler.ItemStateTriggerHandler; import org.openhab.core.automation.util.ModuleBuilder; @@ -47,14 +49,17 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemProvider; +import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.events.ItemCommandEvent; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.storage.VolatileStorageService; import org.slf4j.Logger; @@ -81,6 +86,13 @@ public abstract class BasicConditionHandlerTest extends JavaOSGiTest { */ @BeforeEach public void beforeBase() { + EventPublisher eventPublisher = getService(EventPublisher.class); + ItemRegistry itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); + ItemProvider itemProvider = new ItemProvider() { @Override public void addProviderChangeListener(ProviderChangeListener listener) { @@ -179,7 +191,7 @@ public void receive(Event event) { logger.info("Rule is enabled and idle"); logger.info("Send and wait for item state is ON"); - eventPublisher.post(ItemEventFactory.createStateEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); waitForAssert(() -> { assertThat(itemEvent, is(notNullValue())); @@ -194,7 +206,7 @@ public void receive(Event event) { // prepare the execution itemEvent = null; - eventPublisher.post(ItemEventFactory.createStateEvent(testItemName1, OnOffType.ON)); + eventPublisher.post(ItemEventFactory.createStateUpdatedEvent(testItemName1, OnOffType.ON)); waitForAssert(() -> { assertThat(itemEvent, is(nullValue())); }); diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/DayOfWeekConditionHandlerTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/DayOfWeekConditionHandlerTest.java index 8f5cc114ba8..61d3c6c54a8 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/DayOfWeekConditionHandlerTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/DayOfWeekConditionHandlerTest.java @@ -14,6 +14,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; import java.text.SimpleDateFormat; import java.time.ZonedDateTime; @@ -24,12 +25,18 @@ import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.core.automation.Condition; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.internal.module.handler.DayOfWeekConditionHandler; import org.openhab.core.automation.type.ModuleTypeRegistry; import org.openhab.core.automation.util.ModuleBuilder; import org.openhab.core.config.core.Configuration; +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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +57,16 @@ public DayOfWeekConditionHandlerTest() { logger.info("Today is {}", dayOfWeek); } + @BeforeEach + public void before() { + EventPublisher eventPublisher = getService(EventPublisher.class); + ItemRegistry itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); + } + @Test public void assertThatConditionWorks() { Configuration conditionConfiguration = new Configuration( diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/RuntimeRuleTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/RuntimeRuleTest.java index 1df494a5ceb..120f02c4b17 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/RuntimeRuleTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/RuntimeRuleTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import java.util.Collection; import java.util.HashMap; @@ -37,6 +38,7 @@ import org.openhab.core.automation.RuleStatusInfo; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.internal.module.handler.GenericCronTriggerHandler; import org.openhab.core.automation.type.ModuleTypeRegistry; import org.openhab.core.automation.util.ModuleBuilder; @@ -44,12 +46,16 @@ import org.openhab.core.common.registry.ProviderChangeListener; import org.openhab.core.config.core.Configuration; import org.openhab.core.events.Event; +import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemProvider; +import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.events.ItemCommandEvent; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.storage.VolatileStorageService; import org.slf4j.Logger; @@ -72,6 +78,13 @@ public class RuntimeRuleTest extends JavaOSGiTest { @BeforeEach public void before() { + EventPublisher eventPublisher = getService(EventPublisher.class); + ItemRegistry itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); + ItemProvider itemProvider = new TestItemProvider(Set.of(new SwitchItem("myLampItem"))); registerService(itemProvider); registerService(volatileStorageService); @@ -105,8 +118,6 @@ public void checkDisableAndEnableOfTimerTriggeredRule() { */ logger.info("Create rule"); String testExpression = "* * * * * ?"; - - ; Configuration triggerConfig = new Configuration(Map.of("cronExpression", testExpression)); List triggers = List.of(ModuleBuilder.createTrigger().withId("MyTimerTrigger") .withTypeUID(GenericCronTriggerHandler.MODULE_TYPE_ID).withConfiguration(triggerConfig).build()); @@ -136,8 +147,7 @@ public void checkDisableAndEnableOfTimerTriggeredRule() { final RuleStatusInfo ruleStatus = ruleEngine.getStatusInfo(rule.getUID()); logger.info("Rule status (should be IDLE or RUNNING): {}", ruleStatus); boolean allFine; - if (RuleStatus.IDLE.equals(ruleStatus.getStatus()) - || RuleStatus.RUNNING.equals(ruleStatus.getStatus())) { + if (RuleStatus.IDLE == ruleStatus.getStatus() || RuleStatus.RUNNING == ruleStatus.getStatus()) { allFine = true; } else { allFine = false; diff --git a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/TimeOfDayConditionHandlerTest.java b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/TimeOfDayConditionHandlerTest.java index 2b670aab489..3bbdea8a183 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/TimeOfDayConditionHandlerTest.java +++ b/itests/org.openhab.core.automation.module.timer.tests/src/main/java/org/openhab/core/automation/module/timer/internal/TimeOfDayConditionHandlerTest.java @@ -74,23 +74,20 @@ public void assertThatConditionWorks() { } private TimeOfDayConditionHandler getTimeOfDayConditionHandler(String startTime, String endTime) { - TimeOfDayConditionHandler handler = new TimeOfDayConditionHandler(getTimeCondition(startTime, endTime)); - return handler; + return new TimeOfDayConditionHandler(getTimeCondition(startTime, endTime)); } private Condition getTimeCondition(String startTime, String endTime) { Configuration timeConfig = getTimeConfiguration(startTime, endTime); - Condition condition = ModuleBuilder.createCondition().withId("testTimeOfDayCondition") + return ModuleBuilder.createCondition().withId("testTimeOfDayCondition") .withTypeUID(TimeOfDayConditionHandler.MODULE_TYPE_ID).withConfiguration(timeConfig).build(); - return condition; } private Configuration getTimeConfiguration(String startTime, String endTime) { Map timeMap = new HashMap<>(); timeMap.put("startTime", startTime); timeMap.put("endTime", endTime); - Configuration timeConfig = new Configuration(timeMap); - return timeConfig; + return new Configuration(timeMap); } @Override diff --git a/itests/org.openhab.core.automation.tests/itest.bndrun b/itests/org.openhab.core.automation.tests/itest.bndrun index 3d9499b316a..98fb146d9a8 100644 --- a/itests/org.openhab.core.automation.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.tests/itest.bndrun @@ -61,4 +61,10 @@ Fragment-Host: org.openhab.core.automation junit-jupiter-engine;version='[5.9.2,5.9.3)',\ junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ - junit-platform-launcher;version='[1.9.2,1.9.3)' + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ + org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ + org.objenesis;version='[3.3.0,3.3.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/event/RuleEventTest.java b/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/event/RuleEventTest.java index 42d3e4aa8da..fabce422523 100644 --- a/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/event/RuleEventTest.java +++ b/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/event/RuleEventTest.java @@ -16,6 +16,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.Collection; @@ -41,6 +42,7 @@ import org.openhab.core.automation.events.RuleStatusInfoEvent; import org.openhab.core.automation.events.RuleUpdatedEvent; import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.internal.module.factory.CoreModuleHandlerFactory; import org.openhab.core.automation.util.ModuleBuilder; import org.openhab.core.automation.util.RuleBuilder; import org.openhab.core.common.registry.ProviderChangeListener; @@ -48,6 +50,7 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemProvider; @@ -57,6 +60,7 @@ import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.StartLevelService; import org.openhab.core.test.java.JavaOSGiTest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,6 +84,13 @@ public RuleEventTest() { @BeforeEach public void before() { + EventPublisher eventPublisher = getService(EventPublisher.class); + ItemRegistry itemRegistry = getService(ItemRegistry.class); + CoreModuleHandlerFactory coreModuleHandlerFactory = new CoreModuleHandlerFactory(getBundleContext(), + eventPublisher, itemRegistry, mock(TimeZoneProvider.class), mock(StartLevelService.class)); + mock(CoreModuleHandlerFactory.class); + registerService(coreModuleHandlerFactory); + ItemProvider itemProvider = new ItemProvider() { @Override @@ -148,9 +159,7 @@ public void receive(Event event) { ruleRegistry.add(rule); ruleEngine.setEnabled(rule.getUID(), true); - waitForAssert(() -> { - assertThat(ruleEngine.getStatusInfo(rule.getUID()).getStatus(), is(RuleStatus.IDLE)); - }); + waitForAssert(() -> assertThat(ruleEngine.getStatusInfo(rule.getUID()).getStatus(), is(RuleStatus.IDLE))); // TEST RULE diff --git a/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/internal/TestModuleTypeProvider.java b/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/internal/TestModuleTypeProvider.java index 37011e6c4ac..5ba47dcf1da 100644 --- a/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/internal/TestModuleTypeProvider.java +++ b/itests/org.openhab.core.automation.tests/src/main/java/org/openhab/core/automation/internal/TestModuleTypeProvider.java @@ -55,17 +55,16 @@ private TriggerType createTriggerType() { outputs.add(createOutput("out1", Set.of("tagA"))); outputs.add(createOutput("out2", Set.of("tagB", "tagC"))); outputs.add(createOutput("out3", Set.of("tagA", "tagB", "tagC"))); - TriggerType t = new TriggerType(TRIGGER_TYPE, null, outputs); - return t; + return new TriggerType(TRIGGER_TYPE, null, outputs); } private ConditionType createConditionType() { List inputs = new ArrayList<>(3); inputs.add(createInput("in0", Set.of("tagE"))); // no connection, missing condition tag inputs.add(createInput("in1", Set.of("tagA"))); // conflict in2 -> out1 or in2 -> out3 - inputs.add(createInput("in2", Set.of("tagA", "tagB"))); // in2 -> out3 - ConditionType t = new ConditionType(CONDITION_TYPE, null, inputs); - return t; + inputs.add(createInput("in2", Set.of("tagA", "tagB"))); + // in2 -> out3 + return new ConditionType(CONDITION_TYPE, null, inputs); } private ActionType createActionType() { @@ -78,8 +77,7 @@ private ActionType createActionType() { List outputs = new ArrayList<>(3); outputs.add(createOutput("out4", Set.of("tagD"))); outputs.add(createOutput("out5", Set.of("tagD", "tagE"))); - ActionType t = new ActionType(ACTION_TYPE, null, inputs, outputs); - return t; + return new ActionType(ACTION_TYPE, null, inputs, outputs); } private Output createOutput(String name, Set tags) { diff --git a/itests/org.openhab.core.config.core.tests/.classpath b/itests/org.openhab.core.config.core.tests/.classpath index 9e55698cddc..0d0989f1566 100644 --- a/itests/org.openhab.core.config.core.tests/.classpath +++ b/itests/org.openhab.core.config.core.tests/.classpath @@ -25,5 +25,10 @@ + + + + + diff --git a/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionI18nTest.java b/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionI18nTest.java index caf3d4d990b..dd973d1c41a 100644 --- a/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionI18nTest.java +++ b/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionI18nTest.java @@ -30,6 +30,7 @@ import org.openhab.core.config.core.ConfigDescriptionParameter; import org.openhab.core.config.core.ConfigDescriptionParameterGroup; import org.openhab.core.config.core.ConfigDescriptionProvider; +import org.openhab.core.config.core.ParameterOption; import org.openhab.core.test.BundleCloseable; import org.openhab.core.test.SyntheticBundleInstaller; import org.openhab.core.test.java.JavaOSGiTest; @@ -101,7 +102,7 @@ private static String asString(ConfigDescription description) { sb.append(String.format("refresh.description = %s\n", refresh.getDescription())); sb.append(String.format("question.pattern = %s\n", question.getPattern())); sb.append(String.format("question.options = %s\n", - question.getOptions().stream().map(o -> o.getLabel()).collect(Collectors.joining(", ")))); + question.getOptions().stream().map(ParameterOption::getLabel).collect(Collectors.joining(", ")))); sb.append(String.format("group.label = %s\n", group.getLabel())); sb.append(String.format("group.description = %s", group.getDescription())); diff --git a/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionsTest.java b/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionsTest.java index a1193063edf..63d9f595109 100644 --- a/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionsTest.java +++ b/itests/org.openhab.core.config.core.tests/src/main/java/org/openhab/core/config/core/xml/ConfigDescriptionsTest.java @@ -33,6 +33,8 @@ import org.openhab.core.config.core.ConfigDescriptionParameter.Type; import org.openhab.core.config.core.ConfigDescriptionParameterGroup; import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.config.core.FilterCriteria; +import org.openhab.core.config.core.ParameterOption; import org.openhab.core.test.BundleCloseable; import org.openhab.core.test.SyntheticBundleInstaller; import org.openhab.core.test.java.JavaOSGiTest; @@ -119,7 +121,7 @@ public void assertThatConfigDescriptionsAreLoadedProperly() throws Exception { assertThat(colorItemParameter.getContext(), is("item")); assertThat(colorItemParameter.getFilterCriteria(), is(notNullValue())); assertThat( - colorItemParameter.getFilterCriteria().stream().map(c -> c.toString()) + colorItemParameter.getFilterCriteria().stream().map(FilterCriteria::toString) .collect(Collectors.joining(", ")), is("FilterCriteria [name=\"tags\", value=\"alarm, light\"], FilterCriteria [name=\"type\", value=\"color\"], FilterCriteria [name=\"binding-id\", value=\"hue\"]")); @@ -136,7 +138,7 @@ public void assertThatConfigDescriptionsAreLoadedProperly() throws Exception { assertThat(listParameter1.isVerifyable(), is(false)); assertThat(listParameter1.getLimitToOptions(), is(true)); assertThat(listParameter1.getMultipleLimit(), is(nullValue())); - assertThat(listParameter1.getOptions().stream().map(o -> o.toString()).collect(joining(", ")), is( + assertThat(listParameter1.getOptions().stream().map(ParameterOption::toString).collect(joining(", ")), is( "ParameterOption [value=\"key1\", label=\"label1\"], ParameterOption [value=\"key2\", label=\"label2\"]")); ConfigDescriptionParameter listParameter2 = findParameter(englishDescription, "list2"); diff --git a/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun index 1c4f83e7614..b283f7cac4f 100644 --- a/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun @@ -65,4 +65,6 @@ Fragment-Host: org.openhab.core.config.discovery.mdns net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ - org.objenesis;version='[3.3.0,3.3.1)' + org.objenesis;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.config.discovery.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.tests/itest.bndrun index 2f70c26108f..ad2ab040a73 100644 --- a/itests/org.openhab.core.config.discovery.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.tests/itest.bndrun @@ -64,4 +64,6 @@ Fragment-Host: org.openhab.core.config.discovery net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\ org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ - org.objenesis;version='[3.3.0,3.3.1)' + org.objenesis;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/DiscoveryServiceRegistryOSGiTest.java b/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/DiscoveryServiceRegistryOSGiTest.java index 3aeb235d832..011da7699e8 100644 --- a/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/DiscoveryServiceRegistryOSGiTest.java +++ b/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/DiscoveryServiceRegistryOSGiTest.java @@ -359,7 +359,7 @@ public void testStartScanTwoDiscoveryServices() { discoveryServiceRegistry.addDiscoveryListener(discoveryListenerMock); discoveryServiceRegistry.startScan(new ThingTypeUID(ANY_BINDING_ID_1, ANY_THING_TYPE_1), mockScanListener1); - waitForAssert(() -> mockScanListener1.onFinished()); + waitForAssert(mockScanListener1::onFinished); verify(discoveryListenerMock, times(2)).thingDiscovered(any(), any()); } diff --git a/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/internal/InboxOSGiTest.java b/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/internal/InboxOSGiTest.java index 983c8d0a47e..5646ab4264c 100644 --- a/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/internal/InboxOSGiTest.java +++ b/itests/org.openhab.core.config.discovery.tests/src/main/java/org/openhab/core/config/discovery/internal/InboxOSGiTest.java @@ -219,8 +219,8 @@ public void cleanUp() { EventSubscriber inboxEventSubscriber = new EventSubscriber() { @Override public void receive(Event event) { - if (event instanceof InboxRemovedEvent) { - removedInboxThingUIDs.add(((InboxRemovedEvent) event).getDiscoveryResult().thingUID); + if (event instanceof InboxRemovedEvent removedEvent) { + removedInboxThingUIDs.add(removedEvent.getDiscoveryResult().thingUID); } } @@ -235,7 +235,7 @@ public Set getSubscribedEventTypes() { registry.remove(BRIDGE_THING_UID); managedThingProvider.getAll().forEach(thing -> managedThingProvider.remove(thing.getUID())); - inboxListeners.forEach(listener -> inbox.removeInboxListener(listener)); + inboxListeners.forEach(inbox::removeInboxListener); inbox.getAll().stream().forEach(discoveryResult -> inbox.remove(discoveryResult.getThingUID())); discoveryResults.clear(); @@ -1053,7 +1053,7 @@ public void assertThatResultWithMissingThingTypeNotAdded() throws ExecutionExcep CompletableFuture future = inbox.add(discoveryResult); - waitForAssert(() -> future.isDone(), 30, 5); + waitForAssert(future::isDone, 30, 5); assertThat(future.get(), is(false)); } @@ -1071,7 +1071,7 @@ public void assertThatResultWithLaterAddedThingTypeIsAdded() throws ExecutionExc dummyThingTypeProvider.add(thingTypeUID, ThingTypeBuilder.instance(thingTypeUID, "label").build()); - waitForAssert(() -> future.isDone(), 30, 5); + waitForAssert(future::isDone, 30, 5); assertThat(future.get(), is(true)); } diff --git a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun index 402acf7ea78..5f27c622288 100644 --- a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun @@ -65,4 +65,6 @@ Fragment-Host: org.openhab.core.config.discovery.usbserial.linuxsysfs net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\ org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ - org.objenesis;version='[3.3.0,3.3.1)' + org.objenesis;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java index 8b73f376206..92ec4d60e1c 100644 --- a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java +++ b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/src/main/java/org/openhab/core/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java @@ -88,7 +88,7 @@ public void setup() throws IOException { @Test public void testIOExceptionIfSysfsTtyDoesNotExist() throws IOException { delete(sysfsTtyPath); - assertThrows(IOException.class, () -> scanner.scan()); + assertThrows(IOException.class, scanner::scan); } @Test @@ -246,6 +246,6 @@ private enum DeviceCreationOption { NO_VENDOR_ID, NO_PRODUCT_ID, NO_INTERFACE_NUMBER, - NON_USB_DEVICE; + NON_USB_DEVICE } } diff --git a/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun index 956ee342b1a..20edb85c9d7 100644 --- a/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun @@ -73,4 +73,6 @@ Provide-Capability: \ net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ - org.objenesis;version='[3.3.0,3.3.1)' + org.objenesis;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.ephemeris.tests/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImplOSGiTest.java b/itests/org.openhab.core.ephemeris.tests/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImplOSGiTest.java index 5f086250f77..5a95b81b8d0 100644 --- a/itests/org.openhab.core.ephemeris.tests/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImplOSGiTest.java +++ b/itests/org.openhab.core.ephemeris.tests/src/main/java/org/openhab/core/ephemeris/internal/EphemerisManagerImplOSGiTest.java @@ -47,6 +47,7 @@ public class EphemerisManagerImplOSGiTest extends JavaOSGiTest { private static final String INTERNAL_DAYSET = "internal"; private static final URI CONFIG_URI = URI.create("system:ephemeris"); + private static final String COUNTRY_DENMARK_KEY = "dk"; private static final String COUNTRY_AUSTRALIA_KEY = "au"; private static final String COUNTRY_AUSTRALIA_NAME = "Australia"; private static final String REGION_BAVARIA_KEY = "by"; @@ -263,6 +264,20 @@ public void testIsBankHoliday() { assertFalse(vacation); } + @Test + public void testIsBankHolidayWhenGeneralPrayerDay2023() { + ZonedDateTime generalPrayerDay = ZonedDateTime.of(2023, 05, 05, 0, 0, 0, 0, ZoneId.of("Europe/Paris")); + ephemerisManager.modified(Map.of(CONFIG_COUNTRY, COUNTRY_DENMARK_KEY)); + assertTrue(ephemerisManager.isBankHoliday(generalPrayerDay)); + } + + @Test + public void testIsBankHolidayWhenGeneralPrayerDay2024() { + ZonedDateTime generalPrayerDay = ZonedDateTime.of(2024, 04, 26, 0, 0, 0, 0, ZoneId.of("Europe/Paris")); + ephemerisManager.modified(Map.of(CONFIG_COUNTRY, COUNTRY_DENMARK_KEY)); + assertFalse(ephemerisManager.isBankHoliday(generalPrayerDay)); + } + @Test public void testGetBankHoliday() { ZonedDateTime theDay = ZonedDateTime.of(2019, 01, 01, 0, 0, 0, 0, ZoneId.of("Europe/Paris")); diff --git a/itests/org.openhab.core.io.net.tests/.classpath b/itests/org.openhab.core.io.net.tests/.classpath new file mode 100644 index 00000000000..9e55698cddc --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/itests/org.openhab.core.io.net.tests/.project b/itests/org.openhab.core.io.net.tests/.project new file mode 100644 index 00000000000..3b2eca6a8f9 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.io.net.tests + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/itests/org.openhab.core.io.net.tests/NOTICE b/itests/org.openhab.core.io.net.tests/NOTICE new file mode 100644 index 00000000000..6c17d0d8a45 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/NOTICE @@ -0,0 +1,14 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-core + diff --git a/itests/org.openhab.core.io.net.tests/itest.bndrun b/itests/org.openhab.core.io.net.tests/itest.bndrun new file mode 100644 index 00000000000..9d4983c500e --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/itest.bndrun @@ -0,0 +1,76 @@ +-include: ../itest-common.bndrun + +Bundle-SymbolicName: ${project.artifactId} +Fragment-Host: org.openhab.core.io.net + +-runrequires: \ + bnd.identity;id='org.openhab.core.io.net.tests',\ + bnd.identity;id='org.eclipse.jetty.http2.hpack',\ + bnd.identity;id='org.eclipse.jetty.websocket.server',\ + bnd.identity;id='org.apache.aries.spifly.dynamic.bundle' + +-runbundles: \ + biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ + ch.qos.logback.core;version='[1.2.11,1.2.12)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + com.sun.xml.bind.jaxb-osgi;version='[2.3.3,2.3.4)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + jakarta.annotation-api;version='[2.0.0,2.0.1)',\ + jakarta.inject.jakarta.inject-api;version='[2.0.0,2.0.1)',\ + jakarta.xml.bind-api;version='[2.3.3,2.3.4)',\ + javax.measure.unit-api;version='[2.1.2,2.1.3)',\ + junit-jupiter-api;version='[5.9.2,5.9.3)',\ + junit-jupiter-engine;version='[5.9.2,5.9.3)',\ + junit-platform-commons;version='[1.9.2,1.9.3)',\ + junit-platform-engine;version='[1.9.2,1.9.3)',\ + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.servlet-api;version='[1.2.0,1.2.1)',\ + org.apache.felix.scr;version='[2.2.4,2.2.5)',\ + org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)',\ + org.apache.servicemix.specs.annotation-api-1.3;version='[1.3.0,1.3.1)',\ + org.eclipse.equinox.event;version='[1.4.300,1.4.301)',\ + org.eclipse.jetty.alpn.client;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.client;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.http;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.http2.client;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.http2.common;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.http2.hpack;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.io;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.security;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.server;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.servlet;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.util;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.util.ajax;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.websocket.api;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.websocket.client;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.websocket.common;version='[9.4.50,9.4.51)',\ + org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\ + org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\ + org.hamcrest;version='[2.2.0,2.2.1)',\ + org.openhab.core;version='[4.0.0,4.0.1)',\ + org.openhab.core.io.net;version='[4.0.0,4.0.1)',\ + org.openhab.core.io.net.tests;version='[4.0.0,4.0.1)',\ + org.openhab.core.test;version='[4.0.0,4.0.1)',\ + org.opentest4j;version='[1.2.0,1.2.1)',\ + org.ops4j.pax.logging.pax-logging-api;version='[2.2.0,2.2.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.osgi.service.component;version='[1.5.0,1.5.1)',\ + org.osgi.service.event;version='[1.4.0,1.4.1)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.2.0,1.2.1)',\ + si-units;version='[2.1.0,2.1.1)',\ + si.uom.si-quantity;version='[2.1.0,2.1.1)',\ + tech.units.indriya;version='[2.1.2,2.1.3)',\ + uom-lib-common;version='[2.1.0,2.1.1)',\ + org.eclipse.jetty.http2.server;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.websocket.servlet;version='[9.4.50,9.4.51)',\ + org.eclipse.jetty.websocket.server;version='[9.4.50,9.4.51)',\ + org.objectweb.asm;version='[9.4.0,9.4.1)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.6,1.3.7)',\ + org.objectweb.asm.commons;version='[9.4.0,9.4.1)',\ + org.objectweb.asm.tree;version='[9.4.0,9.4.1)',\ + org.objectweb.asm.tree.analysis;version='[9.4.0,9.4.1)',\ + org.objectweb.asm.util;version='[9.4.0,9.4.1)' diff --git a/itests/org.openhab.core.io.net.tests/pom.xml b/itests/org.openhab.core.io.net.tests/pom.xml new file mode 100644 index 00000000000..f6e7433fc7c --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.core.itests + org.openhab.core.reactor.itests + 4.0.0-SNAPSHOT + + + org.openhab.core.io.net.tests + + openHAB Core :: Integration Tests :: Network I/O Tests + + diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/ClientFactoryTest.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/ClientFactoryTest.java new file mode 100644 index 00000000000..d805dde53a7 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/ClientFactoryTest.java @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.net.tests; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.InetSocketAddress; +import java.util.Objects; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Promise.Completable; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.io.net.http.WebSocketFactory; +import org.openhab.core.io.net.tests.internal.TestServer; +import org.openhab.core.io.net.tests.internal.TestStreamAdapter; +import org.openhab.core.io.net.tests.internal.TestWebSocket; +import org.openhab.core.test.TestPortUtil; +import org.openhab.core.test.java.JavaOSGiTest; + +/** + * Tests for HttpClientFactory and WebSocketFactory implementations. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class ClientFactoryTest extends JavaOSGiTest { + public static final String RESPONSE = "response"; + + private static final String CONSUMER = "consumer"; + private static final String HOST = "127.0.0.1"; + private static final int WAIT_SECONDS = 10; + private static final long CONNECTION_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(WAIT_SECONDS); + + private static @Nullable TestServer server; + + @AfterAll + public static void afterAll() throws Exception { + TestServer testServer = server; + if (testServer != null) { + testServer.stopServer(); + } + } + + @BeforeAll + public static void beforeAll() throws Exception { + TestServer testServer = new TestServer(HOST, TestPortUtil.findFreePort()); + testServer.startServer(); + server = testServer; + } + + private TestServer getServer() { + return Objects.requireNonNull(server); + } + + @Test + public void testHttp1Client() throws Exception { + HttpClientFactory httpClientFactory = getService(HttpClientFactory.class); + assertNotNull(httpClientFactory); + + HttpClient client = httpClientFactory.createHttpClient(CONSUMER); + assertNotNull(client); + + try { + // start client + client.setConnectTimeout(CONNECTION_TIMEOUT_MS); + client.start(); + + // send request + ContentResponse response = client.GET(getServer().getHttpUri()); + + // check response + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertEquals(RESPONSE, response.getContentAsString()); + } finally { + try { + // stop client + client.stop(); + } catch (Exception e) { + } + } + } + + @Test + public void testHttp2Client() throws Exception { + HttpClientFactory httpClientFactory = getService(HttpClientFactory.class); + assertNotNull(httpClientFactory); + + HTTP2Client client = httpClientFactory.createHttp2Client(CONSUMER); + assertNotNull(client); + + Completable<@Nullable Session> sessionCompletable = new Completable<>(); + Completable<@Nullable Stream> streamCompletable = new Completable<>(); + + TestStreamAdapter streamAdapter = new TestStreamAdapter(); + + MetaData.Request request = new MetaData.Request(HttpMethod.GET.toString(), + new HttpURI(getServer().getHttpUri()), HttpVersion.HTTP_2, new HttpFields()); + + HeadersFrame headers = new HeadersFrame(request, null, true); + + try { + // start client + client.setConnectTimeout(CONNECTION_TIMEOUT_MS); + client.start(); + + // establish session + client.connect(new InetSocketAddress(getServer().getHost(), getServer().getPort()), + new Session.Listener.Adapter(), sessionCompletable); + Session session = sessionCompletable.get(WAIT_SECONDS, TimeUnit.SECONDS); + assertNotNull(session); + assertFalse(session.isClosed()); + + // open stream + session.newStream(headers, streamCompletable, streamAdapter); + Stream stream = streamCompletable.get(WAIT_SECONDS, TimeUnit.SECONDS); + assertNotNull(stream); + + // check response + String response = streamAdapter.completable.get(WAIT_SECONDS, TimeUnit.SECONDS); + assertEquals(RESPONSE, response); + } finally { + try { + // stop client + client.stop(); + } catch (Exception e) { + } + } + } + + @Test + public void testWebSocketClient() throws Exception { + WebSocketFactory webSocketFactory = getService(WebSocketFactory.class); + assertNotNull(webSocketFactory); + + WebSocketClient client = webSocketFactory.createWebSocketClient(CONSUMER); + assertNotNull(client); + + try { + // start client + client.setConnectTimeout(CONNECTION_TIMEOUT_MS); + client.start(); + + // create session future + TestWebSocket webSocket = new TestWebSocket(); + Future sessionFuture = client.connect(webSocket, + getServer().getWebSocketUri()); + assertNotNull(sessionFuture); + + // check response + String response = webSocket.completable.get(WAIT_SECONDS, TimeUnit.SECONDS); + assertEquals(RESPONSE, response); + } finally { + try { + // stop client + client.stop(); + } catch (Exception e) { + } + } + } +} diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestHttpServlet.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestHttpServlet.java new file mode 100644 index 00000000000..ebb6c0fb5a0 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestHttpServlet.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.io.net.tests.internal; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.net.tests.ClientFactoryTest; + +/** + * Servlet for org.openhab.core.io.net tests. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class TestHttpServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType(MediaType.TEXT_PLAIN); + response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + response.getWriter().write(ClientFactoryTest.RESPONSE); + } +} diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestServer.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestServer.java new file mode 100644 index 00000000000..344d9346356 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestServer.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.net.tests.internal; + +import java.net.URI; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +/** + * Embedded jetty server used in the tests. + * + * Based on {@code TestServer} of the FS Internet Radio Binding. + * + * @author Velin Yordanov - Initial contribution + * @author Wouter Born - Increase test coverage + * @author Andrew Fiddian-Green - Adapted for org.openhab.core.io.net.tests + */ +@NonNullByDefault +public class TestServer { + private static final String SERVLET_PATH = "/servlet"; + private static final String WEBSOCKET_PATH = "/ws"; + + private final String host; + private final int port; + private final Server server; + + public TestServer(String host, int port) { + this.host = host; + this.port = port; + this.server = new Server(); + } + + public String getHost() { + return host; + } + + public URI getHttpUri() { + return URI.create("http://" + host + ":" + port + SERVLET_PATH); + } + + public int getPort() { + return port; + } + + public URI getWebSocketUri() { + return URI.create("ws://" + host + ":" + port + WEBSOCKET_PATH); + } + + public void startServer() throws Exception { + ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(new ServletHolder(new TestHttpServlet()), SERVLET_PATH); + handler.addServletWithMapping(new ServletHolder(new TestWebSocketServlet()), WEBSOCKET_PATH); + server.setHandler(handler); + + HttpConfiguration httpConfig = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfig); + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + + ServerConnector connector = new ServerConnector(server, h1, h2c); + connector.setHost(host); + connector.setPort(port); + server.addConnector(connector); + + server.start(); + } + + public void stopServer() throws Exception { + server.stop(); + } +} diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestStreamAdapter.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestStreamAdapter.java new file mode 100644 index 00000000000..cc4b17665e9 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestStreamAdapter.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.net.tests.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +/** + * HTTP/2 stream adapter for org.openhab.core.io.net tests. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@WebSocket +@NonNullByDefault +public class TestStreamAdapter extends Stream.Listener.Adapter { + public final CompletableFuture completable = new CompletableFuture<>(); + + @Override + public void onData(@Nullable Stream stream, @Nullable DataFrame frame, @Nullable Callback callback) { + assertNotNull(stream); + assertNotNull(frame); + assertTrue(frame.isEndStream()); + completable.complete(StandardCharsets.UTF_8.decode(frame.getData()).toString()); + } +} diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocket.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocket.java new file mode 100644 index 00000000000..d089d5ebbde --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocket.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.io.net.tests.internal; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +/** + * WebSocket implementer class for org.openhab.core.io.net tests. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@WebSocket +@NonNullByDefault +public class TestWebSocket { + public final CompletableFuture completable = new CompletableFuture<>(); + + @OnWebSocketMessage + public void onMessage(String message) { + completable.complete(message); + } +} diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocketAdapter.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocketAdapter.java new file mode 100644 index 00000000000..f201c24b665 --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocketAdapter.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.net.tests.internal; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.openhab.core.io.net.tests.ClientFactoryTest; + +/** + * WebSocket for org.openhab.core.io.net tests. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class TestWebSocketAdapter extends WebSocketAdapter { + + @Override + public void onWebSocketConnect(@Nullable Session session) { + super.onWebSocketConnect(session); + try { + getRemote().sendString(ClientFactoryTest.RESPONSE); + } catch (IOException e) { + } + } +} diff --git a/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocketServlet.java b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocketServlet.java new file mode 100644 index 00000000000..ded3102f5eb --- /dev/null +++ b/itests/org.openhab.core.io.net.tests/src/main/java/org/openhab/core/io/net/tests/internal/TestWebSocketServlet.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.io.net.tests.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.servlet.WebSocketServlet; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + +/** + * Servlet for org.openhab.core.io.net tests. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class TestWebSocketServlet extends WebSocketServlet { + private static final long serialVersionUID = 1L; + + @Override + public void configure(@Nullable WebSocketServletFactory factory) { + if (factory != null) { + factory.register(TestWebSocketAdapter.class); + } + } +} diff --git a/itests/org.openhab.core.io.rest.core.tests/itest.bndrun b/itests/org.openhab.core.io.rest.core.tests/itest.bndrun index f2d2f06a979..21cf1349327 100644 --- a/itests/org.openhab.core.io.rest.core.tests/itest.bndrun +++ b/itests/org.openhab.core.io.rest.core.tests/itest.bndrun @@ -70,7 +70,6 @@ Fragment-Host: org.openhab.core.io.rest.core io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ com.sun.jna;version='[5.12.1,5.12.2)',\ com.fasterxml.woodstox.woodstox-core;version='[6.4.0,6.4.1)',\ - org.apache.aries.spifly.dynamic.bundle;version='[1.3.4,1.3.5)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.http.servlet-api;version='[1.2.0,1.2.1)',\ org.apache.felix.scr;version='[2.2.4,2.2.5)',\ @@ -83,10 +82,6 @@ Fragment-Host: org.openhab.core.io.rest.core org.eclipse.jetty.util;version='[9.4.50,9.4.51)',\ org.eclipse.jetty.util.ajax;version='[9.4.50,9.4.51)',\ org.eclipse.jetty.xml;version='[9.4.50,9.4.51)',\ - org.objectweb.asm.commons;version='[9.2.0,9.2.1)',\ - org.objectweb.asm.tree;version='[9.2.0,9.2.1)',\ - org.objectweb.asm.tree.analysis;version='[9.2.0,9.2.1)',\ - org.objectweb.asm.util;version='[9.2.0,9.2.1)',\ org.openhab.core.addon;version='[4.0.0,4.0.1)',\ org.ops4j.pax.logging.pax-logging-api;version='[2.2.0,2.2.1)',\ org.ops4j.pax.web.pax-web-api;version='[8.0.15,8.0.16)',\ @@ -106,4 +101,10 @@ Fragment-Host: org.openhab.core.io.rest.core net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\ org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ - org.objenesis;version='[3.3.0,3.3.1)' + org.objenesis;version='[3.3.0,3.3.1)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.6,1.3.7)',\ + org.objectweb.asm.commons;version='[9.4.0,9.4.1)',\ + org.objectweb.asm.tree;version='[9.4.0,9.4.1)',\ + org.objectweb.asm.tree.analysis;version='[9.4.0,9.4.1)',\ + org.objectweb.asm.util;version='[9.4.0,9.4.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java index 5e684f4ed62..b39d4a01192 100644 --- a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java +++ b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java @@ -30,6 +30,7 @@ import java.util.stream.Stream; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; @@ -94,6 +95,7 @@ public class ItemResourceOSGiTest extends JavaOSGiTest { private @Mock @NonNullByDefault({}) ItemProvider itemProviderMock; private @Mock @NonNullByDefault({}) UriBuilder uriBuilderMock; private @Mock @NonNullByDefault({}) UriInfo uriInfoMock; + private @Mock @NonNullByDefault({}) Request request; @BeforeEach public void beforeEach() { @@ -127,7 +129,8 @@ public void beforeEach() { public void shouldReturnUnicodeItems() throws IOException, TransformationException { item4.setLabel(ITEM_LABEL4); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, null, null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, null, null, false, + null, false); assertThat(readItemLabelsFromResponse(response), hasItems(ITEM_LABEL4)); } @@ -147,28 +150,31 @@ public void shouldFilterItemsByTag() throws Exception { item3.addTag("Tag2"); item4.addTag("Tag4"); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "Tag1", null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag1", null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "Tag2", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag2", null, false, null, + false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME2, ITEM_NAME3)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "NotExistingTag", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "NotExistingTag", null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @Test public void shouldFilterItemsByType() throws Exception { - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, CoreItemFactory.SWITCH, null, - null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.SWITCH, + null, null, false, null, false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, CoreItemFactory.DIMMER, null, null, false, - null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.DIMMER, null, + null, false, null, false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME3)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, CoreItemFactory.COLOR, null, null, false, - null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.COLOR, null, null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @@ -176,15 +182,18 @@ public void shouldFilterItemsByType() throws Exception { public void shouldAddAndRemoveTags() throws Exception { managedItemProvider.add(new SwitchItem("Switch")); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasSize(0)); itemResource.addTag("Switch", "MyTag"); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, null, + false); assertThat(readItemNamesFromResponse(response), hasSize(1)); itemResource.removeTag("Switch", "MyTag"); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, null, + false); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @@ -192,8 +201,8 @@ public void shouldAddAndRemoveTags() throws Exception { public void shouldIncludeRequestedFieldsOnly() throws Exception { managedItemProvider.add(new SwitchItem("Switch")); itemResource.addTag("Switch", "MyTag"); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, - "type,name"); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, + false, "type,name", false); JsonElement result = JsonParser .parseString(new String(((InputStream) response.getEntity()).readAllBytes(), StandardCharsets.UTF_8)); diff --git a/itests/org.openhab.core.model.script.tests/itest.bndrun b/itests/org.openhab.core.model.script.tests/itest.bndrun index 6c54b99e210..b9de3468ee1 100644 --- a/itests/org.openhab.core.model.script.tests/itest.bndrun +++ b/itests/org.openhab.core.model.script.tests/itest.bndrun @@ -116,4 +116,9 @@ Fragment-Host: org.openhab.core.model.script junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ junit-platform-launcher;version='[1.9.2,1.9.3)',\ - org.openhab.core.model.thing.runtime;version='[4.0.0,4.0.1)' + org.openhab.core.model.thing.runtime;version='[4.0.0,4.0.1)',\ + net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ + org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\ + org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ + org.objenesis;version='[3.3.0,3.3.1)' diff --git a/itests/org.openhab.core.model.script.tests/src/main/java/org/openhab/core/model/script/engine/ScriptEngineOSGiTest.java b/itests/org.openhab.core.model.script.tests/src/main/java/org/openhab/core/model/script/engine/ScriptEngineOSGiTest.java index 668f4fc0d51..94bd38211ad 100644 --- a/itests/org.openhab.core.model.script.tests/src/main/java/org/openhab/core/model/script/engine/ScriptEngineOSGiTest.java +++ b/itests/org.openhab.core.model.script.tests/src/main/java/org/openhab/core/model/script/engine/ScriptEngineOSGiTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; import java.util.Collection; import java.util.List; @@ -26,8 +27,14 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.openhab.core.common.registry.ProviderChangeListener; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemProvider; import org.openhab.core.items.ItemRegistry; @@ -48,6 +55,8 @@ * @author Henning Treu - Initial contribution */ @NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class ScriptEngineOSGiTest extends JavaOSGiTest { private static final String ITEM_NAME = "Switch1"; @@ -58,9 +67,13 @@ public class ScriptEngineOSGiTest extends JavaOSGiTest { private @NonNullByDefault({}) ItemProvider itemProvider; private @NonNullByDefault({}) ItemRegistry itemRegistry; private @NonNullByDefault({}) ScriptEngine scriptEngine; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; @BeforeEach public void setup() { + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); + when(unitProviderMock.getUnit(Length.class)).thenReturn(SIUnits.METRE); + registerVolatileStorageService(); EventPublisher eventPublisher = event -> { @@ -351,7 +364,7 @@ public void testNoXbaseConflicts() throws ScriptParsingException, ScriptExecutio } private Item createNumberItem(String numberItemName, Class dimension) { - return new NumberItem("Number:" + dimension.getSimpleName(), numberItemName); + return new NumberItem("Number:" + dimension.getSimpleName(), numberItemName, unitProviderMock); } @SuppressWarnings("unchecked") diff --git a/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest.java b/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest.java index a84cda47e23..b57c389917f 100644 --- a/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest.java +++ b/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest.java @@ -271,7 +271,7 @@ public void assertThatBridgeUIDcanBbeSet() { assertThat(actualThings.size(), is(2)); Thing thing = actualThings.stream().filter(t -> !(t instanceof Bridge)).findFirst().get(); - Bridge bridge = (Bridge) actualThings.stream().filter(t -> t instanceof Bridge).findFirst().get(); + Bridge bridge = (Bridge) actualThings.stream().filter(Bridge.class::isInstance).findFirst().get(); assertThat(thing.getBridgeUID().toString(), is("hue:bridge:bridge1")); assertThat(bridge.getThings().contains(thing), is(true)); @@ -302,22 +302,22 @@ public void assertThatChannelDefinitionsCanBeReferenced() { assertThat(actualThings.size(), is(4)); - actualThings.stream().filter(t -> "bulb_default".equals(t.getUID().getId().toString())).findFirst().get(); + actualThings.stream().filter(t -> "bulb_default".equals(t.getUID().getId())).findFirst().get(); - Thing thingDefault = actualThings.stream().filter(t -> "bulb_default".equals(t.getUID().getId().toString())) - .findFirst().get(); + Thing thingDefault = actualThings.stream().filter(t -> "bulb_default".equals(t.getUID().getId())).findFirst() + .get(); assertThat(thingDefault.getChannels().size(), is(2)); - Thing thingCustom = actualThings.stream().filter(t -> "bulb_custom".equals(t.getUID().getId().toString())) - .findFirst().get(); + Thing thingCustom = actualThings.stream().filter(t -> "bulb_custom".equals(t.getUID().getId())).findFirst() + .get(); assertThat(thingCustom.getChannels().size(), is(4)); assertThat(thingCustom.getChannel("manual").getChannelTypeUID(), is(equalTo(new ChannelTypeUID("hue", "color")))); assertThat(thingCustom.getChannel("manual").getLabel(), is("colorLabel")); // default from thing type assertThat(thingCustom.getChannel("manualWithLabel").getLabel(), is("With Label")); // manual overrides default - Thing thingBroken = actualThings.stream().filter(t -> "bulb_broken".equals(t.getUID().getId().toString())) - .findFirst().get(); + Thing thingBroken = actualThings.stream().filter(t -> "bulb_broken".equals(t.getUID().getId())).findFirst() + .get(); assertThat(thingBroken.getChannels().size(), is(4)); assertThat(thingBroken.getChannel("manual").getChannelTypeUID(), is(equalTo(new ChannelTypeUID("hue", "broken")))); @@ -343,8 +343,8 @@ public void assertThatChannelDefinitionsWithDimensionAreParsed() { assertThat(actualThings.size(), is(1)); - Thing thingDefault = actualThings.stream().filter(t -> "sensor_custom".equals(t.getUID().getId().toString())) - .findFirst().get(); + Thing thingDefault = actualThings.stream().filter(t -> "sensor_custom".equals(t.getUID().getId())).findFirst() + .get(); assertThat(thingDefault.getChannels().size(), is(2)); assertThat(thingDefault.getChannel("sensor1").getAcceptedItemType(), is("Number:Temperature")); @@ -363,8 +363,8 @@ public void assertThatConfigParameterListsAreParsed() { assertThat(actualThings.size(), is(1)); - Thing thingDefault = actualThings.stream().filter(t -> "sensor_custom".equals(t.getUID().getId().toString())) - .findFirst().get(); + Thing thingDefault = actualThings.stream().filter(t -> "sensor_custom".equals(t.getUID().getId())).findFirst() + .get(); @SuppressWarnings("unchecked") Collection valueCollection = (Collection) thingDefault.getConfiguration().get("config"); diff --git a/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest4.java b/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest4.java index 2c41f773293..55229369cf2 100644 --- a/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest4.java +++ b/itests/org.openhab.core.model.thing.tests/src/main/java/org/openhab/core/model/thing/test/hue/GenericThingProviderTest4.java @@ -120,8 +120,8 @@ public void setUp() { hueThingHandlerFactory = new TestHueThingHandlerFactoryX(componentContextMock) { @Override protected @Nullable ThingHandler createHandler(final Thing thing) { - if (thing instanceof Bridge) { - return new TestBridgeHandler((Bridge) thing); + if (thing instanceof Bridge bridge) { + return new TestBridgeHandler(bridge); } else { return new BaseThingHandler(thing) { @Override diff --git a/itests/org.openhab.core.model.thing.testsupport/src/main/java/org/openhab/core/model/thing/testsupport/hue/TestHueThingHandlerFactory.java b/itests/org.openhab.core.model.thing.testsupport/src/main/java/org/openhab/core/model/thing/testsupport/hue/TestHueThingHandlerFactory.java index c9f4e324c83..fce64d94361 100644 --- a/itests/org.openhab.core.model.thing.testsupport/src/main/java/org/openhab/core/model/thing/testsupport/hue/TestHueThingHandlerFactory.java +++ b/itests/org.openhab.core.model.thing.testsupport/src/main/java/org/openhab/core/model/thing/testsupport/hue/TestHueThingHandlerFactory.java @@ -110,8 +110,8 @@ private ThingUID getLightUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thing @Override protected @Nullable ThingHandler createHandler(Thing thing) { - if (thing instanceof Bridge) { - return new BaseBridgeHandler((Bridge) thing) { + if (thing instanceof Bridge bridge) { + return new BaseBridgeHandler(bridge) { @Override public void handleCommand(ChannelUID channelUID, Command command) { } diff --git a/itests/org.openhab.core.storage.json.tests/itest.bndrun b/itests/org.openhab.core.storage.json.tests/itest.bndrun index 1f52ea404a7..d029def728f 100644 --- a/itests/org.openhab.core.storage.json.tests/itest.bndrun +++ b/itests/org.openhab.core.storage.json.tests/itest.bndrun @@ -58,4 +58,6 @@ Fragment-Host: org.openhab.core.storage.json junit-jupiter-engine;version='[5.9.2,5.9.3)',\ junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ - junit-platform-launcher;version='[1.9.2,1.9.3)' + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.storage.json.tests/src/main/java/org/openhab/core/storage/json/internal/ThingMigrationOSGiTest.java b/itests/org.openhab.core.storage.json.tests/src/main/java/org/openhab/core/storage/json/internal/ThingMigrationOSGiTest.java index 2cea337292a..722f0e56f4a 100644 --- a/itests/org.openhab.core.storage.json.tests/src/main/java/org/openhab/core/storage/json/internal/ThingMigrationOSGiTest.java +++ b/itests/org.openhab.core.storage.json.tests/src/main/java/org/openhab/core/storage/json/internal/ThingMigrationOSGiTest.java @@ -13,7 +13,7 @@ package org.openhab.core.storage.json.internal; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; import java.io.IOException; import java.nio.file.Files; @@ -38,7 +38,6 @@ public class ThingMigrationOSGiTest extends JavaOSGiTest { private static final Path DB_DIR = Path.of(OpenHAB.getUserDataFolder(), "jsondb"); private static final String DB_NAME = "org.openhab.core.thing.Thing"; - private static final String DB_OLD_NAME = "org.openhab.core.thing.Thing-old"; @Test public void migrationParsable() throws IOException { diff --git a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/internal/items/ItemUpdaterOSGiTest.java b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/internal/items/ItemUpdaterOSGiTest.java index 56169bf46a3..e6e06fb3006 100644 --- a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/internal/items/ItemUpdaterOSGiTest.java +++ b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/internal/items/ItemUpdaterOSGiTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; @@ -43,7 +44,7 @@ public class ItemUpdaterOSGiTest extends JavaOSGiTest { private @NonNullByDefault({}) EventPublisher eventPublisher; private @NonNullByDefault({}) ItemRegistry itemRegistry; - private final ConcurrentLinkedQueue receivedEvents = new ConcurrentLinkedQueue<>(); + private final Queue receivedEvents = new ConcurrentLinkedQueue<>(); @BeforeEach public void setUp() { diff --git a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java index 19501fc7e11..f398598ded4 100644 --- a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java +++ b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/GroupItemOSGiTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; +import static org.openhab.core.library.unit.Units.ONE; import java.util.HashSet; import java.util.LinkedList; @@ -46,7 +47,9 @@ import org.openhab.core.internal.items.ItemStateConverterImpl; import org.openhab.core.items.dto.GroupFunctionDTO; import org.openhab.core.items.events.GroupItemStateChangedEvent; +import org.openhab.core.items.events.GroupStateUpdatedEvent; import org.openhab.core.items.events.ItemCommandEvent; +import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.items.events.ItemUpdatedEvent; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.items.ColorItem; @@ -62,6 +65,7 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.RawType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -82,7 +86,7 @@ public class GroupItemOSGiTest extends JavaOSGiTest { private final List events = new LinkedList<>(); private final GroupFunctionHelper groupFunctionHelper = new GroupFunctionHelper(); - private final EventPublisher publisher = event -> events.add(event); + private final EventPublisher publisher = events::add; private @NonNullByDefault({}) ItemRegistry itemRegistry; private @NonNullByDefault({}) ItemStateConverter itemStateConverter; @@ -133,7 +137,7 @@ public void testItemUpdateWithItemRegistry() { itemRegistry.update(updatedItem); waitForAssert(() -> assertThat(events.size(), is(1))); - List stateChanges = events.stream().filter(it -> it instanceof ItemUpdatedEvent) + List stateChanges = events.stream().filter(ItemUpdatedEvent.class::isInstance) .collect(Collectors.toList()); assertThat(stateChanges.size(), is(1)); @@ -279,7 +283,7 @@ public void testGetAllMembersWithFilter() { subGroup.addMember(member1); rootGroupItem.addMember(subGroup); - Set members = rootGroupItem.getMembers(i -> i instanceof GroupItem); + Set members = rootGroupItem.getMembers(GroupItem.class::isInstance); assertThat(members.size(), is(1)); members = rootGroupItem.getMembers(i -> "mem1".equals(i.getLabel())); @@ -422,6 +426,7 @@ public void assertCyclicGroupItemsCalculateStateWithSubGroupFunction() { public void assertThatGroupItemPostsEventsForChangesCorrectly() { // from ItemEventFactory.GROUPITEM_STATE_CHANGED_EVENT_TOPIC String groupitemStateChangedEventTopic = "openhab/items/{itemName}/{memberName}/statechanged"; + String groupitemStateUpdatedEventTopic = "openhab/items/{itemName}/{memberName}/stateupdated"; events.clear(); GroupItem groupItem = new GroupItem("root", new SwitchItem("mySwitch"), new GroupFunction.Equality()); @@ -432,12 +437,23 @@ public void assertThatGroupItemPostsEventsForChangesCorrectly() { groupItem.setEventPublisher(publisher); State oldGroupState = groupItem.getState(); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired member.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events.size(), is(1))); + waitForAssert(() -> assertThat(events.size(), is(2))); + + List updates = events.stream().filter(GroupStateUpdatedEvent.class::isInstance) + .collect(Collectors.toList()); + assertThat(updates.size(), is(1)); - List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) + GroupStateUpdatedEvent update = (GroupStateUpdatedEvent) updates.get(0); + assertThat(update.getItemName(), is(groupItem.getName())); + assertThat(update.getMemberName(), is(member.getName())); + assertThat(update.getTopic(), is(groupitemStateUpdatedEventTopic.replace("{memberName}", member.getName()) + .replace("{itemName}", groupItem.getName()))); + assertThat(update.getItemState(), is(groupItem.getState())); + + List changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance) .collect(Collectors.toList()); assertThat(changes.size(), is(1)); @@ -451,9 +467,10 @@ public void assertThatGroupItemPostsEventsForChangesCorrectly() { events.clear(); - // State doesn't change -> no events are fired + // State doesn't change -> only update event is posted member.setState(member.getState()); - assertThat(events.size(), is(0)); + waitForAssert(() -> assertThat(events.size(), is(1))); + assertThat(events.get(0), is(instanceOf(ItemStateUpdatedEvent.class))); } @Test @@ -470,12 +487,12 @@ public void assertThatGroupItemChangesRespectGroupFunctionOR() throws Interrupte groupItem.setEventPublisher(publisher); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw1.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); - List groupItemStateChangedEvents = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) + List groupItemStateChangedEvents = events.stream().filter(GroupItemStateChangedEvent.class::isInstance) .collect(Collectors.toList()); assertThat(groupItemStateChangedEvents, hasSize(1)); @@ -490,13 +507,13 @@ public void assertThatGroupItemChangesRespectGroupFunctionOR() throws Interrupte events.clear(); - // State does not change -> no change event is fired + // State does not change -> only update event is fired sw2.setState(OnOffType.ON); // wait to see that the event doesn't fire Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); - waitForAssert(() -> assertThat(events, hasSize(0))); + waitForAssert(() -> assertThat(events, hasSize(1))); } @Test @@ -518,11 +535,11 @@ public void assertThatItemCommandEventsAreEmittedFromCommand() { waitForAssert(() -> assertThat(events, hasSize(2))); - List itemCommandEvents = events.stream().filter(it -> it instanceof ItemCommandEvent) + List itemCommandEvents = events.stream().filter(ItemCommandEvent.class::isInstance) .collect(Collectors.toList()); assertThat(itemCommandEvents, hasSize(2)); - List groupItemStateChangedEvents = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) + List groupItemStateChangedEvents = events.stream().filter(GroupItemStateChangedEvent.class::isInstance) .collect(Collectors.toList()); assertThat(groupItemStateChangedEvents, hasSize(0)); @@ -543,15 +560,19 @@ public void assertThatGroupItemChangesRespectGroupFunctionORWithUNDEF() throws I groupItem.setEventPublisher(publisher); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw1.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); - List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) + List changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance) .collect(Collectors.toList()); assertThat(changes, hasSize(1)); + List updates = events.stream().filter(GroupStateUpdatedEvent.class::isInstance) + .collect(Collectors.toList()); + assertThat(updates, hasSize(1)); + GroupItemStateChangedEvent change = (GroupItemStateChangedEvent) changes.get(0); assertThat(change.getItemName(), is(groupItem.getName())); @@ -563,15 +584,21 @@ public void assertThatGroupItemChangesRespectGroupFunctionORWithUNDEF() throws I events.clear(); - // State does not change -> no change event is fired + // State does not change -> only update event is fired sw2.setState(OnOffType.ON); sw2.setState(UnDefType.UNDEF); - // wait to see that the event doesn't fire + // wait to see that only state updated events are fired Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); - assertThat(events, hasSize(0)); + assertThat(events, hasSize(2)); + + changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance).collect(Collectors.toList()); + assertThat(changes, hasSize(0)); + + updates = events.stream().filter(GroupStateUpdatedEvent.class::isInstance).collect(Collectors.toList()); + assertThat(updates, hasSize(2)); assertThat(groupItem.getState(), is(OnOffType.ON)); } @@ -590,12 +617,12 @@ public void assertThatGroupItemChangesRespectGroupFunctionAND() { groupItem.setEventPublisher(publisher); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw1.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); - List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) + List changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance) .collect(Collectors.toList()); assertThat(changes, hasSize(1)); @@ -610,12 +637,12 @@ public void assertThatGroupItemChangesRespectGroupFunctionAND() { events.clear(); - // State changes -> one change event is fired + // State changes -> one update and one change event is fired sw2.setState(OnOffType.ON); - waitForAssert(() -> assertThat(events, hasSize(1))); + waitForAssert(() -> assertThat(events, hasSize(2))); - changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent).collect(Collectors.toList()); + changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance).collect(Collectors.toList()); assertThat(changes, hasSize(1)); change = (GroupItemStateChangedEvent) changes.get(0); @@ -727,9 +754,9 @@ public void assertThatGroupItemwithDimmeritemAcceptsGetsPercentTypeStateIfMember member1.setState(new PercentType(50)); - waitForAssert(() -> assertThat(events.size(), is(1))); + waitForAssert(() -> assertThat(events.size(), is(2))); - List changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent) + List changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance) .collect(Collectors.toList()); GroupItemStateChangedEvent change = (GroupItemStateChangedEvent) changes.get(0); assertThat(change.getItemName(), is(groupItem.getName())); @@ -746,9 +773,9 @@ public void assertThatGroupItemwithDimmeritemAcceptsGetsPercentTypeStateIfMember member2.setState(new PercentType(10)); - waitForAssert(() -> assertThat(events.size(), is(1))); + waitForAssert(() -> assertThat(events.size(), is(2))); - changes = events.stream().filter(it -> it instanceof GroupItemStateChangedEvent).collect(Collectors.toList()); + changes = events.stream().filter(GroupItemStateChangedEvent.class::isInstance).collect(Collectors.toList()); assertThat(changes.size(), is(1)); change = (GroupItemStateChangedEvent) changes.get(0); @@ -771,7 +798,6 @@ public void assertThatNumberGroupItemWithDimensionCalculatesCorrectState() { gfDTO.name = "sum"; GroupFunction function = groupFunctionHelper.createGroupFunction(gfDTO, baseItem); GroupItem groupItem = new GroupItem("number", baseItem, function); - groupItem.setUnitProvider(unitProviderMock); NumberItem celsius = createNumberItem("C", Temperature.class, new QuantityType<>("23 °C")); groupItem.addMember(celsius); @@ -795,12 +821,14 @@ public void assertThatNumberGroupItemWithDimensionCalculatesCorrectState() { @Test public void assertThatNumberGroupItemWithDifferentDimensionsCalculatesCorrectState() { + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); + when(unitProviderMock.getUnit(Pressure.class)).thenReturn(SIUnits.PASCAL); + when(unitProviderMock.getUnit(Dimensionless.class)).thenReturn(ONE); NumberItem baseItem = createNumberItem("baseItem", Temperature.class, UnDefType.NULL); GroupFunctionDTO gfDTO = new GroupFunctionDTO(); gfDTO.name = "sum"; GroupFunction function = groupFunctionHelper.createGroupFunction(gfDTO, baseItem); GroupItem groupItem = new GroupItem("number", baseItem, function); - groupItem.setUnitProvider(unitProviderMock); groupItem.setItemStateConverter(itemStateConverter); NumberItem celsius = createNumberItem("C", Temperature.class, new QuantityType<>("23 °C")); @@ -819,8 +847,8 @@ public void assertThatNumberGroupItemWithDifferentDimensionsCalculatesCorrectSta } private NumberItem createNumberItem(String name, Class> dimension, State state) { - NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name); - item.setUnitProvider(unitProviderMock); + NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, + unitProviderMock); item.setState(state); return item; diff --git a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/ItemRegistryImplTest.java b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/ItemRegistryImplTest.java index 03dfe1c8cc7..f435ce58cd1 100644 --- a/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/ItemRegistryImplTest.java +++ b/itests/org.openhab.core.tests/src/main/java/org/openhab/core/items/ItemRegistryImplTest.java @@ -77,6 +77,7 @@ public class ItemRegistryImplTest extends JavaTest { private @NonNullByDefault({}) ManagedItemProvider itemProvider; private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock; + private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; @BeforeEach public void beforeEach() { @@ -92,7 +93,7 @@ public void beforeEach() { // setup ManageItemProvider with necessary dependencies: itemProvider = new ManagedItemProvider(new VolatileStorageService(), - new ItemBuilderFactoryImpl(new CoreItemFactory())); + new ItemBuilderFactoryImpl(new CoreItemFactory(unitProviderMock))); itemProvider.add(new SwitchItem(ITEM_NAME)); itemProvider.add(cameraItem1); @@ -107,7 +108,6 @@ public void beforeEach() { setManagedProvider(itemProvider); setEventPublisher(ItemRegistryImplTest.this.eventPublisherMock); setStateDescriptionService(mock(StateDescriptionService.class)); - setUnitProvider(mock(UnitProvider.class)); setItemStateConverter(mock(ItemStateConverter.class)); } }; @@ -132,7 +132,7 @@ public void assertGetItemsByTagReturnsItemFromRegisteredItemProvider() { List items = new ArrayList<>(itemRegistry.getItemsByTag(CAMERA_TAG)); assertThat(items, hasSize(4)); - List itemNames = items.stream().map(i -> i.getName()).collect(toList()); + List itemNames = items.stream().map(Item::getName).collect(toList()); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME1)); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME2)); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME3)); @@ -144,7 +144,7 @@ public void assertGetItemsByTagInUppercaseReturnsItemFromRegisteredItemProvider( List items = new ArrayList<>(itemRegistry.getItemsByTag(CAMERA_TAG_UPPERCASE)); assertThat(items, hasSize(4)); - List itemNames = items.stream().map(i -> i.getName()).collect(toList()); + List itemNames = items.stream().map(Item::getName).collect(toList()); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME1)); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME2)); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME3)); @@ -156,7 +156,7 @@ public void assertGetItemsByTagAndTypeReturnsItemFromRegistereItemProvider() { List items = new ArrayList<>(itemRegistry.getItemsByTagAndType("Switch", CAMERA_TAG)); assertThat(items, hasSize(2)); - List itemNames = items.stream().map(i -> i.getName()).collect(toList()); + List itemNames = items.stream().map(Item::getName).collect(toList()); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME1)); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME2)); } @@ -178,7 +178,7 @@ public void assertGetItemsByTagCanFilterByClassAndTag() { List items = new ArrayList<>(itemRegistry.getItemsByTag(SwitchItem.class, CAMERA_TAG)); assertThat(items, hasSize(2)); - List itemNames = items.stream().map(i -> i.getName()).collect(toList()); + List itemNames = items.stream().map(GenericItem::getName).collect(toList()); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME1)); assertThat(itemNames, hasItem(CAMERA_ITEM_NAME2)); } @@ -369,13 +369,11 @@ public void assertOldItemIsBeingDisposedOnUpdate() { assertNotNull(item.eventPublisher); assertNotNull(item.itemStateConverter); - assertNotNull(item.unitProvider); itemProvider.update(new SwitchItem("Item1")); assertNull(item.eventPublisher); assertNull(item.itemStateConverter); - assertNull(item.unitProvider); assertEquals(0, item.listeners.size()); } @@ -391,18 +389,6 @@ public void assertStateDescriptionServiceGetsInjected() { verify(baseItem).setStateDescriptionService(any(StateDescriptionService.class)); } - @Test - public void assertUnitProviderGetsInjected() { - GenericItem item = spy(new SwitchItem("Item1")); - NumberItem baseItem = spy(new NumberItem("baseItem")); - GenericItem group = new GroupItem("Group", baseItem); - itemProvider.add(item); - itemProvider.add(group); - - verify(item).setUnitProvider(any(UnitProvider.class)); - verify(baseItem).setUnitProvider(any(UnitProvider.class)); - } - @Test public void assertCommandDescriptionServiceGetsInjected() { GenericItem item = spy(new SwitchItem("Item1")); diff --git a/itests/org.openhab.core.thing.tests/itest.bndrun b/itests/org.openhab.core.thing.tests/itest.bndrun index 93bb6e00233..dc229ee0af3 100644 --- a/itests/org.openhab.core.thing.tests/itest.bndrun +++ b/itests/org.openhab.core.thing.tests/itest.bndrun @@ -66,4 +66,5 @@ Fragment-Host: org.openhab.core.thing net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\ org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\ org.mockito.mockito-core;version='[4.11.0,4.11.1)',\ - org.objenesis;version='[3.3.0,3.3.1)' + org.objenesis;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/ThingChannelsTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/ThingChannelsTest.java index 8f09da9de0d..675d063ff18 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/ThingChannelsTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/ThingChannelsTest.java @@ -32,22 +32,22 @@ @NonNullByDefault public class ThingChannelsTest extends JavaOSGiTest { + private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("bindingId", "thingTypeId"); + private static final ThingUID THING_UID = new ThingUID(THING_TYPE_UID, "thingLabel"); + private static final List CHANNEL_IDS = List.of("polarBear", "alligator", "hippopotamus", "aardvark", "whiteRabbit", "redHerring", "orangutan", "kangaroo", "rubberDuck", "timorousBeastie"); @Test public void testThingChannelOrder() { - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId", "thingTypeId"); - ThingUID thingUID = new ThingUID(thingTypeUID, "thingLabel"); - // create and fill the list of origin channels List originChannels = new ArrayList<>(); CHANNEL_IDS.forEach(channelId -> originChannels - .add(ChannelBuilder.create(new ChannelUID(thingUID, channelId), null).build())); + .add(ChannelBuilder.create(new ChannelUID(THING_UID, channelId), null).build())); assertEquals(CHANNEL_IDS.size(), originChannels.size()); // build a thing with the origin channels - Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withChannels(originChannels).build(); + Thing thing = ThingBuilder.create(THING_TYPE_UID, THING_UID).withChannels(originChannels).build(); List resultChannels; @@ -65,4 +65,10 @@ public void testThingChannelOrder() { assertTrue(CHANNEL_IDS.get(i).equals(resultChannels.get(i).getUID().getId())); } } + + @Test + public void testAutoUpdatePolicyNotSetOnNewChannels() { + Channel channel = ChannelBuilder.create(new ChannelUID(THING_UID, CHANNEL_IDS.get(0)), null).build(); + assertNull(channel.getAutoUpdatePolicy()); + } } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProviderOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProviderOSGiTest.java new file mode 100644 index 00000000000..5093159484e --- /dev/null +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/AbstractStorageBasedTypeProviderOSGiTest.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.binding; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.storage.StorageService; +import org.openhab.core.test.java.JavaOSGiTest; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.BridgeType; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeBuilder; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; + +/** + * The {@link AbstractStorageBasedTypeProviderOSGiTest} contains tests for storing and providing {@link ChannelType}s + * and {@link ThingType}s + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AbstractStorageBasedTypeProviderOSGiTest extends JavaOSGiTest { + + private static final String BINDING_ID = "testBinding"; + private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "testThingType"); + private static final ChannelTypeUID CHANNEL_TYPE_UID = new ChannelTypeUID(BINDING_ID, "testChannelType"); + private static final ChannelGroupTypeUID CHANNEL_GROUP_TYPE_UID = new ChannelGroupTypeUID(BINDING_ID, + "testChannelGroupType"); + + private @NonNullByDefault({}) AbstractStorageBasedTypeProvider typeProvider; + + @BeforeEach + public void setup() { + registerVolatileStorageService(); + + StorageService storageService = getService(StorageService.class); + assertThat(storageService, is(notNullValue())); + + typeProvider = new AbstractStorageBasedTypeProvider(storageService) { + }; + + assertThat(typeProvider.getThingTypes(null), hasSize(0)); + assertThat(typeProvider.getChannelTypes(null), hasSize(0)); + } + + @Test + public void testSingleThingTypeIsRestoredAsThingType() { + ThingType thingType = ThingTypeBuilder.instance(THING_TYPE_UID, "label").build(); + + typeProvider.putThingType(thingType); + assertThat(typeProvider.getThingTypes(null), hasSize(1)); + + ThingType registryThingType = typeProvider.getThingType(THING_TYPE_UID, null); + assertThat(registryThingType, is(notNullValue())); + + assertThat(registryThingType, not(instanceOf(BridgeType.class))); + } + + @Test + public void testSingleBridgeTypeIsRestoredAsBridgeType() { + BridgeType bridgeType = ThingTypeBuilder.instance(THING_TYPE_UID, "label").buildBridge(); + + typeProvider.putThingType(bridgeType); + assertThat(typeProvider.getThingTypes(null), hasSize(1)); + + ThingType registryThingType = typeProvider.getThingType(THING_TYPE_UID, null); + assertThat(registryThingType, is(notNullValue())); + + assertThat(registryThingType, is(instanceOf(BridgeType.class))); + } + + @Test + public void testTwoThingTypesCanBeRemovedIndependently() { + ThingType thingType1 = ThingTypeBuilder.instance(BINDING_ID, "type1", "label1").build(); + ThingType thingType2 = ThingTypeBuilder.instance(BINDING_ID, "type2", "label2").build(); + + typeProvider.putThingType(thingType1); + typeProvider.putThingType(thingType2); + assertThat(typeProvider.getThingType(thingType1.getUID(), null), is(notNullValue())); + assertThat(typeProvider.getThingType(thingType2.getUID(), null), is(notNullValue())); + + typeProvider.removeThingType(thingType1.getUID()); + assertThat(typeProvider.getThingType(thingType1.getUID(), null), is(nullValue())); + assertThat(typeProvider.getThingType(thingType2.getUID(), null), is(notNullValue())); + } + + @Test + public void testThingTypeCanBeUpdated() { + String originalLabel = "original Label"; + String updatedLabel = "updated Label"; + ThingType thingTypeOriginal = ThingTypeBuilder.instance(THING_TYPE_UID, originalLabel).build(); + ThingType thingTypeUpdated = ThingTypeBuilder.instance(THING_TYPE_UID, updatedLabel).build(); + + typeProvider.putThingType(thingTypeOriginal); + ThingType registryThingType = typeProvider.getThingType(THING_TYPE_UID, null); + assertThat(registryThingType, is(notNullValue())); + assertThat(registryThingType.getLabel(), is(originalLabel)); + + typeProvider.putThingType(thingTypeUpdated); + registryThingType = typeProvider.getThingType(THING_TYPE_UID, null); + assertThat(registryThingType, is(notNullValue())); + assertThat(registryThingType.getLabel(), is(updatedLabel)); + } + + @Test + public void testSingleChannelTypeIsAddedAndRestored() { + ChannelType channelType = ChannelTypeBuilder.state(CHANNEL_TYPE_UID, "label", "Switch").build(); + + typeProvider.putChannelType(channelType); + assertThat(typeProvider.getChannelTypes(null), hasSize(1)); + + ChannelType registryChannelType = typeProvider.getChannelType(CHANNEL_TYPE_UID, null); + assertThat(registryChannelType, is(notNullValue())); + assertThat(registryChannelType.getKind(), is(ChannelKind.STATE)); + } + + @Test + public void testTwoChannelTypesAreAddedAndCanBeRemovedIndependently() { + ChannelType channelType1 = ChannelTypeBuilder.trigger(new ChannelTypeUID(BINDING_ID, "type1"), "label1") + .build(); + ChannelType channelType2 = ChannelTypeBuilder.trigger(new ChannelTypeUID(BINDING_ID, "type2"), "label2") + .build(); + + typeProvider.putChannelType(channelType1); + typeProvider.putChannelType(channelType2); + + assertThat(typeProvider.getChannelType(channelType1.getUID(), null), is(notNullValue())); + assertThat(typeProvider.getChannelType(channelType2.getUID(), null), is(notNullValue())); + + typeProvider.removeChannelType(channelType1.getUID()); + + assertThat(typeProvider.getChannelType(channelType1.getUID(), null), is(nullValue())); + assertThat(typeProvider.getChannelType(channelType2.getUID(), null), is(notNullValue())); + } + + @Test + public void testChannelTypeCanBeUpdated() { + String originalLabel = "original Label"; + String updatedLabel = "updated Label"; + ChannelType channelTypeOriginal = ChannelTypeBuilder.trigger(CHANNEL_TYPE_UID, originalLabel).build(); + ChannelType channelTypeUpdated = ChannelTypeBuilder.trigger(CHANNEL_TYPE_UID, updatedLabel).build(); + + typeProvider.putChannelType(channelTypeOriginal); + ChannelType registryChannelType = typeProvider.getChannelType(CHANNEL_TYPE_UID, null); + assertThat(registryChannelType, is(notNullValue())); + assertThat(registryChannelType.getLabel(), is(originalLabel)); + + typeProvider.putChannelType(channelTypeUpdated); + registryChannelType = typeProvider.getChannelType(CHANNEL_TYPE_UID, null); + assertThat(registryChannelType, is(notNullValue())); + assertThat(registryChannelType.getLabel(), is(updatedLabel)); + } + + @Test + public void testSingleChannelGroupTypeIsAddedAndRestored() { + ChannelGroupType channelGroupType = ChannelGroupTypeBuilder.instance(CHANNEL_GROUP_TYPE_UID, "label").build(); + + typeProvider.putChannelGroupType(channelGroupType); + assertThat(typeProvider.getChannelGroupTypes(null), hasSize(1)); + + ChannelGroupType registryChannelGroupType = typeProvider.getChannelGroupType(CHANNEL_GROUP_TYPE_UID, null); + assertThat(registryChannelGroupType, is(notNullValue())); + } + + @Test + public void testTwoChannelGroupTypesAreAddedAndCanBeRemovedIndependently() { + ChannelGroupType channelGroupType1 = ChannelGroupTypeBuilder + .instance(new ChannelGroupTypeUID(BINDING_ID, "type1"), "label1").build(); + ChannelGroupType channelGroupType2 = ChannelGroupTypeBuilder + .instance(new ChannelGroupTypeUID(BINDING_ID, "type2"), "label2").build(); + + typeProvider.putChannelGroupType(channelGroupType1); + typeProvider.putChannelGroupType(channelGroupType2); + + assertThat(typeProvider.getChannelGroupType(channelGroupType1.getUID(), null), is(notNullValue())); + assertThat(typeProvider.getChannelGroupType(channelGroupType2.getUID(), null), is(notNullValue())); + + typeProvider.removeChannelGroupType(channelGroupType1.getUID()); + + assertThat(typeProvider.getChannelGroupType(channelGroupType1.getUID(), null), is(nullValue())); + assertThat(typeProvider.getChannelGroupType(channelGroupType2.getUID(), null), is(notNullValue())); + } + + @Test + public void testChannelGroupTypeCanBeUpdated() { + String originalLabel = "original Label"; + String updatedLabel = "updated Label"; + ChannelGroupType channelGroupTypeOriginal = ChannelGroupTypeBuilder + .instance(CHANNEL_GROUP_TYPE_UID, originalLabel).build(); + ChannelGroupType channelGroupTypeUpdated = ChannelGroupTypeBuilder + .instance(CHANNEL_GROUP_TYPE_UID, updatedLabel).build(); + + typeProvider.putChannelGroupType(channelGroupTypeOriginal); + ChannelGroupType registryChannelGroupType = typeProvider.getChannelGroupType(CHANNEL_GROUP_TYPE_UID, null); + assertThat(registryChannelGroupType, is(notNullValue())); + assertThat(registryChannelGroupType.getLabel(), is(originalLabel)); + + typeProvider.putChannelGroupType(channelGroupTypeUpdated); + registryChannelGroupType = typeProvider.getChannelGroupType(CHANNEL_GROUP_TYPE_UID, null); + assertThat(registryChannelGroupType, is(notNullValue())); + assertThat(registryChannelGroupType.getLabel(), is(updatedLabel)); + } +} diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java index 6a81832a98b..c9fb1b9142f 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java @@ -146,7 +146,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { @Override protected @Nullable ThingHandler createHandler(Thing thing) { - ThingHandler handler = (thing instanceof Bridge) ? new SimpleBridgeHandler((Bridge) thing) + ThingHandler handler = (thing instanceof Bridge b) ? new SimpleBridgeHandler(b) : new SimpleThingHandler(thing); handlers.add(handler); return handler; diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/firmware/FirmwareTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/firmware/FirmwareTest.java index 42997f7b59a..50aec670b11 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/firmware/FirmwareTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/firmware/FirmwareTest.java @@ -287,7 +287,7 @@ public void assertThatFirmwareWithInvalidMD5HashValueThrowsExceptionForGetBytes( Firmware firmware = FirmwareBuilder.create(THING_TYPE_UID, "1") .withInputStream(bundleContext.getBundle().getResource(FILE_NAME).openStream()) .withMd5Hash("78805a221a988e79ef3f42d7c5bfd419").build(); - assertThrows(IllegalStateException.class, () -> firmware.getBytes()); + assertThrows(IllegalStateException.class, firmware::getBytes); } @Test diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/AutoUpdateManagerTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/AutoUpdateManagerTest.java new file mode 100644 index 00000000000..7286855c902 --- /dev/null +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/AutoUpdateManagerTest.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.items.Item; +import org.openhab.core.items.MetadataRegistry; +import org.openhab.core.items.events.ItemEventFactory; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.test.java.JavaTest; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.AutoUpdatePolicy; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * Tests for {@link AutoUpdateManager}. + * + * @author Kai Kreuzer - Initial contribution + */ +@NonNullByDefault +public class AutoUpdateManagerTest extends JavaTest { + + private static final ChannelTypeUID CHANNEL_TYPE_UID = new ChannelTypeUID("binding:channelType"); + private static final ChannelUID CHANNEL_UID = new ChannelUID("binding:thingtype1:thing1:channel1"); + private static final String ITEM_NAME = "TestItem"; + + private @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistry; + private @NonNullByDefault({}) ThingRegistry thingRegistry; + private @NonNullByDefault({}) MetadataRegistry metadataRegistry; + private @NonNullByDefault({}) EventPublisher eventPublisher; + private @NonNullByDefault({}) ItemChannelLinkRegistry itemChannelLinkRegistry; + private @NonNullByDefault({}) AutoUpdateManager autoUpdateManager; + private @NonNullByDefault({}) Item item; + private @NonNullByDefault({}) Thing thing; + + @BeforeEach + public void setup() { + channelTypeRegistry = mock(ChannelTypeRegistry.class); + eventPublisher = mock(EventPublisher.class); + itemChannelLinkRegistry = mock(ItemChannelLinkRegistry.class); + assertNotNull(itemChannelLinkRegistry); + + thingRegistry = mock(ThingRegistry.class); + thing = mock(Thing.class); + metadataRegistry = mock(MetadataRegistry.class); + + Channel channel = ChannelBuilder.create(CHANNEL_UID).withType(CHANNEL_TYPE_UID).build(); + + autoUpdateManager = new AutoUpdateManager(Collections.emptyMap(), channelTypeRegistry, eventPublisher, + itemChannelLinkRegistry, metadataRegistry, thingRegistry); + + item = mock(Item.class); + when(item.getName()).thenReturn(ITEM_NAME); + when(item.getAcceptedDataTypes()).thenReturn(List.of(OnOffType.class)); + when(itemChannelLinkRegistry.getLinks(any(String.class))) + .thenReturn(Set.of(new ItemChannelLink(ITEM_NAME, CHANNEL_UID))); + when(thingRegistry.get(any(ThingUID.class))).thenReturn(thing); + when(thing.getStatus()).thenReturn(ThingStatus.ONLINE); + when(thing.getHandler()).thenReturn(mock(ThingHandler.class)); + when(thing.getChannel(any(String.class))).thenReturn(channel); + } + + @Test + public void testAutoUpdateVetoFromChannelType() { + when(channelTypeRegistry.getChannelType(any(ChannelTypeUID.class))) + .thenReturn(ChannelTypeBuilder.state(CHANNEL_TYPE_UID, "label", CoreItemFactory.SWITCH).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build()); + + autoUpdateManager.receiveCommand(ItemEventFactory.createCommandEvent(ITEM_NAME, OnOffType.ON), item); + + // No event should have been sent + verify(eventPublisher, never()).post(any(Event.class)); + } +} diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java index 6ab98da08ab..f6e74158452 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java @@ -15,11 +15,9 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.util.ArrayList; import java.util.HashMap; @@ -116,9 +114,9 @@ public class ChannelLinkNotifierOSGiTest extends JavaOSGiTest { private @NonNullByDefault({}) ManagedThingProvider managedThingProvider; private @NonNullByDefault({}) ThingRegistry thingRegistry; - private @Mock @NonNullByDefault({}) Bundle bundleMock; - private @Mock @NonNullByDefault({}) BundleResolver bundleResolverMock; - private @Mock @NonNullByDefault({}) ThingHandlerFactory thingHandlerFactoryMock; + public @Mock @NonNullByDefault({}) Bundle bundleMock; + public @Mock @NonNullByDefault({}) BundleResolver bundleResolverMock; + public @Mock @NonNullByDefault({}) ThingHandlerFactory thingHandlerFactoryMock; /** * A thing handler which updates the {@link ThingStatus} when initialized to the provided {@code thingStatus} value. @@ -220,14 +218,14 @@ public void afterEach() throws Exception { @Override public void receive(Event event) { logger.debug("Received event: {}", event); - if (event instanceof AbstractItemChannelLinkRegistryEvent) { - ItemChannelLinkDTO link = ((AbstractItemChannelLinkRegistryEvent) event).getLink(); + if (event instanceof AbstractItemChannelLinkRegistryEvent registryEvent) { + ItemChannelLinkDTO link = registryEvent.getLink(); removedItemChannelLinkUIDs .add(AbstractLink.getIDFor(link.itemName, new ChannelUID(link.channelUID))); - } else if (event instanceof AbstractItemRegistryEvent) { - removedItemNames.add(((AbstractItemRegistryEvent) event).getItem().name); - } else if (event instanceof AbstractThingRegistryEvent) { - removedThingUIDs.add(((AbstractThingRegistryEvent) event).getThing().UID); + } else if (event instanceof AbstractItemRegistryEvent registryEvent) { + removedItemNames.add(registryEvent.getItem().name); + } else if (event instanceof AbstractThingRegistryEvent registryEvent) { + removedThingUIDs.add(registryEvent.getThing().UID); } } @@ -297,7 +295,7 @@ private Channel createChannel(ThingUID thingUID, int index) { } private void forEachThingChannelUID(Thing thing, Consumer consumer) { - thing.getChannels().stream().map(Channel::getUID).forEach(channelUID -> consumer.accept(channelUID)); + thing.getChannels().stream().map(Channel::getUID).forEach(consumer::accept); } private void addItemsAndLinks(Thing thing, String itemSuffix) { diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java index 3d4caccf051..3bac9fc85f1 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/CommunicationManagerOSGiTest.java @@ -40,6 +40,8 @@ import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemStateConverter; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; import org.openhab.core.items.events.ItemCommandEvent; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.library.CoreItemFactory; @@ -50,8 +52,8 @@ import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; -import org.openhab.core.service.StateDescriptionService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -81,7 +83,6 @@ import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; import org.openhab.core.types.State; -import org.openhab.core.types.StateDescriptionFragmentBuilder; /** * @@ -103,21 +104,27 @@ protected void addProvider(Provider provider) { } } + private static final UnitProvider unitProviderMock = mock(UnitProvider.class); + private static final String EVENT = "event"; private static final String ITEM_NAME_1 = "testItem1"; private static final String ITEM_NAME_2 = "testItem2"; private static final String ITEM_NAME_3 = "testItem3"; private static final String ITEM_NAME_4 = "testItem4"; + private static final String ITEM_NAME_5 = "testItem5"; private static final SwitchItem ITEM_1 = new SwitchItem(ITEM_NAME_1); private static final SwitchItem ITEM_2 = new SwitchItem(ITEM_NAME_2); private static final NumberItem ITEM_3 = new NumberItem(ITEM_NAME_3); private static final NumberItem ITEM_4 = new NumberItem(ITEM_NAME_4); + private static NumberItem ITEM_5 = new NumberItem(ITEM_NAME_5); // will be replaced later by dimension item + private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("test", "type"); private static final ThingUID THING_UID = new ThingUID("test", "thing"); private static final ChannelUID STATE_CHANNEL_UID_1 = new ChannelUID(THING_UID, "state-channel1"); private static final ChannelUID STATE_CHANNEL_UID_2 = new ChannelUID(THING_UID, "state-channel2"); private static final ChannelUID STATE_CHANNEL_UID_3 = new ChannelUID(THING_UID, "state-channel3"); private static final ChannelUID STATE_CHANNEL_UID_4 = new ChannelUID(THING_UID, "state-channel4"); + private static final ChannelUID STATE_CHANNEL_UID_5 = new ChannelUID(THING_UID, "state-channel5"); private static final ChannelTypeUID CHANNEL_TYPE_UID_4 = new ChannelTypeUID("test", "channeltype"); private static final ChannelUID TRIGGER_CHANNEL_UID_1 = new ChannelUID(THING_UID, "trigger-channel1"); private static final ChannelUID TRIGGER_CHANNEL_UID_2 = new ChannelUID(THING_UID, "trigger-channel2"); @@ -126,6 +133,7 @@ protected void addProvider(Provider provider) { private static final ItemChannelLink LINK_2_S2 = new ItemChannelLink(ITEM_NAME_2, STATE_CHANNEL_UID_2); private static final ItemChannelLink LINK_3_S3 = new ItemChannelLink(ITEM_NAME_3, STATE_CHANNEL_UID_3); private static final ItemChannelLink LINK_4_S4 = new ItemChannelLink(ITEM_NAME_4, STATE_CHANNEL_UID_4); + private static final ItemChannelLink LINK_5_S5 = new ItemChannelLink(ITEM_NAME_5, STATE_CHANNEL_UID_5); private static final ItemChannelLink LINK_1_T1 = new ItemChannelLink(ITEM_NAME_1, TRIGGER_CHANNEL_UID_1); private static final ItemChannelLink LINK_1_T2 = new ItemChannelLink(ITEM_NAME_1, TRIGGER_CHANNEL_UID_2); private static final ItemChannelLink LINK_2_T2 = new ItemChannelLink(ITEM_NAME_2, TRIGGER_CHANNEL_UID_2); @@ -135,6 +143,7 @@ protected void addProvider(Provider provider) { ChannelBuilder.create(STATE_CHANNEL_UID_3, "Number:Temperature").withKind(ChannelKind.STATE).build(), ChannelBuilder.create(STATE_CHANNEL_UID_4, CoreItemFactory.NUMBER).withKind(ChannelKind.STATE) .withType(CHANNEL_TYPE_UID_4).build(), + ChannelBuilder.create(STATE_CHANNEL_UID_5, "Number:Temperature").withKind(ChannelKind.STATE).build(), ChannelBuilder.create(TRIGGER_CHANNEL_UID_1).withKind(ChannelKind.TRIGGER).build(), ChannelBuilder.create(TRIGGER_CHANNEL_UID_2).withKind(ChannelKind.TRIGGER).build()).build(); @@ -158,18 +167,19 @@ protected void addProvider(Provider provider) { @BeforeEach public void beforeEach() { + when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); + ITEM_5 = new NumberItem("Number:Temperature", ITEM_NAME_5, unitProviderMock); + safeCaller = getService(SafeCaller.class); assertNotNull(safeCaller); SystemProfileFactory profileFactory = getService(ProfileTypeProvider.class, SystemProfileFactory.class); assertNotNull(profileFactory); - if (profileFactory == null) { - throw new IllegalStateException("thing is null"); - } manager = new CommunicationManager(autoUpdateManagerMock, channelTypeRegistryMock, profileFactory, iclRegistry, - itemRegistryMock, itemStateConverterMock, eventPublisherMock, safeCaller, thingRegistryMock); + itemRegistryMock, itemStateConverterMock, eventPublisherMock, safeCaller, thingRegistryMock, + unitProviderMock); doAnswer(invocation -> { switch (((Channel) invocation.getArguments()[0]).getKind()) { @@ -208,7 +218,8 @@ public void removeProviderChangeListener(ProviderChangeListener @Override public Collection getAll() { - return List.of(LINK_1_S1, LINK_1_S2, LINK_2_S2, LINK_1_T1, LINK_1_T2, LINK_2_T2, LINK_3_S3, LINK_4_S4); + return List.of(LINK_1_S1, LINK_1_S2, LINK_2_S2, LINK_1_T1, LINK_1_T2, LINK_2_T2, LINK_3_S3, LINK_4_S4, + LINK_5_S5); } }); @@ -216,6 +227,7 @@ public Collection getAll() { when(itemRegistryMock.get(eq(ITEM_NAME_2))).thenReturn(ITEM_2); when(itemRegistryMock.get(eq(ITEM_NAME_3))).thenReturn(ITEM_3); when(itemRegistryMock.get(eq(ITEM_NAME_4))).thenReturn(ITEM_4); + when(itemRegistryMock.get(eq(ITEM_NAME_5))).thenReturn(ITEM_5); ChannelType channelType4 = mock(ChannelType.class); when(channelType4.getItemType()).thenReturn("Number:Temperature"); @@ -225,12 +237,8 @@ public Collection getAll() { THING.setHandler(thingHandlerMock); when(thingRegistryMock.get(eq(THING_UID))).thenReturn(THING); - manager.addItemFactory(new CoreItemFactory()); - UnitProvider unitProvider = mock(UnitProvider.class); - when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); - ITEM_3.setUnitProvider(unitProvider); - ITEM_4.setUnitProvider(unitProvider); + manager.addItemFactory(new CoreItemFactory(unitProviderMock)); } @Test @@ -287,7 +295,7 @@ public void testItemCommandEventSingleLink() { @Test public void testItemCommandEventDecimal2Quantity() { // Take unit from accepted item type (see channel built from STATE_CHANNEL_UID_3) - manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_3, DecimalType.valueOf("20"))); + manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_5, DecimalType.valueOf("20"))); waitForAssert(() -> { verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °C"))); }); @@ -297,33 +305,16 @@ public void testItemCommandEventDecimal2Quantity() { @Test public void testItemCommandEventDecimal2Quantity2() { - // Take unit from state description - StateDescriptionService stateDescriptionService = mock(StateDescriptionService.class); - when(stateDescriptionService.getStateDescription(ITEM_NAME_3, null)).thenReturn( - StateDescriptionFragmentBuilder.create().withPattern("%.1f °F").build().toStateDescription()); - ITEM_3.setStateDescriptionService(stateDescriptionService); + MetadataKey key = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, ITEM_NAME_5); + Metadata metadata = new Metadata(key, ImperialUnits.FAHRENHEIT.toString(), null); + ITEM_5.addedMetadata(metadata); - manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_3, DecimalType.valueOf("20"))); + manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_5, DecimalType.valueOf("20"))); waitForAssert(() -> { verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °F"))); }); verifyNoMoreInteractions(stateProfileMock); verifyNoMoreInteractions(triggerProfileMock); - - ITEM_3.setStateDescriptionService(null); - } - - @Test - public void testItemCommandEventDecimal2QuantityChannelType() { - // The command is sent to an item w/o dimension defined and the channel is legacy (created from a ThingType - // definition before UoM was introduced to the binding). The dimension information might now be defined on the - // current ThingType. - manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_4, DecimalType.valueOf("20"))); - waitForAssert(() -> { - verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °C"))); - }); - verifyNoMoreInteractions(stateProfileMock); - verifyNoMoreInteractions(triggerProfileMock); } @Test @@ -351,7 +342,7 @@ public void testItemCommandEventNotToSource() { @Test public void testItemStateEventSingleLink() { - manager.receive(ItemEventFactory.createStateEvent(ITEM_NAME_2, OnOffType.ON)); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, OnOffType.ON)); waitForAssert(() -> { verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -362,7 +353,7 @@ public void testItemStateEventSingleLink() { @Test public void testItemStateEventMultiLink() { - manager.receive(ItemEventFactory.createStateEvent(ITEM_NAME_1, OnOffType.ON)); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON)); waitForAssert(() -> { verify(stateProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -374,7 +365,7 @@ public void testItemStateEventMultiLink() { @Test public void testItemStateEventNotToSource() { manager.receive( - ItemEventFactory.createStateEvent(ITEM_NAME_1, OnOffType.ON, STATE_CHANNEL_UID_2.getAsString())); + ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_1, OnOffType.ON, STATE_CHANNEL_UID_2.getAsString())); waitForAssert(() -> { verify(stateProfileMock).onStateUpdateFromItem(eq(OnOffType.ON)); verify(triggerProfileMock, times(2)).onStateUpdateFromItem(eq(OnOffType.ON)); @@ -596,7 +587,7 @@ public void testItemStateEventTypeDowncast() { thing.setHandler(thingHandlerMock); when(thingRegistryMock.get(eq(THING_UID))).thenReturn(thing); - manager.receive(ItemEventFactory.createStateEvent(ITEM_NAME_2, HSBType.fromRGB(128, 128, 128))); + manager.receive(ItemEventFactory.createStateUpdatedEvent(ITEM_NAME_2, HSBType.fromRGB(128, 128, 128))); waitForAssert(() -> { ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class); verify(stateProfileMock).onStateUpdateFromItem(stateCaptor.capture()); diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceTest.java index 9a1e46d85c8..b76c45ed81d 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/firmware/FirmwareUpdateServiceTest.java @@ -164,7 +164,7 @@ private void initialFirmwareRegistryMocking() { || THING_TYPE_UID3.equals(thing.getThingTypeUID())) { return Collections.emptySet(); } else { - Supplier> supplier = () -> new TreeSet<>(); + Supplier> supplier = TreeSet::new; return Stream.of(FW009_EN, FW111_EN, FW112_EN).collect(Collectors.toCollection(supplier)); } }; @@ -318,7 +318,7 @@ public void testUpdateFirmware() { firmwareUpdateService.updateFirmware(THING1_UID, V112, null); waitForAssert(() -> { - assertThat(thing1.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V112.toString())); + assertThat(thing1.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V112)); }); assertThat(firmwareUpdateService.getFirmwareStatusInfo(THING1_UID), is(upToDateInfo)); @@ -461,7 +461,7 @@ public void testUpdateFirmwareDowngrade() { firmwareUpdateService.updateFirmware(THING2_UID, V111, null); waitForAssert(() -> { - assertThat(thing2.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V111.toString())); + assertThat(thing2.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V111)); }); assertThat(firmwareUpdateService.getFirmwareStatusInfo(THING2_UID), is(updateExecutableInfoFw112)); @@ -547,7 +547,7 @@ public void testPrerequisiteVersionCheckIllegalVersion() { || THING_TYPE_UID2.equals(thing.getThingTypeUID())) { return Collections.emptySet(); } else { - Supplier> supplier = () -> new TreeSet<>(); + Supplier> supplier = TreeSet::new; return Stream.of(FW111_FIX_EN, FW113_EN).collect(Collectors.toCollection(supplier)); } }); @@ -583,7 +583,7 @@ public void testPrerequisiteVersionCheck() { || THING_TYPE_UID2.equals(thing.getThingTypeUID())) { return Collections.emptySet(); } else { - Supplier> supplier = () -> new TreeSet<>(); + Supplier> supplier = TreeSet::new; return Stream.of(FW111_FIX_EN, FW113_EN).collect(Collectors.toCollection(supplier)); } }; @@ -702,7 +702,7 @@ public void testEvents() { verify(eventPublisherMock, atLeast(SEQUENCE.length + 1)).post(eventCaptor.capture()); }); events.get().addAll(eventCaptor.getAllValues()); - List list = events.get().stream().filter(event -> event instanceof FirmwareUpdateProgressInfoEvent) + List list = events.get().stream().filter(FirmwareUpdateProgressInfoEvent.class::isInstance) .collect(Collectors.toList()); assertTrue(list.size() >= SEQUENCE.length); for (int i = 0; i < SEQUENCE.length; i++) { @@ -758,7 +758,7 @@ public void testUpdateFirmwareError() { assertResultInfoEvent(THING1_UID, FW112_EN, "unexpected-handler-error", Locale.ENGLISH, "english", 1); assertResultInfoEvent(THING1_UID, FW112_EN, "unexpected-handler-error", Locale.GERMAN, "deutsch", 2); - assertThat(thing1.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V111.toString())); + assertThat(thing1.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V111)); assertThat(firmwareUpdateService.getFirmwareStatusInfo(THING1_UID), is(updateExecutableInfoFw112)); } @@ -777,7 +777,7 @@ public void testUpdateFirmwareCustomError() { assertResultInfoEvent(THING1_UID, FW112_EN, "test-error", Locale.ENGLISH, "english", 1); assertResultInfoEvent(THING1_UID, FW112_EN, "test-error", Locale.GERMAN, "deutsch", 2); - assertThat(thing1.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V111.toString())); + assertThat(thing1.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is(V111)); assertThat(firmwareUpdateService.getFirmwareStatusInfo(THING1_UID), is(updateExecutableInfoFw112)); } @@ -821,9 +821,7 @@ public void testBackgroundTransfer() throws Exception { FirmwareUpdateBackgroundTransferHandler handler4 = mock(FirmwareUpdateBackgroundTransferHandler.class); when(handler4.getThing()).thenReturn(thing4); - doAnswer(invocation -> { - return updateExecutable.get(); - }).when(handler4).isUpdateExecutable(); + doAnswer(invocation -> updateExecutable.get()).when(handler4).isUpdateExecutable(); doAnswer(invocation -> { Firmware firmware = (Firmware) invocation.getArguments()[0]; thing4.setProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmware.getVersion()); @@ -905,7 +903,7 @@ private void assertResultInfoEvent(ThingUID thingUID, Firmware firmware, String ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); verify(eventPublisherMock, atLeast(expectedEventCount)).post(eventCaptor.capture()); List allValues = eventCaptor.getAllValues().stream() - .filter(e -> e instanceof FirmwareUpdateResultInfoEvent).collect(Collectors.toList()); + .filter(FirmwareUpdateResultInfoEvent.class::isInstance).collect(Collectors.toList()); assertEquals(expectedEventCount, allValues.size()); assertFailedFirmwareUpdate(THING1_UID, allValues.get(expectedEventCount - 1), text); }); diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java index 18a3f616d4b..b0c698c7812 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java @@ -69,7 +69,7 @@ import org.slf4j.LoggerFactory; /** - * Tests for {@link ThingUpdateInstructionReader} and {@link ThingUpdateInstruction} implementations. + * Tests for {@link ThingUpdateInstructionReaderImpl} and {@link ThingUpdateInstruction} implementations. * * @author Jan N. Klug - Initial contribution */ @@ -149,14 +149,14 @@ public void testSingleChannelAddition() { assertThat(updatedThing.getChannels(), hasSize(3)); Channel channel1 = updatedThing.getChannel("testChannel1"); - assertChannel(channel1, channelTypeUID, null, null); + assertChannel(channel1, channelTypeUID, "Switch", "typeLabel", null); Channel channel2 = updatedThing.getChannel("testChannel2"); - assertChannel(channel2, channelTypeUID, "Test Label", null); + assertChannel(channel2, channelTypeUID, "Switch", "Test Label", null); assertThat(channel2.getDefaultTags(), containsInAnyOrder(ADDED_TAGS)); Channel channel3 = updatedThing.getChannel("testChannel3"); - assertChannel(channel3, channelTypeUID, "Test Label", "Test Description"); + assertChannel(channel3, channelTypeUID, "Switch", "Test Label", "Test Description"); } @Test @@ -186,11 +186,11 @@ public void testSingleChannelUpdate() { assertThat(updatedThing.getChannels(), hasSize(2)); Channel channel1 = updatedThing.getChannel(channelUID1); - assertChannel(channel1, channelTypeNewUID, "New Test Label", null); + assertChannel(channel1, channelTypeNewUID, "Number", "New Test Label", null); assertThat(channel1.getConfiguration(), is(channelConfig)); Channel channel2 = updatedThing.getChannel(channelUID2); - assertChannel(channel2, channelTypeNewUID, null, null); + assertChannel(channel2, channelTypeNewUID, "Number", "typeLabel", null); assertThat(channel2.getConfiguration().getProperties(), is(anEmptyMap())); } @@ -235,10 +235,10 @@ public void testMultipleChannelUpdates() { assertThat(updatedThing.getChannels(), hasSize(2)); Channel channel1 = updatedThing.getChannel("testChannel1"); - assertChannel(channel1, channelTypeNewUID, "Test Label", null); + assertChannel(channel1, channelTypeNewUID, "Number", "Test Label", null); Channel channel2 = updatedThing.getChannel("testChannel2"); - assertChannel(channel2, channelTypeOldUID, "TestLabel", null); + assertChannel(channel2, channelTypeOldUID, "Switch", "TestLabel", null); } @Test @@ -253,8 +253,10 @@ public void testOnlyMatchingInstructionsUpdate() { ChannelUID channelUID1 = new ChannelUID(thingUID, "testChannel1"); Thing thing = ThingBuilder.create(MULTIPLE_CHANNEL_THING_TYPE_UID, thingUID) - .withChannel(ChannelBuilder.create(channelUID0).withType(channelTypeOldUID).build()) - .withChannel(ChannelBuilder.create(channelUID1).withType(channelTypeOldUID).build()) + .withChannel(ChannelBuilder.create(channelUID0).withType(channelTypeOldUID) + .withAcceptedItemType("Switch").build()) + .withChannel(ChannelBuilder.create(channelUID1).withType(channelTypeOldUID) + .withAcceptedItemType("Switch").build()) .withProperty(PROPERTY_THING_TYPE_VERSION, "2").build(); managedThingProvider.add(thing); @@ -263,7 +265,7 @@ public void testOnlyMatchingInstructionsUpdate() { assertThat(updatedThing.getChannels(), hasSize(1)); Channel channel1 = updatedThing.getChannel("testChannel1"); - assertChannel(channel1, channelTypeOldUID, null, null); + assertChannel(channel1, channelTypeOldUID, "Switch", null, null); } @Test @@ -283,11 +285,11 @@ public void testSingleChannelAdditionGroup() { List channels1 = updatedThing.getChannelsOfGroup("group1"); assertThat(channels1, hasSize(1)); - assertChannel(channels1.get(0), channelTypeUID, null, null); + assertChannel(channels1.get(0), channelTypeUID, "Switch", "typeLabel", null); List channels2 = updatedThing.getChannelsOfGroup("group2"); assertThat(channels2, hasSize(1)); - assertChannel(channels2.get(0), channelTypeUID, null, null); + assertChannel(channels2.get(0), channelTypeUID, "Switch", "typeLabel", null); } @Test @@ -328,10 +330,11 @@ private Thing assertThing(Thing oldThing, int expectedNewThingTypeVersion) { return updatedThing; } - private void assertChannel(@Nullable Channel channel, ChannelTypeUID channelTypeUID, @Nullable String label, - @Nullable String description) { + private void assertChannel(@Nullable Channel channel, ChannelTypeUID channelTypeUID, String expectedItemType, + @Nullable String label, @Nullable String description) { assertThat(channel, is(notNullValue())); assertThat(channel.getChannelTypeUID(), is(channelTypeUID)); + assertThat(channel.getAcceptedItemType(), is(expectedItemType)); if (label != null) { assertThat(channel.getLabel(), is(label)); } else { @@ -361,7 +364,8 @@ private void registerChannelTypes(ChannelTypeUID... channelTypeUIDs) { ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); for (ChannelTypeUID channelTypeUID : channelTypeUIDs) { - ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "label", "Number").build(); + String itemType = channelTypeUID.getId().contains("New") ? "Number" : "Switch"; + ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "typeLabel", itemType).build(); when(channelTypeProvider.getChannelType(eq(channelTypeUID), nullable(Locale.class))) .thenReturn(channelType); when(channelTypeRegistry.getChannelType(eq(channelTypeUID))).thenReturn(channelType); diff --git a/itests/org.openhab.core.voice.tests/itest.bndrun b/itests/org.openhab.core.voice.tests/itest.bndrun index b4e85f066ff..5d0738bd7d7 100644 --- a/itests/org.openhab.core.voice.tests/itest.bndrun +++ b/itests/org.openhab.core.voice.tests/itest.bndrun @@ -72,4 +72,5 @@ Fragment-Host: org.openhab.core.voice junit-jupiter-params;version='[5.9.2,5.9.3)',\ junit-platform-commons;version='[1.9.2,1.9.3)',\ junit-platform-engine;version='[1.9.2,1.9.3)',\ - junit-platform-launcher;version='[1.9.2,1.9.3)' + junit-platform-launcher;version='[1.9.2,1.9.3)',\ + org.openhab.core.transform;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java index 0e768737976..3e90f20e5be 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java @@ -73,7 +73,7 @@ public Set getAvailableVoices() { try { Collection> refs = bundleContext.getServiceReferences(Voice.class, null); return refs.stream() // - .map(ref -> bundleContext.getService(ref)) // + .map(bundleContext::getService) // .filter(service -> service.getUID().startsWith(getId())) // .collect(Collectors.toSet()); } catch (InvalidSyntaxException e) { diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java index 89ce42a9786..dcf6b21aee2 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java @@ -38,6 +38,7 @@ import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.test.java.JavaOSGiTest; +import org.openhab.core.voice.DialogContext; import org.openhab.core.voice.DialogRegistration; import org.openhab.core.voice.Voice; import org.openhab.core.voice.VoiceManager; @@ -200,8 +201,10 @@ public void verifyThatADialogIsNotStartedWhenAnyOfTheRequiredServiceIsNull() { ksService = new KSServiceStub(); hliStub = new HumanLanguageInterpreterStub(); - assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, null, null, - List.of(hliStub), source, sink, Locale.ENGLISH, "word", null)); + assertThrows(IllegalStateException.class, + () -> voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(null).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build())); assertFalse(ksService.isWordSpotted()); assertFalse(sink.getIsStreamProcessed()); @@ -218,8 +221,10 @@ public void verifyThatADialogIsNotStartedWhenLocaleIsNotSupported() { registerService(ttsService); registerService(hliStub); - assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, ttsService, - null, List.of(hliStub), source, sink, Locale.FRENCH, "mot", null)); + assertThrows(IllegalStateException.class, + () -> voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub) + .withLocale(Locale.FRENCH).withKeyword("mot").build())); assertFalse(ksService.isWordSpotted()); assertFalse(sink.getIsStreamProcessed()); @@ -236,8 +241,9 @@ public void startDialogWhenAllOfTheRequiredServicesAreAvailable() { registerService(ttsService); registerService(hliStub); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "word", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertTrue(ksService.isWordSpotted()); assertTrue(sttService.isRecognized()); @@ -264,8 +270,9 @@ public void startDialogAndVerifyThatAKSExceptionIsProperlyHandled() { ksService.setExceptionExpected(true); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertFalse(ksService.isWordSpotted()); assertFalse(sttService.isRecognized()); @@ -290,8 +297,9 @@ public void startDialogAndVerifyThatAKSErrorIsProperlyHandled() { ksService.setErrorExpected(true); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "word", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertFalse(ksService.isWordSpotted()); assertFalse(sttService.isRecognized()); @@ -317,8 +325,9 @@ public void startDialogAndVerifyThatASTTExceptionIsProperlyHandled() { sttService.setExceptionExpected(true); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "word", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertTrue(ksService.isWordSpotted()); assertFalse(sttService.isRecognized()); @@ -343,8 +352,9 @@ public void startDialogAndVerifyThatASpeechRecognitionErrorIsProperlyHandled() { sttService.setErrorExpected(true); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "word", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertTrue(ksService.isWordSpotted()); assertFalse(sttService.isRecognized()); @@ -369,8 +379,9 @@ public void startDialogAndVerifyThatAnInterpretationExceptionIsProperlyHandled() hliStub.setExceptionExpected(true); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "word", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertTrue(ksService.isWordSpotted()); assertTrue(sttService.isRecognized()); @@ -407,8 +418,10 @@ public void startDialogWithoutPassingAnyParameters() throws Exception { // Wait some time to be sure that the configuration will be updated Thread.sleep(2000); - voiceManager.startDialog(); + DialogContext context = voiceManager.getDialogContextBuilder().build(); + voiceManager.startDialog(context); + assertThat(voiceManager.getLastDialogContext(), is(context)); assertTrue(ksService.isWordSpotted()); assertTrue(sttService.isRecognized()); assertThat(hliStub.getQuestion(), is("Recognized text")); @@ -432,13 +445,16 @@ public void verifyThatOnlyOneDialogPerSourceIsPossible() { registerService(ttsService); registerService(hliStub); - voiceManager.startDialog(ksService, sttService, ttsService, null, List.of(hliStub), source, sink, - Locale.ENGLISH, "word", null); + voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub).withLocale(Locale.ENGLISH) + .withKeyword("word").build()); assertTrue(ksService.isWordSpotted()); - assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, ttsService, - null, List.of(hliStub), source, sink, Locale.ENGLISH, "word", null)); + assertThrows(IllegalStateException.class, + () -> voiceManager.startDialog(voiceManager.getDialogContextBuilder().withSource(source).withSink(sink) + .withKS(ksService).withSTT(sttService).withTTS(ttsService).withHLI(hliStub) + .withLocale(Locale.ENGLISH).withKeyword("word").build())); voiceManager.stopDialog(source); diff --git a/itests/pom.xml b/itests/pom.xml index c9846b8286c..8deb43e63e1 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -31,6 +31,7 @@ org.openhab.core.config.discovery.usbserial.tests org.openhab.core.config.dispatch.tests org.openhab.core.ephemeris.tests + org.openhab.core.io.net.tests org.openhab.core.io.rest.core.tests org.openhab.core.model.item.tests org.openhab.core.model.rule.tests diff --git a/pom.xml b/pom.xml index 6208674faf1..9743bef665c 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 2.3.0 4.4.0 4.4.3 - 0.13.0 + 0.15.0 1.7.32 2.29.0 2.35.0 @@ -389,7 +389,7 @@ Import-Package: \\ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M7 + 3.0.0 --add-opens java.base/java.net=ALL-UNNAMED diff --git a/tools/i18n-plugin/src/main/java/org/openhab/core/tools/i18n/plugin/BundleInfoReader.java b/tools/i18n-plugin/src/main/java/org/openhab/core/tools/i18n/plugin/BundleInfoReader.java index 6a11b508d62..0462066d2db 100644 --- a/tools/i18n-plugin/src/main/java/org/openhab/core/tools/i18n/plugin/BundleInfoReader.java +++ b/tools/i18n-plugin/src/main/java/org/openhab/core/tools/i18n/plugin/BundleInfoReader.java @@ -116,20 +116,17 @@ private void readThingInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOExcep return; } for (Object type : types) { - if (type instanceof ThingTypeXmlResult) { - ThingTypeXmlResult result = (ThingTypeXmlResult) type; + if (type instanceof ThingTypeXmlResult result) { bundleInfo.getThingTypesXml().add(result); if (bundleInfo.getAddonId().isBlank()) { bundleInfo.setAddonId(result.getUID().getBindingId()); } - } else if (type instanceof ChannelGroupTypeXmlResult) { - ChannelGroupTypeXmlResult result = (ChannelGroupTypeXmlResult) type; + } else if (type instanceof ChannelGroupTypeXmlResult result) { bundleInfo.getChannelGroupTypesXml().add(result); if (bundleInfo.getAddonId().isBlank()) { bundleInfo.setAddonId(result.getUID().getBindingId()); } - } else if (type instanceof ChannelTypeXmlResult) { - ChannelTypeXmlResult result = (ChannelTypeXmlResult) type; + } else if (type instanceof ChannelTypeXmlResult result) { bundleInfo.getChannelTypesXml().add(result); if (bundleInfo.getAddonId().isBlank()) { bundleInfo.setAddonId(result.toChannelType().getUID().getBindingId()); diff --git a/tools/pom.xml b/tools/pom.xml index 14eab0fefb6..d427db4c478 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -19,6 +19,7 @@ archetype i18n-plugin + upgradetool diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml new file mode 100644 index 00000000000..874ed8d18b6 --- /dev/null +++ b/tools/upgradetool/pom.xml @@ -0,0 +1,123 @@ + + + + 4.0.0 + + + org.openhab.core.tools + org.openhab.core.reactor.tools + 4.0.0-SNAPSHOT + + + upgradetool + + jar + + openHAB Core :: Tools :: Upgrade tool + A tool for upgrading openHAB from 3.4 to 4.0 + + + + org.openhab.core.bundles + org.openhab.core + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.thing + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.storage.json + ${project.version} + + + commons-cli + commons-cli + 1.5.0 + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + com.google.code.gson + gson + 2.9.1 + + + javax.measure + unit-api + 2.1.3 + + + si.uom + si-units + 2.1 + + + tech.units + indriya + 2.1.2 + + + org.eclipse.jdt + org.eclipse.jdt.annotation + 2.2.600 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.3.0 + + + unpack-eea + + unpack + + + + + org.lastnpe.eea + eea-all + ${eea.version} + true + + + + + + + + maven-assembly-plugin + 3.4.2 + + + + org.openhab.core.tools.UpgradeTool + + + + jar-with-dependencies + + + + + make-assembly + + single + + package + + + + + + diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java new file mode 100644 index 00000000000..b310d813979 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java @@ -0,0 +1,94 @@ +/** + * 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.tools; + +import static org.openhab.core.tools.internal.Upgrader.*; + +import java.util.Set; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.tools.internal.Upgrader; + +/** + * The {@link UpgradeTool} is a tool for upgrading openHAB to mitigate breaking changes + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class UpgradeTool { + private static final Set LOG_LEVELS = Set.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR"); + private static final String OPT_COMMAND = "command"; + private static final String OPT_DIR = "dir"; + private static final String OPT_LOG = "log"; + private static final String OPT_FORCE = "force"; + + private static Options getOptions() { + Options options = new Options(); + + options.addOption(Option.builder().longOpt(OPT_DIR).desc("directory to process").numberOfArgs(1).build()); + options.addOption(Option.builder().longOpt(OPT_COMMAND).numberOfArgs(1) + .desc("command to execute (executes all if omitted)").build()); + options.addOption(Option.builder().longOpt(OPT_LOG).numberOfArgs(1).desc("log verbosity").build()); + options.addOption(Option.builder().longOpt(OPT_FORCE).desc("force execution (even if already done)").build()); + + return options; + } + + public static void main(String[] args) { + Options options = getOptions(); + try { + CommandLine commandLine = new DefaultParser().parse(options, args); + + String loglevel = commandLine.hasOption(OPT_LOG) ? commandLine.getOptionValue(OPT_LOG).toUpperCase() + : "INFO"; + if (!LOG_LEVELS.contains(loglevel)) { + System.out.println("Allowed log-levels are " + LOG_LEVELS); + System.exit(0); + } + + System.setProperty(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, loglevel); + + String baseDir = commandLine.hasOption(OPT_DIR) ? commandLine.getOptionValue(OPT_DIR) + : System.getenv("OPENHAB_USERDATA"); + if (baseDir == null || baseDir.isBlank()) { + System.out.println( + "Please either set the environment variable ${OPENHAB_USERDATA} or provide a directory through the --dir option."); + System.exit(0); + } else { + boolean force = commandLine.hasOption(OPT_FORCE) ? true : false; + + Upgrader upgrader = new Upgrader(baseDir, force); + if (!commandLine.hasOption(OPT_COMMAND) + || ITEM_COPY_UNIT_TO_METADATA.equals(commandLine.getOptionValue(OPT_COMMAND))) { + upgrader.itemCopyUnitToMetadata(); + } + if (!commandLine.hasOption(OPT_COMMAND) + || LINK_UPGRADE_JS_PROFILE.equals(commandLine.getOptionValue(OPT_COMMAND))) { + upgrader.linkUpgradeJsProfile(); + } + } + } catch (ParseException e) { + HelpFormatter formatter = new HelpFormatter(); + String commands = Set.of(ITEM_COPY_UNIT_TO_METADATA, LINK_UPGRADE_JS_PROFILE).toString(); + formatter.printHelp("upgradetool", "", options, "Available commands: " + commands, true); + } + + System.exit(0); + } +} diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java new file mode 100644 index 00000000000..ddcbcc5f882 --- /dev/null +++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java @@ -0,0 +1,244 @@ +/** + * 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.tools.internal; + +import static org.openhab.core.thing.DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_ATMOSPHERIC_HUMIDITY; +import static org.openhab.core.thing.DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_BATTERY_LEVEL; +import static org.openhab.core.thing.DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_COLOR_TEMPERATURE_ABS; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.items.ManagedItemProvider; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.storage.json.internal.JsonStorage; +import org.openhab.core.thing.dto.ThingDTO; +import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.types.util.UnitUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link Upgrader} contains the implementation of the upgrade methods + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class Upgrader { + public static final String ITEM_COPY_UNIT_TO_METADATA = "itemCopyUnitToMetadata"; + public static final String LINK_UPGRADE_JS_PROFILE = "linkUpgradeJsProfile"; + + private final Logger logger = LoggerFactory.getLogger(Upgrader.class); + private final String baseDir; + private final boolean force; + private final JsonStorage upgradeRecords; + + public Upgrader(String baseDir, boolean force) { + this.baseDir = baseDir; + this.force = force; + + Path upgradeJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.tools.UpgradeTool"); + + upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); + } + + private boolean checkUpgradeRecord(String key) { + UpgradeRecord upgradeRecord = upgradeRecords.get(key); + if (upgradeRecord != null && !force) { + logger.info("Already executed '{}' on {}. Use '--force' to execute it again.", key, + upgradeRecord.executionDate); + return false; + } + return true; + } + + public void itemCopyUnitToMetadata() { + boolean noLink; + + if (!checkUpgradeRecord(ITEM_COPY_UNIT_TO_METADATA)) { + return; + } + Path itemJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.items.Item.json"); + Path metadataJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.items.Metadata.json"); + Path linkJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); + Path thingJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.Thing.json"); + logger.info("Copying item unit from state description to metadata in database '{}'", itemJsonDatabasePath); + + if (!Files.isReadable(itemJsonDatabasePath)) { + logger.error("Cannot access item database '{}', check path and access rights.", itemJsonDatabasePath); + return; + } + + if (!Files.isReadable(linkJsonDatabasePath) || !Files.isReadable(thingJsonDatabasePath)) { + logger.warn("Cannot access thing or link database '{}', update may be incomplete.", linkJsonDatabasePath); + noLink = true; + } else { + noLink = false; + } + + // missing metadata database is also fine, we create one in that case + if (!Files.isWritable(metadataJsonDatabasePath) && Files.exists(metadataJsonDatabasePath)) { + logger.error("Cannot access metadata database '{}', check path and access rights.", + metadataJsonDatabasePath); + return; + } + + JsonStorage itemStorage = new JsonStorage<>(itemJsonDatabasePath.toFile(), + null, 5, 0, 0, List.of()); + JsonStorage metadataStorage = new JsonStorage<>(metadataJsonDatabasePath.toFile(), null, 5, 0, 0, + List.of()); + JsonStorage linkStorage = noLink ? null + : new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); + JsonStorage thingStorage = noLink ? null + : new JsonStorage<>(thingJsonDatabasePath.toFile(), null, 5, 0, 0, List.of()); + + itemStorage.getKeys().forEach(itemName -> { + ManagedItemProvider.PersistedItem item = itemStorage.get(itemName); + if (item != null && item.itemType.startsWith("Number:")) { + if (metadataStorage.containsKey(NumberItem.UNIT_METADATA_NAMESPACE + ":" + itemName)) { + logger.debug("{}: Already contains a 'unit' metadata, skipping it", itemName); + } else { + String unit = null; + if (!noLink) { + List links = linkStorage.getValues().stream().map(Objects::requireNonNull) + .filter(link -> itemName.equals(link.getItemName())).toList(); + // check if we can find the channel for these links + for (ItemChannelLink link : links) { + ThingDTO thing = thingStorage.get(link.getLinkedUID().getThingUID().toString()); + if (thing == null) { + logger.info( + "{}: Could not find thing for channel '{}'. Check if you need to set unit metadata.", + itemName, link.getLinkedUID()); + continue; + } + String channelTypeUID = thing.channels.stream() + .filter(channel -> link.getLinkedUID().toString().equals(channel.uid)) + .map(channel -> channel.channelTypeUID).findFirst().orElse(null); + if (channelTypeUID == null) { + continue; + } + // replace . with :, if the database is already correct, we can ignore that + channelTypeUID = channelTypeUID.replace(".", ":"); + if (channelTypeUID.startsWith("system")) { + if (channelTypeUID.equals(SYSTEM_CHANNEL_TYPE_UID_BATTERY_LEVEL.toString()) + || channelTypeUID + .equals(SYSTEM_CHANNEL_TYPE_UID_ATMOSPHERIC_HUMIDITY.toString())) { + unit = "%"; + } else if (channelTypeUID + .equals(SYSTEM_CHANNEL_TYPE_UID_COLOR_TEMPERATURE_ABS.toString())) { + unit = "K"; + } + } else { + logger.warn( + "{}: Could not determine if channel '{}' sets a state description. Check if you need to set unit metadata.", + itemName, link.getLinkedUID()); + } + } + } + + // metadata state description has higher priority, so we check that and override the unit if + // necessary + Metadata metadata = metadataStorage.get("stateDescription:" + itemName); + if (metadata == null) { + logger.debug("{}: No state description in metadata found.", itemName); + } else { + String pattern = (String) metadata.getConfiguration().get("pattern"); + if (pattern != null) { + if (pattern.contains(UnitUtils.UNIT_PLACEHOLDER)) { + logger.warn( + "{}: State description contains unit place-holder '%unit%', check if 'unit' metadata is needed!", + itemName); + } else { + Unit stateDescriptionUnit = UnitUtils.parseUnit(pattern); + if (stateDescriptionUnit != null) { + unit = stateDescriptionUnit.toString(); + } + } + } else { + logger.debug("{}: Nothing to do, no pattern found.", itemName); + } + } + if (unit != null) { + MetadataKey defaultUnitMetadataKey = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, + itemName); + Metadata defaultUnitMetadata = new Metadata(defaultUnitMetadataKey, unit, null); + metadataStorage.put(defaultUnitMetadataKey.toString(), defaultUnitMetadata); + logger.info("{}: Wrote 'unit={}' to metadata.", itemName, unit); + } + } + } + }); + + metadataStorage.flush(); + upgradeRecords.put(ITEM_COPY_UNIT_TO_METADATA, new UpgradeRecord(ZonedDateTime.now())); + upgradeRecords.flush(); + } + + public void linkUpgradeJsProfile() { + if (!checkUpgradeRecord(LINK_UPGRADE_JS_PROFILE)) { + return; + } + + Path linkJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json"); + logger.info("Upgrading JS profile configuration in database '{}'", linkJsonDatabasePath); + + if (!Files.isWritable(linkJsonDatabasePath)) { + logger.error("Cannot access link database '{}', check path and access rights.", linkJsonDatabasePath); + return; + } + JsonStorage linkStorage = new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0, + List.of()); + + List.copyOf(linkStorage.getKeys()).forEach(linkUid -> { + ItemChannelLink link = Objects.requireNonNull(linkStorage.get(linkUid)); + Configuration configuration = link.getConfiguration(); + String profileName = (String) configuration.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); + if ("transform:JS".equals(profileName)) { + String function = (String) configuration.get("function"); + if (function != null) { + configuration.put("toItemScript", function); + configuration.put("toHandlerScript", "|input"); + configuration.remove("function"); + configuration.remove("sourceFormat"); + + linkStorage.put(linkUid, link); + logger.info("{}: rewrote JS profile link to new format", linkUid); + } else { + logger.info("{}: link already has correct configuration", linkUid); + } + } + }); + + linkStorage.flush(); + upgradeRecords.put(LINK_UPGRADE_JS_PROFILE, new UpgradeRecord(ZonedDateTime.now())); + upgradeRecords.flush(); + } + + private static class UpgradeRecord { + public final String executionDate; + + public UpgradeRecord(ZonedDateTime executionDate) { + this.executionDate = executionDate.toString(); + } + } +}