diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.module.scss b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.module.scss index 12b438302..50a6023b5 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.module.scss +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.module.scss @@ -27,7 +27,8 @@ .titleSection, .filterSection, .oscSection, -.httpSection { +.httpSection, +.actionSection { display: grid; grid-gap: 0.5rem; @@ -40,8 +41,10 @@ .ruleSection, .filterSection, .oscSection, -.httpSection { - label, div { +.httpSection, +.actionSection { + label, + div { // we use the div as non-interactive placeholder for button cells // it needs to match the size of the label element font-size: calc(1rem - 3px); @@ -51,7 +54,6 @@ } } - .titleSection { grid-template-columns: 1fr; } @@ -68,6 +70,14 @@ grid-template-columns: 1fr auto; } +.actionSection { + grid-template-columns: auto 1fr 1fr auto; + + .test { + grid-column: -1; + } +} + .outputCard { border-left: 0.25rem solid $gray-1200; padding-left: 0.5rem; diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx index bf4232c25..e0fa04202 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx @@ -3,7 +3,16 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { Automation, AutomationDTO, HTTPOutput, isHTTPOutput, isOSCOutput, OSCOutput } from 'ontime-types'; +import { + Automation, + AutomationDTO, + HTTPOutput, + isHTTPOutput, + isOntimeAction, + isOSCOutput, + OntimeAction, + OSCOutput, +} from 'ontime-types'; import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation'; import { maybeAxiosError } from '../../../../common/api/utils'; @@ -18,6 +27,7 @@ import * as Panel from '../../panel-utils/PanelUtils'; import TemplateInput from './template-input/TemplateInput'; import { isAutomation, makeFieldList } from './automationUtils'; +import OntimeActionForm from './OntimeActionForm'; import style from './AutomationForm.module.scss'; @@ -42,6 +52,7 @@ export default function AutomationForm(props: AutomationFormProps) { register, setError, setFocus, + setValue, formState: { errors, isSubmitting, isDirty, isValid }, } = useForm({ mode: 'onChange', @@ -92,6 +103,11 @@ export default function AutomationForm(props: AutomationFormProps) { appendOutput({ type: 'http', url: '' }); }; + const handleAddnewOntimeAction = () => { + // @ts-expect-error -- we dont want to choose an action + appendOutput({ type: 'ontime', action: undefined }); + }; + const handleTestOSCOutput = async (index: number) => { try { const values = getValues(`outputs.${index}`) as OSCOutput; @@ -125,6 +141,19 @@ export default function AutomationForm(props: AutomationFormProps) { } }; + const handleTestOntimeAction = async (index: number) => { + try { + const values = getValues(`outputs.${index}`) as OntimeAction; + // NOTE: there is no meaningful validation to do here, we let the server deal with the data + await testOutput({ + ...values, + type: 'ontime', + }); + } catch (_error) { + /** we dont handle errors here */ + } + }; + const onSubmit = async (values: AutomationDTO) => { if (isAutomation(automation)) { await handleEdit(automation.id, { id: automation.id, ...values }); @@ -374,8 +403,6 @@ export default function AutomationForm(props: AutomationFormProps) { size='sm' onClick={() => removeOutput(index)} color='#FA5656' // $red-500 - isDisabled={false} - isLoading={false} /> @@ -423,8 +450,6 @@ export default function AutomationForm(props: AutomationFormProps) { size='sm' onClick={() => removeOutput(index)} color='#FA5656' // $red-500 - isDisabled={false} - isLoading={false} /> @@ -432,30 +457,59 @@ export default function AutomationForm(props: AutomationFormProps) { ); } + + if (isOntimeAction(output)) { + const rowErrors = errors.outputs?.[index] as + | { + action?: { message?: string }; + time?: { message?: string }; + text?: { message?: string }; + visible?: { message?: string }; + secondarySource?: { message?: string }; + } + | undefined; + return ( +
+ Ontime action + +   + + + } + variant='ontime-ghosted' + size='sm' + onClick={() => removeOutput(index)} + color='#FA5656' // $red-500 + /> + + +
+ ); + } + // there should be no other output types return null; })} - - + diff --git a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx new file mode 100644 index 000000000..177760e65 --- /dev/null +++ b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx @@ -0,0 +1,110 @@ +import { PropsWithChildren, useState } from 'react'; +import { UseFormRegister, UseFormSetValue } from 'react-hook-form'; +import { Input, Select } from '@chakra-ui/react'; +import { AutomationDTO, OntimeAction } from 'ontime-types'; + +import { cx } from '../../../../common/utils/styleUtils'; +import * as Panel from '../../panel-utils/PanelUtils'; + +import style from './AutomationForm.module.scss'; + +interface OntimeActionFormProps { + index: number; + register: UseFormRegister; + rowErrors?: { + action?: { message?: string }; + time?: { message?: string }; + text?: { message?: string }; + visible?: { message?: string }; + secondarySource?: { message?: string }; + }; + value: OntimeAction['action']; + setValue: UseFormSetValue; +} + +export default function OntimeActionForm(props: PropsWithChildren) { + const { index, register, setValue, rowErrors, value, children } = props; + const [selectedAction, setSelectedAction] = useState(value || 'aux-start'); + + const updateSelectedAction = (value: string) => { + setSelectedAction(value as OntimeAction['action']); + setValue(`outputs.${index}.action`, value as OntimeAction['action']); + }; + + return ( +
+ + + + {selectedAction === 'aux-set' && ( + + )} + + {selectedAction === 'message-set' && ( + <> + + + + )} + + {selectedAction === 'message-secondary' && ( + + )} +
{children}
+
+ ); +} diff --git a/apps/server/src/api-data/automation/__tests__/automation.validation.test.ts b/apps/server/src/api-data/automation/__tests__/automation.validation.test.ts new file mode 100644 index 000000000..c22060009 --- /dev/null +++ b/apps/server/src/api-data/automation/__tests__/automation.validation.test.ts @@ -0,0 +1,167 @@ +import { parseOutput } from '../automation.validation.js'; + +describe('parseOutput', () => { + describe('handles OSC outputs', () => { + it('parses a valid payload', () => { + const payload = { + type: 'osc', + targetIP: 'localhost', + targetPort: 1234, + address: '/test', + args: 'test', + }; + const result = parseOutput(payload); + expect(result).toStrictEqual(payload); + }); + + it('throws on a invalid payload', () => { + const payload = { + type: 'osc', + targetIP: 1234, + targetPort: 1234, + address: '/test', + args: 'test', + }; + expect(() => parseOutput(payload)).toThrow(); + }); + }); + describe('handles HTTP outputs', () => { + it('parses a valid payload', () => { + const payload = { + type: 'http', + url: 'http://asdasdas', + }; + const result = parseOutput(payload); + expect(result).toStrictEqual(payload); + }); + + it('throws on a invalid payload', () => { + const payload = { + type: 'http', + }; + expect(() => parseOutput(payload)).toThrow(); + }); + }); + describe('handles Ontime outputs', () => { + it('parses a valid payload', () => { + const auxStart = { + type: 'ontime', + action: 'aux-start', + }; + expect(parseOutput(auxStart)).toStrictEqual(auxStart); + const auxStop = { + type: 'ontime', + action: 'aux-stop', + }; + expect(parseOutput(auxStop)).toStrictEqual(auxStop); + const auxPause = { + type: 'ontime', + action: 'aux-pause', + }; + expect(parseOutput(auxPause)).toStrictEqual(auxPause); + }); + + it('removes extra properties', () => { + expect( + parseOutput({ + type: 'ontime', + action: 'aux-start', + time: 10, + }), + ).toStrictEqual({ + type: 'ontime', + action: 'aux-start', + }); + }); + + it('throws on a invalid payload', () => { + const payload = { + type: 'ontime', + action: 'not-exist', + }; + expect(() => parseOutput(payload)).toThrow(); + }); + + it('parses message-set', () => { + expect( + parseOutput({ + type: 'ontime', + action: 'message-set', + text: 'test', + visible: 'true', + }), + ).toMatchObject({ + text: 'test', + visible: true, + }); + expect( + parseOutput({ + type: 'ontime', + action: 'message-set', + text: '', + visible: 'false', + }), + ).toMatchObject({ + text: undefined, + visible: false, + }); + expect( + parseOutput({ + type: 'ontime', + action: 'message-set', + text: '', + visible: '', + }), + ).toMatchObject({ + text: undefined, + visible: undefined, + }); + expect(() => + parseOutput({ + type: 'ontime', + action: 'message-set', + text: 123, + visible: '', + }), + ).toThrow(); + }); + + it('parses message-secondary', () => {}); + expect( + parseOutput({ + type: 'ontime', + action: 'message-secondary', + secondarySource: 'test', + }), + ).toMatchObject({ + secondarySource: null, + }); + expect( + parseOutput({ + type: 'ontime', + action: 'message-secondary', + secondarySource: '', + }), + ).toMatchObject({ + secondarySource: null, + }); + expect( + parseOutput({ + type: 'ontime', + action: 'message-secondary', + secondarySource: 'aux', + }), + ).toMatchObject({ + secondarySource: 'aux', + }); + expect( + parseOutput({ + type: 'ontime', + action: 'message-secondary', + secondarySource: 'external', + }), + ).toMatchObject({ + secondarySource: 'external', + }); + }); +}); diff --git a/apps/server/src/api-data/automation/automation.controller.ts b/apps/server/src/api-data/automation/automation.controller.ts index 9b177c956..4d121c523 100644 --- a/apps/server/src/api-data/automation/automation.controller.ts +++ b/apps/server/src/api-data/automation/automation.controller.ts @@ -1,11 +1,13 @@ import { getErrorMessage } from 'ontime-utils'; -import { Automation, AutomationOutput, AutomationSettings, ErrorResponse, Trigger } from 'ontime-types'; +import { Automation, AutomationSettings, ErrorResponse, Trigger } from 'ontime-types'; import type { Request, Response } from 'express'; +import { oscServer } from '../../adapters/OscAdapter.js'; + import * as automationDao from './automation.dao.js'; import * as automationService from './automation.service.js'; -import { oscServer } from '../../adapters/OscAdapter.js'; +import { parseOutput } from './automation.validation.js'; export function getAutomationSettings(_req: Request, res: Response) { res.json(automationDao.getAutomationSettings()); @@ -114,8 +116,9 @@ export async function deleteAutomation(req: Request, res: Response) { try { - const payload = req.body as AutomationOutput; - automationService.testOutput(payload); + const payload = req.body; + const parsed = parseOutput(payload); + automationService.testOutput(parsed); res.status(200).send(); } catch (error) { const message = getErrorMessage(error); diff --git a/apps/server/src/api-data/automation/automation.service.ts b/apps/server/src/api-data/automation/automation.service.ts index c1b4fea45..2811e5ee7 100644 --- a/apps/server/src/api-data/automation/automation.service.ts +++ b/apps/server/src/api-data/automation/automation.service.ts @@ -1,6 +1,8 @@ import { isHTTPOutput, + isOntimeAction, isOSCOutput, + LogOrigin, type AutomationFilter, type AutomationOutput, type FilterRule, @@ -8,6 +10,7 @@ import { } from 'ontime-types'; import { getPropertyFromPath } from 'ontime-utils'; +import { logger } from '../../classes/Logger.js'; import { getState, type RuntimeState } from '../../stores/runtimeState.js'; import { isOntimeCloud } from '../../externals.js'; @@ -15,6 +18,7 @@ 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'; +import { toOntimeAction } from './clients/ontime.client.js'; /** * Exposes a method for triggering actions based on a TimerLifeCycle event @@ -117,12 +121,14 @@ export function testConditions( function send(output: AutomationOutput[], state?: RuntimeState) { const stateSnapshot = state ?? getState(); output.forEach((payload) => { - if (isOSCOutput(payload)) { - if (!isOntimeCloud) { - emitOSC(payload, stateSnapshot); - } + if (isOSCOutput(payload) && !isOntimeCloud) { + emitOSC(payload, stateSnapshot); } else if (isHTTPOutput(payload)) { emitHTTP(payload, stateSnapshot); + } else if (isOntimeAction(payload)) { + toOntimeAction(payload); + } else { + logger.warning(LogOrigin.Tx, `Unknown output type: ${payload}`); } }); } diff --git a/apps/server/src/api-data/automation/automation.utils.ts b/apps/server/src/api-data/automation/automation.utils.ts index b3929d584..188d50c9d 100644 --- a/apps/server/src/api-data/automation/automation.utils.ts +++ b/apps/server/src/api-data/automation/automation.utils.ts @@ -1,4 +1,4 @@ -import { FilterRule, MaybeNumber } from 'ontime-types'; +import { FilterRule, MaybeNumber, OntimeAction } from 'ontime-types'; import { millisToString, removeLeadingZero, splitWhitespace, getPropertyFromPath } from 'ontime-utils'; import type { OscArgOrArrayInput, OscArgInput } from 'osc-min'; @@ -12,6 +12,10 @@ export function isFilterRule(value: string): value is FilterRule { return value === 'all' || value === 'any'; } +export function isOntimeActionAction(value: string): value is OntimeAction['action'] { + return ['aux-start', 'aux-stop', 'aux-pause', 'aux-set', 'message-set', 'message-secondary'].includes(value); +} + function toOscValue(argString: string): OscArgInput { const argAsNum = Number(argString); // NOTE: number like: 1 2.0 33333 diff --git a/apps/server/src/api-data/automation/automation.validation.ts b/apps/server/src/api-data/automation/automation.validation.ts index 4b2e946ad..b55414354 100644 --- a/apps/server/src/api-data/automation/automation.validation.ts +++ b/apps/server/src/api-data/automation/automation.validation.ts @@ -3,16 +3,19 @@ import { AutomationFilter, AutomationOutput, HTTPOutput, + OntimeAction, OSCOutput, + SecondarySource, timerLifecycleValues, } from 'ontime-types'; +import { parseUserTime } from 'ontime-utils'; import type { Request, Response, NextFunction } from 'express'; import { body, oneOf, param, validationResult } from 'express-validator'; import * as assert from '../../utils/assert.js'; -import { isFilterOperator, isFilterRule } from './automation.utils.js'; +import { isFilterOperator, isFilterRule, isOntimeActionAction } from './automation.utils.js'; export const paramContainsId = [ param('id').exists(), @@ -131,43 +134,13 @@ function validateFilters(filters: Array): filters is AutomationFilter[] function validateOutput(output: Array): output is AutomationOutput[] { output.forEach((payload) => { - assert.isObject(payload); - assert.hasKeys(payload, ['type']); - const { type } = payload; - assert.isString(type); - - if (type === 'osc') { - validateOSCOutput(payload); - } else if (type === 'http') { - validateHttpOutput(payload); - } else { - throw new Error('Invalid automation'); - } + parseOutput(payload); }); return true; } -function validateOSCOutput(payload: object): payload is OSCOutput { - assert.hasKeys(payload, ['targetIP', 'targetPort', 'address', 'args']); - const { targetIP, targetPort, address, args } = payload; - assert.isString(targetIP); - assert.isNumber(targetPort); - assert.isString(address); - if (typeof args !== 'string' && typeof args !== 'number') { - throw new Error('Invalid automation'); - } - return true; -} - -function validateHttpOutput(payload: object): payload is HTTPOutput { - assert.hasKeys(payload, ['url']); - const { url } = payload; - assert.isString(url); - return true; -} - export const validateTestPayload = [ - body('type').exists().isIn(['osc', 'http']), + body('type').exists().isIn(['osc', 'http', 'ontime']), // validation for OSC message oneOf([ @@ -182,9 +155,143 @@ export const validateTestPayload = [ // validation for HTTP message body('url').if(body('type').equals('http')).isURL({ require_tld: false }).trim(), + // validation for Ontime actions + body('action').if(body('type').equals('ontime')).isString().trim(), + body('text').if(body('type').equals('ontime')).optional().isString().trim(), + body('time').if(body('type').equals('ontime')).optional().isString().trim(), + body('visible').if(body('type').equals('ontime')).optional().isString().trim(), + body('secondarySource').if(body('type').equals('ontime')).optional().isString().trim(), + (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); next(); }, ]; + +/** + * Sanitises an output object + * @Throws if the output is invalid + */ +export function parseOutput(maybeOutput: unknown): AutomationOutput { + assert.isObject(maybeOutput); + assert.hasKeys(maybeOutput, ['type']); + + const { type } = maybeOutput; + assert.isString(type); + + if (type === 'osc') { + return parseOSCOutput(maybeOutput); + } else if (type === 'http') { + return parseHTTPOutput(maybeOutput); + } else if (type === 'ontime') { + return parseOntimeAction(maybeOutput); + } else { + throw new Error('Invalid automation output'); + } +} + +function parseOSCOutput(maybeOSCOutput: object): OSCOutput { + assert.hasKeys(maybeOSCOutput, ['targetIP', 'targetPort', 'address', 'args']); + assert.isString(maybeOSCOutput.targetIP); + assert.isNumber(maybeOSCOutput.targetPort); + assert.isString(maybeOSCOutput.address); + assert.isString(maybeOSCOutput.args); + + return { + type: 'osc', + targetIP: maybeOSCOutput.targetIP, + targetPort: maybeOSCOutput.targetPort, + address: maybeOSCOutput.address, + args: maybeOSCOutput.args, + }; +} + +function parseHTTPOutput(maybeHTTPOutput: object): HTTPOutput { + assert.hasKeys(maybeHTTPOutput, ['url']); + assert.isString(maybeHTTPOutput.url); + + return { + type: 'http', + url: maybeHTTPOutput.url, + }; +} + +function parseOntimeAction(maybeOntimeAction: object): OntimeAction { + assert.hasKeys(maybeOntimeAction, ['action']); + assert.isString(maybeOntimeAction.action); + + if (!isOntimeActionAction(maybeOntimeAction.action)) { + throw new Error('Invalid Ontime action'); + } + + // we know we have a valid action, deal with special cases + + if (maybeOntimeAction.action === 'aux-set') { + assert.hasKeys(maybeOntimeAction, ['time']); + assert.isString(maybeOntimeAction.time); + + return { + type: 'ontime', + action: 'aux-set', + time: parseUserTime(maybeOntimeAction.time), + }; + } + + if (maybeOntimeAction.action === 'message-set') { + assert.hasKeys(maybeOntimeAction, ['text', 'visible']); + assert.isString(maybeOntimeAction.text); + assert.isString(maybeOntimeAction.visible); + + return { + type: 'ontime', + action: 'message-set', + text: indeterminateText(maybeOntimeAction.text), + visible: indeterminateBooleanString(maybeOntimeAction.visible), + }; + } + + if (maybeOntimeAction.action === 'message-secondary') { + assert.hasKeys(maybeOntimeAction, ['secondarySource']); + assert.isString(maybeOntimeAction.secondarySource); + + return { + type: 'ontime', + action: 'message-secondary', + secondarySource: chooseSecondarySource(maybeOntimeAction.secondarySource), + }; + } + + return { + type: 'ontime', + action: maybeOntimeAction.action, + }; +} + +/** + * Helper function to parse a text which may be indeterminate + * "some text" -> string + * "" -> undefined + */ +function indeterminateText(value: string): string | undefined { + return value === '' ? undefined : value; +} + +/** + * Helper function to parse boolean values in transit + * "true" -> true + * "false" -> false + * "" | "null" -> undefined + */ +function indeterminateBooleanString(value: string): boolean | undefined { + return value === '' ? undefined : value === 'true'; +} + +/** + * Helper function to validate the secondary source + */ +function chooseSecondarySource(value: string): SecondarySource { + if (value === 'aux') return 'aux'; + if (value === 'external') return 'external'; + return null; +} diff --git a/apps/server/src/api-data/automation/clients/ontime.client.ts b/apps/server/src/api-data/automation/clients/ontime.client.ts new file mode 100644 index 000000000..2a2612f67 --- /dev/null +++ b/apps/server/src/api-data/automation/clients/ontime.client.ts @@ -0,0 +1,48 @@ +import { LogOrigin, OntimeAction } from 'ontime-types'; + +import { logger } from '../../../classes/Logger.js'; +import { auxTimerService } from '../../../services/aux-timer-service/AuxTimerService.js'; +import * as messageService from '../../../services/message-service/MessageService.js'; + +export function toOntimeAction(action: OntimeAction) { + switch (action.action) { + // Aux timer actions + case 'aux-start': + auxTimerService.start(); + break; + case 'aux-stop': + auxTimerService.stop(); + break; + case 'aux-pause': + auxTimerService.pause(); + break; + case 'aux-set': { + auxTimerService.setTime(action.time); + break; + } + + // Message actions + case 'message-set': { + messageService.patch({ + timer: { + text: action.text, + visible: action.visible, + }, + }); + break; + } + case 'message-secondary': { + messageService.patch({ + timer: { + secondarySource: action.secondarySource, + }, + }); + break; + } + + default: + // @ts-expect-error -- this guard checks that we handled all the cases, but we still want to log just in case + logger.warning(LogOrigin.Tx, `Unknown action type: ${action.type}`); + break; + } +} diff --git a/packages/types/src/definitions/core/Automation.type.ts b/packages/types/src/definitions/core/Automation.type.ts index 67bc14151..583df9b19 100644 --- a/packages/types/src/definitions/core/Automation.type.ts +++ b/packages/types/src/definitions/core/Automation.type.ts @@ -1,3 +1,4 @@ +import type { SecondarySource } from '../runtime/MessageControl.type.js'; import type { TimerLifeCycle } from './TimerLifecycle.type.js'; export type AutomationSettings = { @@ -38,7 +39,7 @@ export type AutomationFilter = { value: string; // we use string but would coerce to the field value }; -export type AutomationOutput = OSCOutput | HTTPOutput; +export type AutomationOutput = OSCOutput | HTTPOutput | OntimeAction; export type OSCOutput = { type: 'osc'; @@ -52,3 +53,25 @@ export type HTTPOutput = { type: 'http'; url: string; }; + +export type OntimeAction = + | { + type: 'ontime'; + action: 'aux-start' | 'aux-stop' | 'aux-pause'; + } + | { + type: 'ontime'; + action: 'aux-set'; + time: number; + } + | { + type: 'ontime'; + action: 'message-set'; + text?: string; + visible?: boolean; + } + | { + type: 'ontime'; + action: 'message-secondary'; + secondarySource: SecondarySource; + }; diff --git a/packages/types/src/definitions/runtime/MessageControl.type.ts b/packages/types/src/definitions/runtime/MessageControl.type.ts index 74c5d63a9..850574ddc 100644 --- a/packages/types/src/definitions/runtime/MessageControl.type.ts +++ b/packages/types/src/definitions/runtime/MessageControl.type.ts @@ -1,9 +1,11 @@ +export type SecondarySource = 'aux' | 'external' | null; + export type TimerMessage = { text: string; visible: boolean; blink: boolean; blackout: boolean; - secondarySource: 'aux' | 'external' | null; + secondarySource: SecondarySource; }; export type MessageState = { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 396c13594..5a9a75c26 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -26,6 +26,7 @@ export type { FilterRule, HTTPOutput, NormalisedAutomation, + OntimeAction, OSCOutput, Trigger, TriggerDTO, @@ -80,7 +81,7 @@ export type { export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js'; export { Playback } from './definitions/runtime/Playback.type.js'; export { TimerLifeCycle, timerLifecycleValues } from './definitions/core/TimerLifecycle.type.js'; -export type { TimerMessage, MessageState } from './definitions/runtime/MessageControl.type.js'; +export type { TimerMessage, MessageState, SecondarySource } from './definitions/runtime/MessageControl.type.js'; export type { Runtime } from './definitions/runtime/Runtime.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; @@ -104,5 +105,6 @@ export { isKeyOfType, isOSCOutput, isHTTPOutput, + isOntimeAction, } from './utils/guards.js'; export type { MaybeNumber, MaybeString } from './utils/utils.type.js'; diff --git a/packages/types/src/utils/guards.ts b/packages/types/src/utils/guards.ts index 272a06414..318511b2e 100644 --- a/packages/types/src/utils/guards.ts +++ b/packages/types/src/utils/guards.ts @@ -1,4 +1,4 @@ -import type { AutomationOutput, HTTPOutput, OSCOutput } from '../definitions/core/Automation.type.js'; +import type { AutomationOutput, HTTPOutput, OntimeAction, OSCOutput } from '../definitions/core/Automation.type.js'; import type { OntimeBlock, OntimeDelay, OntimeEvent, PlayableEvent } from '../definitions/core/OntimeEvent.type.js'; import { SupportedEvent } from '../definitions/core/OntimeEvent.type.js'; import type { OntimeRundownEntry } from '../definitions/core/Rundown.type.js'; @@ -41,3 +41,7 @@ export function isOSCOutput(output: AutomationOutput): output is OSCOutput { export function isHTTPOutput(output: AutomationOutput): output is HTTPOutput { return output.type === 'http'; } + +export function isOntimeAction(output: AutomationOutput): output is OntimeAction { + return output.type === 'ontime'; +}