diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ProviderScriptExtension.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ProviderScriptExtension.java new file mode 100644 index 0000000000..6ce1d34b7d --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ProviderScriptExtension.java @@ -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> 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 getDefaultPresets() { + return Set.of(); + } + + @Override + public Collection getPresets() { + return Set.of(PRESET_NAME); + } + + @Override + public Collection getTypes() { + return Set.of(ITEM_REGISTRY_NAME, THING_REGISTRY_NAME); + } + + @Override + public @Nullable Object get(String scriptIdentifier, String type) throws IllegalArgumentException { + Map 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 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 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(); + } + } + } +} diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ProviderItemRegistryDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ProviderItemRegistryDelegate.java new file mode 100644 index 0000000000..4d2d9f2471 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ProviderItemRegistryDelegate.java @@ -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. + *

+ * 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 items = new HashSet<>(); + + private final ScriptedItemProvider itemProvider; + + public ProviderItemRegistryDelegate(ItemRegistry itemRegistry, ScriptedItemProvider itemProvider) { + this.itemRegistry = itemRegistry; + this.itemProvider = itemProvider; + } + + @Override + public void addRegistryChangeListener(RegistryChangeListener listener) { + itemRegistry.addRegistryChangeListener(listener); + } + + @Override + public Collection getAll() { + return itemRegistry.getAll(); + } + + @Override + public Stream stream() { + return itemRegistry.stream(); + } + + @Override + public @Nullable Item get(String key) { + return itemRegistry.get(key); + } + + @Override + public void removeRegistryChangeListener(RegistryChangeListener 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 getItems() { + return itemRegistry.getItems(); + } + + @Override + public Collection getItemsOfType(String type) { + return itemRegistry.getItemsOfType(type); + } + + @Override + public Collection getItems(String pattern) { + return itemRegistry.getItems(pattern); + } + + @Override + public Collection getItemsByTag(String... tags) { + return itemRegistry.getItemsByTag(tags); + } + + @Override + public Collection getItemsByTagAndType(String type, String... tags) { + return itemRegistry.getItemsByTagAndType(type, tags); + } + + @Override + public Collection getItemsByTag(Class 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 getMemberNamesRecursively(GroupItem groupItem, Collection allItems) { + List 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; + } +} diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ProviderThingRegistryDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ProviderThingRegistryDelegate.java new file mode 100644 index 0000000000..2ca8faa960 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ProviderThingRegistryDelegate.java @@ -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. + *

+ * 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 things = new HashSet<>(); + + private final ScriptedThingProvider thingProvider; + + public ProviderThingRegistryDelegate(ThingRegistry thingRegistry, ScriptedThingProvider thingProvider) { + this.thingRegistry = thingRegistry; + this.thingProvider = thingProvider; + } + + @Override + public void addRegistryChangeListener(RegistryChangeListener listener) { + thingRegistry.addRegistryChangeListener(listener); + } + + @Override + public Collection getAll() { + return thingRegistry.getAll(); + } + + @Override + public Stream stream() { + return thingRegistry.stream(); + } + + @Override + public void removeRegistryChangeListener(RegistryChangeListener 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 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); + } +} diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedItemProvider.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedItemProvider.java new file mode 100644 index 0000000000..d50601a9bf --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedItemProvider.java @@ -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 + implements ItemProvider, ManagedProvider { + private final Logger logger = LoggerFactory.getLogger(ScriptedItemProvider.class); + private final Map items = new HashMap<>(); + + @Override + public Collection 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; + } +} diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedThingProvider.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedThingProvider.java new file mode 100644 index 0000000000..9863f78269 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedThingProvider.java @@ -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 + implements ThingProvider, ManagedProvider, EventSubscriber { + private final Logger logger = LoggerFactory.getLogger(ScriptedThingProvider.class); + private final Map things = new HashMap<>(); + + @Override + public Collection 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 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()); + } + } + } +}