/**
* Items namespace.
* This namespace handles querying and updating openHAB Items.
* @namespace items
*/
const osgi = require('../osgi');
const utils = require('../utils');
const log = require('../log')('items');
const metadata = require('./metadata/metadata');
const ItemHistory = require('./item-history');
const ItemSemantics = require('./item-semantics');
/** @typedef {import('@js-joda/core').ZonedDateTime} time.ZonedDateTime */
const time = require('../time');
const { UnDefType, OnOffType, events, itemRegistry } = require('@runtime');
const itemBuilderFactory = osgi.getService('org.openhab.core.items.ItemBuilderFactory');
const managedItemProvider = osgi.getService('org.openhab.core.items.ManagedItemProvider');
// typedefs need to be global for TypeScript to fully work
/**
* @typedef {object} ItemConfig configuration describing an Item
* @property {string} type the type of the Item
* @property {string} name Item name for the Item to create
* @property {string} [label] the label for the Item
* @property {string} [category] the category (icon) for the Item
* @property {string[]} [groups] an array of groups the Item is a member of
* @property {string[]} [tags] an array of tags for the Item
* @property {string|Object} [channels] for single channel link a string or for multiple an object { channeluid: configuration }; configuration is an object
* @property {ItemMetadata} [metadata] either object { namespace: value } or { namespace: { value: value, config: {} } }
* @property {string} [giBaseType] the group Item base type for the Item
* @property {HostGroupFunction} [groupFunction] the group function used by the Item
*/
/**
* @typedef {object} ItemMetadata Item metadata configuration, not fully documented
* @property {object} [stateDescription] `stateDescription` configuration, required for most UIs
* @property {object} [stateDescription.config] config of this metadata namespace
* @property {string} [stateDescription.config.pattern] state formatting pattern, required for most UIs, see {@link https://www.openhab.org/docs/configuration/items.html#state-presentation}
* @property {object} [expire] `expire` configuration, see {@link https://www.openhab.org/docs/configuration/items.html#parameter-expire}
* @property {string} [expire.value] e.g. `0h30m0s,command=OFF`
* @property {object} [expire.config]
* @property {string} [expire.config.ignoreStateUpdates] If the ignore state updates checkbox is set, only state changes will reset the timer; `true` or `false`
* @property {object} [autoupdate] `autoupdate` configuration, see {@link https://www.openhab.org/docs/configuration/items.html#parameter-expire}
* @property {string} [autoupdate.value] `true` or `false`
*/
/**
* Tag value to be attached to all dynamically created Items.
*
* @memberof items
*/
const DYNAMIC_ITEM_TAG = '_DYNAMIC_';
/**
* Class representing an openHAB Item
*
* @memberof items
*/
class Item {
/**
* Create an Item, wrapping a native Java openHAB Item. Don't use this constructor, instead call {@link getItem}.
* @param {HostItem} rawItem Java Item from Host
* @hideconstructor
*/
constructor (rawItem) {
if (typeof rawItem === 'undefined') {
throw Error('Supplied Item is undefined');
}
/**
* raw Java Item
* @type {HostItem}
*/
this.rawItem = rawItem;
/**
* Access historical states for this Item {@link items.ItemHistory}
* @type {ItemHistory}
*/
this.history = new ItemHistory(rawItem);
/**
* Access Semantic information of this Item {@link items.ItemSemantics}
* @type {ItemSemantics}
*/
this.semantics = new ItemSemantics(rawItem);
}
/**
* The type of the Item: the Simple (without package) name of the Java Item type, such as 'Switch'.
* @type {string}
*/
get type () {
return this.rawItem.getClass().getSimpleName();
}
/**
* The name of the Item.
* @type {string}
*/
get name () {
return this.rawItem.getName();
}
/**
* The label attached to the Item.
* @type {string}
*/
get label () {
return this.rawItem.getLabel();
}
/**
* The state of the Item.
* @type {string}
*/
get state () {
return this.rawState.toString();
}
/**
* The raw state of the Item, as a Java {@link https://www.openhab.org/javadoc/latest/org/openhab/core/types/state State object}.
* @type {HostState}
*/
get rawState () {
return this.rawItem.getState();
}
/**
* Members / children / direct descendents of the current group Item (as returned by 'getMembers()'). Must be a group Item.
* @type {Item[]}
*/
get members () {
return utils.javaSetToJsArray(this.rawItem.getMembers()).map(raw => new Item(raw));
}
/**
* All descendents of the current group Item (as returned by 'getAllMembers()'). Must be a group Item.
* @type {Item[]}
*/
get descendents () {
return utils.javaSetToJsArray(this.rawItem.getAllMembers()).map(raw => new Item(raw));
}
/**
* Whether this Item is uninitialized (`true if it has not been initialized`).
* @type {boolean}
*/
get isUninitialized () {
if (this.rawItem.getState() instanceof UnDefType ||
this.rawItem.getState().toString() === 'Undefined' ||
this.rawItem.getState().toString() === 'Uninitialized'
) {
return true;
} else {
return false;
}
}
/**
* Gets metadata with the given name for this Item.
* @param {string} namespace The namespace for the metadata to retrieve
* @returns {{configuration: *, value: string}|null} metadata or null if the Item has no metadata with the given name
*/
getMetadata (namespace) {
return metadata.getMetadata(this.name, namespace);
}
/**
* Updates or adds the given metadata for this Item.
* @param {string} namespace name of the metadata
* @param {string} value value for this metadata
* @param {object} [configuration] optional metadata configuration
* @returns {{configuration: *, value: string}|null} old metadata or `null` if the Item has no metadata with the given name
*/
replaceMetadata (namespace, value, configuration) {
return metadata.replaceMetadata(this.name, namespace, value, configuration);
}
/**
* Removes metadata with a given name from a given Item.
* @param {string} namespace name of the metadata
* @returns {{configuration: *, value: string}|null} removed metadata or `null` if the Item has no metadata with the given name
*/
removeMetadata (namespace) {
return metadata.removeMetadata(this.name, namespace);
}
/**
* Sends a command to the Item.
*
* @param {string|time.ZonedDateTime|HostState} value the value of the command to send, such as 'ON'
* @see sendCommandIfDifferent
* @see postUpdate
*/
sendCommand (value) {
if (value instanceof time.ZonedDateTime) value = value.toOpenHabString();
events.sendCommand(this.rawItem, value);
}
/**
* Sends a command to the Item, but only if the current state is not what is being sent.
*
* @param {string|time.ZonedDateTime|HostState} value the value of the command to send, such as 'ON'
* @returns {boolean} true if the command was sent, false otherwise
* @see sendCommand
*/
sendCommandIfDifferent (value) {
if (value instanceof time.ZonedDateTime) value = value.toOpenHabString();
if (value.toString() !== this.state.toString()) {
this.sendCommand(value);
return true;
}
return false;
}
/**
* Calculates the toggled state of this Item. For Items like Color and
* Dimmer, getStateAs(OnOffType) is used and the toggle calculated off
* of that.
* @returns the toggled state (e.g. 'OFF' if the Item is 'ON')
* @throws error if the Item is uninitialized or is a type that doesn't make sense to toggle
*/
getToggleState () {
if (this.isUninitialized) {
throw Error('Cannot toggle uninitialized Items');
}
switch (this.type) {
case 'PlayerItem' :
return this.state === 'PAUSE' ? 'PLAY' : 'PAUSE';
case 'ContactItem' :
return this.state === 'OPEN' ? 'CLOSED' : 'OPEN';
default: {
const oldState = this.rawItem.getStateAs(OnOffType);
if (oldState) {
return oldState.toString() === 'ON' ? 'OFF' : 'ON';
} else {
throw Error('Toggle not supported for Items of type ' + this.type);
}
}
}
}
/**
* Sends a command to flip the Item's state (e.g. if it is 'ON' an 'OFF'
* command is sent).
* @throws error if the Item is uninitialized or a type that cannot be toggled or commanded
*/
sendToggleCommand () {
if (this.type === 'ContactItem') {
throw Error('Cannot command Contact Items');
}
this.sendCommand(this.getToggleState());
}
/**
* Posts an update to flip the Item's state (e.g. if it is 'ON' an 'OFF'
* update is posted).
* @throws error if the Item is uninitialized or a type that cannot be toggled
*/
postToggleUpdate () {
this.postUpdate(this.getToggleState());
}
/**
* Posts an update to the Item.
*
* @param {string|time.ZonedDateTime|HostState} value the value of the command to send, such as 'ON'
* @see postToggleUpdate
* @see sendCommand
*/
postUpdate (value) {
if (value instanceof time.ZonedDateTime) value = value.toOpenHabString();
events.postUpdate(this.rawItem, value);
}
/**
* Gets the names of the groups this Item is member of.
* @returns {string[]}
*/
get groupNames () {
return utils.javaListToJsArray(this.rawItem.getGroupNames());
}
/**
* Adds groups to this Item
* @param {...string|...Item} groupNamesOrItems one or more names of the groups (or the group Items themselves)
*/
addGroups (...groupNamesOrItems) {
const groupNames = groupNamesOrItems.map((x) => (typeof x === 'string') ? x : x.name);
this.rawItem.addGroupNames(groupNames);
managedItemProvider.update(this.rawItem);
}
/**
* Removes groups from this Item
* @param {...string|...Item} groupNamesOrItems one or more names of the groups (or the group Items themselves)
*/
removeGroups (...groupNamesOrItems) {
const groupNames = groupNamesOrItems.map((x) => (typeof x === 'string') ? x : x.name);
for (const groupName of groupNames) {
this.rawItem.removeGroupName(groupName);
}
managedItemProvider.update(this.rawItem);
}
/**
* Gets the tags from this Item
* @type {string[]}
*/
get tags () {
return utils.javaSetToJsArray(this.rawItem.getTags());
}
/**
* Adds tags to this Item
* @param {...string} tagNames names of the tags to add
*/
addTags (...tagNames) {
this.rawItem.addTags(tagNames);
managedItemProvider.update(this.rawItem);
}
/**
* Removes tags from this Item
* @param {...string} tagNames names of the tags to remove
*/
removeTags (...tagNames) {
for (const tagName of tagNames) {
this.rawItem.removeTag(tagName);
}
managedItemProvider.update(this.rawItem);
}
toString () {
return this.rawItem.toString();
}
}
/**
* Creates a new Item object. This Item is not registered with any provider and therefore can not be accessed.
*
* Note that all Items created this way have an additional tag attached, for simpler retrieval later. This tag is
* created with the value {@link DYNAMIC_ITEM_TAG}.
*
* @memberof items
* @private
* @param {ItemConfig} itemConfig the Item config describing the Item
* @returns {Item} {@link items.Item}
* @throws {Error} {@link ItemConfig}.name or {@link ItemConfig}.type not set
* @throws failed to create Item
*/
const createItem = function (itemConfig) {
if (typeof itemConfig.name !== 'string' || typeof itemConfig.type !== 'string') throw Error('itemConfig.name or itemConfig.type not set');
itemConfig.name = safeItemName(itemConfig.name);
let baseItem;
if (itemConfig.type === 'Group' && typeof itemConfig.giBaseType !== 'undefined') {
baseItem = itemBuilderFactory.newItemBuilder(itemConfig.giBaseType, itemConfig.name + '_baseItem').build();
}
if (itemConfig.type !== 'Group') {
itemConfig.groupFunction = undefined;
}
if (typeof itemConfig.tags === 'undefined') {
itemConfig.tags = [];
}
itemConfig.tags.push(DYNAMIC_ITEM_TAG);
try {
let builder = itemBuilderFactory.newItemBuilder(itemConfig.type, itemConfig.name)
.withCategory(itemConfig.category)
.withLabel(itemConfig.label)
.withTags(utils.jsArrayToJavaSet(itemConfig.tags));
if (typeof itemConfig.groups !== 'undefined') {
builder = builder.withGroups(utils.jsArrayToJavaList(itemConfig.groups));
}
if (typeof baseItem !== 'undefined') {
builder = builder.withBaseItem(baseItem);
}
if (typeof itemConfig.groupFunction !== 'undefined') {
builder = builder.withGroupFunction(itemConfig.groupFunction);
}
const item = builder.build();
return new Item(item);
} catch (e) {
log.error('Failed to create Item: ' + e);
throw e;
}
};
/**
* Creates a new Item within OpenHab. This Item will persist to the provider regardless of the lifecycle of the script creating it.
*
* Note that all Items created this way have an additional tag attached, for simpler retrieval later. This tag is
* created with the value {@link DYNAMIC_ITEM_TAG}.
*
* @memberof items
* @param {ItemConfig} itemConfig the Item config describing the Item
* @returns {Item} {@link Items.Item}
* @throws {Error} {@link ItemConfig}.name or {@link ItemConfig}.type not set
* @throws failed to create Item
*/
const addItem = function (itemConfig) {
const item = createItem(itemConfig);
managedItemProvider.add(item.rawItem);
if (typeof itemConfig.metadata === 'object') {
const namespace = Object.keys(itemConfig.metadata);
for (const i in namespace) {
const namespaceValue = itemConfig.metadata[namespace[i]];
log.debug('addItem: Processing metadata namespace {}', namespace[i]);
if (typeof namespaceValue === 'string') { // namespace as key and it's value as value
metadata.replaceMetadata(itemConfig.name, namespace[i], namespaceValue);
} else if (typeof namespaceValue === 'object') { // namespace as key and { value: 'string', configuration: object } as value
metadata.replaceMetadata(itemConfig.name, namespace[i], namespaceValue.value, namespaceValue.config);
}
}
}
if (itemConfig.type !== 'Group') {
if (typeof itemConfig.channels === 'string') { // single channel link with string
metadata.itemchannellink.replaceItemChannelLink(itemConfig.name, itemConfig.channels);
} else if (typeof itemConfig.channels === 'object') { // multiple/complex channel links with channel as key and config object as value
const channels = Object.keys(itemConfig.channels);
for (const i in channels) metadata.itemchannellink.replaceItemChannelLink(itemConfig.name, channels[i], itemConfig.channels[channels[i]]);
}
}
return getItem(itemConfig.name);
};
/**
* Removes an Item from openHAB. The Item is removed immediately and cannot be recovered.
*
* @memberof items
* @param {string|Item} itemOrItemName the Item or the name of the Item to remove
* @returns {Item|null} the Item that has been removed or `null` if it has not been removed
*/
const removeItem = function (itemOrItemName) {
let itemName;
if (typeof itemOrItemName === 'string') {
itemName = itemOrItemName;
} else if (itemOrItemName instanceof Item) {
itemName = itemOrItemName.name;
} else {
log.warn('Item name is undefined (no Item supplied or supplied name is not a string) so cannot be removed');
return false;
}
let item;
try { // If the Item is not registered, ItemNotFoundException is thrown.
item = getItem(itemName);
} catch (e) {
if (Java.typeName(e.getClass()) === 'org.openhab.core.items.ItemNotFoundException') {
log.error('Item {} not registered so cannot be removed: {}', itemName, e.message);
return null;
} else { // If exception/error is not ItemNotFoundException, rethrow.
throw Error(e);
}
}
managedItemProvider.remove(itemName);
try { // If the Item has been successfully removed, ItemNotFoundException is thrown.
itemRegistry.getItem(itemName);
log.warn('Failed to remove Item: {}', itemName);
return null;
} catch (e) {
if (Java.typeName(e.getClass()) === 'org.openhab.core.items.ItemNotFoundException') {
return item;
} else { // If exception/error is not ItemNotFoundException, rethrow.
throw Error(e);
}
}
};
/**
* Replaces (or adds) an Item. If an Item exists with the same name, it will be removed and a new Item with
* the supplied parameters will be created in its place. If an Item does not exist with this name, a new
* Item will be created with the supplied parameters.
*
* This function can be useful in scripts which create a static set of Items which may need updating either
* periodically, during startup or even during development of the script. Using fixed Item names will ensure
* that the Items remain up-to-date, but won't fail with issues related to duplicate Items.
*
* @memberof items
* @param {ItemConfig} itemConfig the Item config describing the Item
* @returns {Item|null} the old Item or `null` if it did not exist
* @throws {Error} {@link ItemConfig}.name or {@link ItemConfig}.type not set
* @throws failed to create Item
*/
const replaceItem = function (itemConfig) {
const item = getItem(itemConfig.name, true);
if (item !== null) { // Item already existed
removeItem(itemConfig.name);
}
addItem(itemConfig);
return item;
};
/**
* Gets an openHAB Item.
* @memberof items
* @param {string} name the name of the Item
* @param {boolean} [nullIfMissing=false] whether to return null if the Item cannot be found (default is to throw an exception)
* @returns {Item|null} {@link items.Item} Item or `null` if `nullIfMissing` is true and Item is missing
*/
const getItem = (name, nullIfMissing = false) => {
try {
if (typeof name === 'string' || name instanceof String) {
return new Item(itemRegistry.getItem(name));
}
} catch (e) {
if (nullIfMissing) {
return null;
} else {
throw e;
}
}
};
/**
* Gets all openHAB Items.
*
* @memberof items
* @returns {Item[]} {@link items.Item}[]: all Items
*/
const getItems = () => {
return utils.javaSetToJsArray(itemRegistry.getItems()).map(i => new Item(i));
};
/**
* Gets all openHAB Items with a specific tag.
*
* @memberof items
* @param {string[]} tagNames an array of tags to match against
* @returns {Item[]} {@link items.Item}[]: the Items with a tag that is included in the passed tags
*/
const getItemsByTag = (...tagNames) => {
return utils.javaSetToJsArray(itemRegistry.getItemsByTag(tagNames)).map(i => new Item(i));
};
/**
* Helper function to ensure an Item name is valid. All invalid characters are replaced with an underscore.
* @memberof items
* @param {string} s the name to make value
* @returns {string} a valid Item name
*/
const safeItemName = s => s
.replace(/["']/g, '') // delete
.replace(/[^a-zA-Z0-9]/g, '_'); // replace with underscore
module.exports = {
safeItemName,
getItem,
getItems,
addItem,
getItemsByTag,
replaceItem,
createItem,
removeItem,
Item,
/**
* Custom indexer, to allow static Item lookup.
* @example
* let { my_object_name } = require('openhab').items.objects;
* ...
* let my_state = my_object_name.state; //my_object_name is an Item
*
* @returns {object} a collection of items allowing indexing by Item name
*/
objects: () => new Proxy({}, {
get: function (target, name) {
if (typeof name === 'string' && /^-?\d+$/.test(name)) { return getItem(name); }
throw Error('unsupported function call: ' + name);
}
}),
metadata
};