triggers.js

/**
 * Triggers namespace.
 * This namespace allows creation of openHAB rule triggers.
 *
 * It is possible to skip parameter configuration in triggers by using `undefined`.
 *
 * @namespace triggers
 */

const log = require('./log')('triggers');
const { randomUUID } = require('./utils');
const { _isItem } = require('./helpers');

const ModuleBuilder = Java.type('org.openhab.core.automation.util.ModuleBuilder');
const Configuration = Java.type('org.openhab.core.config.core.Configuration');

/**
 * @typedef { import("./items/items").Item } Item
 * @private
 */

/**
 * Creates a trigger. Internal function, instead use predefined trigger types.
 *
 * @private
 * @param {string} typeString the type of trigger to create
 * @param {string} [name] the name of the trigger
 * @param {object} config the trigger configuration
 * @returns {HostTrigger} {@link HostTrigger}
 */
function _createTrigger (typeString, name, config) {
  if (typeof name === 'undefined' || name === null) {
    name = randomUUID();
  }

  log.debug('Creating {} trigger as {} with config: {}', typeString, name, JSON.stringify(config || {}));

  return ModuleBuilder.createTrigger()
    .withId(name)
    .withTypeUID(typeString)
    .withConfiguration(new Configuration(config))
    .build();
}

/**
 * Creates a trigger that fires upon specific events in a channel.
 *
 * @example
 * ChannelEventTrigger('astro:sun:local:rise#event', 'START');
 *
 * @memberof triggers
 * @param {string} channel the name of the channel
 * @param {string} event the name of the event to listen for
 * @param {string} [triggerName] the optional name of the trigger to create
 *
 */
const ChannelEventTrigger = (channel, event, triggerName) =>
  _createTrigger('core.ChannelEventTrigger', triggerName, {
    channelUID: channel,
    event
  });

