[automation] Extend provider script extension for metadata & ItemChannelLinks (#4865)

* [automation] Move provider script extension to a new bundle
* [automation] Add ProviderRegistryDelegate interface and improve ProviderScriptExtension code
* [automation] Add ScriptedMetadataProvider & ProviderMetadataRegistryDelegate
* Rename ProviderRegistryDelegate.java to ProviderRegistry.java
* [automation] Add ScriptedItemChannelLinkProvider & ProviderItemChannelLinkRegistry
* [automation] Implement ProviderItemChannelLinkRegistry::purge

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze
2025-06-29 11:32:10 +02:00
committed by GitHub
parent 9efdc0401c
commit 80d20d1f3e
19 changed files with 823 additions and 163 deletions
+6
View File
@@ -412,6 +412,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script.providersupport</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.core.bundles</groupId> <groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script.rulesupport</artifactId> <artifactId>org.openhab.core.automation.module.script.rulesupport</artifactId>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.automation.module.script.providersupport</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
@@ -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
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.automation.module.script.providersupport</artifactId>
<name>openHAB Core :: Bundles :: Automation Script ProviderSupport</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.providersupport.internal;
/**
* Interface to be implemented by all {@link org.openhab.core.common.registry.Registry} (delegates) that are used to
* provide openHAB entities from scripts.
*
* @author Florian Hotze - Initial contribution
*/
public interface ProviderRegistry {
/**
* Removes all elements that are provided by the script the {@link ProviderRegistry} instance is bound to.
* To be called when the script is unloaded or reloaded.
*/
void removeAllAddedByScript();
}
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.providersupport.internal;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptExtensionProvider;
import org.openhab.core.automation.module.script.providersupport.shared.ProviderItemChannelLinkRegistry;
import org.openhab.core.automation.module.script.providersupport.shared.ProviderItemRegistryDelegate;
import org.openhab.core.automation.module.script.providersupport.shared.ProviderMetadataRegistryDelegate;
import org.openhab.core.automation.module.script.providersupport.shared.ProviderThingRegistryDelegate;
import org.openhab.core.automation.module.script.providersupport.shared.ScriptedItemChannelLinkProvider;
import org.openhab.core.automation.module.script.providersupport.shared.ScriptedItemProvider;
import org.openhab.core.automation.module.script.providersupport.shared.ScriptedMetadataProvider;
import org.openhab.core.automation.module.script.providersupport.shared.ScriptedThingProvider;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.MetadataRegistry;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link ProviderScriptExtension} extends scripts to provide openHAB entities like items.
* It handles the lifecycle of these entities, ensuring that they are removed when the script is unloaded.
*
* @author Florian Hotze - Initial contribution
*/
@Component(immediate = true)
@NonNullByDefault
public class ProviderScriptExtension implements ScriptExtensionProvider {
private static final String PRESET_NAME = "provider";
private static final String ITEM_CHANNEL_LINK_REGISTRY_NAME = "itemChannelLinkRegistry";
private static final String ITEM_REGISTRY_NAME = "itemRegistry";
private static final String METADATA_REGISTRY_NAME = "metadataRegistry";
private static final String THING_REGISTRY_NAME = "thingRegistry";
private final Map<String, Map<String, ProviderRegistry>> registryCache = new ConcurrentHashMap<>();
private final ItemChannelLinkRegistry itemChannelLinkRegistry;
private final ScriptedItemChannelLinkProvider itemChannelLinkProvider;
private final ItemRegistry itemRegistry;
private final ScriptedItemProvider itemProvider;
private final MetadataRegistry metadataRegistry;
private final ScriptedMetadataProvider metadataProvider;
private final ThingRegistry thingRegistry;
private final ScriptedThingProvider thingProvider;
@Activate
public ProviderScriptExtension( //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ScriptedItemChannelLinkProvider itemChannelLinkProvider,
final @Reference ItemRegistry itemRegistry, final @Reference ScriptedItemProvider itemProvider,
final @Reference MetadataRegistry metadataRegistry,
final @Reference ScriptedMetadataProvider metadataProvider, final @Reference ThingRegistry thingRegistry,
final @Reference ScriptedThingProvider thingProvider) {
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.itemChannelLinkProvider = itemChannelLinkProvider;
this.itemRegistry = itemRegistry;
this.itemProvider = itemProvider;
this.metadataRegistry = metadataRegistry;
this.metadataProvider = metadataProvider;
this.thingRegistry = thingRegistry;
this.thingProvider = thingProvider;
}
@Override
public Collection<String> getDefaultPresets() {
return Set.of();
}
@Override
public Collection<String> getPresets() {
return Set.of(PRESET_NAME);
}
@Override
public Collection<String> getTypes() {
return Set.of(ITEM_CHANNEL_LINK_REGISTRY_NAME, ITEM_REGISTRY_NAME, METADATA_REGISTRY_NAME, THING_REGISTRY_NAME);
}
@Override
public @Nullable Object get(String scriptIdentifier, String type) throws IllegalArgumentException {
Map<String, ProviderRegistry> registries = Objects
.requireNonNull(registryCache.computeIfAbsent(scriptIdentifier, k -> new HashMap<>()));
ProviderRegistry registry = registries.get(type);
if (registry != null) {
return registry;
}
return switch (type) {
case ITEM_CHANNEL_LINK_REGISTRY_NAME -> {
ProviderItemChannelLinkRegistry providerItemChannelLinkRegistry = new ProviderItemChannelLinkRegistry(
itemChannelLinkRegistry, itemChannelLinkProvider);
registries.put(type, providerItemChannelLinkRegistry);
yield providerItemChannelLinkRegistry;
}
case ITEM_REGISTRY_NAME -> {
ProviderItemRegistryDelegate itemRegistryDelegate = new ProviderItemRegistryDelegate(itemRegistry,
itemProvider);
registries.put(ITEM_REGISTRY_NAME, itemRegistryDelegate);
yield itemRegistryDelegate;
}
case METADATA_REGISTRY_NAME -> {
ProviderMetadataRegistryDelegate metadataRegistryDelegate = new ProviderMetadataRegistryDelegate(
metadataRegistry, metadataProvider);
registries.put(METADATA_REGISTRY_NAME, metadataRegistryDelegate);
yield metadataRegistryDelegate;
}
case THING_REGISTRY_NAME -> {
ProviderThingRegistryDelegate thingRegistryDelegate = new ProviderThingRegistryDelegate(thingRegistry,
thingProvider);
registries.put(THING_REGISTRY_NAME, thingRegistryDelegate);
yield thingRegistryDelegate;
}
default -> null;
};
}
@Override
public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
if (PRESET_NAME.equals(preset)) {
return Map.of(ITEM_CHANNEL_LINK_REGISTRY_NAME,
Objects.requireNonNull(get(scriptIdentifier, ITEM_CHANNEL_LINK_REGISTRY_NAME)), ITEM_REGISTRY_NAME,
Objects.requireNonNull(get(scriptIdentifier, ITEM_REGISTRY_NAME)), METADATA_REGISTRY_NAME,
Objects.requireNonNull(get(scriptIdentifier, METADATA_REGISTRY_NAME)), THING_REGISTRY_NAME,
Objects.requireNonNull(get(scriptIdentifier, THING_REGISTRY_NAME)));
}
return Map.of();
}
@Override
public void unload(String scriptIdentifier) {
Map<String, ProviderRegistry> registries = registryCache.remove(scriptIdentifier);
if (registries != null) {
for (ProviderRegistry registry : registries.values()) {
registry.removeAllAddedByScript();
}
}
}
}
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.providersupport.shared;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
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.automation.module.script.providersupport.internal.ProviderRegistry;
import org.openhab.core.common.registry.Registry;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
/**
* The {@link ProviderItemChannelLinkRegistry} is implementing a {@link Registry} to provide a comfortable way to
* provide {@link ItemChannelLink}s from scripts without worrying about the need to remove them again when the script is
* unloaded.
* Nonetheless, using the {@link #addPermanent(ItemChannelLink)} method it is still possible to them permanently.
* <p>
* Use a new instance of this class for each {@link javax.script.ScriptEngine}.
* <p>
* ATTENTION: This class does not provide the same methods as {@link ItemChannelLinkRegistry}.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
public class ProviderItemChannelLinkRegistry implements Registry<ItemChannelLink, String>, ProviderRegistry {
private final ItemChannelLinkRegistry itemChannelLinkRegistry;
private final Set<String> uids = new HashSet<>();
private final ScriptedItemChannelLinkProvider scriptedProvider;
public ProviderItemChannelLinkRegistry(ItemChannelLinkRegistry itemChannelLinkRegistry,
ScriptedItemChannelLinkProvider scriptedProvider) {
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.scriptedProvider = scriptedProvider;
}
@Override
public void addRegistryChangeListener(RegistryChangeListener<ItemChannelLink> listener) {
itemChannelLinkRegistry.addRegistryChangeListener(listener);
}
@Override
public Collection<ItemChannelLink> getAll() {
return itemChannelLinkRegistry.getAll();
}
@Override
public Stream<ItemChannelLink> stream() {
return itemChannelLinkRegistry.stream();
}
@Override
public @Nullable ItemChannelLink get(String key) {
return itemChannelLinkRegistry.get(key);
}
@Override
public void removeRegistryChangeListener(RegistryChangeListener<ItemChannelLink> listener) {
itemChannelLinkRegistry.removeRegistryChangeListener(listener);
}
@Override
public ItemChannelLink add(ItemChannelLink element) {
String uid = element.getUID();
// Check for item->channel link already existing here because the item->channel link might exist in a different
// provider, so we need to
// check the registry and not only the provider itself
if (get(uid) != null) {
throw new IllegalArgumentException(
"Cannot add item->channel link, because an item->channel link with same UID (" + uid
+ ") already exists.");
}
scriptedProvider.add(element);
uids.add(uid);
return element;
}
/**
* Adds an {@link ItemChannelLink} permanently to the registry.
* This {@link ItemChannelLink} will be kept in the registry even if the script is unloaded
*
* @param element the {@link ItemChannelLink} to be added (must not be null)
* @return the added {@link ItemChannelLink}
*/
public ItemChannelLink addPermanent(ItemChannelLink element) {
return itemChannelLinkRegistry.add(element);
}
@Override
public @Nullable ItemChannelLink update(ItemChannelLink element) {
if (uids.contains(element.getUID())) {
return scriptedProvider.update(element);
}
return itemChannelLinkRegistry.update(element);
}
@Override
public @Nullable ItemChannelLink remove(String key) {
if (uids.contains(key)) {
return scriptedProvider.remove(key);
}
return itemChannelLinkRegistry.remove(key);
}
public int removeLinksForThing(ThingUID thingUID) {
int removedLinks = 0;
Collection<ItemChannelLink> itemChannelLinks = getAll();
for (ItemChannelLink itemChannelLink : itemChannelLinks) {
if (itemChannelLink.getLinkedUID().getThingUID().equals(thingUID)) {
this.remove(itemChannelLink.getUID());
removedLinks++;
}
}
return removedLinks;
}
public int removeLinksForItem(String itemName) {
int removedLinks = 0;
Collection<ItemChannelLink> itemChannelLinks = getAll();
for (ItemChannelLink itemChannelLink : itemChannelLinks) {
if (itemChannelLink.getItemName().equals(itemName)) {
this.remove(itemChannelLink.getUID());
removedLinks++;
}
}
return removedLinks;
}
public int purge() {
List<String> toRemove = itemChannelLinkRegistry.getOrphanLinks().keySet().stream().map(ItemChannelLink::getUID)
.filter(i -> scriptedProvider.get(i) != null).toList();
toRemove.forEach(this::remove);
return toRemove.size() + itemChannelLinkRegistry.purge();
}
@Override
public void removeAllAddedByScript() {
for (String uid : uids) {
scriptedProvider.remove(uid);
}
uids.clear();
}
}
@@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.core.automation.module.script.rulesupport.shared; package org.openhab.core.automation.module.script.providersupport.shared;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -21,6 +21,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.providersupport.internal.ProviderRegistry;
import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
@@ -38,16 +39,16 @@ import org.openhab.core.items.ItemRegistry;
* @author Florian Hotze - Initial contribution * @author Florian Hotze - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class ProviderItemRegistryDelegate implements ItemRegistry { public class ProviderItemRegistryDelegate implements ItemRegistry, ProviderRegistry {
private final ItemRegistry itemRegistry; private final ItemRegistry itemRegistry;
private final Set<String> items = new HashSet<>(); private final Set<String> items = new HashSet<>();
private final ScriptedItemProvider itemProvider; private final ScriptedItemProvider scriptedProvider;
public ProviderItemRegistryDelegate(ItemRegistry itemRegistry, ScriptedItemProvider itemProvider) { public ProviderItemRegistryDelegate(ItemRegistry itemRegistry, ScriptedItemProvider scriptedProvider) {
this.itemRegistry = itemRegistry; this.itemRegistry = itemRegistry;
this.itemProvider = itemProvider; this.scriptedProvider = scriptedProvider;
} }
@Override @Override
@@ -85,7 +86,7 @@ public class ProviderItemRegistryDelegate implements ItemRegistry {
"Cannot add item, because an item with same name (" + itemName + ") already exists."); "Cannot add item, because an item with same name (" + itemName + ") already exists.");
} }
itemProvider.add(element); scriptedProvider.add(element);
items.add(itemName); items.add(itemName);
return element; return element;
@@ -105,7 +106,7 @@ public class ProviderItemRegistryDelegate implements ItemRegistry {
@Override @Override
public @Nullable Item update(Item element) { public @Nullable Item update(Item element) {
if (items.contains(element.getName())) { if (items.contains(element.getName())) {
return itemProvider.update(element); return scriptedProvider.update(element);
} }
return itemRegistry.update(element); return itemRegistry.update(element);
} }
@@ -113,7 +114,7 @@ public class ProviderItemRegistryDelegate implements ItemRegistry {
@Override @Override
public @Nullable Item remove(String key) { public @Nullable Item remove(String key) {
if (items.remove(key)) { if (items.remove(key)) {
return itemProvider.remove(key); return scriptedProvider.remove(key);
} }
return itemRegistry.remove(key); return itemRegistry.remove(key);
@@ -175,13 +176,10 @@ public class ProviderItemRegistryDelegate implements ItemRegistry {
} }
} }
/** @Override
* Removes all items that are provided by this script.
* To be called when the script is unloaded or reloaded.
*/
public void removeAllAddedByScript() { public void removeAllAddedByScript() {
for (String item : items) { for (String item : items) {
itemProvider.remove(item); scriptedProvider.remove(item);
} }
items.clear(); items.clear();
} }
@@ -0,0 +1,146 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.providersupport.shared;
import java.util.Collection;
import java.util.HashSet;
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.automation.module.script.providersupport.internal.ProviderRegistry;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.MetadataPredicates;
import org.openhab.core.items.MetadataRegistry;
/**
* The {@link ProviderMetadataRegistryDelegate} is wrapping a {@link MetadataRegistry} to provide a comfortable way to
* provide items from scripts without worrying about the need to remove metadata again when the script is unloaded.
* Nonetheless, using the {@link #addPermanent(Metadata)} method it is still possible to add metadata permanently.
* <p>
* Use a new instance of this class for each {@link javax.script.ScriptEngine}.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
public class ProviderMetadataRegistryDelegate implements MetadataRegistry, ProviderRegistry {
private final MetadataRegistry metadataRegistry;
private final Set<MetadataKey> metadataKeys = new HashSet<>();
private final ScriptedMetadataProvider scriptedProvider;
public ProviderMetadataRegistryDelegate(MetadataRegistry metadataRegistry,
ScriptedMetadataProvider scriptedProvider) {
this.metadataRegistry = metadataRegistry;
this.scriptedProvider = scriptedProvider;
}
@Override
public void addRegistryChangeListener(RegistryChangeListener<Metadata> listener) {
metadataRegistry.addRegistryChangeListener(listener);
}
@Override
public Collection<Metadata> getAll() {
return metadataRegistry.getAll();
}
@Override
public Stream<Metadata> stream() {
return metadataRegistry.stream();
}
@Override
public @Nullable Metadata get(MetadataKey key) {
return metadataRegistry.get(key);
}
@Override
public void removeRegistryChangeListener(RegistryChangeListener<Metadata> listener) {
metadataRegistry.removeRegistryChangeListener(listener);
}
@Override
public Metadata add(Metadata element) {
MetadataKey key = element.getUID();
// Check for metadata already existing here because the metadata might exist in a different provider, so we need
// to check the registry and not only the provider itself
if (get(key) != null) {
throw new IllegalArgumentException(
"Cannot add metadata, because metadata with same name (" + key + ") already exists.");
}
scriptedProvider.add(element);
metadataKeys.add(key);
return element;
}
/**
* Adds metadata permanently to the registry.
* This metadata will be kept in the registry even if the script is unloaded.
*
* @param element the metadata to be added (must not be null)
* @return the added metadata
*/
public Metadata addPermanent(Metadata element) {
return metadataRegistry.add(element);
}
@Override
public @Nullable Metadata update(Metadata element) {
if (metadataKeys.contains(element.getUID())) {
return scriptedProvider.update(element);
}
return metadataRegistry.update(element);
}
@Override
public @Nullable Metadata remove(MetadataKey key) {
if (metadataKeys.contains(key)) {
return scriptedProvider.remove(key);
}
return metadataRegistry.remove(key);
}
@Override
public boolean isInternalNamespace(String namespace) {
return metadataRegistry.isInternalNamespace(namespace);
}
@Override
public Collection<String> getAllNamespaces(String itemname) {
return metadataRegistry.getAllNamespaces(itemname);
}
@Override
public void removeItemMetadata(String itemname) {
if (scriptedProvider.getAll().stream().anyMatch(MetadataPredicates.ofItem(itemname))) {
scriptedProvider.removeItemMetadata(itemname);
return;
}
metadataRegistry.removeItemMetadata(itemname);
}
@Override
public void removeAllAddedByScript() {
for (MetadataKey key : metadataKeys) {
scriptedProvider.remove(key);
}
metadataKeys.clear();
}
}
@@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.core.automation.module.script.rulesupport.shared; package org.openhab.core.automation.module.script.providersupport.shared;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
@@ -20,6 +20,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.providersupport.internal.ProviderRegistry;
import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
@@ -39,16 +40,16 @@ import org.openhab.core.thing.ThingUID;
* @author Florian Hotze - Initial contribution * @author Florian Hotze - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class ProviderThingRegistryDelegate implements ThingRegistry { public class ProviderThingRegistryDelegate implements ThingRegistry, ProviderRegistry {
private final ThingRegistry thingRegistry; private final ThingRegistry thingRegistry;
private final Set<ThingUID> things = new HashSet<>(); private final Set<ThingUID> things = new HashSet<>();
private final ScriptedThingProvider thingProvider; private final ScriptedThingProvider scriptedProvider;
public ProviderThingRegistryDelegate(ThingRegistry thingRegistry, ScriptedThingProvider thingProvider) { public ProviderThingRegistryDelegate(ThingRegistry thingRegistry, ScriptedThingProvider scriptedProvider) {
this.thingRegistry = thingRegistry; this.thingRegistry = thingRegistry;
this.thingProvider = thingProvider; this.scriptedProvider = scriptedProvider;
} }
@Override @Override
@@ -81,7 +82,7 @@ public class ProviderThingRegistryDelegate implements ThingRegistry {
"Cannot add Thing, because a Thing with same UID (" + thingUID + ") already exists."); "Cannot add Thing, because a Thing with same UID (" + thingUID + ") already exists.");
} }
thingProvider.add(element); scriptedProvider.add(element);
things.add(thingUID); things.add(thingUID);
return element; return element;
@@ -101,7 +102,7 @@ public class ProviderThingRegistryDelegate implements ThingRegistry {
@Override @Override
public @Nullable Thing update(Thing element) { public @Nullable Thing update(Thing element) {
if (things.contains(element.getUID())) { if (things.contains(element.getUID())) {
return thingProvider.update(element); return scriptedProvider.update(element);
} }
return thingRegistry.update(element); return thingRegistry.update(element);
@@ -131,13 +132,10 @@ public class ProviderThingRegistryDelegate implements ThingRegistry {
return thingRegistry.remove(thingUID); return thingRegistry.remove(thingUID);
} }
/** @Override
* Removes all Things that are provided by this script.
* To be called when the script is unloaded or reloaded.
*/
public void removeAllAddedByScript() { public void removeAllAddedByScript() {
for (ThingUID thing : things) { for (ThingUID thing : things) {
thingProvider.remove(thing); scriptedProvider.remove(thing);
} }
things.clear(); things.clear();
} }
@@ -145,7 +143,7 @@ public class ProviderThingRegistryDelegate implements ThingRegistry {
@Override @Override
public @Nullable Thing forceRemove(ThingUID thingUID) { public @Nullable Thing forceRemove(ThingUID thingUID) {
if (things.remove(thingUID)) { if (things.remove(thingUID)) {
return thingProvider.remove(thingUID); return scriptedProvider.remove(thingUID);
} }
return thingRegistry.forceRemove(thingUID); return thingRegistry.forceRemove(thingUID);
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.providersupport.shared;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.common.registry.ManagedProvider;
import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.link.ItemChannelLinkProvider;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link ItemChannelLinkProvider} keeps {@link ItemChannelLink}s provided by scripts during runtime.
* This ensures that {@link ItemChannelLink}s are not kept on reboot, but have to be provided by the scripts again.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { ScriptedItemChannelLinkProvider.class, ItemChannelLinkProvider.class })
public class ScriptedItemChannelLinkProvider extends AbstractProvider<ItemChannelLink>
implements ItemChannelLinkProvider, ManagedProvider<ItemChannelLink, String> {
private final Logger logger = LoggerFactory.getLogger(ScriptedItemChannelLinkProvider.class);
private final Map<String, ItemChannelLink> itemChannelLinks = new HashMap<>();
@Override
public Collection<ItemChannelLink> getAll() {
return itemChannelLinks.values();
}
@Override
public @Nullable ItemChannelLink get(String key) {
return itemChannelLinks.get(key);
}
@Override
public void add(ItemChannelLink itemChannelLink) {
if (get(itemChannelLink.getUID()) != null) {
throw new IllegalArgumentException(
"Cannot add item->channel link, because an item->channel link with same UID ("
+ itemChannelLink.getUID() + ") already exists.");
}
itemChannelLinks.put(itemChannelLink.getUID(), itemChannelLink);
notifyListenersAboutAddedElement(itemChannelLink);
}
@Override
public @Nullable ItemChannelLink update(ItemChannelLink itemChannelLink) {
ItemChannelLink oldItemChannelLink = itemChannelLinks.get(itemChannelLink.getUID());
if (oldItemChannelLink != null) {
itemChannelLinks.put(itemChannelLink.getUID(), itemChannelLink);
notifyListenersAboutUpdatedElement(oldItemChannelLink, itemChannelLink);
} else {
logger.warn("Cannot update item->channel link with UID '{}', because it does not exist.",
itemChannelLink.getUID());
}
return oldItemChannelLink;
}
@Override
public @Nullable ItemChannelLink remove(String key) {
ItemChannelLink itemChannelLink = itemChannelLinks.remove(key);
if (itemChannelLink != null) {
notifyListenersAboutRemovedElement(itemChannelLink);
}
return itemChannelLink;
}
}
@@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.core.automation.module.script.rulesupport.shared; package org.openhab.core.automation.module.script.providersupport.shared;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.providersupport.shared;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.items.ManagedMetadataProvider;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.MetadataPredicates;
import org.openhab.core.items.MetadataProvider;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link org.openhab.core.items.MetadataProvider} keeps metadata provided by scripts during runtime.
* This ensures that metadata is not kept on reboot, but has to be provided by the scripts again.
*
* @author Florian Hotze
*/
@NonNullByDefault
@Component(immediate = true, service = { ScriptedMetadataProvider.class, MetadataProvider.class })
public class ScriptedMetadataProvider extends AbstractProvider<Metadata> implements ManagedMetadataProvider {
private final Logger logger = LoggerFactory.getLogger(ScriptedMetadataProvider.class);
private final Map<MetadataKey, Metadata> metadataStorage = new HashMap<>();
@Override
public void removeItemMetadata(String name) {
getAll().stream().filter(MetadataPredicates.ofItem(name)).map(Metadata::getUID).forEach(this::remove);
}
@Override
public Collection<Metadata> getAll() {
return metadataStorage.values();
}
@Override
public @Nullable Metadata get(MetadataKey metadataKey) {
return metadataStorage.get(metadataKey);
}
@Override
public void add(Metadata metadata) {
if (metadataStorage.containsKey(metadata.getUID())) {
throw new IllegalArgumentException("Cannot add metadata, because metadata with the same UID ("
+ metadata.getUID() + ") already exists");
}
metadataStorage.put(metadata.getUID(), metadata);
notifyListenersAboutAddedElement(metadata);
}
@Override
public @Nullable Metadata update(Metadata metadata) {
Metadata oldMetadata = metadataStorage.get(metadata.getUID());
if (oldMetadata != null) {
metadataStorage.put(metadata.getUID(), metadata);
notifyListenersAboutUpdatedElement(oldMetadata, metadata);
} else {
logger.warn("Could not update metadata with UID '{}', because it does not exist", metadata.getUID());
}
return oldMetadata;
}
@Override
public @Nullable Metadata remove(MetadataKey metadataKey) {
Metadata metadata = metadataStorage.remove(metadataKey);
if (metadata != null) {
notifyListenersAboutRemovedElement(metadata);
}
return metadata;
}
}
@@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.core.automation.module.script.rulesupport.shared; package org.openhab.core.automation.module.script.providersupport.shared;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@@ -1,133 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public 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.rulesupport.internal;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptExtensionProvider;
import org.openhab.core.automation.module.script.rulesupport.shared.ProviderItemRegistryDelegate;
import org.openhab.core.automation.module.script.rulesupport.shared.ProviderThingRegistryDelegate;
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedItemProvider;
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedThingProvider;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.thing.ThingRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link ProviderScriptExtension} extends scripts to provide openHAB entities like items.
* It handles the lifecycle of these entities, ensuring that they are removed when the script is unloaded.
*
* @author Florian Hotze - Initial contribution
*/
@Component(immediate = true)
@NonNullByDefault
public class ProviderScriptExtension implements ScriptExtensionProvider {
private static final String PRESET_NAME = "provider";
private static final String ITEM_REGISTRY_NAME = "itemRegistry";
private static final String THING_REGISTRY_NAME = "thingRegistry";
private final Map<String, Map<String, Object>> objectCache = new ConcurrentHashMap<>();
private final ItemRegistry itemRegistry;
private final ScriptedItemProvider itemProvider;
private final ThingRegistry thingRegistry;
private final ScriptedThingProvider thingProvider;
@Activate
public ProviderScriptExtension(final @Reference ItemRegistry itemRegistry,
final @Reference ScriptedItemProvider itemProvider, final @Reference ThingRegistry thingRegistry,
final @Reference ScriptedThingProvider thingProvider) {
this.itemRegistry = itemRegistry;
this.itemProvider = itemProvider;
this.thingRegistry = thingRegistry;
this.thingProvider = thingProvider;
}
@Override
public Collection<String> getDefaultPresets() {
return Set.of();
}
@Override
public Collection<String> getPresets() {
return Set.of(PRESET_NAME);
}
@Override
public Collection<String> getTypes() {
return Set.of(ITEM_REGISTRY_NAME, THING_REGISTRY_NAME);
}
@Override
public @Nullable Object get(String scriptIdentifier, String type) throws IllegalArgumentException {
Map<String, Object> objects = Objects
.requireNonNull(objectCache.computeIfAbsent(scriptIdentifier, k -> new HashMap<>()));
Object obj = objects.get(type);
if (obj != null) {
return obj;
}
return switch (type) {
case ITEM_REGISTRY_NAME -> {
ProviderItemRegistryDelegate itemRegistryDelegate = new ProviderItemRegistryDelegate(itemRegistry,
itemProvider);
objects.put(ITEM_REGISTRY_NAME, itemRegistryDelegate);
yield itemRegistryDelegate;
}
case THING_REGISTRY_NAME -> {
ProviderThingRegistryDelegate thingRegistryDelegate = new ProviderThingRegistryDelegate(thingRegistry,
thingProvider);
objects.put(THING_REGISTRY_NAME, thingRegistryDelegate);
yield thingRegistryDelegate;
}
default -> null;
};
}
@Override
public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
if (PRESET_NAME.equals(preset)) {
return Map.of(ITEM_REGISTRY_NAME, Objects.requireNonNull(get(scriptIdentifier, ITEM_REGISTRY_NAME)),
THING_REGISTRY_NAME, Objects.requireNonNull(get(scriptIdentifier, THING_REGISTRY_NAME)));
}
return Map.of();
}
@Override
public void unload(String scriptIdentifier) {
Map<String, Object> objects = objectCache.remove(scriptIdentifier);
if (objects != null) {
Object itemRegistry = objects.get(ITEM_REGISTRY_NAME);
if (itemRegistry != null) {
ProviderItemRegistryDelegate itemRegistryDelegate = (ProviderItemRegistryDelegate) itemRegistry;
itemRegistryDelegate.removeAllAddedByScript();
}
Object thingRegistry = objects.get(THING_REGISTRY_NAME);
if (thingRegistry != null) {
ProviderThingRegistryDelegate thingRegistryDelegate = (ProviderThingRegistryDelegate) thingRegistry;
thingRegistryDelegate.removeAllAddedByScript();
}
}
}
}
@@ -952,7 +952,7 @@ public class ItemResource implements RESTResource {
public Response getSemanticsHealth(@Context HttpHeaders headers) { public Response getSemanticsHealth(@Context HttpHeaders headers) {
List<ItemSemanticsProblem> semanticsProblems = this.itemRegistry.stream().flatMap(item -> { List<ItemSemanticsProblem> semanticsProblems = this.itemRegistry.stream().flatMap(item -> {
return semanticsService.getItemSemanticsProblems(item).stream() return semanticsService.getItemSemanticsProblems(item).stream()
.map(p -> p.setEditable(isEditable(p.item()))); .map(p -> p.setEditable(isItemEditable(p.item())));
}).toList(); }).toList();
return JSONResponse.createResponse(Status.OK, semanticsProblems, null); return JSONResponse.createResponse(Status.OK, semanticsProblems, null);
} }
@@ -1061,7 +1061,11 @@ public class ItemResource implements RESTResource {
} }
private boolean isEditable(EnrichedItemDTO item) { private boolean isEditable(EnrichedItemDTO item) {
return managedItemProvider.get(item.name) != null; return isItemEditable(item.name);
}
private boolean isItemEditable(String itemName) {
return managedItemProvider.get(itemName) != null;
} }
private boolean isEditable(MetadataKey metadataKey) { private boolean isEditable(MetadataKey metadataKey) {
+1
View File
@@ -26,6 +26,7 @@
<module>org.openhab.core.automation</module> <module>org.openhab.core.automation</module>
<module>org.openhab.core.automation.module.media</module> <module>org.openhab.core.automation.module.media</module>
<module>org.openhab.core.automation.module.script</module> <module>org.openhab.core.automation.module.script</module>
<module>org.openhab.core.automation.module.script.providersupport</module>
<module>org.openhab.core.automation.module.script.rulesupport</module> <module>org.openhab.core.automation.module.script.rulesupport</module>
<module>org.openhab.core.automation.rest</module> <module>org.openhab.core.automation.rest</module>
<module>org.openhab.core.config.core</module> <module>org.openhab.core.config.core</module>
@@ -142,6 +142,12 @@
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.automation.module.script/${project.version}</bundle> <bundle>mvn:org.openhab.core.bundles/org.openhab.core.automation.module.script/${project.version}</bundle>
</feature> </feature>
<feature name="openhab-core-automation-module-script-providersupport" version="${project.version}">
<feature>openhab-core-base</feature>
<feature dependency="true">openhab-core-automation-module-script</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.automation.module.script.providersupport/${project.version}</bundle>
</feature>
<feature name="openhab-core-automation-module-script-rulesupport" version="${project.version}"> <feature name="openhab-core-automation-module-script-rulesupport" version="${project.version}">
<feature>openhab-core-base</feature> <feature>openhab-core-base</feature>
<feature dependency="true">openhab-core-automation-module-script</feature> <feature dependency="true">openhab-core-automation-module-script</feature>
@@ -448,6 +454,7 @@
<feature>openhab-core-auth-jaas</feature> <feature>openhab-core-auth-jaas</feature>
<feature>openhab-core-automation-rest</feature> <feature>openhab-core-automation-rest</feature>
<feature>openhab-core-automation-module-script</feature> <feature>openhab-core-automation-module-script</feature>
<feature>openhab-core-automation-module-script-providersupport</feature>
<feature>openhab-core-automation-module-script-rulesupport</feature> <feature>openhab-core-automation-module-script-rulesupport</feature>
<feature>openhab-core-automation-module-media</feature> <feature>openhab-core-automation-module-media</feature>
<feature>openhab-core-io-console-karaf</feature> <feature>openhab-core-io-console-karaf</feature>