Skip to content

Latest commit

 

History

History
861 lines (630 loc) · 53.7 KB

File metadata and controls

861 lines (630 loc) · 53.7 KB
layout documentation
title JSR223 Scripting

JSR223 Scripting

::: tip Note This feature is for users who have or are willing to learn some programming skills and are comfortable working with the command line prompt of the operating system hosting openHAB. :::

[[toc]]

Overview

JSR223 (spec) is a standard scripting API for Java Virtual Machine (JVM) languages. The JVM languages provide varying levels of support for the JSR223 API and interoperability with the Java runtime. Currently, the following languages are known to work well for openHAB scripting: Apache Groovy (JVM scripting language), JavaScript/ECMAScript 2024+, JRuby, and Python (Python 3 on GraalVM). Legacy languages JavaScript/ECMA - 262 Edition 5.1 and Jython (Python 2.7 on the JVM) are also still supported.

Please note that JavaScript/ECMA - 262 Edition 5.1 which is referenced here is using an older engine called NashornJS. This version of JavaScript/ECMAScript is end of life and lacks many important features that have been added to the language since 2009. If you plan to use JavaScript/ECMAScript as your scripting language, it is recommended to use JavaScript/ECMAScript 2024+, also known as JavaScript Scripting, which includes the latest ES6 language features, supports third party node libraries (please keep in mind that not all NodeJS APIs are available in GraalJS), and comes with a well documented helper library (openhab-js) that makes interaction with the APIs documented on this page (and more) much simpler and straight forward in plain JavaScript.

Although JSR223 scripts can be used as a general-purpose extension language for openHAB, it is most commonly used for the creation of rules, and within scripted Actions or Conditions. Currently, openHAB allows JSR223 scripting to access all packages, which may not be included in the official APIs. This provides great flexibility for the users of JSR223, but is also use at your own risk, since changes outside the official APIs occur frequently, and are not considered to be breaking changes. New APIs are planned to be implemented in the future, which will provide standardized interfaces for interacting with openHAB through scripted automation.

Example rules for a first impression

:::: tabs

::: tab JS Nashorn

'use strict';

scriptExtension.importPreset("RuleSupport");
scriptExtension.importPreset("RuleSimple");

var sRule = new SimpleRule() {
    execute: function( module, input) {
        print("This is a 'hello world!' from a JavaScript rule.");
    }
};

sRule.setTriggers([
    TriggerBuilder.create()
        .withId("aTimerTrigger")
        .withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(
            new Configuration({
                "cronExpression": "0 * * * * ?"
            })).build()
    ]);

automationManager.addRule(sRule);

:::

::: tab JS Scripting

// Import openHAB Java API
Object.assign(this, require('@runtime'));
Object.assign(this, require('@runtime/RuleSimple'));
Object.assign(this, require('@runtime/RuleSupport'));

// Override non-working TriggerBuilder import
const TriggerBuilder = Java.type('org.openhab.core.automation.util.TriggerBuilder');

const sRule = new SimpleRule({
  execute: function(module, input) {
    print('This is a \'hello world!\' from a JavaScript rule.');
  }
});
sRule.setName('A JavaScript/ECMAScript 262 Edition 11')
sRule.setTriggers([
  TriggerBuilder.create()
    .withId('aTimerTrigger')
    .withTypeUID('timer.GenericCronTrigger')
    .withConfiguration(
      new Configuration({
        'cronExpression': '0 * * * * ?'
      }))
    .build()
]);

automationManager.addRule(sRule);
// And the same rule using the helper library:

const { rules, triggers } = require('openhab');

rules.JSRule({
    name: 'A JavaScript/ECMAScript 262 Edition 11',
    description: 'This is a JavaScript rule.',
    triggers: [
        triggers.GenericCronTrigger('0 * * * * ?')
    ],
    execute: (event) => {
        console.log('This is a \'hello world!\' from a JavaScript rule.');
    }
});

:::

::: tab Jython

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

class myRule(SimpleRule):
    def execute(self, module, inputs):
        print "This is a 'hello world!' from Jython rule."

sRule = myRule()
sRule.setTriggers([
    TriggerBuilder.create()
        .withId("aTimerTrigger")
        .withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(
            Configuration({
                "cronExpression": "0 * * * * ?"
            })).build()
    ])

