[automation] Add provider script extension (#4513)

* [automation] Add provider script extension

This new script extension allow scripts to provide openHAB entities like Items without needing to manually handle the lifecycle of those.
First, we will only provide an itemRegistry, but this can easily be extended later.

Signed-off-by: Florian Hotze <dev@florianhotze.com>
pull/4491/merge
Florian Hotze 2025-06-14 15:46:59 +03:00 committed by GitHub
parent 4e948db729
commit f695acfc4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 683 additions and 0 deletions

View File

@ -0,0 +1,133 @@
/*
* 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();
}
}
}
}

View File

@ -0,0 +1,201 @@
/*
* 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.shared;
import java.util.ArrayList;
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.common.registry.RegistryChangeListener;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemNotUniqueException;
import org.openhab.core.items.ItemRegistry;
/**
* The {@link ProviderItemRegistryDelegate} is wrapping a {@link ItemRegistry} to provide a comfortable way to provide
* items from scripts without worrying about the need to remove items again when the script is unloaded.
* Nonetheless, using the {@link #addPermanent(Item)} method it is still possible to add items permanently.
* <p>
* Use a new instance of this class for each {@link javax.script.ScriptEngine}.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
public class ProviderItemRegistryDelegate implements ItemRegistry {
private final ItemRegistry itemRegistry;
private final Set<String> items = new HashSet<>();
private final ScriptedItemProvider itemProvider;
public ProviderItemRegistryDelegate(ItemRegistry itemRegistry, ScriptedItemProvider itemProvider) {
this.itemRegistry = itemRegistry;
this.itemProvider = itemProvider;
}
@Override
public void addRegistryChangeListener(RegistryChangeListener<Item> listener) {
itemRegistry.addRegistryChangeListener(listener);
}
@Override
public Collection<Item> getAll() {
return itemRegistry.getAll();
}
@Override
public Stream<Item> stream() {
return itemRegistry.stream();
}
@Override
public @Nullable Item get(String key) {
return itemRegistry.get(key);
}
@Override
public void removeRegistryChangeListener(RegistryChangeListener<Item> listener) {
itemRegistry.removeRegistryChangeListener(listener);
}
@Override
public Item add(Item element) {
String itemName = element.getName();
// Check for item already existing here because the item might exist in a different provider, so we need to
// check the registry and not only the provider itself
if (get(itemName) != null) {
throw new IllegalArgumentException(
"Cannot add item, because an item with same name (" + itemName + ") already exists.");
}
itemProvider.add(element);
items.add(itemName);
return element;
}
/**
* Add an item permanently to the registry.
* This item will be kept in the registry even if the script is unloaded.
*
* @param element the item to be added (must not be null)
* @return the added item
*/
public Item addPermanent(Item element) {
return itemRegistry.add(element);
}
@Override
public @Nullable Item update(Item element) {
if (items.contains(element.getName())) {
return itemProvider.update(element);
}
return itemRegistry.update(element);
}
@Override
public @Nullable Item remove(String key) {
if (items.remove(key)) {
return itemProvider.remove(key);
}
return itemRegistry.remove(key);
}
@Override
public Item getItem(String name) throws ItemNotFoundException {
return itemRegistry.getItem(name);
}
@Override
public Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException {
return itemRegistry.getItemByPattern(name);
}
@Override
public Collection<Item> getItems() {
return itemRegistry.getItems();
}
@Override
public Collection<Item> getItemsOfType(String type) {
return itemRegistry.getItemsOfType(type);
}
@Override
public Collection<Item> getItems(String pattern) {
return itemRegistry.getItems(pattern);
}
@Override
public Collection<Item> getItemsByTag(String... tags) {
return itemRegistry.getItemsByTag(tags);
}
@Override
public Collection<Item> getItemsByTagAndType(String type, String... tags) {
return itemRegistry.getItemsByTagAndType(type, tags);
}
@Override
public <T extends Item> Collection<T> getItemsByTag(Class<T> typeFilter, String... tags) {
return itemRegistry.getItemsByTag(typeFilter, tags);
}
@Override
public @Nullable Item remove(String itemName, boolean recursive) {
Item item = get(itemName);
if (recursive && item instanceof GroupItem groupItem) {
for (String member : getMemberNamesRecursively(groupItem, getAll())) {
remove(member);
}
}
if (item != null) {
remove(item.getName());
return item;
} else {
return null;
}
}
/**
* Removes all items that are provided by this script.
* To be called when the script is unloaded or reloaded.
*/
public void removeAllAddedByScript() {
for (String item : items) {
itemProvider.remove(item);
}
items.clear();
}
private List<String> getMemberNamesRecursively(GroupItem groupItem, Collection<Item> allItems) {
List<String> memberNames = new ArrayList<>();
for (Item item : allItems) {
if (item.getGroupNames().contains(groupItem.getName())) {
memberNames.add(item.getName());
if (item instanceof GroupItem groupItem1) {
memberNames.addAll(getMemberNamesRecursively(groupItem1, allItems));
}
}
}
return memberNames;
}
}

