rules/rules.js

/**
 * Rules namespace.
 * This namespace allows creation of openHAB rules.
 *
 * @namespace rules
 */

// typedefs need to be global for TypeScript to fully work
/**
 * @typedef {object} EventObject When a rule is triggered, the script is provided the event instance that triggered it. The specific data depends on the event type. The `EventObject` provides several information about that trigger.
 *
 * Note:
 * `Group****Trigger`s use the equivalent `Item****Trigger` as trigger for each member.
 * Time triggers do not provide any event instance, therefore no property is populated.
 *
 * @property {string} oldState only for {@link triggers.ItemStateChangeTrigger} & {@link triggers.GroupStateChangeTrigger}: Previous state of Item or Group that triggered event
 * @property {string} newState only for {@link triggers.ItemStateChangeTrigger} & {@link triggers.GroupStateChangeTrigger}: New state of Item or Group that triggered event
 * @property {string} receivedState only for {@link triggers.ItemStateUpdateTrigger} & {@link triggers.GroupStateUpdateTrigger}: State that triggered event
 * @property {string} receivedCommand only for {@link triggers.ItemCommandTrigger}, {@link triggers.GroupCommandTrigger}, {@link triggers.PWMTrigger} & {@link triggers.PIDTrigger} : Command that triggered event
 * @property {string} itemName for all Item-related triggers: name of Item that triggered event
 * @property {string} groupName for all `Group****Trigger`s: name of the group whose member triggered event
 * @property {string} receivedEvent only for {@link triggers.ChannelEventTrigger}: Channel event that triggered event
 * @property {string} channelUID only for {@link triggers.ChannelEventTrigger}: UID of channel that triggered event
 * @property {string} oldStatus only for {@link triggers.ThingStatusChangeTrigger}: Previous state of Thing that triggered event
 * @property {string} newStatus only for {@link triggers.ThingStatusChangeTrigger}: New state of Thing that triggered event
 * @property {string} status only for {@link triggers.ThingStatusUpdateTrigger}: State of Thing that triggered event
 * @property {string} thingUID for all Thing-related triggers: UID of Thing that triggered event
 * @property {string} cronExpression for {@link triggers.GenericCronTrigger}: cron expression of the trigger
 * @property {string} time for {@link triggers.TimeOfDayTrigger}: time of day value of the trigger
 * @property {boolean} timeOnly for {@link triggers.DateTimeTrigger}: whether the trigger only considers the time part of the DateTime Item
 * @property {number} offset for {@link triggers.DateTimeTrigger}: offset in seconds added to the time of the DateTime Item
 * @property {string} eventType for all triggers except {@link triggers.PWMTrigger}, {@link triggers.PIDTrigger}: Type of event that triggered event (change, command, time, triggered, update, time)
 * @property {string} triggerType for all triggers except {@link triggers.PWMTrigger}, {@link triggers.PIDTrigger}: Type of trigger that triggered event
 * @property {string} eventName for all triggers: simple Java class name of the triggering event
 * @property {string} eventClass for all triggers: full Java class name of the triggering event
 * @property {string} module (user-defined or auto-generated) name of trigger
 * @property {*} raw original contents of the event including data passed from a calling rule
 * @property {*} payload if provided by event: payload of event in Java data types
 */

/**
 * @callback RuleCallback When a rule is run, a callback is executed.
 * @param {EventObject} event
 */

/**
 * @typedef {object} RuleConfig
 * @property {string} name name of the rule (used in UI)
 * @property {string} [description] description of the rule (used in UI)
 * @property {HostTrigger|HostTrigger[]} [triggers] which will fire the rule
 * @property {RuleCallback} execute callback to run when the rule fires
 * @property {string[]} [tags] tags for the rule (used in UI)
 * @property {string} [id] UID of the rule, if not provided, one is generated
 * @property {boolean} [overwrite=false] whether to overwrite an existing rule with the same UID
 * @property {boolean} [dedicatedContext=false] whether to run the rule in a separate dedicated context
 * @property {string} [switchItemName] (optional and only for {@link SwitchableJSRule}) name of the switch Item, which will get created automatically if it is not existent
 * @property {string} [ruleGroup] (optional and only for {@link SwitchableJSRule}) name of an Item group to use for the switch Item, which will get created automatically if it is not existent
 *
 * @description
 * Configuration object for {@link rules.JSRule}.
 * <p>A note about the `dedicatedContext` option:
 * When this option is enabled, the `execute` callback runs in a dedicated, separate context.
 * Therefore, the `execute` callback can **not** access any variables, functions, classes or other objects defined outside the callback function.
 * The benefit of this approach is that the rule does not share the context with any other rule and therefore can run at any time without waiting for other rule executions to complete.
 */

