diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/action/BusEvent.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/action/BusEvent.java new file mode 100644 index 00000000000..f71d1762ac7 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/action/BusEvent.java @@ -0,0 +1,134 @@ +/* + * 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.action; + +import java.time.ZonedDateTime; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; + +/** + * The {@link BusEvent} allows write access to the openHAB event bus from within scripts. + * Items should not be updated directly (setting the state property), but updates should + * be sent to the bus, so that all interested bundles are notified. + * + * @author Florian Hotze - Initial contribution + */ +@NonNullByDefault +public interface BusEvent { + /** + * Sends a command for a specified item to the event bus. + * + * @param item the item to send the command to + * @param commandString the command to send + */ + void sendCommand(Item item, String commandString); + + /** + * Sends a number as a command for a specified item to the event bus. + * + * @param item the item to send the command to + * @param command the number to send as a command + */ + void sendCommand(Item item, Number command); + + /** + * Sends a command for a specified item to the event bus. + * + * @param itemName the name of the item to send the command to + * @param commandString the command to send + */ + void sendCommand(String itemName, String commandString); + + /** + * Sends a command for a specified item to the event bus. + * + * @param item the item to send the command to + * @param command the command to send + */ + void sendCommand(Item item, Command command); + + /** + * Posts a status update for a specified item to the event bus. + * + * @param item the item to send the status update for + * @param stateString the new state of the item + */ + void postUpdate(Item item, String stateString); + + /** + * Posts a status update for a specified item to the event bus. + * + * @param item the item to send the status update for + * @param state the new state of the item as a number + */ + void postUpdate(Item item, Number state); + + /** + * Posts a status update for a specified item to the event bus. + * + * @param itemName the name of the item to send the status update for + * @param stateString the new state of the item + */ + void postUpdate(String itemName, String stateString); + + /** + * Posts a status update for a specified item to the event bus. + * + * @param item the item to send the status update for + * @param state the new state of the item + */ + void postUpdate(Item item, State state); + + /** + * Sends a time series to the event bus + * + * @param item the item to send the time series for + * @param timeSeries a {@link TimeSeries} containing policy and values + */ + void sendTimeSeries(@Nullable Item item, @Nullable TimeSeries timeSeries); + + /** + * Sends a time series to the event bus + * + * @param itemName the name of the item to send the status update for + * @param values a {@link Map} containing the timeseries, composed of pairs of {@link ZonedDateTime} and + * {@link State} + * @param policy either ADD or REPLACE + */ + void sendTimeSeries(@Nullable String itemName, @Nullable Map values, String policy); + + /** + * Stores the current states for a list of items in a map. + * A group item is not itself put into the map, but instead all its members. + * + * @param items the items for which the state should be stored + * @return the map of items with their states + */ + Map storeStates(Item... items); + + /** + * Restores item states from a map. + * If the saved state can be interpreted as a command, a command is sent for the item + * (and the physical device can send a status update if occurred). If it is no valid + * command, the item state is directly updated to the saved value. + * + * @param statesMap a map with ({@link Item}, {@link State}) entries + */ + void restoreStates(Map statesMap); +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/BusEventImpl.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/BusEventImpl.java new file mode 100644 index 00000000000..69da65df12a --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/BusEventImpl.java @@ -0,0 +1,190 @@ +/* + * 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.internal.action; + +import java.time.ZonedDateTime; +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.module.script.action.BusEvent; +import org.openhab.core.events.EventPublisher; +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.events.ItemEventFactory; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; +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.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The static methods of this class are made available as functions in the scripts. + * This allows a script to write to the openHAB event bus. + * + * @author Kai Kreuzer - Initial contribution + */ +@Component(immediate = true, service = BusEvent.class) +@NonNullByDefault +public class BusEventImpl implements BusEvent { + + private final Logger logger = LoggerFactory.getLogger(BusEventImpl.class); + private final ItemRegistry itemRegistry; + private final EventPublisher publisher; + + @Activate + public BusEventImpl(final @Reference ItemRegistry itemRegistry, final @Reference EventPublisher publisher) { + this.itemRegistry = itemRegistry; + this.publisher = publisher; + } + + @Override + public void sendCommand(Item item, String commandString) { + if (item != null) { + sendCommand(item.getName(), commandString); + } + } + + @Override + public void sendCommand(Item item, Number command) { + if (item != null && command != null) { + sendCommand(item.getName(), command.toString()); + } + } + + @Override + public void sendCommand(String itemName, String commandString) { + try { + Item item = itemRegistry.getItem(itemName); + Command command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString); + if (command != null) { + publisher.post(ItemEventFactory.createCommandEvent(itemName, command)); + } else { + logger.warn("Cannot convert '{}' to a command type which item '{}' accepts: {}.", commandString, + itemName, getAcceptedCommandNames(item)); + } + } catch (ItemNotFoundException e) { + logger.warn("Item '{}' does not exist.", itemName); + } + } + + private static List getAcceptedCommandNames(Item item) { + return item.getAcceptedCommandTypes().stream().map(Class::getSimpleName).toList(); + } + + @Override + public void sendCommand(Item item, Command command) { + if (item != null) { + publisher.post(ItemEventFactory.createCommandEvent(item.getName(), command)); + } + } + + @Override + public void postUpdate(Item item, String stateString) { + if (item != null) { + postUpdate(item.getName(), stateString); + } + } + + @Override + public void postUpdate(Item item, Number state) { + if (item != null && state != null) { + postUpdate(item.getName(), state.toString()); + } + } + + private static List getAcceptedDataTypeNames(Item item) { + return item.getAcceptedDataTypes().stream().map(Class::getSimpleName).toList(); + } + + @Override + public void postUpdate(String itemName, String stateString) { + try { + Item item = itemRegistry.getItem(itemName); + State state = TypeParser.parseState(item.getAcceptedDataTypes(), stateString); + if (state != null) { + publisher.post(ItemEventFactory.createStateEvent(itemName, state)); + } else { + logger.warn("Cannot convert '{}' to a state type which item '{}' accepts: {}.", stateString, itemName, + getAcceptedDataTypeNames(item)); + } + } catch (ItemNotFoundException e) { + logger.warn("Item '{}' does not exist.", itemName); + } + } + + @Override + public void postUpdate(Item item, State state) { + if (item != null) { + publisher.post(ItemEventFactory.createStateEvent(item.getName(), state)); + } + } + + @Override + public void sendTimeSeries(@Nullable Item item, @Nullable TimeSeries timeSeries) { + if (item != null && timeSeries != null) { + publisher.post(ItemEventFactory.createTimeSeriesEvent(item.getName(), timeSeries, null)); + } + } + + @Override + public void sendTimeSeries(@Nullable String itemName, @Nullable Map values, String policy) { + if (itemName != null && values != null) { + try { + TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.valueOf(policy)); + values.forEach((key, value) -> timeSeries.add(key.toInstant(), value)); + publisher.post(ItemEventFactory.createTimeSeriesEvent(itemName, timeSeries, null)); + } catch (IllegalArgumentException e) { + logger.warn("Policy '{}' does not exist.", policy); + } + } + } + + @Override + public Map storeStates(Item... items) { + Map statesMap = new HashMap<>(); + if (items != null) { + for (Item item : items) { + if (item instanceof GroupItem groupItem) { + for (Item member : groupItem.getAllMembers()) { + statesMap.put(member, member.getState()); + } + } else { + statesMap.put(item, item.getState()); + } + } + } + return statesMap; + } + + @Override + public void restoreStates(Map statesMap) { + if (statesMap != null) { + for (Map.Entry entry : statesMap.entrySet()) { + if (entry.getValue() instanceof Command) { + sendCommand(entry.getKey(), (Command) entry.getValue()); + } else { + postUpdate(entry.getKey(), entry.getValue()); + } + } + } + } +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/ScriptActionScriptScopeProvider.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/ScriptActionScriptScopeProvider.java index 4d2b3ef31c3..1b6654c512d 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/ScriptActionScriptScopeProvider.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/action/ScriptActionScriptScopeProvider.java @@ -19,6 +19,7 @@ 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.action.BusEvent; import org.openhab.core.automation.module.script.action.ScriptExecution; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -38,8 +39,9 @@ public class ScriptActionScriptScopeProvider implements ScriptExtensionProvider private final Map elements; @Activate - public ScriptActionScriptScopeProvider(final @Reference ScriptExecution scriptExecution) { - elements = Map.of("scriptExecution", scriptExecution); + public ScriptActionScriptScopeProvider(final @Reference BusEvent busEvent, + final @Reference ScriptExecution scriptExecution) { + elements = Map.of("busEvent", busEvent, "scriptExecution", scriptExecution); } @Override @@ -72,6 +74,6 @@ public Map importPreset(String scriptIdentifier, String preset) @Override public void unload(String scriptIdentifier) { - // nothing todo + // nothing to do } } 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 06af2b2c3ac..b5b0a0cd2db 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 @@ -13,263 +13,79 @@ package org.openhab.core.model.script.actions; import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.events.EventPublisher; -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.events.ItemEventFactory; -import org.openhab.core.model.script.ScriptServiceUtil; +import org.openhab.core.model.script.internal.engine.action.BusEventActionService; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TimeSeries; -import org.openhab.core.types.TypeParser; -import org.slf4j.LoggerFactory; /** - * The static methods of this class are made available as functions in the scripts. - * This gives direct write access to the openHAB event bus from within scripts. - * Items should not be updated directly (setting the state property), but updates should - * be sent to the bus, so that all interested bundles are notified. + * The {@link BusEvent} is a wrapper for the BusEvent actions. * - * @author Kai Kreuzer - Initial contribution - * @author Stefan Bußweiler - Migration to new ESH event concept + * @author Florian Hotze - Initial contribution */ public class BusEvent { - /** - * Sends a command for a specified item to the event bus. - * - * @param item the item to send the command to - * @param commandString the command to send - */ public static Object sendCommand(Item item, String commandString) { - if (item != null) { - return sendCommand(item.getName(), commandString); - } else { - return null; - } + BusEventActionService.getBusEvent().sendCommand(item, commandString); + return null; } - /** - * Sends a number as a command for a specified item to the event bus. - * - * @param item the item to send the command to - * @param number the number to send as a command - */ public static Object sendCommand(Item item, Number number) { - if (item != null && number != null) { - return sendCommand(item.getName(), number.toString()); - } else { - return null; - } + BusEventActionService.getBusEvent().sendCommand(item, number); + return null; } - /** - * Sends a command for a specified item to the event bus. - * - * @param itemName the name of the item to send the command to - * @param commandString the command to send - */ public static Object sendCommand(String itemName, String commandString) { - ItemRegistry registry = ScriptServiceUtil.getItemRegistry(); - EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); - if (publisher != null && registry != null) { - try { - Item item = registry.getItem(itemName); - Command command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString); - if (command != null) { - publisher.post(ItemEventFactory.createCommandEvent(itemName, command)); - } else { - LoggerFactory.getLogger(BusEvent.class).warn( - "Cannot convert '{}' to a command type which item '{}' accepts: {}.", commandString, - itemName, getAcceptedCommandNames(item)); - } - - } catch (ItemNotFoundException e) { - LoggerFactory.getLogger(BusEvent.class).warn("Item '{}' does not exist.", itemName); - } - } + BusEventActionService.getBusEvent().sendCommand(itemName, commandString); return null; } - private static List getAcceptedCommandNames(Item item) { - return item.getAcceptedCommandTypes().stream().map(Class::getSimpleName).toList(); - } - - /** - * Sends a command for a specified item to the event bus. - * - * @param item the item to send the command to - * @param command the command to send - */ public static Object sendCommand(Item item, Command command) { - EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); - if (publisher != null && item != null) { - publisher.post(ItemEventFactory.createCommandEvent(item.getName(), command)); - } + BusEventActionService.getBusEvent().sendCommand(item, command); return null; } - /** - * Posts a status update for a specified item to the event bus. - * - * @param item the item to send the status update for - * @param state the new state of the item as a number - */ public static Object postUpdate(Item item, Number state) { - if (item != null && state != null) { - return postUpdate(item.getName(), state.toString()); - } else { - return null; - } + BusEventActionService.getBusEvent().postUpdate(item, state); + return null; } - /** - * Posts a status update for a specified item to the event bus. - * - * @param item the item to send the status update for - * @param stateAsString the new state of the item - */ public static Object postUpdate(Item item, String stateAsString) { - if (item != null) { - return postUpdate(item.getName(), stateAsString); - } else { - return null; - } + BusEventActionService.getBusEvent().postUpdate(item, stateAsString); + return null; } - /** - * Posts a status update for a specified item to the event bus. - * - * @param itemName the name of the item to send the status update for - * @param stateString the new state of the item - */ public static Object postUpdate(String itemName, String stateString) { - ItemRegistry registry = ScriptServiceUtil.getItemRegistry(); - EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); - if (publisher != null && registry != null) { - try { - Item item = registry.getItem(itemName); - State state = TypeParser.parseState(item.getAcceptedDataTypes(), stateString); - if (state != null) { - publisher.post(ItemEventFactory.createStateEvent(itemName, state)); - } else { - LoggerFactory.getLogger(BusEvent.class).warn( - "Cannot convert '{}' to a state type which item '{}' accepts: {}.", stateString, itemName, - getAcceptedDataTypeNames(item)); - } - } catch (ItemNotFoundException e) { - LoggerFactory.getLogger(BusEvent.class).warn("Item '{}' does not exist.", itemName); - } - } + BusEventActionService.getBusEvent().postUpdate(itemName, stateString); return null; } - /** - * Sends a time series to the event bus - * - * @param item the item to send the time series for - * @param timeSeries a {@link TimeSeries} containing policy and values - */ public static Object sendTimeSeries(@Nullable Item item, @Nullable TimeSeries timeSeries) { - EventPublisher eventPublisher1 = ScriptServiceUtil.getEventPublisher(); - if (eventPublisher1 != null && item != null && timeSeries != null) { - eventPublisher1.post(ItemEventFactory.createTimeSeriesEvent(item.getName(), timeSeries, null)); - } + BusEventActionService.getBusEvent().sendTimeSeries(item, timeSeries); return null; } - /** - * Sends a time series to the event bus - * - * @param itemName the name of the item to send the status update for - * @param values a {@link Map} containing the timeseries, composed of pairs of {@link ZonedDateTime} and - * {@link State} - * @param policy either ADD or REPLACE - */ public static Object sendTimeSeries(@Nullable String itemName, @Nullable Map values, String policy) { - EventPublisher eventPublisher1 = ScriptServiceUtil.getEventPublisher(); - if (eventPublisher1 != null && itemName != null && values != null && policy != null) { - try { - TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.valueOf(policy)); - values.forEach((key, value) -> timeSeries.add(key.toInstant(), value)); - eventPublisher1.post(ItemEventFactory.createTimeSeriesEvent(itemName, timeSeries, null)); - } catch (IllegalArgumentException e) { - LoggerFactory.getLogger(BusEvent.class).warn("Policy '{}' does not exist.", policy); - } - } + BusEventActionService.getBusEvent().sendTimeSeries(itemName, values, policy); return null; } - private static List getAcceptedDataTypeNames(Item item) { - return item.getAcceptedDataTypes().stream().map(Class::getSimpleName).toList(); - } - - /** - * Posts a status update for a specified item to the event bus. - * t - * - * @param item the item to send the status update for - * @param state the new state of the item - */ public static Object postUpdate(Item item, State state) { - EventPublisher publisher = ScriptServiceUtil.getEventPublisher(); - if (publisher != null && item != null) { - publisher.post(ItemEventFactory.createStateEvent(item.getName(), state)); - } + BusEventActionService.getBusEvent().postUpdate(item, state); return null; } - /** - * Stores the current states for a list of items in a map. - * A group item is not itself put into the map, but instead all its members. - * - * @param items the items for which the state should be stored - * @return the map of items with their states - */ public static Map storeStates(Item... items) { - Map statesMap = new HashMap<>(); - if (items != null) { - for (Item item : items) { - if (item instanceof GroupItem groupItem) { - for (Item member : groupItem.getAllMembers()) { - statesMap.put(member, member.getState()); - } - } else { - statesMap.put(item, item.getState()); - } - } - } - return statesMap; + return BusEventActionService.getBusEvent().storeStates(items); } - /** - * Restores item states from a map. - * If the saved state can be interpreted as a command, a command is sent for the item - * (and the physical device can send a status update if occurred). If it is no valid - * command, the item state is directly updated to the saved value. - * - * @param statesMap a map with ({@link Item}, {@link State}) entries - * @return null - */ public static Object restoreStates(Map statesMap) { - if (statesMap != null) { - for (Entry entry : statesMap.entrySet()) { - if (entry.getValue() instanceof Command) { - sendCommand(entry.getKey(), (Command) entry.getValue()); - } else { - postUpdate(entry.getKey(), entry.getValue()); - } - } - } + BusEventActionService.getBusEvent().restoreStates(statesMap); return null; } } diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/BusEventActionService.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/BusEventActionService.java new file mode 100644 index 00000000000..bf2c51a68b6 --- /dev/null +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/action/BusEventActionService.java @@ -0,0 +1,49 @@ +/* + * 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.model.script.internal.engine.action; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.module.script.action.BusEvent; +import org.openhab.core.model.script.engine.action.ActionService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This class registers an OSGi service for the BusEvent action. + * + * @author Florian Hotze - Initial contribution + */ +@Component(immediate = true) +@NonNullByDefault +public class BusEventActionService implements ActionService { + + private static @Nullable BusEvent busEvent; + + @Activate + public BusEventActionService(final @Reference BusEvent busEvent) { + BusEventActionService.busEvent = busEvent; + } + + @Override + public Class getActionClass() { + return BusEvent.class; + } + + public static BusEvent getBusEvent() { + return Objects.requireNonNull(busEvent); + } +}