/**
 * Creates a trigger that fires upon an Item changing state.
 *
 * @example
 * ItemStateChangeTrigger('my_item'); // changed
 * ItemStateChangeTrigger('my_item', 'OFF', 'ON'); // changed from OFF to ON
 * ItemStateChangeTrigger('my_item', undefined, 'ON'); // changed to ON
 * ItemStateChangeTrigger('my_item', 'OFF', undefined); // changed from OFF
 *
 * @memberof triggers
 * @param {Item|string} itemOrName the {@link Item} or the name of the Item to monitor for change
 * @param {string} [oldState] the previous state of the Item
 * @param {string} [newState] the new state of the Item
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const ItemStateChangeTrigger = (itemOrName, oldState, newState, triggerName) =>
  _createTrigger('core.ItemStateChangeTrigger', triggerName, {
    itemName: _isItem(itemOrName) ? itemOrName.name : itemOrName,
    state: newState,
    previousState: oldState
  });

/**
 * Creates a trigger that fires upon an Item receiving a state update. Note that the Item does not need to change state.
 *
 * @example
 * ItemStateUpdateTrigger('my_item'); // received update
 * ItemStateUpdateTrigger('my_item', 'OFF'); // received update OFF
 *
 * @memberof triggers
 * @param {Item|string} itemOrName the {@link Item} or the name of the Item to monitor for change
 * @param {string} [state] the new state of the Item
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const ItemStateUpdateTrigger = (itemOrName, state, triggerName) =>
  _createTrigger('core.ItemStateUpdateTrigger', triggerName, {
    itemName: _isItem(itemOrName) ? itemOrName.name : itemOrName,
    state
  });

/**
 * Creates a trigger that fires upon an Item receiving a command. Note that the Item does not need to change state.
 *
 * @example
 * ItemCommandTrigger('my_item'); // received command
 * ItemCommandTrigger('my_item', 'OFF'); // received command OFF
 *
 * @memberof triggers
 * @param {Item|string} itemOrName the {@link Item} or the name of the Item to monitor for change
 * @param {string} [command] the command received
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const ItemCommandTrigger = (itemOrName, command, triggerName) =>
  _createTrigger('core.ItemCommandTrigger', triggerName, {
    itemName: _isItem(itemOrName) ? itemOrName.name : itemOrName,
    command
  });

/**
 * Creates a trigger that fires upon a member of a group changing state. Note that group Item does not need to change state.
 *
 * @example
 * GroupStateChangeTrigger('my_group', 'OFF', 'ON');
 *
 * @memberof triggers
 * @param {Item|string} groupOrName the group {@link Item} or the name of the group to monitor for change
 * @param {string} [oldState] the previous state of the group
 * @param {string} [newState] the new state of the group
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const GroupStateChangeTrigger = (groupOrName, oldState, newState, triggerName) =>
  _createTrigger('core.GroupStateChangeTrigger', triggerName, {
    groupName: _isItem(groupOrName) ? groupOrName.name : groupOrName,
    state: newState,
    previousState: oldState
  });

/**
 * Creates a trigger that fires upon a member of a group receiving a state update. Note that group Item does not need to change state.
 *
 * @example
 * GroupStateUpdateTrigger('my_group', 'OFF');
 *
 * @memberof triggers
 * @param {Item|string} groupOrName the group {@link Item} or the name of the group to monitor for change
 * @param {string} [state] the new state of the group
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const GroupStateUpdateTrigger = (groupOrName, state, triggerName) =>
  _createTrigger('core.GroupStateUpdateTrigger', triggerName, {
    groupName: _isItem(groupOrName) ? groupOrName.name : groupOrName,
    state
  });

/**
 * Creates a trigger that fires upon a member of a group receiving a command. Note that the group Item does not need to change state.
 *
 * @example
 * GroupCommandTrigger('my_group', 'OFF');
 *
 * @memberof triggers
 * @param {Item|string} groupOrName the group {@link Item} or the name of the group to monitor for commands
 * @param {string} [command] the command received
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const GroupCommandTrigger = (groupOrName, command, triggerName) =>
  _createTrigger('core.GroupCommandTrigger', triggerName, {
    groupName: _isItem(groupOrName) ? groupOrName.name : groupOrName,
    command
  });

/**
 * Creates a trigger that fires upon a Thing status updating.
 *
 * @example
 * ThingStatusUpdateTrigger('some:thing:uuid', 'OFFLINE');
 *
 * @memberof triggers
 * @param {string} thingUID the name of the thing to monitor for a status updating
 * @param {string} [status] the optional status to monitor for
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const ThingStatusUpdateTrigger = (thingUID, status, triggerName) =>
  _createTrigger('core.ThingStatusUpdateTrigger', triggerName, {
    thingUID,
    status
  });

/**
 * Creates a trigger that fires upon a Thing status changing.
 *
 * @example
 * ThingStatusChangeTrigger('some:thing:uuid', 'ONLINE', 'OFFLINE');
 *
 * @memberof triggers
 * @param {string} thingUID the name of the thing to monitor for a status change
 * @param {string} [status] the optional status to monitor for
 * @param {string} [previousStatus] the optional previous state to monitor from
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const ThingStatusChangeTrigger = (thingUID, status, previousStatus, triggerName) =>
  _createTrigger('core.ThingStatusChangeTrigger', triggerName, {
    thingUID,
    status,
    previousStatus
  });

/**
 * Creates a trigger that fires if a given start level is reached by the system
 *
 * @example
 * SystemStartlevelTrigger(40)  // Rules loaded
 * SystemStartlevelTrigger(50)  // Rule engine started
 * SystemStartlevelTrigger(70)  // User interfaces started
 * SystemStartlevelTrigger(80)  // Things initialized
 * SystemStartlevelTrigger(100) // Startup Complete
 *
 * @memberof triggers
 * @param {string|number} startlevel the system start level to be triggered on
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const SystemStartlevelTrigger = (startlevel, triggerName) =>
  _createTrigger('core.SystemStartlevelTrigger', triggerName, {
    startlevel: startlevel.toString()
  });

/**
 * Creates a trigger that fires on a cron schedule. The supplied cron expression defines when the trigger will fire.
 *
 * @example
 * GenericCronTrigger('0 30 16 * * ? *');
 *
 * @memberof triggers
 * @param {string} expression the cron expression defining the triggering schedule
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const GenericCronTrigger = (expression, triggerName) =>
  _createTrigger('timer.GenericCronTrigger', triggerName, {
    cronExpression: expression
  });

/**
 * Creates a trigger that fires daily at a specific time. The supplied time defines when the trigger will fire.
 *
 * @example
 * TimeOfDayTrigger('19:00');
 *
 * @memberof triggers
 * @param {string} time the time expression (in `HH:mm`) defining the triggering schedule
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const TimeOfDayTrigger = (time, triggerName) =>
  _createTrigger('timer.TimeOfDayTrigger', triggerName, {
    time
  });

/**
 * Creates a trigger that fires at an (optional) date and time specified in a DateTime Item.
 *
 * @example
 * DateTimeTrigger('MyDateTimeItem');
 *
 * @memberof triggers
 * @param {Item|string} itemOrName the {@link Item} or the name of the Item to monitor for change
 * @param {boolean} [timeOnly=false] Specifies whether only the time of the DateTime Item should be compared or the date and time.
 * @param {number} [offset=0] The offset in seconds to add to the time of the DateTime Item
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const DateTimeTrigger = (itemOrName, timeOnly = false, offset = 0, triggerName) => {
  // backward compatibility for (itemOrName, timeOnly, triggerName) signature:
  if (typeof offset === 'string') {
    triggerName = offset;
    offset = 0;
    console.warn('The signature DateTimeTrigger(itemOrName, timeOnly, triggerName) is deprecated. Please use DateTimeTrigger(itemOrName, timeOnly, offset, triggerName) instead.');
  }
  return _createTrigger('timer.DateTimeTrigger', triggerName, {
    itemName: _isItem(itemOrName) ? itemOrName.name : itemOrName,
    timeOnly,
    offset
  });
};

/**
 * Creates a trigger that fires upon a matching event from the event bus.
 *
 * Please have a look at the {@link https://www.openhab.org/docs/developer/utils/events.html Event Bus docs} to learn about events.
 *
 * @example
 * // Triggers when an Item is added or removed
 * GenericEventTrigger('openhab/items/**', '', ['ItemAddedEvent', 'ItemRemovedEvent'])
 * // Triggers when the Item "OutdoorLights" is commanded by expire
 * GenericEventTrigger('openhab/items/OutdoorLights/*', 'org.openhab.core.expire', 'ItemCommandEvent')
 *
 *
 * @memberof triggers
 * @param {string} eventTopic Specifies the event topic to match, asa file-system style glob (`*` and `**` operators)
 * @param {string} eventSource Specifies the event source such as `org.openhab.core.expire`,
 * @param {string|string[]} eventTypes Specifies the event type(s) to match, e.g. `ItemAddedEvent`, `ItemRemovedEvent`, `ItemCommandEvent`, etc.
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const GenericEventTrigger = (eventTopic, eventSource, eventTypes, triggerName) =>
  _createTrigger('core.GenericEventTrigger', triggerName, {
    topic: eventTopic,
    source: eventSource,
    types: (Array.isArray(eventTypes) ? eventTypes.join(',') : eventTypes),
    payload: ''
  });

/**
 * Creates a trigger for the {@link https://openhab.org/addons/automation/pwm/ Pulse Width Modulation (PWM) Automation} add-on.
 *
 * @example
 * rules.JSRule({
 *   name: 'PWM rule',
 *   triggers: [
 *     triggers.PWMTrigger('pwm_dimmer', 10);
 *   ],
 *   execute: (event) => {
 *     items.getItem('pwm_switch').sendCommand(event.receivedCommand);
 *   }
 * });
 *
 * @memberof triggers
 * @param {string} dutycycleItem Item (PercentType) to read the duty cycle from
 * @param {number} interval constant interval in which the output is switch ON and OFF again (in sec)
 * @param {number} [minDutyCycle] any duty cycle below this value will be increased to this value
 * @param {number} [maxDutyCycle] any duty cycle above this value will be decreased to this value
 * @param {number} [deadManSwitch] output will be switched off, when the duty cycle is not updated within this time (in ms)
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const PWMTrigger = (dutycycleItem, interval, minDutyCycle, maxDutyCycle, deadManSwitch, triggerName) =>
  _createTrigger('pwm.trigger', triggerName, {
    dutycycleItem,
    interval,
    minDutyCycle,
    maxDutyCycle,
    deadManSwitch
  });

/**
 * Creates a trigger for the {@link https://www.openhab.org/addons/automation/pidcontroller/ PID Controller Automation} add-on.
 *
 * @example
 * rules.JSRule({
 *   name: 'PID rule',
 *   triggers: [
 *     triggers.PIDTrigger('currentTemperature', 'targetTemperature', 1, 1, 1, 1, 10000, undefined, 1, 100);
 *   ],
 *   execute: (event) => {
 *     // Look out what the max value for your Item is!
 *     const command = parseInt(event.receivedCommand) > 100 ? '100' : event.receivedCommand;
 *     items.getItem('thermostat').sendCommand(command);
 *   }
 * });
 *
 * @memberof triggers
 * @param {string} inputItem name of the input Item (e.g. temperature sensor value)
 * @param {string} setpointItem name of the setpoint Item (e.g. desired room temperature)
 * @param {number} kp P: {@link https://www.openhab.org/addons/automation/pidcontroller/#proportional-p-gain-parameter Proportional Gain} parameter
 * @param {number} ki I: {@link https://www.openhab.org/addons/automation/pidcontroller/#integral-i-gain-parameter Integral Gain} parameter
 * @param {number} kd D: {@link https://www.openhab.org/addons/automation/pidcontroller/#derivative-d-gain-parameter Deritative Gain} parameter
 * @param {number} kdTimeConstant D-T1: {@link https://www.openhab.org/addons/automation/pidcontroller/#derivative-time-constant-d-t1-parameter Derivative Gain Time Constant} in sec
 * @param {number} loopTime The interval the output value will be updated in milliseconds. Note: the output will also be updated when the input value or the setpoint changes.
 * @param {string} [commandItem] Name of the item to send a String "RESET" to reset the I- and the D-part to 0.
 * @param {number} [integralMinValue] The I-part will be limited (min) to this value.
 * @param {number} [integralMaxValue] The I-part will be limited (max) to this value.
 * @param {string} [pInspectorItem] name of the debug Item for the current P-part
 * @param {string} [iInspectorItem] name of the debug Item for the current I-part
 * @param {string} [dInspectorItem] name of the debug Item for the current D-part
 * @param {string} [errorInspectorItem] name of the debug Item for the current regulation difference (error)
 * @param {string} [triggerName] the optional name of the trigger to create
 */