const SCRIPT_TYPE = 'application/javascript';
const GENERATED_RULE_ITEM_TAG = 'GENERATED_RULE_ITEM';

const items = require('../items/items');
const { randomUUID, jsArrayToJavaSet } = require('../utils');
const log = require('../log')('rules');
const { getService } = require('../osgi');
const triggers = require('../triggers');
const time = require('../time');

const { automationManager, ruleRegistry } = require('@runtime/RuleSupport');
const ruleManager = getService('org.openhab.core.automation.RuleManager');

/**
 * {@link https://www.openhab.org/javadoc/latest/org/openhab/core/automation/util/rulebuilder org.openhab.core.automation.util.RuleBuilder}
 */
const RuleBuilder = Java.type('org.openhab.core.automation.util.RuleBuilder');
/**
 * {@link https://www.openhab.org/javadoc/latest/org/openhab/core/automation/util/actionbuilder org.openhab.core.automation.util.ActionBuilder}
 */
const ActionBuilder = Java.type('org.openhab.core.automation.util.ActionBuilder');
const Configuration = Java.type('org.openhab.core.config.core.Configuration');

/**
  * Links an Item to a rule. When the Item is switched on or off, so will the rule be.
  *
  * @private
  * @param {HostRule} rule The rule to link to the Item.
  * @param {items.Item} item the Item to link to the rule.
  */
function _linkItemToRule (rule, item) {
  if (item.type !== 'Switch') {
    throw new Error('The linked Item for SwitchableJSRule must be a Switch Item!');
  }
  JSRule({
    name: 'vProxyRuleFor' + rule.getName(),
    description: 'Generated Rule to toggle real rule for ' + rule.getName(),
    triggers: [
      triggers.ItemStateUpdateTrigger(item.name)
    ],
    execute: function (data) {
      try {
        const itemState = data.receivedState;
        log.debug('Rule toggle Item state received as ' + itemState);
        ruleManager.setEnabled(rule.getUID(), itemState !== 'OFF');
        log.info((itemState === 'OFF' ? 'Disabled' : 'Enabled') + ' rule ' + rule.getName() + ' [' + rule.getUID() + ']');
      } catch (e) {
        log.error('Failed to toggle rule ' + rule.getName() + ': ' + e);
      }
    }
  });
}

/**
  * Gets the groups that a rule-toggling Item should be a member of. Will create the group Item if necessary.
  *
  * @private
  * @param {RuleConfig} ruleConfig The rule config describing the rule
  * @returns {string} the group name to put the Item in
  */
function _getGroupForItem (ruleConfig) {
  if (ruleConfig.ruleGroup) {
    const groupName = 'gRules' + items.safeItemName(ruleConfig.ruleGroup);
    log.debug('Creating rule group ' + ruleConfig.ruleGroup);
    items.replaceItem({
      name: groupName,
      type: 'Group',
      groups: ['gRules'],
      label: ruleConfig.ruleGroup,
      tags: [GENERATED_RULE_ITEM_TAG]
    });
    return groupName;
  }

  return 'gRules';
}

/**
  * Check whether a rule exists.
  * Only works for rules created in the same file.
  *
  * @private
  * @param {string} uid the UID of the rule
  * @returns {boolean} whether the rule exists
  */
function _ruleExists (uid) {
  return !(ruleManager.getStatusInfo(uid) == null);
}

/**
  * Remove a rule when it exists. The rule will be immediately removed.
  * Only works for rules created in the same file.
  *
  * @memberof rules
  * @param {string} uid the UID of the rule
  * @returns {boolean} whether the rule was actually removed
  */
function removeRule (uid) {
  if (_ruleExists(uid)) {
    log.info('Removing rule: {}', ruleRegistry.get(uid).name ? ruleRegistry.get(uid).name : uid);
    ruleRegistry.remove(uid);
    return !_ruleExists(uid);
  } else {
    return false;
  }
}

