UNPKG

@hotzware/openhab-tools

Version:

Tools for the openHAB JavaScript Automation Add-On.

208 lines (200 loc) 9.72 kB
/** * Copyright (c) 2021 Florian Hotze * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 */ const { items, rules, triggers } = require('openhab'); /** * Scene Engine * * Call scenes using an Item as controller and update the Item's state to the matching scene number on scene members' change. * To create a new scene engine, use {@link rulesx.createSceneEngine}. * @memberof rulesx */ class SceneEngine { /** * Constructor to create an instance. Do not call directly, instead call {@link getSceneEngine}. * * @hideconstructor * @param {object} sceneDefinition scenes definition * @param {string} sceneDefinition.controller name of Item that calls the scenes * @param {object[]} sceneDefinition.scenes Array of scenes * @param {number} sceneDefinition.scenes[].value integer identifying the scene * @param {object[]} sceneDefinition.scenes[].targets Array of scene members * @param {string} sceneDefinition.scenes[].targets[].item name of Item * @param {string} sceneDefinition.scenes[].targets[].value target state of Item * @param {boolean} [sceneDefinition.scenes[].targets[].required=true] whether the Item's state must match the target state when the engine gets the current scene on change of a member * @param {function} [sceneDefinition.scenes[].targets[].conditionFn] the Item is only commanded and required for scene checks if the evaluation of this function returns true */ constructor (sceneDefinition) { if (typeof sceneDefinition.controller !== 'string') { throw Error('controller is not supplied or is not string!'); } if (typeof sceneDefinition.scenes !== 'object') { throw Error('scenes is not an Array!'); } this.controller = sceneDefinition.controller; this.scenes = sceneDefinition.scenes; } /** * Gets all required triggers for the scene rule. * For the controller a command trigger, for scene members change triggers. * Scene members that are not required are excluded from the triggers. * * @private * @returns {Array} rule triggers */ getTriggers () { const ruleTriggers = []; const updateTriggers = []; console.debug(`Adding ItemCommandTrigger for [${this.controller}].`); ruleTriggers.push(triggers.ItemCommandTrigger(this.controller)); // For each selectorState. for (let j = 0; j < this.scenes.length; j++) { const currentScene = this.scenes[j]; // For for each sceneTarget, the member items that are required (default is required). for (let k = 0; k < currentScene.targets.length; k++) { const target = currentScene.targets[k]; if (target.required !== false && updateTriggers.indexOf(target.item) === -1) { updateTriggers.push(target.item); } } } for (let i = 0; i < updateTriggers.length; i++) { console.debug(`Adding ItemStateChangeTrigger for [${updateTriggers[i]}].`); ruleTriggers.push(triggers.ItemStateChangeTrigger(updateTriggers[i])); } return ruleTriggers; } /** * Calls the scene. Sets the scene members to the given target state. * * @private * @param {string|number} sceneNumber value of controller / number of scene to call */ callScene (sceneNumber) { sceneNumber = (typeof sceneNumber === 'number') ? sceneNumber : parseInt(sceneNumber); // Get the correct scene value. for (let j = 0; j < this.scenes.length; j++) { // Get the correct scene targets. if (this.scenes[j].value === sceneNumber) { console.info(`Call scene: Found value [${this.scenes[j].value}] of controller [${this.controller}].`); const targets = this.scenes[j].targets; // Send commands to member items. for (let curTarget = 0; curTarget < targets.length; curTarget++) { if (typeof targets[curTarget].conditionFn === 'function') { const result = targets[curTarget].conditionFn(); if (result === true) { console.info(`Call scene: Commanding ${targets[curTarget].item} to ${targets[curTarget].value} as condition is met (conditionFn returned ${result}).`); items.getItem(targets[curTarget].item).sendCommand(targets[curTarget].value); } else { console.info(`Call scene: Not commanding ${targets[curTarget].item} to ${targets[curTarget].value} as condition is not met (conditionFn returned ${result}).`); } } else { console.info(`Call scene: Commanding ${targets[curTarget].item} to ${targets[curTarget].value}.`); items.getItem(targets[curTarget].item).sendCommand(targets[curTarget].value); } } } } } /** * When a scene member changes, check whether a scene and which scene matches all required targets. * * @private */ checkScene () { let selectorValueMatching = 0; // The selector value of the matching scene. let sceneFound = false; // Check each scene. The first one matching is used. for (let curState = 0; curState < this.scenes.length && sceneFound === false; curState++) { let statesMatchingValue = true; // Checks whether scene's targets are matching. As soon as one is not matching it's target value, the next selector state is checked. for (let curTarget = 0; curTarget < this.scenes[curState].targets.length && statesMatchingValue === true; curTarget++) { const target = this.scenes[curState].targets[curTarget]; if (!(target.required === false)) { const itemState = items.getItem(target.item).state.toString(); console.debug(`Check scene (value [${this.scenes[curState].value}] of controller [${this.controller}]): Checking scene member [${target.item}] with state [${itemState}].`); let result = true; if (typeof target.conditionFn === 'function') { if (target.conditionFn() !== true) { console.debug(`Check scene (value [${this.scenes[curState].value}] of controller [${this.controller}]): Scene member [${target.item}] with state [${itemState}] is not required to match as conditionFn did not return true.`); result = false; } } if (result === true) { // Check whether the current Item state does not match the target state. if (!( (itemState === target.value) || (itemState === '0' && target.value.toString().toUpperCase() === 'OFF') || (itemState === '100' && target.value.toString().toUpperCase() === 'ON') || (itemState === '0' && target.value.toString().toUpperCase() === 'UP') || (itemState === '100' && target.value.toString().toUpperCase() === 'DOWN') )) { statesMatchingValue = false; console.debug(`Check scene (value [${this.scenes[curState].value}] of controller [${this.controller}]): Scene member [${target.item}] with state [${itemState}] does not match [${target.value}] or is not required to match.`); } } } } // When all members match the target value if (statesMatchingValue === true) { console.info(`Check scene: Found matching value [${this.scenes[curState].value}] of controller [${this.controller}].`); // Store the current value that is matching all required targets. selectorValueMatching = this.scenes[curState].value; sceneFound = true; } // Update controller. items.getItem(this.controller).postUpdate(selectorValueMatching.toString()); } } /** * Returns the scene engine rule. * Do NOT call directly, instead use {@link getSceneEngine}. * * @private */ getRule () { rules.JSRule({ name: `SceneEngine for controller ${this.controller}`, description: 'Rule to run the SceneEngine.', triggers: this.getTriggers(), execute: event => { if (event.triggerType === 'ItemCommandTrigger') { console.info(`Call scene: Event [${event.triggerType}] of [${event.itemName}].`); this.callScene(event.receivedCommand); } else if (event.triggerType === 'ItemStateChangeTrigger') { console.info(`Check scene: Event [${event.triggerType}] of [${event.itemName}].`); this.checkScene(); } }, id: `sceneEngine-for-${this.controller}`, tags: ['@hotzware/openhab-tools', 'createSceneEngine'] }); } } /** * Provides the {@link rulesx.SceneEngine}. * * @memberof rulesx * @param {object} sceneDefinition scenes definition * @param {string} sceneDefinition.controller name of Item that calls the scenes * @param {object[]} sceneDefinition.scenes Array of scenes * @param {number} sceneDefinition.scenes[].value integer identifying the scene * @param {object[]} sceneDefinition.scenes[].targets Array of scene members * @param {string} sceneDefinition.scenes[].targets[].item name of Item * @param {string} sceneDefinition.scenes[].targets[].value target state of Item * @param {boolean} [sceneDefinition.scenes[].targets[].required=true] whether the Item's state must match the target state when the engine gets the current scene on change of a member * @param {function} [sceneDefinition.scenes[].targets[].conditionFn] the Item is only commanded and required for scene checks if the evaluation of this function returns true */ const createSceneEngine = (sceneDefinition) => { // @ts-ignore new SceneEngine(sceneDefinition).getRule(); }; module.exports = { createSceneEngine };