const PIDTrigger = (inputItem, setpointItem, kp = 1, ki = 1, kd = 1, kdTimeConstant = 1, loopTime = 1000, commandItem, integralMinValue, integralMaxValue, pInspectorItem, iInspectorItem, dInspectorItem, errorInspectorItem, triggerName) =>
  _createTrigger('pidcontroller.trigger', triggerName, {
    input: inputItem,
    setpoint: setpointItem,
    kp,
    ki,
    kd,
    kdTimeConstant,
    loopTime,
    commandItem,
    integralMinValue,
    integralMaxValue,
    pInspector: pInspectorItem,
    iInspector: iInspectorItem,
    dInspector: dInspectorItem,
    eInspector: errorInspectorItem
  });

/* not yet tested
ItemStateCondition: (itemName, state, triggerName) => createTrigger("core.ItemStateCondition", triggerName, {
    "itemName": itemName,
    "operator": "=",
    "state": state
}
GenericCompareCondition: (itemName, state, operator, triggerName) => createTrigger("core.GenericCompareCondition", triggerName, {
        "itemName": itemName,
        "operator": operator,// matches, ==, <, >, =<, =>
        "state": state
})
*/

module.exports = {
  ChannelEventTrigger,
  ItemStateChangeTrigger,
  ItemStateUpdateTrigger,
  ItemCommandTrigger,
  GroupStateChangeTrigger,
  GroupStateUpdateTrigger,
  GroupCommandTrigger,
  ThingStatusUpdateTrigger,
  ThingStatusChangeTrigger,
  SystemStartlevelTrigger,
  GenericCronTrigger,
  GenericEventTrigger,
  TimeOfDayTrigger,
  DateTimeTrigger,
  PWMTrigger,
  PIDTrigger
};