diff --git a/bundles/org.openhab.automation.jsscripting/README.md b/bundles/org.openhab.automation.jsscripting/README.md index 77ae84528..ca04165e7 100644 --- a/bundles/org.openhab.automation.jsscripting/README.md +++ b/bundles/org.openhab.automation.jsscripting/README.md @@ -1,132 +1,319 @@ # JavaScript Scripting This add-on provides support for JavaScript (ECMAScript 2021+) that can be used as a scripting language within automation rules. -JavaScript scripts provide access to almost all the functionality in an openHAB runtime environment. -- [Creating JavaScript Scripts](#creating-javascript-scripts) -- [Logging](#logging) -- [Core Actions](#core-actions) - - [itemRegistry](#itemregistry) - - [Event Bus Actions](#event-bus-actions) - - [Exec Actions](#exec-actions) - - [HTTP Actions](#http-actions) - - [Timers](#timers) - - [Scripts Actions](#scripts-actions) -- [Cloud Notification Actions](#cloud-notification-actions) -- [Persistence Extensions](#persistence-extensions) -- [Ephemeris Actions](#ephemeris-actions) -- [Types and Units](#types-and-units) +Also included is [openhab-js](https://github.com/openhab/openhab-js/), a fairly high-level ES6 library to support automation in openHAB. It provides convenient access +to common openHAB functionality within rules including items, things, actions, logging and more. -## Creating JavaScript Scripts +- [Configuration](#configuration) +- [UI Based Rules](#ui-based-rules) +- [Scripting Basics](#scripting-basics) + - [require](#require) + - [console](#console) + - [setInterval](#setinterval) + - [setTimeout](#settimeout) + - [scriptLoaded](#scriptloaded) + - [scriptUnLoaded](#scriptunloaded) + - [Paths](#paths) +- [Standard Library](#standard-library) + - [items](#items) + - [actions](#actions) + - [cache](#cache) + - [log](#log) + - [time](#time) +- [File Based Rules](#file-based-rules) + - [JSRule](#jsrule) + - [Rule Builder](#rule-builder) -When this add-on is installed, JavaScript script actions will be run by this add-on and allow ECMAScript 2021+ features. -Alternatively, you can create scripts in the `automation/jsr223` configuration directory. -If you create an empty file called `test.js`, you will see a log line with information similar to: +## Configuration + +This add-on includes by default the [openhab-js](https://github.com/openhab/openhab-js/) NPM library and exports it's namespaces onto the global namespace. This allows the use of `items`, `actions`, `cache` and other objects without the need to explicitly import using `require()`. This functionality can be disabled for users who prefer to manage their own imports via the add-on configuration options. + +![OpenHAB Rule Configuration](./docs/settings.png) + +## UI Based Rules + +The quickest way to add rules is through the openHAB Web UI. + +Advanced users, or users migrating scripts from existing systems may want to use [File Based Rules](#file-based-rules) for managing rules using files in the user configuration directory. + +### Adding Triggers + +Using the openHAB UI, first create a new rule and set a trigger condition + +![OpenHAB Rule Configuration](./docs/rule-config.png) + +### Adding Actions + +Select "Add Action" and then select "ECMAScript 262 Edition 11". Its important this is "Edition 11" or higher, earlier versions will not work. This will bring up a empty script editor where you can enter your javascript. + +![OpenHAB Rule Engines](./docs/rule-engines.png) + +You can now write rules using standard ES6 Javascript along with the included openHAB [standard library](#standard-library). + +![OpenHAB Rule Script](./docs/rule-script.png) + +For example, turning a light on: +```javascript +items.getItem("KitchenLight").sendCommand("ON"); +console.log("Kitchen Light State", items.getItem("KitchenLight").state); +``` + +Sending a notification +```javascript +actions.NotificationAction.sendNotification("romeo@montague.org", "Balcony door is open"); +``` + +Querying the status of a thing +```javascript +const thingStatusInfo = actions.Things.getThingStatusInfo("zwave:serial_zstick:512"); +console.log("Thing status",thingStatusInfo.getStatus()); +``` + +See [openhab-js](https://openhab.github.io/openhab-js) for a complete list of functionality + +## Scripting Basics + +The openHAB JSScripting runtime attempts to provide a familiar environment to Javascript developers. + +### Require + +Scripts may include standard NPM based libraries by using CommonJS require. The library search path will look in `automation/js/node_modules` in the user configuration directory. + +### Console + +The JSScripting binding supports the standard `console` object for logging. +Script debug logging is enabled by default at the `TRACE` level, but can be configured using the [console logging]({{base}}/administration/logging.html) commands. ```text - ... [INFO ] [.a.m.s.r.i.l.ScriptFileWatcher:150 ] - Loading script 'test.js' +log:set DEBUG org.openhab.automation.script ``` -To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to enable debug logging for the automation functionality: +Supported logging functions include: +- `console.log(obj1 [, obj2, ..., objN])` +- `console.info(obj1 [, obj2, ..., objN])` +- `console.warn(obj1 [, obj2, ..., objN])` +- `console.error(obj1 [, obj2, ..., objN])` +- `console.debug(obj1 [, obj2, ..., objN])` +- `console.trace(obj1 [, obj2, ..., objN])` -```text -log:set DEBUG org.openhab.core.automation +where `obj1 ... objN` is a list of JavaScript objects to output. The string representations of each of these objects are appended together in the order listed and output. + +see https://developer.mozilla.org/en-US/docs/Web/API/console for more information about console logging. + +### setTimeout + +The global setTimeout() method sets a timer which executes a function or specified piece of code once the timer expires. +```javascript +var timeoutID = setTimeout(function[, delay, arg1, arg2, ...]); +var timeoutID = setTimeout(function[, delay]); ``` -For more information on the available APIs in scripts see the [JSR223 Scripting]({{base}}/configuration/jsr223.html) documentation. +The global `clearTimeout()` method cancels a timeout previously established by calling `setTimeout()`. -The following examples show how to access common openHAB functionalities. +see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout for more information about setTimeout. -## Logging +### setInterval -As a simple example, the following script logs "Hello, World!". -Note that `console.log` will usually not work since the output has no terminal to display the text. -__Please note:__ Support for `console.log` will likely be added together with a logging API in the [helper library](https://github.com/openhab/openhab-js). -The openHAB server uses the [SLF4J](https://www.slf4j.org/) library for logging. +The setInterval() method repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. ```javascript -let logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + this.ruleUID); -logger.info('Hello world!'); -logger.warn('Successfully logged warning.'); -logger.error('Successfully logged error.'); +var intervalID = setInterval(func, [delay, arg1, arg2, ...]); +var intervalID = setInterval(function[, delay]); ``` -The script uses the [LoggerFactory](https://www.slf4j.org/apidocs/org/slf4j/Logger.html) to obtain a named logger and then logs a message like: +The global `clearInterval()` method cancels a timed, repeating action which was previously established by a call to `setInterval()`. -```text - ... [INFO ] [org.openhab.rule. ] - Hello world! - ... [WARN ] [org.openhab.rule. ] - Successfully logged warning. - ... [ERROR] [org.openhab.rule. ] - Successfully logged error. -``` +NOTE: Timers will not be canceled if a script is deleted or modified, it is up to the user to manage timers. See using the [cache](#cache) namespace as well as [ScriptLoaded](#scriptloaded) and [ScriptUnLoaded](#scriptunloaded) for a convenient way of managing persisted objects, such as timers between reloads or deletions of scripts. -## Core Actions +see https://developer.mozilla.org/en-US/docs/Web/API/setInterval for more information about setInterval. -The openHAB services, which are pre-included in the integrated JavaScript engine, must explicitely be imported. +### ScriptLoaded -__Please note:__ The [helper library](https://github.com/openhab/openhab-js) is on the way and will become the preferred API to work with openHAB. +For file based scripts, this function will be called if found when the script is loaded. ```javascript -let openhab = require('@runtime'); +scriptLoaded = function () { + console.log("script loaded"); + loadedDate = Date.now(); +} ``` -### itemRegistry +### ScriptUnLoaded + +For file based scripts, this function will be called if found when the script is unloaded. ```javascript -let state = openhab.itemRegistry.getItem(itemName).getState(); +scriptUnloaded = function () { + console.log("script unloaded"); + //clean up rouge timers + clearInterval(timer); +} ``` -You can use `toString()` to convert an item's state to string or `toBigDecimal()` to convert to number. +### Paths -### Event Bus Actions +For [file based rules](#file-based-rules), scripts will be loaded from `automation/js` in the user configuration directory. + +NPM libraries will be loaded from `automation/js/node_modules` in the user configuration directory. + +## Standard Library + +Full documentation for the openHAB JavaScript library can be found at [openhab-js](https://openhab.github.io/openhab-js) + +### Items + +The items namespace allows interactions with openHAB items. + +See [openhab-js : items ](https://openhab.github.io/openhab-js/items.html) for full API documentation + +* items : object + * .getItem(name, nullIfMissing) ⇒ Item + * .getItemsByTag(...tagNames) ⇒ Array.<Item> + * .createItem(itemName, [itemType], [category], [groups], [label], [tags], [giBaseType], [groupFunction], [itemMetadata]) + * .addItem(itemName, [itemType], [category], [groups], [label], [tags], [giBaseType], [groupFunction]) + * .removeItem(itemOrItemName) ⇒ Boolean + * .replaceItem(itemName, [itemType], [category], [groups], [label], [tags], [giBaseType], [groupFunction]) + * .safeItemName(s) ⇒ String ```javascript -openhab.events.sendCommand(itemName, command); -openhab.events.postUpdate(itemName, state); +const item = items.getItem("KitchenLight"); +console.log("Kitchen Light State", item.state); ``` -`command` and `state` can be a string `'string'` or a number depending on the item. +Calling `getItem(...)` returns an `Item` object with the following properties: -### Exec Actions +* Item : object + * .type ⇒ String + * .name ⇒ String + * .label ⇒ String + * .history ⇒ ItemHistory + * .state ⇒ String + * .rawState ⇒ HostState + * .members ⇒ Array.<Item> + * .descendents ⇒ Array.<Item> + * .isUninitialized ⇒ Boolean + * .tags ⇒ Array.<String> + * .getMetadataValue(namespace) ⇒ String + * .updateMetadataValue(namespace, value) ⇒ String + * .upsertMetadataValue(namespace, value) ⇒ Boolean + * .updateMetadataValues(namespaceToValues) + * .sendCommand(value) + * .sendCommandIfDifferent(value) ⇒ Boolean + * .postUpdate(value) + * .addGroups(...groupNamesOrItems) + * .removeGroups(...groupNamesOrItems) + * .addTags(...tagNames) + * .removeTags(...tagNames) + +```javascript +const item = items.getItem("KitchenLight"); +//send a ON command +item.sendCommand("ON"); +//Post an update +item.postUpdate("OFF"); +//Get state +console.log("KitchenLight state", item.state) +``` + +calling `item.history...` returns a ItemHistory object with the following functions: + +Note `serviceId` is optional, if omitted, the default persistance service will be used. + +* ItemHistory : object + * .averageSince(timestamp, serviceId) ⇒ Number + * .changedSince(timestamp, serviceId) ⇒ Number + * .deltaSince(timestamp, serviceId) ⇒ Number + * .deviationSince(timestamp, serviceId) ⇒ Number + * .evolutionRate(timestamp, serviceId) ⇒ Number + * .historicState(timestamp, serviceId) ⇒ state + * .lastUpdate(serviceId) ⇒ Date + * .latestState(serviceId) ⇒ state + * .maximumSince(timestamp,serviceId) ⇒ state + * .minimumSince(timestamp,serviceId) ⇒ state + * .persist(serviceId) + * .previousState(skipEqual,serviceId) ⇒ state + * .sumSince(timestamp, serviceId) ⇒ Number + * .updatedSince(timestamp, serviceId) ⇒ Boolean + * .varianceSince(timestamp,serviceId) ⇒ state + +```javascript +var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)); +var item = items.getItem("KitchenDimmer"); +console.log("KitchenDimmer averageSince", item.history.averageSince(yesterday)); +``` + +### Actions + +The actions namespace allows interactions with openHAB actions. The following are a list of standard actions. + +Additional actions provided by user installed addons can be accessed using their common name on the actions name space +(example: `actions.Pushsafer.pushsafer(...)`) + +See [openhab-js : actions ](https://openhab.github.io/openhab-js/actions.html) for full API documentation and additional actions. + +#### Audio Actions + +See [openhab-js : actions.Audio ](https://openhab.github.io/openhab-js/actions.html#.Audio) for complete documentation + +### BusEvent + +See [openhab-js : actions.BusEvent ](https://openhab.github.io/openhab-js/actions.html#.BusEvent) for complete documentation + +## Ephemeris Actions + +See [openhab-js : actions.Ephemeris ](https://openhab.github.io/openhab-js/actions.html#.Ephemeris) for complete documentation + +Ephemeris is a way to determine what type of day today or a number of days before or after today is. For example, a way to determine if today is a weekend, a bank holiday, someone’s birthday, trash day, etc. + +Additional information can be found on the [Ephemeris Actions Docs](https://www.openhab.org/docs/configuration/actions.html#ephemeris) as well as the [Ephemeris JavaDoc](https://www.openhab.org/javadoc/latest/org/openhab/core/model/script/actions/ephemeris). + +```javascript +// Example +let weekend = actions.Ephemeris.isWeekend(); +``` + +#### Exec Actions + +See [openhab-js : actions.Exec ](https://openhab.github.io/openhab-js/actions.html#.Exec) for complete documentation Execute a command line. ```javascript -openhab.Exec = Java.type('org.openhab.core.model.script.actions.Exec'); -let Duration = Java.type('java.time.Duration'); // Execute command line. -openhab.Exec.executeCommandLine('echo', 'Hello World!'); +actions.Exec.executeCommandLine('echo', 'Hello World!'); // Execute command line with timeout. -openhab.Exec.executeCommandLine(Duration.ofSeconds(20), 'echo', 'Hello World!'); +let Duration = Java.type('java.time.Duration'); +actions.Exec.executeCommandLine(Duration.ofSeconds(20), 'echo', 'Hello World!'); // Get response from command line. -let response = openhab.Exec.executeCommandLine('echo', 'Hello World!'); +let response = actions.Exec.executeCommandLine('echo', 'Hello World!'); // Get response from command line with timeout. -response = openhab.Exec.executeCommandLine(Duration.ofSeconds(20), 'echo', 'Hello World!'); +response = actions.Exec.executeCommandLine(Duration.ofSeconds(20), 'echo', 'Hello World!'); ``` ### HTTP Actions -For available actions have a look at the [HTTP Actions Docs](https://www.openhab.org/docs/configuration/actions.html#http-actions). +See [openhab-js : actions.HTTP ](https://openhab.github.io/openhab-js/actions.html#.HTTP) for complete documentation ```javascript -openhab.HTTP = Java.type('org.openhab.core.model.script.actions.HTTP'); - // Example GET Request -var response = openhab.HTTP.sendHttpGetRequest(''); +var response = actions.HTTP.sendHttpGetRequest(''); ``` Replace `` with the request url. -### Timers +### ScriptExecution Actions + +See [openhab-js : actions.ScriptExecution ](https://openhab.github.io/openhab-js/actions.html#.ScriptExecution) for complete documentation + ```javascript -let ZonedDateTime = Java.type('java.time.ZonedDateTime'); -let now = ZonedDateTime.now(); -openhab.ScriptExecution = Java.type('org.openhab.core.model.script.actions.ScriptExecution'); +let now = time.ZonedDateTime.now(); // Function to run when the timer goes off. function timerOver () { @@ -134,7 +321,7 @@ function timerOver () { } // Create the Timer. -this.myTimer = openhab.ScriptExecution.createTimer(now.plusSeconds(10), timerOver); +this.myTimer = actions.ScriptExecution.createTimer(now.plusSeconds(10), timerOver); // Cancel the timer. this.myTimer.cancel(); @@ -145,106 +332,279 @@ let active = this.myTimer.isActive(); // Reschedule the timer. this.myTimer.reschedule(now.plusSeconds(5)); ``` +### Semantics Actions -### Scripts Actions +See [openhab-js : actions.Semantics ](https://openhab.github.io/openhab-js/actions.html#.Semantics) for complete documentation -Call scripts created in the UI (Settings -> Scripts) with or without parameters. +### Things Actions -```javascript -openhab.scriptExtension = Java.type('org.openhab.core.automation.module.script.ScriptExtensionProvider'); -let bundleContext = Java.type('org.osgi.framework.FrameworkUtil').getBundle(openhab.scriptExtension.class).getBundleContext(); -openhab.RuleManager = bundleContext.getService(bundleContext.getServiceReference('org.openhab.core.automation.RuleManager')); +See [openhab-js : actions.Things ](https://openhab.github.io/openhab-js/actions.html#.Things) for complete documentation -// Simple call. -openhab.RuleManager.runNow(''); +### Voice Actions -// Advanced call with arguments. -let map = new java.util.HashMap(); -map.put('identifier1', 'value1'); -map.put('identifier2', 'value2'); -// Second argument is whether to consider the conditions, third is a Map (a way to pass data). -openhab.RuleManager.runNow('', true, map); -``` +See [openhab-js : actions.Voice ](https://openhab.github.io/openhab-js/actions.html#.Voice) for complete documentation -Replace `` with your script's (unique-)id. +### Cloud Notification Actions -## Cloud Notification Actions +(optional action if openhab-cloud is installed) Notification actions may be placed in Rules to send alerts to mobile devices registered with an [openHAB Cloud instance](https://github.com/openhab/openhab-cloud) such as [myopenHAB.org](https://myopenhab.org/). For available actions have a look at the [Cloud Notification Actions Docs](https://www.openhab.org/docs/configuration/actions.html#cloud-notification-actions). ```javascript -openhab.NotificationAction = Java.type('org.openhab.io.openhabcloud.NotificationAction') - // Example -openhab.NotificationAction.sendNotification('', ''); // to a single myopenHAB user identified by e-mail -openhab.NotificationAction.sendBroadcastNotification(''); // to all myopenHAB users +actions.NotificationAction.sendNotification('', ''); // to a single myopenHAB user identified by e-mail +actions.NotificationAction.sendBroadcastNotification(''); // to all myopenHAB users ``` Replace `` with the e-mail address of the user. Replace `` with the notification text. -## Persistence Extensions +### Cache -For available commands have a look at [Persistence Extensions in Scripts ans Rules](https://www.openhab.org/docs/configuration/persistence.html#persistence-extensions-in-scripts-and-rules). +The cache namespace provides a default cache that can be use to set and retrieve objects that will be persisted between reloads of scripts. -For deeper information have a look at the [Persistence Extensions JavaDoc](https://www.openhab.org/javadoc/latest/org/openhab/core/persistence/extensions/persistenceextensions). +See [openhab-js : cache ](https://openhab.github.io/openhab-js/cache.html) for full API documentation -```javascript -openhab.PersistenceExtensions = Java.type('org.openhab.core.persistence.extensions.PersistenceExtensions'); -let ZonedDateTime = Java.type('java.time.ZonedDateTime'); -let now = ZonedDateTime.now(); +* cache : object + * .get(key, defaultSupplier) ⇒ Object | null + * .put(key, value) ⇒ Previous Object | null + * .remove(key) ⇒ Previous Object | null -// Example -var avg = openhab.PersistenceExtensions.averageSince(itemRegistry.getItem(''), now.minusMinutes(5), "influxdb"); +The `defaultSupplier` provided function will return a default value if a specified key is not already associated with a value + +**Example** *(Get a previously set value with a default value (times = 0))* +```js +let counter = cache.get("counter", () => ({ "times": 0 })); +console.log("Count",counter.times++); ``` -Replace `` with the persistence service to use. -Replace `` with the itemname. +**Example** *(Get a previously set object)* +```js +let counter = cache.get("counter"); +if(counter == null){ + counter = {times: 0}; + cache.put("counter", counter); +} +console.log("Count",counter.times++); +``` +### Log -## Ephemeris Actions - -Ephemeris is a way to determine what type of day today or a number of days before or after today is. For example, a way to determine if today is a weekend, a bank holiday, someone’s birthday, trash day, etc. - -For available actions, have a look at the [Ephemeris Actions Docs](https://www.openhab.org/docs/configuration/actions.html#ephemeris). - -For deeper information have a look at the [Ephemeris JavaDoc](https://www.openhab.org/javadoc/latest/org/openhab/core/model/script/actions/ephemeris). +By default the JS Scripting binding supports console logging like `console.log()` and `console.debug()` to the openHAB +default log. Additionally scripts may create their own native openHAB logs using the log namespace ```javascript -openhab.Ephemeris = Java.type('org.openhab.core.model.script.actions.Ephemeris'); +let logger = log('my_logger'); -// Example -let weekend = openhab.Ephemeris.isWeekend(); +//prints "Hello World!" +logger.debug("Hello {}!", "world"); ``` -## Types and Units +### Time -Import types from openHAB Core for type conversion and more. -Import Units from openHAB Core for unit conversion and more. +openHAB internally makes extensive use of the `java.time` package. openHAB-JS exports the excellent [JS-Joda](#https://js-joda.github.io/js-joda/) library via the `time` namespace, which is a native Javascript port of the same API standard used in Java for `java.time`. Anywhere that a native Java `ZonedDateTime` or `Duration` is required, the runtime will automatically convert a JS-Joda `ZonedDateTime` or `Duration` to its Java counterpart. + +Examples: +```javascript +var now = time.ZonedDateTime.now(); +var yesterday = time.ZonedDateTime.now().minusHours(24); + +var item = items.getItem("Kitchen"); +console.log("averageSince", item.history.averageSince(yesterday)); +``` ```javascript -openhab.typeOrUnit = Java.type('org.openhab.core.library.types.typeOrUnit'); - -// Example -openhab.HSBType = Java.type('org.openhab.core.library.types.HSBType'); -let hsb = openhab.HSBType.fromRGB(4, 6, 9); +actions.Exec.executeCommandLine(time.Duration.ofSeconds(20), 'echo', 'Hello World!'); ``` -Available types are: -* `QuantityType` -* `StringListType` -* `RawType` -* `DateTimeType` -* `DecimalType` -* `HSBType` -* `PercentType` -* `PointType` -* `StringType` +See [JS-Joda](#https://js-joda.github.io/js-joda/) for more examples and complete API usage. -Available untis are: -* `SIUnits` -* `ImperialUnits` -* `MetricPrefix` -* `Units` -* `BinaryPrefix` +## File Based Rules + +The JSScripting binding will load scripts from `automation/js` in the user configuration directory. The system will automatically reload scripts when changes are detected to files. Local variable state is not persisted among reloads, see using the [cache](#cache) for a connivent way to persist objects. + +File based rules can be created in 2 different ways: using [JSRule](#jsrule) or the [Rule Builder](#rule-builder). + +See [openhab-js : rules ](https://openhab.github.io/openhab-js/rules.html) for full API documentation + +### JSRule + +JSRules provides a simple, declarative syntax for defining rules that will be executed based on a trigger condition + +```javascript +const email = "juliet@capulet.org" + +rules.JSRule({ + name: "Balcony Lights ON at 5pm", + description: "Light will turn on when it's 5:00pm", + triggers: [triggers.GenericCronTrigger("0 0 17 * * ?")], + execute: data => { + items.getItem("BalconyLights").sendCommand("ON"); + actions.NotificationAction.sendNotification(email, "Balcony lights are ON"); + } +}); +``` + +Multiple triggers can be added, some example triggers include: + +```javascript +triggers.ChannelEventTrigger('astro:sun:local:rise#event', 'START') + +triggers.ItemStateChangeTrigger('my_item', 'OFF', 'ON') + +triggers.ItemStateUpdateTrigger('my_item', 'OFF') + +triggers.ItemCommandTrigger('my_item', 'OFF') + +triggers.GroupStateChangeTrigger('my_group', 'OFF', 'ON') + +triggers.GroupStateUpdateTrigger('my_group', 'OFF') + +triggers.GroupCommandTrigger('my_group', 'OFF') + +triggers.ThingStatusUpdateTrigger('some:thing:uuid','OFFLINE') + +triggers.ThingStatusChangeTrigger('some:thing:uuid','ONLINE','OFFLINE') + +triggers.SystemStartlevelTrigger(40) //Rules loaded + +triggers.SystemStartlevelTrigger(50) //Rule engine started + +triggers.SystemStartlevelTrigger(70) //User interfaces started + +triggers.SystemStartlevelTrigger(80) //Things initialized + +triggers.SystemStartlevelTrigger(100) //Startup Complete + +triggers.GenericCronTrigger('0 30 16 * * ? *') + +triggers.TimeOfDayTrigger('19:00') + +``` + +See [openhab-js : triggers ](https://openhab.github.io/openhab-js/triggers.html) in the API documentation for a full list of all triggers. + +### Rule Builder + +The Rule Builder provides a convenient API to write rules in a high-level, readable style using a builder pattern. + +Rules are started by calling `rules.when()` and can chain together [triggers](#rule-builder-triggers), +[conditions](#rule-builder-conditions) and [operations](#rule-builder-operations) in the following pattern: + +```javascript +rules.when().triggerType()...if().conditionType().then().operationType()...build(name,description); +``` + +Rule are completed by calling `.build(name,description)` , if name or description are omitted, a generated value will be used. + +A simple example of this would look like: + +```javascript +rules.when().item("F1_Light").changed().then().send("changed").toItem("F2_Light").build("My Rule", "My First Rule"); +``` + +Operations and conditions can also optionally take functions: + +```javascript +rules.when().item("F1_light").changed().then(event => { + console.log(event); +}).build("Test Rule", "My Test Rule"); +``` +see [Examples](#rule-builder-examples) for further patterns + +#### Rule Builder Triggers + +* `when()` +* `or()` + * `.channel(channelName)` Specifies a channel event as a source for the rule to fire. + * `.triggered(event)` Trigger on a specific event name + * `.cron(cronExpression)` Specifies a cron schedule for the rule to fire. + * `.item(itemName)` Specifies an item as the source of changes to trigger a rule. + * `.for(duration)` + * `.from(state)` + * `.to(state)` + * `.fromOff()` + * `.toOn()` + * `.receivedCommand()` + * `.receivedUpdate()` + * `.memberOf(groupName)` + * `.for(duration)` + * `.from(state)` + * `.to(state)` + * `.fromOff()` + * `.toOn()` + * `.receivedCommand()` + * `.receivedUpdate()` + * `.system()` + * `.ruleEngineStarted()` + * `.rulesLoaded()` + * `.startupComplete()` + * `.thingsInitialized()` + * `.userInterfacesStarted()` + * `.startLevel(level)` + * `.thing(thingName)` + * `changed()` + * `updated()` + * `from(state)` + * `to(state)` + +Additionally all the above triggers have the following functions: +* `.if()` or `.if(fn)` -> a [rule condition](#rule-builder-conditions) +* `.then()` or `.then(fn)` -> a [rule operation](#rule-builder-operations) +* `.or()` -> a [rule trigger](#rule-builder-triggers) (chain additional triggers) + +#### Rule Builder Conditions + +* `if(optionalFunction)` + * `.stateOfItem(state)` + +#### Rule Builder Operations +* `then(optionalFunction)` + * `.build(name, description)` + * `.copyAndSendState()` + * `.copyState()` + * `.inGroup(groupName)` + * `.postIt()` + * `.postUpdate(state)` + * `.send(command)` + * `.sendIt()` + * `.sendOff()` + * `.sendOn()` + * `.sendToggle()` + +#### Rule Builder Examples + +```javascript +//Basic rule, when the BedroomLight1 is changed, run a custom function +rules.when().item('BedroomLight1').changed().then(e => { + console.log("BedroomLight1 state", e.newState) +}.build(); + +//turn on the kitchen light at SUNSET +rules.when().timeOfDay("SUNSET").then().sendOn().toItem("KitchenLight").build("Sunset Rule","turn on the kitchen light +at SUNSET"); + +//turn off the kitchen light at 9PM +rules.when().cron("0 0 21 * * ?").then().sendOff().toItem("KitchenLight").build("9PM Rule", "turn off the kitchen light +at 9PM"); + +//set the colour of the hall light to pink at 9PM +rules.when().cron("0 0 21 * * ?").then().send("300,100,100").toItem("HallLight").build("Pink Rule", "set the colour of +the hall light to pink at 9PM"); + +//when the switch S1 status changes to ON, then turn on the HallLight +rules.when().item('S1').changed().toOn().then(sendOn().toItem('HallLight')).build("S1 Rule"); + +//when the HallLight colour changes pink, if the function fn returns true, then toggle the state of the OutsideLight +rules.when().item('HallLight').changed().to("300,100,100").if(fn).then().sendToggle().toItem('OutsideLight').build(); + +//and some rules which can be toggled by the items created in the 'gRules' Group: + +//when the HallLight receives a command, send the same command to the KitchenLight +rules.when().item('HallLight').receivedCommand().then().sendIt().toItem('KitchenLight').build("Hall Light", ""); + +//when the HallLight is updated to ON, make sure that BedroomLight1 is set to the same state as the BedroomLight2 +rules.when().item('HallLight').receivedUpdate().then().copyState().fromItem('BedroomLight1').toItem('BedroomLight2').build(); + +``` diff --git a/bundles/org.openhab.automation.jsscripting/docs/rule-config.png b/bundles/org.openhab.automation.jsscripting/docs/rule-config.png new file mode 100644 index 000000000..e26c6a07c Binary files /dev/null and b/bundles/org.openhab.automation.jsscripting/docs/rule-config.png differ diff --git a/bundles/org.openhab.automation.jsscripting/docs/rule-engines.png b/bundles/org.openhab.automation.jsscripting/docs/rule-engines.png new file mode 100644 index 000000000..6faa0fb6c Binary files /dev/null and b/bundles/org.openhab.automation.jsscripting/docs/rule-engines.png differ diff --git a/bundles/org.openhab.automation.jsscripting/docs/rule-script.png b/bundles/org.openhab.automation.jsscripting/docs/rule-script.png new file mode 100644 index 000000000..585d16e15 Binary files /dev/null and b/bundles/org.openhab.automation.jsscripting/docs/rule-script.png differ diff --git a/bundles/org.openhab.automation.jsscripting/docs/settings.png b/bundles/org.openhab.automation.jsscripting/docs/settings.png new file mode 100644 index 000000000..a342844e4 Binary files /dev/null and b/bundles/org.openhab.automation.jsscripting/docs/settings.png differ diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml index aad7fb28b..af6b049e2 100644 --- a/bundles/org.openhab.automation.jsscripting/pom.xml +++ b/bundles/org.openhab.automation.jsscripting/pom.xml @@ -25,7 +25,7 @@ 21.3.0 6.2.1 ${project.version} - openhab@1.2.0 + openhab@1.2.1 diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java index f4939baa0..d9a9e6091 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java @@ -25,6 +25,8 @@ import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; +import java.time.Duration; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -38,6 +40,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.Value; import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem; import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel; import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel; @@ -77,10 +81,27 @@ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngi public OpenhabGraalJSScriptEngine(@Nullable String injectionCode) { super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately this.globalScript = GLOBAL_REQUIRE + (injectionCode != null ? injectionCode : ""); + + // Custom translate JS Objects - > Java Objects + HostAccess hostAccess = HostAccess.newBuilder(HostAccess.ALL) + // Translate JS-Joda ZonedDateTime to java.time.ZonedDateTime + .targetTypeMapping(Value.class, ZonedDateTime.class, (v) -> v.hasMember("withFixedOffsetZone"), v -> { + return ZonedDateTime + .parse(v.invokeMember("withFixedOffsetZone").invokeMember("toString").asString()); + }, HostAccess.TargetMappingPrecedence.LOW) + + // Translate JS-Joda Duration to java.time.Duration + .targetTypeMapping(Value.class, Duration.class, + // picking two members to check as Duration has many common function names + (v) -> v.hasMember("minusDuration") && v.hasMember("toNanos"), v -> { + return Duration.ofNanos(v.invokeMember("toNanos").asLong()); + }, HostAccess.TargetMappingPrecedence.LOW) + .build(); + delegate = GraalJSScriptEngine.create( Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false") .build(), - Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true) + Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true).allowHostAccess(hostAccess) .option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH) .option("js.nashorn-compat", "true") // to ease migration .option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we