automationManager.addRule(sRule)

:::

::: tab Groovy

import org.openhab.core.automation.*
import org.openhab.core.automation.module.script.rulesupport.shared.simple.*
import org.openhab.core.config.core.Configuration

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")

def sRule = new SimpleRule() {
    Object execute(Action module, Map<String, ?> inputs) {
        println("Hello World from Groovy")
    }
}
sRule.setTriggers([
    TriggerBuilder.create()
        .withId("aTimerTrigger")
        .withTypeUID("timer.GenericCronTrigger")
        .withConfiguration(new Configuration([cronExpression: "0 * * * * ?"]))
        .build()
    ])

automationManager.addRule(sRule)

:::

::: tab JRuby

$scriptExtension.importPreset('RuleSupport')
$scriptExtension.importPreset('RuleSimple')

class MyRule < SimpleRule
  def execute(_mod, _inputs)
    logger = org.slf4j.LoggerFactory.getLogger('org.openhab.automation.example')
    logger.info('Hello World from JRuby')
  end
end

s_rule = MyRule.new
s_rule.set_triggers([
        TriggerBuilder.create
                      .with_id('aTimerTrigger')
                      .with_type_uid('timer.GenericCronTrigger')
                      .with_configuration(Configuration.new({ 'cronExpression' => '0 * * * * ?' }))
                      .build
    ])

$automationManager.add_rule(s_rule)
# And the same rule using the bundled helper library:

rule "A Cron Rule in Ruby" do
  every :minute
  run do
    logger.info "Hello World from JRuby"
  end
end

:::

::::

Script Locations

Scripts should be placed in the ${OPENHAB_CONF}/automation/jsr223/ directory. This directory will vary, based on the type of openHAB installation used. For example, Linux installations created with a package installer will use /etc/openhab/automation/jsr223/, and manual installations will use /opt/openhab/conf/automation/jsr223/.

When openHAB starts, scripts are loaded at start level 40 by default (in no particular order). The start level for each script can be overridden by specifying a start level either in the filename (./my_script.sl50.py) or containing directory (./sl50/my_script.py). The runtime provides no explicit dependency mechanism or ordering, yet scripts are loaded one at a time so can be ordered via start level if desired. For example, with the following scripts and directory structure...

├── automation/jsr223
│   ├── dir1
│   │   ├── script_a.py
│   │   └── script_b.py
│   ├── script.sl38.py
│   ├── sl30
│   │   ├── script_x.py
│   │   └── script_y.py
│   └── script.py

... the load order will be: (sl30/script_x.py & sl30/script_y.py) at start level 30, script.sl38.py at start level 38, then (/dir1/script_a.py, /dir1/script_b.py, script.py) at start level 40. The script file watching mechanism itself is activated at start level 20, so scripts cannot be executed earlier than this.

Note that prior to openHAB 3, script ordering was performed alphanumerically based on file path. This is no longer supported as of openHAB 3 and later versions.

Script Lifecycle

Script files from the above location are loaded on openHAB startup. openHAB watches the above location for changes, and loads new scripts on creation as well as reloads existing scripts on change. Upon reload of a script file, the old script is unloaded first and then the new script is loaded.

Other places in openHAB where scripting is used, e.g. transformations, might have a different script lifecycle.

ScriptExtension Presets

To facilitate JSR223 scripting, several openHAB-related variables are automatically predefined within ScriptExtension presets. They can be loaded into the script context using scriptExtension.importPreset(String preset), e.g. scriptExtension.importPreset("RuleSimple"). The default, lifecycle and media presets are preloaded, so they do not require importing. With scriptExtension.get("automationManager") the automationManager can be made available without loading a preset.

default Preset

Imported by default (importPreset not required).