/**
  * Runs the rule with the given UID. Throws errors when the rule doesn't exist
  * or is unable to run (e.g. it's disabled).
  *
  * @memberof rules
  * @param {string} uid the UID of the rule to run
  * @param {object} [args={}] args optional dict of data to pass to the called rule
  * @param {boolean} [cond=true] when true, the called rule will only run if it's conditions are met
  * @throws {Error} throws an error if the rule does not exist or is not initialized.
  */
function runRule (uid, args = {}, cond = true) {
  const status = ruleManager.getStatus(uid);
  if (!status) {
    throw Error('There is no rule with UID ' + uid);
  }
  if (status.toString() === 'UNINITIALIZED') {
    throw Error('Rule ' + uid + ' is UNINITIALIZED');
  }

  ruleManager.runNow(uid, cond, args);
}

/**
  * Tests to see if the rule with the given UID is enabled or disabled. Throws
  * and error if the rule doesn't exist.
  *
  * @memberof rules
  * @param {string} uid
  * @returns {boolean} whether or not the rule is enabled
  * @throws {Error} an error when the rule is not found.
  */
function isEnabled (uid) {
  if (!_ruleExists(uid)) {
    throw Error('There is no rule with UID ' + uid);
  }
  return ruleManager.isEnabled(uid);
}

/**
  * Enables or disables the rule with the given UID. Throws an error if the rule doesn't exist.
  *
  * @memberof rules
  * @param {string} uid UID of the rule
  * @param {boolean} isEnabled when true, the rule is enabled, otherwise the rule is disabled
  * @throws {Error} an error when the rule is not found.
  */
function setEnabled (uid, isEnabled) {
  if (!_ruleExists(uid)) {
    throw Error('There is no rule with UID ' + uid);
  }
  ruleManager.setEnabled(uid, isEnabled);
}

/**
 * Creates a {@link https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java org.openhab.core.automation.internal.RuleImpl}.
 *
 * <p>This function allows creating rules that execute JavaScript code in a sandboxed context that is not shared with the script that created the rule.
 * This does NOT allow using anything defined outside the rule's callback function, such as functions, classes or variables from the context of the script that created the rule.
 * This approach, however, has the benefit that the rule runs in its own context and can therefore run at any time without the need to wait for other rule executions to complete.
 *
 * <p>Through the {@link RuleConfig} it can be opted into creating these rules instead of SimpleRules by the {@link rules.JSRule} function.
 *
 * @param {string} ruleUID
 * @param {RuleConfig} ruleConfig
 * @return {HostRule} the created {@link https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java RuleImpl}
 * @private
 */
