Skip to content

Commit 456800c

Browse files
authored
[items] Add support for providing Items, metadata & channel links from file-based scripts (#441)
Signed-off-by: Florian Hotze <dev@florianhotze.com>
1 parent e461854 commit 456800c

26 files changed

Lines changed: 667 additions & 455 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
| Type | Namespace | Description | | Reference | Breaking |
6+
|-------------|---------------|-----------------------------------------------------------------------------------|:--|--------------------------------------------------------|----------|
7+
| Enhancement | `environment` | Add `isFileBasedScript()` function to determine whether script is file-based | | [#441](https://github.com/openhab/openhab-js/pull/441) | no |
8+
| Enhancement | `items` | Add support for providing Items, metadata & channel links from file-based scripts | | [#441](https://github.com/openhab/openhab-js/pull/441) | no |
9+
310
## 5.11.1 (5.11.0)
411

512
| Type | Namespace | Description | Reference | Breaking |

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,9 @@ See [openhab-js : items](https://openhab.github.io/openhab-js/items.html) for fu
373373
- .getItem(name, nullIfMissing) ⇒ `Item`
374374
- .getItems() ⇒ `Array[Item]`
375375
- .getItemsByTag(...tagNames) ⇒ `Array[Item]`
376-
- .addItem([itemConfig](#itemconfig))
377-
- .removeItem(itemOrItemName) ⇒ `boolean`
378-
- .replaceItem([itemConfig](#itemconfig))
376+
- .addItem([itemConfig](#itemconfig), persist) ⇒ `Item`
377+
- .removeItem(itemOrItemName) ⇒ `Item|null`
378+
- .replaceItem([itemConfig](#itemconfig))`Item|null`
379379
- .safeItemName(s) ⇒ `string`
380380

381381
```javascript
@@ -502,6 +502,20 @@ items.replaceItem({
502502

503503
See [openhab-js : ItemConfig](https://openhab.github.io/openhab-js/global.html#ItemConfig) for full API documentation.
504504

505+
#### Providing Items (& metadata & channel links) from Scripts
506+
507+
The `addItem` method can be used to provide Items from scripts in a configuration-as-code manner.
508+
It also allows providing metadata and channel configurations for the Item, basically creating the Item as if it was defined in a `.items` file.
509+
The benefit of using `addItem` is that you can use loops, conditions or generator functions to create lots of Items without the need to write them all out in a file or manually in the UI.
510+
511+
When called from file-based scripts, the created Item will share the lifecycle with the script, meaning it will be removed when the script is unloaded.
512+
You can use the `persist` parameter to optionally persist the Item from file-based scripts.
513+
514+
When called from UI-based scripts, the Item will be stored permanently and will not be removed when the script is unloaded.
515+
Keep in mind that attempting to add an Item with the same name as an existing Item will result in an error.
516+
517+
See [openhab-js : Item](https://openhab.github.io/openhab-js/items.html#.addItem) for full API documentation.
518+
505519
#### `ItemPersistence`
506520

507521
Calling `Item.persistence` returns an `ItemPersistence` object with the following functions:

src/environment.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// This module MUST NOT depend on any other library code to avoid circular dependencies
2+
3+
/**
4+
* Environment namespace.
5+
* This namespace handles utilities for determining the script environment and retrieving information about it.
6+
*
7+
* @namespace environment
8+
*/
9+
10+
/**
11+
* Returns whether the code is running from a file-based script.
12+
* This is determined by checking if the `javax.script.filename` global variable is defined.
13+
* This is useful to distinguish between file-based scripts and UI-based scripts in openHAB.
14+
*
15+
* @memberOf environment
16+
* @return {boolean} true if the script is file-based, false otherwise
17+
*/
18+
function isFileBasedScript () {
19+
return globalThis['javax.script.filename'] !== undefined;
20+
}
21+
22+
/**
23+
* Returns whether the host openHAB version supports providing openHAB entities via the `@runtime/provider` module.
24+
*
25+
* @private
26+
* @return {boolean} true if the provider module is available, false otherwise
27+
*/
28+
function _hasProviderSupport () {
29+
return !!require('@runtime/provider').itemRegistry;
30+
}
31+
32+
/**
33+
* Returns whether the registry implementations from the `@runtime/provider` module should be used instead of the default ones from the `@runtime` module.
34+
* Provider implementations should be used if the host openHAB version supports it and the script is running from a file-based script.
35+
*
36+
* @memberOf environment
37+
* @return {boolean} true if the provider registry implementations should be used, false otherwise
38+
*/
39+
function useProviderRegistries () {
40+
return _hasProviderSupport() && isFileBasedScript();
41+
}
42+
43+
module.exports = {
44+
isFileBasedScript,
45+
useProviderRegistries
46+
};

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ module.exports = {
2121
get osgi () { return require('./osgi'); },
2222
get cache () { return require('./cache'); },
2323
get time () { return require('./time'); },
24-
get Quantity () { return require('./quantity').getQuantity; }
24+
get Quantity () { return require('./quantity').getQuantity; },
25+
get environment () { return require('./environment'); }
2526
};

src/items/itemchannellink.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
const osgi = require('../osgi');
2+
const utils = require('../utils');
3+
const environment = require('../environment');
4+
const log = require('../log')('itemchannellink');
5+
const { _getItemName } = require('../helpers');
6+
7+
const itemChannelLinkRegistry = environment.useProviderRegistries()
8+
? require('@runtime/provider').itemChannelLinkRegistry
9+
: osgi.getService('org.openhab.core.thing.link.ItemChannelLinkRegistry');
10+
const JavaItemChannelLink = Java.type('org.openhab.core.thing.link.ItemChannelLink');
11+
const ChannelUID = Java.type('org.openhab.core.thing.ChannelUID');
12+
const Configuration = Java.type('org.openhab.core.config.core.Configuration');
13+
14+
/**
15+
* Item channel link namespace.
16+
* This namespace provides access to Item channel links.
17+
*
18+
* @namespace items.itemChannelLink
19+
*/
20+
21+
/**
22+
* @typedef {import('./items').Item} Item
23+
* @private
24+
*/
25+
26+
/**
27+
* Class representing an openHAB Item -> channel link.
28+
*
29+
* @memberof items.itemChannelLink
30+
* @hideconstructor
31+
*/
32+
class ItemChannelLink {
33+
itemName;
34+
channelUID;
35+
configuration;
36+
37+
/**
38+
* @param {*} rawItemChannelLink {@link https://www.openhab.org/javadoc/latest/org/openhab/core/thing/link/itemchannellink org.openhab.core.thing.link.ItemChannelLink}
39+
*/
40+
constructor (rawItemChannelLink) {
41+
this.itemName = rawItemChannelLink.getItemName().toString();
42+
this.channelUID = rawItemChannelLink.getLinkedUID().toString();
43+
this.configuration = utils.javaMapToJsObj(rawItemChannelLink.getConfiguration().getProperties());
44+
}
45+
}
46+
47+
/**
48+
* Gets a channel link of from an Item.
49+
*
50+
* @memberof items.itemChannelLink
51+
* @param {Item|string} itemOrName {@link Item} or the name of the Item
52+
* @param {string} channelUID
53+
* @returns {ItemChannelLink|null} the ItemChannelLink or `null` if none exists
54+
*/
55+
function getItemChannelLink (itemOrName, channelUID) {
56+
const itemName = _getItemName(itemOrName);
57+
log.debug(`Getting ItemChannelLink ${itemName} -> ${channelUID} from registry.`);
58+
const itemChannelLink = itemChannelLinkRegistry.get(itemName + ' -> ' + channelUID);
59+
if (itemChannelLink === null) return null;
60+
return new ItemChannelLink(itemChannelLink);
61+
}
62+
63+
/**
64+
* Creates a new ItemChannelLink object.
65+
* This ItemChannelLink is not registered with any provider and therefore cannot be accessed.
66+
*
67+
* @private
68+
* @param {string} itemName the name of the Item
69+
* @param {string} channelUID
70+
* @param {object} [conf] channel configuration
71+
* @returns {JavaItemChannelLink} ItemChannelLink object
72+
*/
73+
function _createItemChannelLink (itemName, channelUID, conf) {
74+
log.debug(`Creating ItemChannelLink ${itemName} -> ${channelUID}`);
75+
if (typeof conf === 'object') {
76+
log.debug(` with configuration: ${JSON.stringify(conf)}`);
77+
return new JavaItemChannelLink(itemName, new ChannelUID(channelUID), new Configuration(conf));
78+
} else {
79+
return new JavaItemChannelLink(itemName, new ChannelUID(channelUID));
80+
}
81+
}
82+
83+
/**
84+
* Adds a new channel link to an Item.
85+
*
86+
* If this is called from file-based scripts, the Item -> channel link is registered with the ScriptedItemChannelLinkProvider and shares the same lifecycle as the script.
87+
* You can still persist the Item -> channel link permanently in this case by setting the `persist` parameter to `true`.
88+
* If this is called from UI-based scripts, the Item -> channel link is stored to the ManagedItemChannelLinkProvider and independent of the script's lifecycle.
89+
*
90+
* @memberOf items.itemChannelLink
91+
* @param {Item|string} itemOrName {@link Item} or the name of the Item
92+
* @param {string} channelUID
93+
* @param {object} [configuration] channel configuration
94+
* @param {boolean} [persist=false] whether to persist the Item -> channel link permanently (only respected for file-based scripts)
95+
* @returns {ItemChannelLink} the ItemChannelLink
96+
* @throws {Error} if the Item -> channel link already exists
97+
*/
98+
function addItemChannelLink (itemOrName, channelUID, configuration, persist = false) {
99+
const itemName = _getItemName(itemOrName);
100+
log.debug(`Adding ItemChannelLink ${itemName} -> ${channelUID} to registry...`);
101+
let itemChannelLink = _createItemChannelLink(itemName, channelUID, configuration);
102+
try {
103+
itemChannelLink = (persist && environment.useProviderRegistries()) ? itemChannelLinkRegistry.addPermanent(itemChannelLink) : itemChannelLinkRegistry.add(itemChannelLink);
104+
} catch (e) {
105+
if (e instanceof Java.type('java.lang.IllegalArgumentException')) {
106+
throw new Error(`Cannot add ItemChannelLink ${itemName} -> ${channelUID}: already exists`);
107+
} else {
108+
throw e; // re-throw other errors
109+
}
110+
}
111+
return new ItemChannelLink(itemChannelLink);
112+
}
113+
114+
/**
115+
* Updates a channel link of an Item.
116+
*
117+
* @private
118+
* @param {string} itemName the name of the Item
119+
* @param {string} channelUID
120+
* @param {object} [configuration] channel configuration
121+
* @returns {ItemChannelLink|null} the old ItemChannelLink or `null` if none exists
122+
*/
123+
function _updateItemChannelLink (itemName, channelUID, configuration) {
124+
log.debug(`Updating ItemChannelLink ${itemName} -> ${channelUID} in registry...`);
125+
let itemChannelLink = _createItemChannelLink(itemName, channelUID, configuration);
126+
itemChannelLink = itemChannelLinkRegistry.update(itemChannelLink);
127+
if (itemChannelLink === null) return null;
128+
return new ItemChannelLink(itemChannelLink);
129+
}
130+
131+
/**
132+
* Adds or updates a channel link of an Item.
133+
* If you use this in file-based scripts, better use {@link addItemChannelLink} to provide channel links.
134+
*
135+
* If an Item -> channel link is not provided by this script or the ManagedItemChannelLinkProvider, it is not editable and a warning is logged.
136+
*
137+
* @memberof items.itemChannelLink
138+
* @param {Item|string} itemOrName {@link Item} or the name of the Item
139+
* @param {string} channelUID
140+
* @param {object} [configuration] channel configuration
141+
* @returns {ItemChannelLink|null} the old ItemChannelLink or `null` if it did not exist
142+
*/
143+
function replaceItemChannelLink (itemOrName, channelUID, configuration) {
144+
const itemName = _getItemName(itemOrName);
145+
const itemChannelLink = getItemChannelLink(itemName, channelUID);
146+
return (itemChannelLink === null) ? addItemChannelLink(itemName, channelUID, configuration) : _updateItemChannelLink(itemName, channelUID, configuration);
147+
}
148+
149+
/**
150+
* Removes a channel link from an Item.
151+
*
152+
* @memberof items.itemChannelLink
153+
* @param {Item|string} itemOrName {@link Item} or the name of the Item
154+
* @param {string} channelUID
155+
* @returns {ItemChannelLink|null} the removed ItemChannelLink or `null` if none exists, or it cannot be removed
156+
*/
157+
function removeItemChannelLink (itemOrName, channelUID) {
158+
const itemName = _getItemName(itemOrName);
159+
log.debug(`Removing ItemChannelLink ${itemName} -> ${channelUID} from registry...`);
160+
const itemChannelLink = itemChannelLinkRegistry.remove(itemName + ' -> ' + channelUID);
161+
if (itemChannelLink === null) return null;
162+
return new ItemChannelLink(itemChannelLink);
163+
}
164+
165+
/**
166+
* Removes all channel links from the given Item.
167+
*
168+
* @memberof items.itemChannelLink
169+
* @param {string} itemName the name of the Item
170+
* @returns {number} number of links removed
171+
*/
172+
function removeLinksForItem (itemName) {
173+
return itemChannelLinkRegistry.removeLinksForItem(itemName);
174+
}
175+
176+
/**
177+
* Removes all orphaned (Item or channel missing) links.
178+
*
179+
* @memberof items.itemChannelLink
180+
* @returns {number} number of links removed
181+
*/
182+
function removeOrphanedItemChannelLinks () {
183+
return itemChannelLinkRegistry.purge();
184+
}
185+
186+
module.exports = {
187+
getItemChannelLink,
188+
addItemChannelLink,
189+
replaceItemChannelLink,
190+
removeItemChannelLink,
191+
removeLinksForItem,
192+
removeOrphanedItemChannelLinks,
193+
ItemChannelLink
194+
};

0 commit comments

Comments
 (0)