View File

@ -0,0 +1,159 @@
/*
* 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.shared;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
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.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.Configuration;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* The {@link ProviderThingRegistryDelegate} is wrapping a {@link ThingRegistry} to provide a comfortable way to provide
* Things from scripts without worrying about the need to remove Things again when the script is unloaded.
* Nonetheless, using the {@link #addPermanent(Thing)} method it is still possible to add Things permanently.
* <p>
* Use a new instance of this class for each {@link javax.script.ScriptEngine}.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
public class ProviderThingRegistryDelegate implements ThingRegistry {
private final ThingRegistry thingRegistry;
private final Set<ThingUID> things = new HashSet<>();
private final ScriptedThingProvider thingProvider;
public ProviderThingRegistryDelegate(ThingRegistry thingRegistry, ScriptedThingProvider thingProvider) {
this.thingRegistry = thingRegistry;
this.thingProvider = thingProvider;
}
@Override
public void addRegistryChangeListener(RegistryChangeListener<Thing> listener) {
thingRegistry.addRegistryChangeListener(listener);
}
@Override
public Collection<Thing> getAll() {
return thingRegistry.getAll();
}
@Override
public Stream<Thing> stream() {
return thingRegistry.stream();
}
@Override
public void removeRegistryChangeListener(RegistryChangeListener<Thing> listener) {
thingRegistry.removeRegistryChangeListener(listener);
}
@Override
public Thing add(Thing element) {
ThingUID thingUID = element.getUID();
// Check for Thing already existing here because the Thing might exist in a different provider, so we need to
// check the registry and not only the provider itself
if (get(thingUID) != null) {
throw new IllegalArgumentException(
"Cannot add Thing, because a Thing with same UID (" + thingUID + ") already exists.");
}
thingProvider.add(element);
things.add(thingUID);
return element;
}
/**
* Add a Thing permanently to the registry.
* This Thing will be kept in the registry even if the script is unloaded.
*
* @param element the Thing to be added (must not be null)
* @return the added Thing
*/
public Thing addPermanent(Thing element) {
return thingRegistry.add(element);
}
@Override
public @Nullable Thing update(Thing element) {
if (things.contains(element.getUID())) {
return thingProvider.update(element);
}
return thingRegistry.update(element);
}
@Override
public @Nullable Thing get(ThingUID uid) {
return thingRegistry.get(uid);
}
@Override
public @Nullable Channel getChannel(ChannelUID channelUID) {
return thingRegistry.getChannel(channelUID);
}
@Override
public void updateConfiguration(ThingUID thingUID, Map<String, Object> configurationParameters) {
thingRegistry.updateConfiguration(thingUID, configurationParameters);
}
@Override
public @Nullable Thing remove(ThingUID thingUID) {
// Give the ThingHandler the chance to perform any removal operations instead of forcefully removing from
// ScriptedThingProvider
// If the Thing was provided by ScriptedThingProvider, it will be removed from there by listening to
// ThingStatusEvent for ThingStatus.REMOVED in ScriptedThingProvider
return thingRegistry.remove(thingUID);
}
/**
* Removes all Things that are provided by this script.
* To be called when the script is unloaded or reloaded.
*/
public void removeAllAddedByScript() {
for (ThingUID thing : things) {
thingProvider.remove(thing);
}
things.clear();
}
@Override
public @Nullable Thing forceRemove(ThingUID thingUID) {
if (things.remove(thingUID)) {
return thingProvider.remove(thingUID);
}
return thingRegistry.forceRemove(thingUID);
}
@Override
public @Nullable Thing createThingOfType(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
@Nullable ThingUID bridgeUID, @Nullable String label, Configuration configuration) {
return thingRegistry.createThingOfType(thingTypeUID, thingUID, bridgeUID, label, configuration);
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.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.items.Item;
import org.openhab.core.items.ItemProvider;
import org.openhab.core.items.ItemUtil;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link ItemProvider} keeps items provided by scripts during runtime.
* This ensures that items are not kept on reboot, but have to be provided by the scripts again.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { ScriptedItemProvider.class, ItemProvider.class })
public class ScriptedItemProvider extends AbstractProvider<Item>
implements ItemProvider, ManagedProvider<Item, String> {
private final Logger logger = LoggerFactory.getLogger(ScriptedItemProvider.class);
private final Map<String, Item> items = new HashMap<>();
@Override
public Collection<Item> getAll() {
return items.values();
}
@Override
public @Nullable Item get(String itemName) {
return items.get(itemName);
}
@Override
public void add(Item item) {
if (!ItemUtil.isValidItemName(item.getName())) {
throw new IllegalArgumentException("The item name '" + item.getName() + "' is invalid.");
}
if (items.get(item.getName()) != null) {
throw new IllegalArgumentException(
"Cannot add item, because an item with same name (" + item.getName() + ") already exists.");
}
items.put(item.getName(), item);
notifyListenersAboutAddedElement(item);
}
@Override
public @Nullable Item update(Item item) {
Item oldItem = items.get(item.getName());
if (oldItem != null) {
items.put(item.getName(), item);
notifyListenersAboutUpdatedElement(oldItem, item);
} else {
logger.warn("Could not update item with name '{}', because it does not exist.", item.getName());
}
return oldItem;
}
@Override
public @Nullable Item remove(String itemName) {
Item item = items.remove(itemName);
if (item != null) {
notifyListenersAboutRemovedElement(item);
}
return item;
}
}