Variable Description
State org.openhab.core.types.State
Command org.openhab.core.types.Command
URLEncoder java.net.URLEncoder
File java.io.File
Files java.nio.file.Files
Path java.nio.file.Path
Paths java.nio.file.Paths
ChronoUnit java.time.temporal.ChronoUnit
DayOfWeek java.time.DayOfWeek
Duration java.time.Duration
Month java.time.Month
ZoneId java.time.ZoneId
ZonedDateTime java.time.ZonedDateTime
IncreaseDecreaseType org.openhab.core.library.types.IncreaseDecreaseType
DECREASE IncreaseDecreaseType enum item
INCREASE IncreaseDecreaseType enum item
OnOffType org.openhab.core.library.types.OnOffType
ON OnOffType enum item
OFF OnOffType enum item
OpenClosedType org.openhab.core.library.types.OpenClosedType
OPEN OpenClosedType enum item
CLOSED OpenClosedType enum item
StopMoveType org.openhab.core.library.types.StopMoveType
STOP StopMoveType enum item
MOVE StopMoveType enum item
UpDownType org.openhab.core.library.types.UpDownType
UP UpDownType enum item
DOWN UpDownType enum item
UnDefType org.openhab.core.library.types.UnDefType
NULL UnDefType enum item
UNDEF UnDefType enum item
RefreshType org.openhab.core.library.types.RefreshType
REFRESH RefreshType enum item
NextPreviousType org.openhab.core.library.types.NextPreviusType
NEXT NextPreviousType enum item
PREVIOUS NextPreviousType enum item
PlayPauseType org.openhab.core.library.types.PlayPauseType
PLAY PlayPauseType enum item
PAUSE PlayPauseType enum item
RewindFastforwardType org.openhab.core.library.types.RewindFastforwardType
REWIND RewindFastforwardType enum item
FASTFORWARD RewindFastforwardType enum item
QuantityType org.openhab.core.library.types.QuantityType
StringListType org.openhab.core.library.types.StringListType
RawType org.openhab.core.library.types.RawType
DateTimeType org.openhab.core.library.types.DateTimeType
DecimalType org.openhab.core.library.types.DecimalType
HSBType org.openhab.core.library.types.HSBType
PercentType org.openhab.core.library.types.PercentType
PointType org.openhab.core.library.types.PointType
StringType org.openhab.core.library.types.StringType
SIUnits org.openhab.core.library.unit.SIUnits
ImperialUnits org.openhab.core.library.unit.ImperialUnits
MetricPrefix org.openhab.core.library.unit.MetricPrefix
Units org.openhab.core.library.unit.Units
BinaryPrefix org.openhab.core.library.unit.BinaryPrefix
items Instance of java.util.Map<String, State>
ir Alias for itemRegistry
itemRegistry Instance of org.openhab.core.items.ItemRegistry
things Instance of org.openhab.core.thing.ThingRegistry
rules Instance of org.openhab.core.automation.RuleRegistry
events (Internal) Used to send events, post commands, etc. Details below
actions Instance of org.openhab.core.automation.module.script.defaultscope.ScriptThingActions
scriptExtension (internal) For loading script presets.
se Alias for scriptExtension

events operations

Instance of org.openhab.core.automation.module.script.action.BusEvent.

For all post update, send command, and send time series operations, methods with an additional String parameter are available to specify the source of the event. Refer to Developer: Event Bus for more information about event source.

Post Update
  • events.postUpdate(String, String)
  • events.postUpdate(Item, Number)
  • events.postUpdate(Item, String)
  • events.postUpdate(Item, State)
Send Command
  • events.sendCommand(String, String)
  • events.sendCommand(Item, Number)
  • events.sendCommand(Item, String)
  • events.sendCommand(Item, Command)
Send Time Series
  • events.sendTimeSeries(String, Map<ZonedDateTime, State>, String)
  • events.sendTimeSeries(Item, TimeSeries)
Store and Restore States
  • events.storeStates(Item...)
  • events.restoreStates(Map<Item, State>)

lifecycle Preset

Imported by default (importPreset not required).

Provides a mechanism to execute a callback on disposal. A script is disposed both on modification and removal, see Script Lifecycle.

Variable Description
lifecycleTracker org.openhab.core.automation.module.script.LifecycleScriptExtensionProvider.LifecycleTracker

This is demonstrated in the scriptLoaded and scriptUnloaded functions section.

media Preset

Imported by default (importPreset not required).

Variable Description
audio org.openhab.core.audio.AudioManager
voice org.openhab.core.voice.VoiceManager