function _createRule (ruleUID, ruleConfig) {
  let script = ruleConfig.execute.toString();
  script = script.match(/(function)?[^{]+\{([\s\S]*)}$/);
  if (script === null || script.length < 3) {
    throw Error(`Failed to add rule: Could not extract script from execute function for ${ruleConfig.name ? ruleConfig.name : ruleUID}!`);
  }
  script = script[2].trim();

  const action = ActionBuilder
    .create()
    .withId('1')
    .withTypeUID('script.ScriptAction')
    .withConfiguration(new Configuration({
      type: SCRIPT_TYPE,
      script
    }))
    .build();

  let ruleBuilder = RuleBuilder
    .create(ruleUID)
    .withActions([action]);

  if (ruleConfig.name) {
    ruleBuilder = ruleBuilder.withName(ruleConfig.name);
  }
  if (ruleConfig.description) {
    ruleBuilder = ruleBuilder.withDescription(ruleConfig.description);
  }
  if (Array.isArray(ruleConfig.tags)) {
    ruleBuilder = ruleBuilder.withTags(...ruleConfig.tags);
  }

  if (ruleConfig.triggers) {
    const triggers = Array.isArray(ruleConfig.triggers) ? ruleConfig.triggers : [ruleConfig.triggers];
    ruleBuilder = ruleBuilder.withTriggers(triggers);
  }

  return ruleBuilder.build();
}

/**
 * Creates a {@link https://www.openhab.org/javadoc/latest/org/openhab/core/automation/module/script/rulesupport/shared/simple/simplerule org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule}.
 *
 * <p>Simple rules allow creating rules that execute JavaScript code sharing the context of the script that created the rule.
 * This allows defining functions, classes and variables outside the rule's callback function and referencing them from within the rule.
 * This approach, however, comes with the limitation that only a single SimpleRule created from a script file can execute at a time.
 *
 * <p>SimpleRules are the default rules created by the {@link rules.JSRule} function.
 *
 * @private
 * @param {string} ruleUID
 * @param {RuleConfig} ruleConfig
 * @return {HostRule} the created {@link https://www.openhab.org/javadoc/latest/org/openhab/core/automation/module/script/rulesupport/shared/simple/simplerule SimpleRule}
 */
function _createSimpleRule (ruleUID, ruleConfig) {
  const SimpleRule = Java.extend(Java.type('org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule'));

  function doExecute (module, input) {
    try {
      return ruleConfig.execute(_getTriggeredData(input));
    } catch (error) {
      // logging an error is required for meaningful error log messages
      // when throwing error: error is caught by openHAB core and no meaningful message is logged
      let msg;
      if (error.stack) {
        msg = `Failed to execute rule ${ruleUID}: ${error}: ${error.stack}`;
      } else {
        msg = `Failed to execute rule ${ruleUID}: ${error}`;
      }
      console.error(msg);
      throw Error(msg);
    }
  }

  const rule = new SimpleRule({
    execute: doExecute,
    getUID: () => ruleUID
  });

  if (ruleConfig.description) {
    rule.setDescription(ruleConfig.description);
  }
  if (ruleConfig.name) {
    rule.setName(ruleConfig.name);
  }
  if (Array.isArray(ruleConfig.tags)) {
    rule.setTags(jsArrayToJavaSet(ruleConfig.tags));
  }

  if (ruleConfig.triggers) {
    const triggers = Array.isArray(ruleConfig.triggers) ? ruleConfig.triggers : [ruleConfig.triggers];
    rule.setTriggers(triggers);
  }

  rule.setConfiguration(new Configuration({
    type: SCRIPT_TYPE,
    source: '// Code to run when the rule fires:\n// Note that Rule Builder is currently not supported!\n\n' + ruleConfig.execute.toString()
  }));

  return rule;
}

/**
  * Creates a rule. The rule will be created and immediately available.
  *
  * @example
  * import { rules, triggers } = require('openhab');
  *
  * rules.JSRule({
  *   name: "my_new_rule",
  *   description: "this rule swizzles the swallows",
  *   triggers: triggers.GenericCronTrigger("0 30 16 * * ? *"),
  *   execute: (event) => {
  *     // do stuff
  *   }
  * });
  *
  * @memberof rules
  * @param {RuleConfig} ruleConfig The rule config describing the rule
  * @returns {HostRule} the created rule
  * @throws {Error} an error if the rule with the passed in uid already exists and {@link RuleConfig.overwrite} is not `true`
  */
function JSRule (ruleConfig) {
  const ruleUID = ruleConfig.id?.replace(/\W/g, '-') || ruleConfig.name.replace(/\W/g, '-') + '-' + randomUUID();
  if (ruleConfig.overwrite === true) {
    removeRule(ruleUID);
  }
  if (_ruleExists(ruleUID)) {
    throw Error(`Failed to add rule: ${ruleConfig.name ? ruleConfig.name : ruleUID}, a rule with same UID [${ruleUID}] already exists!`);
  }
  log.info('Adding rule: {}', ruleConfig.name ? ruleConfig.name : ruleUID);

  let rule = ruleConfig.dedicatedContext === true ? _createRule(ruleUID, ruleConfig) : _createSimpleRule(ruleUID, ruleConfig);

  if (!ruleConfig.triggers) {
    log.info(`Rule "${ruleConfig.name ? ruleConfig.name : ruleUID}" has no triggers and will only run when manually triggered.`);
  }

  // Register rule here
  rule = automationManager.addRule(rule);

  return rule;
}

/**
 * Creates a rule, with an associated Switch Item that can be used to toggle the rule's enabled state.
 * The rule will be created and immediately available.
 * The Switch Item will be created automatically unless you pass a {@link RuleConfig}`switchItemName` and an Item with that name already exists.
 *
 * @memberof rules
 * @param {RuleConfig} ruleConfig The rule config describing the rule
 * @returns {HostRule} the created rule
 * @throws {Error} an error is a rule with the given UID already exists.
 */
function SwitchableJSRule (ruleConfig) {
  if (!ruleConfig.name) {
    throw Error('No name specified for rule!');
  }

  // First create a toggling Item
  const itemName = ruleConfig.switchItemName || 'vRuleItemFor' + items.safeItemName(ruleConfig.name);
  if (!items.existsItem(itemName)) {
    log.info(`Creating Item: ${itemName}`);
    items.addItem({
      name: itemName,
      type: 'Switch',
      groups: [_getGroupForItem(ruleConfig)],
      label: ruleConfig.description,
      tags: [GENERATED_RULE_ITEM_TAG]
    });
  }
  const item = items.getItem(itemName);

  // create the real rule
  const rule = JSRule(ruleConfig);

  // hook up a rule to link the item to the actual rule
  _linkItemToRule(rule, item);

  if (item.isUninitialized) {
    // possibly load item's prior state
    let historicState = null;
    try {
      historicState = item.persistence.persistedState(time.ZonedDateTime.now()).state;
    } catch (e) {
      log.warn(`Failed to get historic state of ${item.name} for rule ${ruleConfig.name}: ${e}`);
    }

    if (historicState !== null) {
      item.postUpdate(historicState);
    } else {
      item.sendCommand('ON');
    }
  }

  ruleManager.setEnabled(rule.getUID(), item.state !== 'OFF');
}

/**
 * Adds a key's value from a Java HashMap to a JavaScript object (as string) if the HashMap has that key.
 * This function uses the mutable nature of JS objects and does not return anything.
 *
 * @private
 * @param {*} hashMap Java HashMap
 * @param {string} key key from the HashMap to add to the JS object
 * @param {object} object JavaScript object
 */
function _addFromHashMap (hashMap, key, object) {
  if (hashMap.containsKey(key)) object[key] = hashMap[key].toString();
}

/**
 * Get rule trigger data from raw Java input and generate JavaScript object.
 * This method is not intended for direct use in user scripts, but used internally by JS Scripting.
 *
 * @private
 * @param {*} input raw Java input/context, see
 * {@link https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java ScriptActionHandler}
 * and {@link https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandler.java SimpleRuleActionHandler}
 * @param {boolean} [javaEventBackwardCompat=false] enables backwards compatibility with pure Java event object in UI-based rules
 * @returns {EventObject}
 */
function _getTriggeredData (input, javaEventBackwardCompat = false) {
  const event = input.get('event');
  /**
   * @type {EventObject}
   */
  const data = {};

  // Add input to data to passthrough any properties not captured below
  data.raw = input;

  // Dynamically added properties, depending on their availability

  // Item triggers
  if (input.containsKey('command')) data.receivedCommand = input.get('command').toString();
  _addFromHashMap(input, 'oldState', data);
  _addFromHashMap(input, 'newState', data);
  if (input.containsKey('state')) data.receivedState = input.get('state').toString();

  // Group Item triggers
  if (input.containsKey('triggeringGroup')) data.groupName = input.get('triggeringGroup').getName();

  // Thing triggers
  _addFromHashMap(input, 'oldStatus', data);
  _addFromHashMap(input, 'newStatus', data);
  _addFromHashMap(input, 'status', data);

  // Properties added if event is available

  if (event) {
    data.eventClass = event.getClass().getName();
    data.eventName = event.getClass().getSimpleName();

    try {
      if (event.getPayload()) {
        data.payload = JSON.parse(event.getPayload());
        log.debug('Extracted event payload {}', data.payload);
      }
    } catch (e) {
      log.warn('Failed to extract payload: {}', e.message);
    }

    // The source code of the trigger handlers provide an insight into the respective events,
    // see https://github.com/openhab/openhab-core/tree/main/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler
    switch (data.eventClass) {
      case 'org.openhab.core.automation.events.ExecutionEvent':
        data.eventType = event.toString().split(' ').pop();
        break;
      case 'org.openhab.core.automation.events.TimerEvent':
        data.eventType = 'time';
        if (data.payload.cronExpression) {
          data.triggerType = 'GenericCronTrigger';
          data.cronExpression = data.payload.cronExpression.toString();
        } else if (data.payload.time) {
          data.triggerType = 'TimeOfDayTrigger';
          data.time = data.payload.time.toString();
        } else if (data.payload.itemName) {
          data.triggerType = 'DateTimeTrigger';
          data.itemName = data.payload.itemName.toString();
          data.timeOnly = data.payload.timeOnly; // boolean
          data.offset = data.payload.offset; // integer
        }
        break;
      case 'org.openhab.core.items.events.GroupItemCommandEvent':
      case 'org.openhab.core.items.events.ItemCommandEvent':
        data.itemName = event.getItemName();
        data.eventType = 'command';
        data.triggerType = 'ItemCommandTrigger';
        break;
      case 'org.openhab.core.items.events.GroupItemStateChangedEvent':
      case 'org.openhab.core.items.events.ItemStateChangedEvent':
        data.itemName = event.getItemName();
        data.eventType = 'change';
        data.triggerType = 'ItemStateChangeTrigger';
        break;
      // **StateEvents replaced by **StateUpdatedEvents in https://github.com/openhab/openhab-core/pull/3141
      case 'org.openhab.core.items.events.ItemStateUpdatedEvent':
      case 'org.openhab.core.items.events.GroupStateUpdatedEvent':
      case 'org.openhab.core.items.events.GroupItemStateEvent':
      case 'org.openhab.core.items.events.ItemStateEvent':
        data.itemName = event.getItemName();
        data.eventType = 'update';
        data.triggerType = 'ItemStateUpdateTrigger';
        break;
      case 'org.openhab.core.thing.events.ThingStatusInfoChangedEvent':
        data.thingUID = event.getThingUID().toString();
        data.eventType = 'change';
        data.triggerType = 'ThingStatusChangeTrigger';
        break;
      case 'org.openhab.core.thing.events.ThingStatusInfoEvent':
        data.thingUID = event.getThingUID().toString();
        data.eventType = 'update';
        data.triggerType = 'ThingStatusUpdateTrigger';
        break;
      case 'org.openhab.core.thing.events.ChannelTriggeredEvent':
        data.channelUID = event.getChannel().toString();
        data.receivedEvent = event.getEvent();
        data.eventType = 'triggered';
        data.triggerType = 'ChannelEventTrigger';
        break;
    }
  }

  _addFromHashMap(input, 'module', data);

  // backward compatibility with the pure Java event object
  if (javaEventBackwardCompat) {
    const itemState = data.receivedState ?? data.newState;
    if (itemState) {
      Reflect.defineProperty(data, 'itemState', {
        get () {
          console.warn('event.itemState is deprecated, use event.receivedState for update triggers or event.newState for change triggers instead.');
          return itemState;
        }
      });
    }
    if (data.oldState) {
      Reflect.defineProperty(data, 'oldItemState', {
        get () {
          console.warn('event.oldItemState is deprecated, use event.oldState instead. If you use Blockly, simply resave the script.');
          return data.oldState;
        }
      });
    }
    if (data.receivedCommand) {
      Reflect.defineProperty(data, 'itemCommand', {
        get () {
          console.warn('event.itemCommand is deprecated, use event.receivedCommand instead. If you use Blockly, simply resave the script.');
          return data.receivedCommand;
        }
      });
    }
    Reflect.defineProperty(data, 'type', {
      get () {
        console.warn('event.type is deprecated, use event.eventName instead. If you use Blockly, simply resave the script.');
        return data.eventName;
      }
    });
    if (data.channelUID) {
      Reflect.defineProperty(data, 'channel', {
        get () {
          console.warn('event.channel is deprecated, use event.channelUID instead. If you use Blockly, simply resave the script.');
          return data.channelUID;
        }
      });
    }
    if (data.receivedEvent) {
      Reflect.defineProperty(data, 'event', {
        get () {
          console.warn('event.event is deprecated, use event.receivedEvent instead. If you use Blockly, simply resave the script.');
          return data.receivedEvent;
        }
      });
    }
  }

  return data;
}

module.exports = {
  removeRule,
  runRule,
  isEnabled,
  setEnabled,
  JSRule,
  SwitchableJSRule,
  _getTriggeredData // exposed for converting the event in UI-based rules inside the add-on
};