View File

@ -0,0 +1,103 @@
/*
* 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.shared;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
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.events.Event;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingProvider;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.events.ThingStatusInfoEvent;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link ThingProvider} keeps things provided by scripts during runtime.
* This ensures that things are not kept on reboot, but have to be provided by the scripts again.
*
* @author Florian Hotze - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { ScriptedThingProvider.class, ThingProvider.class, EventSubscriber.class })
public class ScriptedThingProvider extends AbstractProvider<Thing>
implements ThingProvider, ManagedProvider<Thing, ThingUID>, EventSubscriber {
private final Logger logger = LoggerFactory.getLogger(ScriptedThingProvider.class);
private final Map<ThingUID, Thing> things = new HashMap<>();
@Override
public Collection<Thing> getAll() {
return things.values();
}
@Override
public @Nullable Thing get(ThingUID uid) {
return things.get(uid);
}
@Override
public void add(Thing thing) {
if (things.get(thing.getUID()) != null) {
throw new IllegalArgumentException(
"Cannot add thing, because a thing with same UID (" + thing.getUID() + ") already exists.");
}
things.put(thing.getUID(), thing);
notifyListenersAboutAddedElement(thing);
}
@Override
public @Nullable Thing update(Thing thing) {
Thing oldThing = things.get(thing.getUID());
if (oldThing != null) {
things.put(thing.getUID(), thing);
notifyListenersAboutUpdatedElement(oldThing, thing);
} else {
logger.warn("Cannot update thing with UID '{}', because it does not exist.", thing.getUID());
}
return oldThing;
}
@Override
public @Nullable Thing remove(ThingUID uid) {
Thing thing = things.remove(uid);
if (thing != null) {
notifyListenersAboutRemovedElement(thing);
}
return thing;
}
@Override
public Set<String> getSubscribedEventTypes() {
return Set.of(ThingStatusInfoEvent.TYPE);
}
@Override
public void receive(Event event) {
if (event instanceof ThingStatusInfoEvent thingStatusInfoEvent) {
if (thingStatusInfoEvent.getStatusInfo().getStatus() == ThingStatus.REMOVED) {
remove(thingStatusInfoEvent.getThingUID());
}
}
}
}