RuleSimple Preset

These variables and classes are loaded using:

scriptExtension.importPreset("RuleSimple")

The primary usage of this preset is for defining rule (SimpleRule) subclasses. See language-specific documentation for examples.

Variable Description
ActionType org.openhab.core.automation.type.ActionType
ConfigDescriptionParameter org.openhab.core.config.core.ConfigDescriptionParameter
ModuleType org.openhab.core.automation.type.ModuleType
SimpleActionHandler org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler
SimpleConditionHandler org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler
SimpleRule Base class for Rules org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule
SimpleTriggerHandler org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler
TriggerType org.openhab.core.automation.type.TriggerType
Visibility org.openhab.core.automation.Visibility enum

RuleSupport Preset

These variables and classes are loaded using:

scriptExtension.importPreset("RuleSupport")
Variable Description
Action org.openhab.core.automation.Action
ActionBuilder org.openhab.core.automation.util.ActionBuilder
Condition org.openhab.core.automation.Condition
ConditionBuilder org.openhab.core.automation.util.ConditionBuilder
Configuration org.openhab.core.config.core.Configuration
ModuleBuilder org.openhab.core.automation.util.ModuleBuilder
Rule org.openhab.core.automation.Rule (use SimpleRule for defining rules)
Trigger org.openhab.core.automation.Trigger
TriggerBuilder org.openhab.core.automation.util.TriggerBuilder
automationManager org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager instance for managing rules and other openHAB module instances. (e.g., addRule)
ruleRegistry org.openhab.core.automation.Rule

RuleFactories Preset

Note: Advanced usage

scriptExtension.importPreset("RuleFactories")
Variable Description
ActionHandlerFactory org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedActionHandlerFactory
ActionType org.openhab.core.automation.type.ActionType
ConditionHandlerFactory org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedConditionHandlerFactory
ConfigDescriptionParameter org.openhab.core.config.core.ConfigDescriptionParameter
ModuleType org.openhab.core.automation.type.ModuleType
TriggerHandlerFactory org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedTriggerHandlerFactory
TriggerType org.openhab.core.automation.type.TriggerType
Visibility org.openhab.core.automation.Visibility

ScriptAction Preset

This preset can be useful for scheduling asynchronous code execution with org.openhab.core.automation.module.script.action.Timer.

Variable Description
busEvent org.openhab.core.automation.module.script.action.BusEvent
scriptExecution org.openhab.core.automation.module.script.action.ScriptExecution

The busEvent variable is identical to the events variable in the default preset.

:::: tabs

::: tab Groovy

scriptExtension.importPreset("ScriptAction")

scriptExecution.createTimer(ZonedDateTime.now().plusSeconds(1), {
  org.slf4j.LoggerFactory.getLogger('Test logger').warn('Timer ran')
})

:::

::: tab JS Scripting

actions.scriptExecution.createTimer(time.toZDT(1000), () => {
  console.info('Timer ran');
})

:::

::: tab DSL

createTimer(now.plusSeconds(1)) [
  logInfo('Test logger', 'Timer ran')
]

:::

::: tab JRuby

after(1.second) do
  logger.info("Timer ran")
end

:::

::::

cache Preset

The cache preset does not provide a default import and needs to be imported explicitly.

:::: tabs

::: tab Groovy

scriptExtension.importPreset("cache")

sharedCache.put("x", "y")

:::

::: tab JS Nashorn

scriptExtension.importPreset("cache")

var valueX = sharedCache.get("x")

:::

::: tab JS Scripting

var { sharedCache, privateCache } = require('@runtime/cache')

sharedCache.remove("x")

:::

::: tab DSL

sharedCache.put('foo', 'bar')
sharedCache.get('foo') // returns null if doesn't exist
sharedCache.remove('foo') // deletes the entry

:::

::: tab Jython

scriptExtension.importPreset("cache")

:::

::: tab JRuby

# the preset is imported automatically by the helper library.
value_x = shared_cache[:x]
shared_cache.delete(:x)
shared_cache[:x] = "y"

:::

::::

