Refactor BusEvent to ScriptExtension (#5064)

Comparable to #3155.

JSR-223 automation add-ons often use the script actions from the org.openhab.core.model.script bundle.
The downside is that the actions are tied to XText or at least use some hacks to provide static access to OSGi services.

This refactors the BusEvent actions, making them available as ScriptExtension via import-preset ScriptAction.
The actions are wrapped for DSL rules, to stay backward compatible.

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze
2025-11-10 22:49:19 +01:00
committed by GitHub
parent 43f16e73bb
commit ed8f54a41c
5 changed files with 397 additions and 206 deletions
@@ -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 <code>ADD</code> or <code>REPLACE</code>
*/
void sendTimeSeries(@Nullable String itemName, @Nullable Map<ZonedDateTime, State> 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<Item, State> 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<Item, State> statesMap);
}
@@ -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 <T extends State> List<String> 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 <T extends State> List<String> 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<ZonedDateTime, State> 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<Item, State> storeStates(Item... items) {
Map<Item, State> 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<Item, State> statesMap) {
if (statesMap != null) {
for (Map.Entry<Item, State> entry : statesMap.entrySet()) {
if (entry.getValue() instanceof Command) {
sendCommand(entry.getKey(), (Command) entry.getValue());
} else {
postUpdate(entry.getKey(), entry.getValue());
}
}
}
}
}
@@ -19,6 +19,7 @@ import java.util.Set;
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<String, Object> 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 class ScriptActionScriptScopeProvider implements ScriptExtensionProvider
@Override
public void unload(String scriptIdentifier) {
// nothing todo
// nothing to do
}
}
@@ -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 <T extends State> List<String> 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 <code>ADD</code> or <code>REPLACE</code>
*/
public static Object sendTimeSeries(@Nullable String itemName, @Nullable Map<ZonedDateTime, State> 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 <T extends State> List<String> 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<Item, State> storeStates(Item... items) {
Map<Item, State> 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<Item, State> statesMap) {
if (statesMap != null) {
for (Entry<Item, State> 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;
}
}
@@ -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);
}
}