thingsx/health.js

/**
 * Copyright (c) 2023 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 { actions, cache, items, rules, things, triggers } = require('openhab');

/**
 * Get a Thing's UID from Item name by replacing patterns with replacements.
 * If patterns is an Array, replacements must be an Array and search-replace pairs must have the same index.
 * This method is using {@link replaceAll}.
 *
 * @example
 * _getThingNameFromStateItemName('KNX_FF_Lights_state', ['_state', 'KNX_'], ['', 'knx:device:bridge:']);
 * // Returns: knx:device:bridge:FF_Lights
 *
 * @private
 * @param {string} itemName Item's name
 * @param {string|Array<string>} patterns Pattern(s) to replace.
 * @param {string|Array<string>} replacements String(s) that is/are used for replacing.
 */
function _getThingNameFromStateItemName (itemName, patterns, replacements) {
  if (typeof patterns === 'string' && typeof replacements === 'string') return itemName.replaceAll(patterns, replacements);
  // If patterns and replacements are Arrays:
  let thingUID = itemName;
  if (typeof patterns === 'object') {
    for (const i in patterns) {
      thingUID = thingUID.replaceAll(patterns[i], replacements[i]);
    }
    return thingUID;
  }
}

/**
 * Re-enables a Thing by disabling, and then enabling it again.
 *
 * @memberof thingsx
 * @param {string} thingUID
 */
function reEnableThing (thingUID) {
  const thing = things.getThing(thingUID);
  thing.setEnabled(false);
  setTimeout(() => {
    thing.setEnabled(true);
  }, 1000);
}

/**
 * Creates a rule that posts Thing statuses to String Items.
 * The rule takes the name of a group of String Items, generates a Thing UID for each member Item using string replace operations, and then posts the Thing status on every Thing status change to the Items.
 * The rule also runs when start level 100 is reached and regularly (every 5 minutes, starting with minute 0).
 *
 * @example
 * thingsx.createThingStatusRule('gYamahaState', ['_state', '_'], ['', ':']);
 * // This removes "_state" fromt the Item name and replaces all "_" with ":" to get the Thing UID from the members of the "gYamahaState" group members.
 *
 * @memberof thingsx
 * @param {string} groupName
 * @param {string|Array<string>} patterns Pattern(s) to replace.
 * @param {string|Array<string>} replacements String(s) that is/are used for replacing.
 */
function createThingStatusToItemRule (groupName, patterns, replacements) {
  // Set up default triggers for thing status rules.
  const triggersList = [
    triggers.SystemStartlevelTrigger('100'),
    triggers.GenericCronTrigger('0 0/5 * ? * * *')
  ];

  // Add ThingStatusChangeTrigger for the matching Thing of each group member
  // Translates from Item name to Thing UID using replace based on patterns and replacements
  const members = items.getItem(groupName).members.map(item => item.name);
  const pairs = new Map();
  for (const i in members) {
    const thingUID = _getThingNameFromStateItemName(members[i], patterns, replacements);
    triggersList.push(triggers.ThingStatusChangeTrigger(thingUID));
    // Store the pairs, so we don't have to get the Thing names again later.
    pairs.set(members[i], thingUID);
  }

  // Create the rule.
  rules.JSRule({
    name: 'Thing states to Item group ' + groupName,
    description: 'Send the Thing\'s states on change to the according String Items which hold the state.',
    triggers: triggersList,
    execute: (event) => {
      // Update the state of each member Item with the matching Thing's state.
      for (const i in members) {
        const thingUID = pairs.get(members[i]);
        const thingStatus = String(actions.Things.getThingStatusInfo(thingUID));
        items.getItem(members[i]).postUpdate(thingStatus);
      }
    },
    id: 'thing-status-to-items-of-group-' + groupName,
    tags: ['@hotzware/openhab-tools', 'createThingStatusToItemRule']
  });
}

/**
 * Creates a rule that posts Thing statuses to String Items.
 * The rule takes the name of a group of String Items, generates a Thing UID for each member Item using string replace operations, and then posts the Thing status on every Thing status change to the Items.
 * The rule also runs when start level 100 is reached and regularly (every 5 minutes, starting with minute 0).
 *
 * @example
 * thingsx.createThingStatusRule('gYamahaState', ['_state', '_'], ['', ':']);
 * // This removes "_state" fromt the Item name and replaces all "_" with ":" to get the Thing UID from the members of the "gYamahaState" group members.
 *
 * @memberof thingsx
 * @param {string} thingUID
 * @param {string|Array<string>} patterns Pattern(s) to replace.
 * @param {string|Array<string>} replacements String(s) that is/are used for replacing.
 */

