Skip to content

Commit d06980d

Browse files
authored
[pythonscripting] Simplify configuration and update helper lib to version 1.0.18 (#20289)
Signed-off-by: Holger Hees <holger.hees@gmail.com>
1 parent c439b87 commit d06980d

6 files changed

Lines changed: 127 additions & 109 deletions

File tree

bundles/org.openhab.automation.pythonscripting/README.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,29 +70,21 @@ Additionally, you can configure the Add-on via a config file `/openhab/services/
7070
If you use the marketplace version of this Add-on, it is necessary to use the config file. OpenHAB has a bug which prevents the web based config dialog to work correctly for `kar` file based Add-ons.
7171
:::
7272

73-
```text
74-
# Use scope and import wrapper
75-
#
76-
# This enables a scope module and and import wrapper.
77-
# A scope module is an encapsulated module containing all openHAB jsr223 objects and can be imported with <code>import scope</code>
78-
# Additionally you can run an import like <code>from org.openhab.core import OpenHAB</code>
73+
```ini
74+
# Activate openHAB Python helper module and inject scope and helper objects into rules
7975
#
80-
#org.openhab.automation.pythonscripting:scopeEnabled = true
81-
82-
# Install openHAB Python helper module (requires scope module)
76+
# Install openHAB Python helper module to support helper classes like rule, logger, Registry, Timer, etc.
77+
# and automatically injects `from openhab import rule, Registry, logger` into you Python code.
8378
#
84-
# Install openHAB Python helper module to support helper classes like rule, logger, Registry, Timer etc...
85-
# If disabled, the openHAB python helper module can be installed manually by copying it to /conf/automation/python/lib/openhab"
86-
#
87-
#org.openhab.automation.pythonscripting:helperEnabled = true
88-
89-
# Inject scope and helper objects into rules (requires helper modules)
79+
# If auto injection is disabled, the helper module can still be used by importing it manually.
9080
#
91-
# This injects the scope and helper Registry and logger into rules.
81+
# When completely disabled, you get a "pure" Graalpy context that has only been initialized with the default JSR223 presets.
9282
#
93-
# 2 => Auto injection enabled only for UI and Transformation scripts (preferred)
94-
# 1 => Auto injection enabled for all scripts
95-
# 0 => Disable auto injection and use 'import' statements instead
83+
# 4 => Auto injection everywhere, including script files and transformations
84+
# 3 => Auto injection for Script Actions, Script Conditions and transformations
85+
# 2 => Auto injection only for Script Actions &amp; Script Conditions (recommended)
86+
# 1 => Disable auto-injection and import manually instead
87+
# 0 => Disable completely
9688
#
9789
#org.openhab.automation.pythonscripting:injectionEnabled = 2
9890

bundles/org.openhab.automation.pythonscripting/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<name>openHAB Add-ons :: Bundles :: Automation :: Python Scripting</name>
1616

1717
<properties>
18-
<helperlib.version>1.0.15</helperlib.version>
18+
<helperlib.version>1.0.18</helperlib.version>
1919
</properties>
2020

2121
<dependencies>

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/PythonScriptEngine.java

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.openhab.automation.pythonscripting.internal.scriptengine.InvocationInterceptingPythonScriptEngine;
6060
import org.openhab.automation.pythonscripting.internal.scriptengine.graal.GraalPythonScriptEngine;
6161
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
62+
import org.openhab.core.automation.module.script.internal.handler.AbstractScriptModuleHandler;
6263
import org.openhab.core.library.types.DateTimeType;
6364
import org.openhab.core.library.types.DecimalType;
6465
import org.openhab.core.library.types.OnOffType;
@@ -289,7 +290,7 @@ public void accept(Path path) {
289290
}
290291
}
291292

292-
if (pythonScriptEngineConfiguration.isScopeEnabled()) {
293+
if (pythonScriptEngineConfiguration.isHelperEnabled()) {
293294
ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
294295
.getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
295296
if (scriptExtensionAccessor == null) {
@@ -305,6 +306,12 @@ public void accept(Path path) {
305306
getBindings(ScriptContext.ENGINE_SCOPE).put(ScriptExtensionModuleProvider.IMPORT_PROXY_NAME,
306307
wrapImportFn);
307308
try {
309+
if (!isScriptFile() && !isScriptModule() && !isTransformation()) {
310+
logger.warn(
311+
"Unknown script environment detected for engine '{}': Neither script file, script module nor transformation.",
312+
engineIdentifier);
313+
}
314+
308315
String wrapperContent = new String(
309316
Files.readAllBytes(PythonScriptEngineConfiguration.PYTHON_WRAPPER_FILE_PATH));
310317
getPolyglotContext()
@@ -314,9 +321,11 @@ public void accept(Path path) {
314321
.build());
315322

316323
// inject scope, Registry and logger
317-
if (!pythonScriptEngineConfiguration.isInjection(PythonScriptEngineConfiguration.INJECTION_DISABLED)
318-
&& (ctx.getAttribute(CONTEXT_KEY_SCRIPT_FILENAME) == null || pythonScriptEngineConfiguration
319-
.isInjection(PythonScriptEngineConfiguration.INJECTION_ENABLED_FOR_ALL_SCRIPTS))) {
324+
if (pythonScriptEngineConfiguration.isInjectionEnabledForAllScripts()
325+
|| (isScriptModule()
326+
&& pythonScriptEngineConfiguration.isInjectionEnabledForScriptModules())
327+
|| (isTransformation()
328+
&& pythonScriptEngineConfiguration.isInjectionEnabledForTransformations())) {
320329
String injectionContent = "import scope\nfrom openhab import Registry, logger";
321330
getPolyglotContext().eval(
322331
Source.newBuilder(GraalPythonScriptEngine.LANGUAGE_ID, injectionContent, "<generated>")
@@ -404,7 +413,7 @@ public void put(@Nullable String key, @Nullable Object value) {
404413
value = lifecycleTracker;
405414
}
406415
if (key != null && value != null) {
407-
if (pythonScriptEngineConfiguration.isScopeEnabled()) {
416+
if (pythonScriptEngineConfiguration.isHelperEnabled()) {
408417
scriptExtensionModuleProvider.put(key, value);
409418
} else {
410419
super.put(key, value);
@@ -447,32 +456,8 @@ public void unlock() {
447456
logger.debug("Lock released for engine '{}'.", this.engineIdentifier);
448457
}
449458

450-
@Override
451-
public Object invokeFunction(String name, Object... objects) throws ScriptException, NoSuchMethodException {
452-
if ("scriptUnloaded".equals(name)) {
453-
/*
454-
* is called from
455-
* => org.openhab.core.automation.module.script.internal.ScriptEngineManagerImpl:removeEngine
456-
*
457-
* must be skipped, because ScriptTransformationService:disposeScriptEngine is calling engine.close several
458-
* times before. Specially if the
459-
* same script is used for more then 1 transformations. If the engine is already closed, the script
460-
* "scriptUnloaded" will fail.
461-
*/
462-
return null;
463-
} else {
464-
return super.invokeFunction(name, objects);
465-
}
466-
}
467-
468459
@Override
469460
public void close() {
470-
/*
471-
* is called from
472-
* => org.openhab.core.automation.module.script.ScriptTransformationService:disposeScriptEngine
473-
* => org.openhab.core.automation.module.script.internal.ScriptEngineManagerImpl:removeEngine
474-
*/
475-
476461
lock.lock();
477462

478463
if (!isClosed()) {
@@ -538,6 +523,64 @@ private String stringifyThrowable(Throwable throwable) {
538523
return (message != null) ? message + System.lineSeparator() + stackTrace : stackTrace;
539524
}
540525

526+
/**
527+
* Tests if the script is a script file, i.e. it is loaded from a Python file.
528+
*
529+
* @return true if the script is loaded from a Python file, false otherwise
530+
*/
531+
private boolean isScriptFile() {
532+
ScriptContext ctx = getContext();
533+
if (ctx == null) {
534+
logger.warn("Failed to retrieve script context from engine '{}'.", engineIdentifier);
535+
return false;
536+
}
537+
return ctx.getAttribute("javax.script.filename") != null;
538+
}
539+
540+
/**
541+
* Get the module type id (if any) of the module executing the script.
542+
*
543+
* @return the module type id (if any) of the module executing the script, or null if the script is not a module
544+
*/
545+
private @Nullable String getModuleTypeId() {
546+
ScriptContext ctx = getContext();
547+
if (ctx == null) {
548+
logger.warn("Failed to retrieve script context from engine '{}'.", engineIdentifier);
549+
return null;
550+
}
551+
552+
Object value = ctx.getAttribute(AbstractScriptModuleHandler.CONTEXT_KEY_MODULE_TYPE_ID);
553+
if (value instanceof String str) {
554+
return str;
555+
}
556+
return null;
557+
}
558+
559+
/**
560+
* Tests if the script is a script module, i.e. executed by an implementation of
561+
* {@link AbstractScriptModuleHandler}.
562+
*
563+
* @return true if the script is a script module, false otherwise
564+
*/
565+
private boolean isScriptModule() {
566+
String moduleTypeId = getModuleTypeId();
567+
return moduleTypeId != null && moduleTypeId.startsWith("script.");
568+
}
569+
570+
/**
571+
* Tests if the script is a transformation script, i.e. created from the script transformation service.
572+
*
573+
* @return true if it is a transformation script, false otherwise
574+
*/
575+
private boolean isTransformation() {
576+
ScriptContext ctx = getContext();
577+
if (ctx == null) {
578+
logger.warn("Failed to retrieve script context from engine '{}'.", engineIdentifier);
579+
return false;
580+
}
581+
return engineIdentifier.startsWith(OPENHAB_TRANSFORMATION_SCRIPT);
582+
}
583+
541584
private static Set<String> transformArrayToSet(Value value) {
542585
try {
543586
Set<String> set = new HashSet<>();

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/PythonScriptEngineConfiguration.java

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,14 @@ public class PythonScriptEngineConfiguration {
6262
public static final Path PYTHON_WRAPPER_FILE_PATH = PYTHON_OPENHAB_LIB_PATH.resolve("__wrapper__.py");
6363
public static final Path PYTHON_INIT_FILE_PATH = PYTHON_OPENHAB_LIB_PATH.resolve("__init__.py");
6464

65-
public static final int INJECTION_DISABLED = 0;
66-
public static final int INJECTION_ENABLED_FOR_ALL_SCRIPTS = 1;
67-
public static final int INJECTION_ENABLED_FOR_NON_FILE_BASED_SCRIPTS = 2;
65+
private static final int INJECTION_DISABLED = 1;
66+
private static final int INJECTION_ENABLED_FOR_SCRIPT_MODULES_ONLY = 2;
67+
private static final int INJECTION_ENABLED_FOR_SCRIPT_MODULES_AND_TRANSFORMATIONS = 3;
68+
private static final int INJECTION_ENABLED_FOR_ALL_SCRIPTS = 4;
6869

6970
// The variable names must match the configuration keys in config.xml
7071
public static class PythonScriptingConfiguration {
71-
public boolean scopeEnabled = true;
72-
public boolean helperEnabled = true;
73-
public int injectionEnabled = INJECTION_ENABLED_FOR_NON_FILE_BASED_SCRIPTS;
72+
public int injectionEnabled = INJECTION_ENABLED_FOR_SCRIPT_MODULES_ONLY;
7473
public boolean dependencyTrackingEnabled = true;
7574
public boolean cachingEnabled = true;
7675
public boolean jythonEmulation = false;
@@ -152,8 +151,7 @@ public void init(PythonScriptEngineFactory factory) {
152151
* @param initial
153152
*/
154153
public void modified(Map<String, Object> config, PythonScriptEngineFactory factory) {
155-
boolean oldScopeEnabled = configuration.scopeEnabled;
156-
boolean oldInjectionEnabled = !isInjection(PythonScriptEngineConfiguration.INJECTION_DISABLED);
154+
int oldInjectionEnabled = configuration.injectionEnabled;
157155
boolean oldDependencyTrackingEnabled = isDependencyTrackingEnabled();
158156

159157
String oldPipModules = configuration.pipModules;
@@ -162,16 +160,13 @@ public void modified(Map<String, Object> config, PythonScriptEngineFactory facto
162160
PythonScriptEngineHelper.initPipModules(this, factory);
163161
}
164162

165-
if (oldScopeEnabled != isScopeEnabled()) {
166-
logger.info("{} scope for Python Scripting. Please resave your scripts to apply this change.",
167-
isScopeEnabled() ? "Enabled" : "Disabled");
168-
}
169-
if (oldInjectionEnabled != !isInjection(PythonScriptEngineConfiguration.INJECTION_DISABLED)) {
170-
logger.info("{} injection for Python Scripting. Please resave your UI-based scripts to apply this change.",
171-
!isInjection(PythonScriptEngineConfiguration.INJECTION_DISABLED) ? "Enabled" : "Disabled");
163+
if (oldInjectionEnabled != configuration.injectionEnabled) {
164+
logger.info(
165+
"Changed helper module setting for Python Scripting. Please resave your python scripts to apply this change.");
172166
}
173167
if (oldDependencyTrackingEnabled != isDependencyTrackingEnabled()) {
174-
logger.info("{} dependency tracking for Python Scripting. Please resave your scripts to apply this change.",
168+
logger.info(
169+
"{} dependency tracking for Python Scripting. Please resave your python scripts to apply this change.",
175170
isDependencyTrackingEnabled() ? "Enabled" : "Disabled");
176171
}
177172
}
@@ -180,16 +175,20 @@ public void setHelperLibVersion(Version version) {
180175
installedHelperLibVersion = version;
181176
}
182177

183-
public boolean isScopeEnabled() {
184-
return configuration.scopeEnabled;
178+
public boolean isInjectionEnabledForAllScripts() {
179+
return configuration.injectionEnabled == INJECTION_ENABLED_FOR_ALL_SCRIPTS;
185180
}
186181

187-
public boolean isHelperEnabled() {
188-
return configuration.helperEnabled;
182+
public boolean isInjectionEnabledForScriptModules() {
183+
return configuration.injectionEnabled == INJECTION_ENABLED_FOR_SCRIPT_MODULES_ONLY;
189184
}
190185

191-
public boolean isInjection(int type) {
192-
return configuration.injectionEnabled == type;
186+
public boolean isInjectionEnabledForTransformations() {
187+
return configuration.injectionEnabled == INJECTION_ENABLED_FOR_SCRIPT_MODULES_AND_TRANSFORMATIONS;
188+
}
189+
190+
public boolean isHelperEnabled() {
191+
return configuration.injectionEnabled > 0;
193192
}
194193

195194
public boolean isDependencyTrackingEnabled() {

bundles/org.openhab.automation.pythonscripting/src/main/resources/OH-INF/config/config.xml

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,29 @@
1616
<advanced>true</advanced>
1717
</parameter-group>
1818

19-
<parameter name="scopeEnabled" type="boolean" required="true" groupName="environment">
20-
<label>Use scope and import wrapper</label>
19+
<parameter name="injectionEnabled" type="integer" required="true" min="0" max="4" groupName="environment">
20+
<label>Activate openHAB Python helper module and inject scope and helper objects into rules.</label>
2121
<description><![CDATA[
22-
This enables a scope module and import wrapper.<br>
23-
A scope module is an encapsulated module containing all openHAB jsr223 objects and can be imported with <code>import scope</code><br>
24-
Additionally, you can run an import like <code>from org.openhab.core import openHAB</code>
22+
Install openHAB Python helper module to support helper classes like rule, logger, Registry, Timer, etc. and automatically injects `from openhab import rule, Registry, logger` into you Python code.<br>
23+
If auto injection is disabled, the helper module can still be used by importing it manually.<br>
24+
When completely disabled, you get a "pure" Graalpy context that has only been initialized with the default JSR223 presets. The openHAB Python helper module cannot be imported manually in this case.
2525
]]></description>
26-
<default>true</default>
27-
<advanced>true</advanced>
28-
</parameter>
29-
<parameter name="helperEnabled" type="boolean" required="true" groupName="environment">
30-
<label>Install openHAB Python helper module (requires scope module)</label>
31-
<description><![CDATA[
32-
Install openHAB Python helper module to support helper classes like rule, logger, Registry, Timer, etc.<br>
33-
If disabled, the openHAB python helper module can be installed manually by copying it to /conf/automation/python/lib/openhab"
34-
]]></description>
35-
<default>true</default>
36-
</parameter>
37-
<parameter name="injectionEnabled" type="integer" required="true" min="0" max="2" groupName="environment">
38-
<label>Inject scope and helper objects into rules (requires helper modules)</label>
39-
<description>This injects the scope and helper Registry and logger into rules.</description>
4026
<options>
41-
<option value="2">Auto injection enabled only for UI and Transformation scripts (preferred)</option>
42-
<option value="1">Auto injection enabled for all scripts</option>
43-
<option value="0">Disable auto injection and use 'import' statements instead</option>
27+
<option value="4">Auto injection everywhere, including script files and transformations</option>
28+
<option value="3">Auto injection for Script Actions, Script Conditions and transformations</option>
29+
<option value="2">Auto injection only for Script Actions &amp; Script Conditions (recommended)</option>
30+
<option value="1">Disable auto-injection and import manually instead</option>
31+
<option value="0">Disable completely</option>
4432
</options>
4533
<default>2</default>
4634
</parameter>
4735
<parameter name="pipModules" type="text" required="false" groupName="system">
4836
<label>Python pip modules (requires a manually configured venv)</label>
49-
<description>
50-
<![CDATA[A comma separated list of Python modules to install.
51-
Versions may be constrained by separating with an <code>==</code> followed by standard
52-
python pip version constraint, such as "<code>tzdata==2025.2</code>".]]>
53-
</description>
37+
<description><![CDATA[
38+
A comma separated list of Python modules to install.
39+
Versions may be constrained by separating with an <code>==</code> followed by standard
40+
python pip version constraint, such as "<code>tzdata==2025.2</code>".
41+
]]></description>
5442
<default></default>
5543
<advanced>true</advanced>
5644
</parameter>

0 commit comments

Comments
 (0)