Files
ontime/apps/server/src/api-data/automation/automation.service.ts
T
2025-01-24 13:53:17 +01:00

129 lines
3.8 KiB
TypeScript

import {
isHTTPOutput,
isOSCOutput,
type AutomationFilter,
type AutomationOutput,
type FilterRule,
type TimerLifeCycle,
} from 'ontime-types';
import { getPropertyFromPath } from 'ontime-utils';
import { getState, type RuntimeState } from '../../stores/runtimeState.js';
import { isOntimeCloud } from '../../externals.js';
import { emitOSC } from './clients/osc.client.js';
import { emitHTTP } from './clients/http.client.js';
import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js';
import { isBooleanEquals, isGreaterThan, isLessThan } from './automation.utils.js';
/**
* Exposes a method for triggering actions based on a TimerLifeCycle event
*/
export function triggerAutomations(event: TimerLifeCycle, state: RuntimeState) {
if (!getAutomationsEnabled()) {
return;
}
const triggers = getAutomationTriggers();
const triggerAutomations = triggers.filter((trigger) => trigger.trigger === event);
if (triggerAutomations.length === 0) {
return;
}
const automations = getAutomations();
if (Object.keys(automations).length === 0) {
return;
}
triggerAutomations.forEach((trigger) => {
const automation = automations[trigger.automationId];
if (!automation) {
return;
}
const shouldSend = testConditions(automation.filters, automation.filterRule, state);
if (shouldSend) {
send(automation.outputs, state);
}
});
}
/**
* Exposes a method for bypassing the condition check and testing the sending of an output
*/
export function testOutput(payload: AutomationOutput) {
send([payload]);
}
/**
* Checks whether the automation conditions are met
*/
export function testConditions(
filters: AutomationFilter[],
filterRule: FilterRule,
state: Partial<RuntimeState>,
): boolean {
if (filters.length === 0) {
return true;
}
if (filterRule === 'all') {
return filters.every(evaluateCondition);
}
return filters.some(evaluateCondition);
function evaluateCondition(filter: AutomationFilter): boolean {
const { field, operator, value } = filter;
const lowerCasedValue = value.toLowerCase();
const fieldValue = getPropertyFromPath(field, state);
// if value is empty string, the user could be meaning to check if the value does not exist
// we use loose equality to be able to check for converted values (eg '10' == 10)
switch (operator) {
case 'equals':
// handle the case where we are comparing boolean strings
if (typeof fieldValue === 'boolean') {
return isBooleanEquals(fieldValue, lowerCasedValue);
}
// make string comparisons case insensitive
if (typeof fieldValue === 'string') {
return fieldValue.toLowerCase() === lowerCasedValue;
}
// overload the edge case where we use empty string to check if a value does not exist
if (value === '' && fieldValue === undefined) {
return true;
}
return fieldValue == value;
case 'not_equals':
return fieldValue != value;
case 'greater_than':
return isGreaterThan(fieldValue, value);
case 'less_than':
return isLessThan(fieldValue, value);
case 'contains':
return typeof fieldValue === 'string' && fieldValue.includes(value);
case 'not_contains':
return typeof fieldValue === 'string' && !fieldValue.includes(value);
default:
return false;
}
}
}
/**
* Handles preparing and sending of the data
* Returns a boolean indicating whether a message was sent
*/
function send(output: AutomationOutput[], state?: RuntimeState) {
const stateSnapshot = state ?? getState();
output.forEach((payload) => {
if (isOSCOutput(payload)) {
if (!isOntimeCloud) {
emitOSC(payload, stateSnapshot);
}
} else if (isHTTPOutput(payload)) {
emitHTTP(payload, stateSnapshot);
}
});
}