/**
 * Creates a rule that sends a notification if a Thing goes offline and another one if it goes back online.
 *
 * @memberof thingsx
 * @param {string} thingUID
 * @param {string[]} [recipients] the recipients of the notification: leave empty to send a broadcast notification or put the email addresses of the openHAB Cloud users to receive the notification
 * @param {number} [offlineDuration=60] the duration to wait for the Thing to come back online before sending the offline notification
 * @param {number} [onlineDuration=30] the duration to wait for the Thing to stay online before sending the online notification
 * @param {string} [offlineMessage='WARNUNG: %LABEL nicht mehr erreichbar (%STATUS)!'] the message to send when the Thing goes offline: use %UID, %LABEL, and %STATUS as placeholders for the respective values
 * @param {string} [onlineMessage='%LABEL wieder erreichbar.'] the message to send when the Thing goes back online: use %UID and %LABEL as placeholders for the respective values
 */
function createThingStatusNotificationRule (thingUID, recipients = [], offlineDuration = 60, onlineDuration = 30,
  offlineMessage = 'WARNUNG: %LABEL nicht mehr erreichbar (%STATUS)!', onlineMessage = '%LABEL wieder erreichbar.') {
  const cacheKey = `${thingUID}-offlineNotificationReferenceId`;

  rules.JSRule({
    name: `Thing ${thingUID} status notification`,
    description: 'Send a broadcast notification if the Thing goes offline and if it goes back online',
    triggers: [
      triggers.ThingStatusChangeTrigger(thingUID, undefined, 'ONLINE'),
      triggers.ThingStatusChangeTrigger(thingUID, 'ONLINE')
    ],
    execute: (event) => {
      if (event.newStatus !== 'ONLINE') {
        if (cache.private.exists(cacheKey)) return;
        setTimeout(() => {
          const thing = things.getThing(thingUID);
          if (thing.status === 'ONLINE') return;
          if (cache.private.exists(cacheKey)) return;
          const builder = actions.notificationBuilder(offlineMessage.replace('%UID', thingUID).replace('%LABEL', thing.label).replace('%STATUS', event.newStatus))
            .withIcon('error').withTitle('Thing offline');
          for (const recipient of recipients) {
            builder.addUserId(recipient);
          }
          builder.send();
          const referenceId = builder.send();
          cache.private.put(cacheKey, referenceId);
        }, offlineDuration * 1000);
      } else {
        if (!cache.private.exists(cacheKey)) return;
        setTimeout(() => {
          const thing = things.getThing(thingUID);
          if (thing.status !== 'ONLINE') return;
          if (!cache.private.exists(cacheKey)) return;
          const referenceId = cache.private.get(cacheKey);
          const builder = actions.notificationBuilder(onlineMessage.replace('%UID', thingUID).replace('%LABEL', thing.label))
            .withIcon('error').withTitle('Thing wieder online').withReferenceId(referenceId);
          for (const recipient of recipients) {
            builder.addUserId(recipient);
          }
          builder.send();
          cache.private.remove(cacheKey);
        }, onlineDuration * 1000);
      }
    },
    id: 'thing-status-notification-' + thingUID,
    tags: ['@hotzware/openhab-tools', 'createThingStatusNotificationRule']
  });
}

/**
 * Creates a rule that re-enables a Thing on command ON to a given Item.
 *
 * @memberof thingsx
 * @param {string} itemName
 * @param {string} thingUID
 */
function createReEnableThingWithItemRule (itemName, thingUID) {
  rules.JSRule({
    name: 'Re-enable ' + thingUID + ' with ' + itemName,
    description: 'Disables and then enables a Thing again on command ON.',
    triggers: triggers.ItemCommandTrigger(itemName, 'ON'),
    execute: (event) => {
      reEnableThing(thingUID);
      // Set command Item to OFF.
      items.getItem(itemName).postUpdate('OFF');
    },
    id: 're-enable-' + thingUID + '-with-' + itemName,
    tags: ['@hotzware/openhab-tools', 'createReEnableThingWithItemRule']
  });
}

module.exports = {
  reEnableThing,
  createThingStatusToItemRule,
  createThingStatusNotificationRule,
  createReEnableThingWithItemRule
};