It provides two different caches:

  • sharedCache: This cache is shared over all languages and all scripts (file-based and UI). Usage of entries is tracked and entries will be removed if the last script that ever accessed an entry is removed.
  • privateCache: This cache is private to a script engine, usually that means it is private to a script or rule, depending on the implementation of the scripting language. It is cleared when the script engine is unloaded (i.e. usually when the script is unloaded).

In both cases values that are either a ScheduledFuture<?> or a Timer are cancelled by calling .cancel() on the object if the object is removed automatically.

Both caches implement the org.openhab.core.automation.module.script.rulesupport.shared.ValueCache interface and therefore can be accessed by

  • Object put(String key, Object value): Put a key/value pair to the cache. Returns old value if already set, otherwise null.
  • Object remove(String key): Remove the key/value pair from the cache. Returns old value if already set, otherwise null.
  • Object get(String key): Get the value for the given key from the cache. Non-existent keys return null.
  • Object get(String key, Supplier<Object> supplier: Get the value for the given key. If no value is present, add the value that is return from the supplier.

provider Preset

Variable Description
itemRegistry org.openhab.core.automation.module.script.providersupport.shared.ProviderItemRegistryDelegate
metadataRegistry org.openhab.core.automation.module.script.providersupport.shared.ProviderMetadataRegistryDelegate
thingRegistry org.openhab.core.automation.module.script.providersupport.shared.ProviderThingRegistryDelegate
itemChannelLinkRegistry org.openhab.core.automation.module.script.providersupport.shared.ProviderItemChannelLinkRegistry

The above instances provide implementations of the interfaces org.openhab.core.items.ItemRegistry, org.openhab.core.items.MetadataRegistry and org.openhab.core.thing.ThingRegistry respectively. The itemChannelLinkRegistry does not provide the same methods as the org.openhab.core.thing.link.ItemChannelLinkRegistry interface.

All instances from this preset have an addPermanent(...) method, in addition to add(...). With the add(...) method, elements added over the above instances are removed when the script that inserted the elements is unloaded. When addPermanent(...) is used, the inserted elements are not removed. itemRegistry.addPermanent(...)/thingRegistry.addPermanent(...) from this preset offer the same functionality as itemRegistry.add(...)/things.add(...) from the default preset.

TriggerType Objects

The following trigger types are defined by openHAB (custom triggers can also be defined) and take the specified parameters in the constructor of org.openhab.core.config.core.Configuration. The configuration can be passed to TriggerBuilder.withConfiguration(). All parameters are Strings. Read the JSR223 language specific documentation for examples of using these TriggerType objects.

::: details timer.DateTimeTrigger

Parameter Description
itemName The name of the Item
timeOnly Whether only the time of the item should be compared or the date and time
offset The offset in seconds to add to the time of the item

:::

::: details timer.GenericCronTrigger

Parameter Description
cronExpression The cron expression

:::

::: details timer.TimeOfDayTrigger

Parameter Description
time The time in "hh:mm" format

:::

::: details core.ItemCommandTrigger

Parameter Description
itemName The name of the Item
command The Command (optional)

:::

::: details core.ItemStateUpdateTrigger

Parameter Description
itemName The name of the Item
state The State (optional)

:::

::: details core.ItemStateChangeTrigger

Parameter Description
itemName The name of the Item
previousState The previous State (optional)
state The State (optional)

:::

::: details core.GroupCommandTrigger

Parameter Description
groupName The name of the GroupItem
command The Command (optional)

:::

::: details core.GroupStateUpdateTrigger

Parameter Description
groupName The name of the GroupItem
state The State (optional)

:::

::: details core.GroupStateChangeTrigger

Parameter Description
groupName The name of the GroupItem
previousState The previous State (optional)
state The State (optional)

:::

::: details core.ThingStatusUpdateTrigger

Parameter Description
thingUID The thingUID
status The ThingStatus (optional)

:::

::: details core.ThingStatusChangeTrigger

Parameter Description
thingUID The thingUID
previousStatus The previous ThingStatus (optional)
status The ThingStatus (optional)

:::

::: details core.ChannelEventTrigger

Parameter Description
channelUID The ChannelUID of the Channel
event The Channel trigger Event (optional)

:::

::: details core.GenericEventTrigger

Parameter Description
topic The topic to match, as a file-system style glob (*, **, ?, and {} operators).

Example filters:
Item events: "openhab/items/*/"
Channel events: "openhab/channels/*/triggered"
Thing events: "openhab/things/**"
source The object that triggered the event, such as org.openhab.core.expire
types ItemCommandEvent, ItemStateEvent, ItemStateChangedEvent, GroupItemStateChangedEvent, ItemAddedEvent, ItemRemovedEvent, ThingAddedEvent, ThingRemovedEvent, ThingStatusInfoChangedEvent, ThingStatusInfoEvent, ThingUpdatedEvent, etc. A non-exhaustive list can be found in the Javadocs.
payload Regex to match against the actual event data.

:::

::: details core.SystemStartlevelTrigger

Parameter Description
startlevel The system StartLevel

:::

scriptLoaded and scriptUnloaded functions

When a script file is loaded, the scriptLoaded(String) function is invoked if it is defined. The path of the script file will be passed as parameter. Similarly, when a script file is unloaded, the scriptUnloaded() function is invoked if it is defined. Note that scriptLoaded(String) and scriptUnloaded() are only executed for script files – not transformations, Script Actions or Script conditions of rules. Refer to the Script Lifecycle section for more information on load and unload behavior.

lifecycleTracker's addDisposeHook(Disposable) and the scriptUnloaded() function serve similar purposes, but it is recommended to use addDisposeHook(Disposable) as its functionality is not limited to script files only. The lifecyleTracker also supports adding multiple dispose hooks, contrary to scriptUnloaded() that can only be defined once.

:::: tabs

::: tab Groovy

static @groovy.transform.Field logger = org.slf4j.LoggerFactory.getLogger("org.openhab.automation.example")

lifecycleTracker.addDisposeHook(new org.openhab.core.automation.module.script.LifecycleScriptExtensionProvider.Disposable() {
  public void dispose() {
    logger.error("I am logged fourth.")
  }
})

lifecycleTracker.addDisposeHook{ logger.error("I am logged fifth. Bye!") }

void scriptLoaded(String filename) { // can be static
  logger.error("I am logged second and I am from ${filename}.")
}

static void scriptUnloaded() { // can be non-static
  logger.error("I am logged third.")
}
logger.error("I am logged first.")

:::

::: tab JS Nashorn

var logger = Java.type('org.slf4j.LoggerFactory').getLogger("org.openhab.core.automation.examples")
lifecycleTracker.addDisposeHook(function() {
  logger.error("I am logged fourth. Bye!")
})
function scriptLoaded(filename) {
  logger.error("I am logged second and I am from " + filename + ".")
}
function scriptUnloaded() {
  logger.error("I am logged third.")
}
logger.error("I am logged first.")

:::

::: tab JS Scripting

require('@runtime').lifecycleTracker.addDisposeHook( () => console.log("I am logged fourth. Bye!") )

const scriptUnloaded = () => console.log("I am logged third.")

function scriptLoaded(filename) {
  console.log(`I am logged second and I am from ${filename}.`)
}

console.log("I am logged first.")

:::

::: tab Jython

from org.slf4j import LoggerFactory

logger = LoggerFactory.getLogger("org.openhab.core.automation.examples")

def scriptUnloaded():
   logger.error("I am logged third.")

def scriptLoaded(filename):
   logger.error("I am logged second and I am from " + filename + ".")

lifecycleTracker.addDisposeHook(lambda : logger.error("I am logged fourth. Bye!"))
logger.error("I am logged first.")

:::

::: tab Python

When the setting “Python environment” is not “Disable completely”:

import sys
import scope

def scriptUnloaded():
   print("I am logged third.")

def scriptLoaded(filename):
   print("I am logged second and I am from " + filename + ".")

scope.lifecycleTracker.addDisposeHook(lambda : print("I am logged fourth. Bye!"))

print("I am logged first.")

When the setting “Python Environment” is “Disable completely”:

import sys

def scriptUnloaded():
   print("I am logged third.")

def scriptLoaded(filename):
   print("I am logged second and I am from " + filename + ".")

lifecycleTracker.addDisposeHook(lambda : print("I am logged fourth. Bye!"))

print("I am logged first.")

:::

::::