├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yaml └── workflows │ ├── build.yml │ └── companion-module-checks.yaml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc ├── .yarnrc.yml ├── LICENSE ├── README.md ├── companion ├── HELP.md └── manifest.json ├── eslint.config.mjs ├── package.json ├── src ├── assets │ ├── colours.ts │ └── icons.ts ├── config.ts ├── enums.ts ├── index.ts ├── upgrades.ts ├── utilities.ts └── v3 │ ├── actions │ ├── auxTimer.ts │ ├── change.ts │ ├── changePicker.ts │ ├── commands.ts │ ├── eventPicker.ts │ ├── index.ts │ ├── message.ts │ └── playback.ts │ ├── connection.ts │ ├── feedbacks │ ├── auxTimer.ts │ ├── customFields.ts │ ├── index.ts │ ├── message.ts │ ├── offset.ts │ ├── playback.ts │ ├── progress.ts │ └── timerPhase.ts │ ├── ontime-types.ts │ ├── ontimev3.ts │ ├── presets.ts │ ├── state.ts │ └── variables.ts ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.preset.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | /.yarn/releases/** binary 3 | /.yarn/plugins/** binary -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: what-happened 11 | attributes: 12 | label: What happened? 13 | description: Also tell us, what did you expect to happen? 14 | placeholder: Tell us what you see! 15 | value: "A bug happened!" 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: version 20 | attributes: 21 | label: Versions 22 | description: What versions of soffware are you running? 23 | placeholder: | 24 | Ontime: vX.X.X 25 | Companion: vX.X.X 26 | Module: vX.X.X 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: logs 31 | attributes: 32 | label: Relevant log output 33 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 34 | render: shell 35 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | 16 | - name: Run install 17 | uses: borales/actions-yarn@v4 18 | with: 19 | cmd: install # will run `yarn install` command 20 | 21 | - name: Build production bundle 22 | uses: borales/actions-yarn@v4 23 | with: 24 | cmd: package 25 | 26 | - name: Upload production bundle 27 | uses: actions/upload-artifact@v4 28 | with: 29 | path: './pkg' 30 | name: 'getontime-ontime' 31 | -------------------------------------------------------------------------------- /.github/workflows/companion-module-checks.yaml: -------------------------------------------------------------------------------- 1 | name: Companion Module Checks 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | check: 8 | name: Check module 9 | 10 | if: ${{ !contains(github.repository, 'companion-module-template-') }} 11 | 12 | permissions: 13 | packages: read 14 | 15 | uses: bitfocus/actions/.github/workflows/module-checks.yaml@main 16 | # with: 17 | # upload-artifact: true # uncomment this to upload the built package as an artifact to this workflow that you can download and share with others 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pnp.* 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/plugins 5 | !.yarn/releases 6 | !.yarn/sdks 7 | !.yarn/versions 8 | 9 | node_modules/ 10 | 11 | dist/ 12 | pkg 13 | pkg.tgz 14 | DEBUG-* 15 | /.vscode 16 | 17 | .DS_STORE -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v18.12.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | pkg 3 | /LICENSE.md 4 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "printWidth": 120, 5 | "semi": false, 6 | "singleQuote": true, 7 | "useTabs": true, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bitfocus AS - Open Source 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # companion-module-getontime-ontime 2 | 3 | This module is created for use with [Ontime App](https://www.getontime.no/) 4 | 5 | - [Download ontime](https://www.getontime.no/) 6 | - [Read the docs](https://docs.getontime.no/) 7 | - [Follow on github](https://github.com/cpvalente/ontime) 8 | 9 | ## Help 10 | 11 | See [HELP.md](./companion/HELP.md) 12 | 13 | ## License 14 | 15 | [MIT License](./LICENSE) 16 | -------------------------------------------------------------------------------- /companion/HELP.md: -------------------------------------------------------------------------------- 1 | # Ontime Companion Module 2 | 3 | This module gives control over Ontime leveraging its [WebSockets API](https://docs.getontime.no/api/protocols/websockets/) 4 | 5 | ## Requirements 6 | 7 | - This module version requires Ontime v3 8 | 9 | ## Links 10 | 11 | You can download ontime from the website [www.getontime.no](https://www.getontime.no/) \ 12 | Read the docs at [http://docs.getontime.no](https://docs.getontime.no/) \ 13 | Follow Ontime's development on [GitHub](https://github.com/cpvalente/ontime) 14 | -------------------------------------------------------------------------------- /companion/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "getontime-ontime", 3 | "name": "getontime-ontime", 4 | "shortname": "ontime", 5 | "description": "Companion module for ontime", 6 | "version": "4.6.3", 7 | "license": "MIT", 8 | "repository": "git+https://github.com/bitfocus/companion-module-getontime-ontime.git", 9 | "bugs": "https://github.com/bitfocus/companion-module-getontime-ontime/issues", 10 | "maintainers": [ 11 | { 12 | "name": "Carlos Valente", 13 | "email": "mail@ontime.no" 14 | }, 15 | { 16 | "name": "Fabian Posenau" 17 | }, 18 | { 19 | "name": "Alex Christoffer Rasmussen" 20 | } 21 | ], 22 | "legacyIds": [], 23 | "runtime": { 24 | "type": "node22", 25 | "api": "nodejs-ipc", 26 | "apiVersion": "0.0.0", 27 | "entrypoint": "../dist/index.js" 28 | }, 29 | "manufacturer": "GetOntime", 30 | "products": ["Ontime"], 31 | "keywords": ["ontime", "companion"] 32 | } 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { generateEslintConfig } from '@companion-module/tools/eslint/config.mjs' 2 | 3 | export default generateEslintConfig({ 4 | enableTypescript: true, 5 | }) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getontime-ontime", 3 | "version": "4.6.3", 4 | "main": "/dist/index.js", 5 | "license": "MIT", 6 | "prettier": "@companion-module/tools/.prettierrc.json", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/bitfocus/companion-module-getontime-ontime.git" 10 | }, 11 | "type": "module", 12 | "scripts": { 13 | "prepare": "husky install", 14 | "dev": "yarn build:watch", 15 | "build": "rimraf dist && yarn build:main", 16 | "build:main": "tsc -p tsconfig.build.json", 17 | "build:watch": "tsc -p tsconfig.build.json --watch", 18 | "lint": "eslint ./src --ext .ts", 19 | "lint:raw": "eslint --ext .ts --ext .js --ignore-pattern dist --ignore-pattern pkg", 20 | "format": "prettier -w .", 21 | "package": "yarn build && companion-module-build", 22 | "postinstall": "yarn build" 23 | }, 24 | "engines": { 25 | "node": "22.2" 26 | }, 27 | "dependencies": { 28 | "@companion-module/base": "1.12.0", 29 | "companion-module-utils": "0.5.0", 30 | "ws": "8.18.2" 31 | }, 32 | "devDependencies": { 33 | "@companion-module/tools": "2.3.0", 34 | "@types/node": "22.15.19", 35 | "@types/ws": "8.18.1", 36 | "eslint": "9.27.0", 37 | "husky": "9.1.7", 38 | "lint-staged": "16.0.0", 39 | "prettier": "3.5.3", 40 | "rimraf": "6.0.1", 41 | "typescript": "5.8.3", 42 | "typescript-eslint": "8.32.1" 43 | }, 44 | "lint-staged": { 45 | "*.{js,json,md}": [ 46 | "run prettier --write" 47 | ], 48 | "*.ts": [ 49 | "run lint --fix" 50 | ] 51 | }, 52 | "packageManager": "yarn@4.8.1" 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/colours.ts: -------------------------------------------------------------------------------- 1 | import { combineRgb } from '@companion-module/base' 2 | 3 | export const White = combineRgb(255, 255, 255) 4 | export const Black = combineRgb(0, 0, 0) 5 | 6 | export const PlaybackGreen = combineRgb(51, 158, 78) 7 | export const PlaybackRed = combineRgb(228, 40, 30) 8 | export const PauseOrange = combineRgb(192, 86, 33) 9 | export const RollBlue = combineRgb(2, 116, 182) 10 | 11 | export const ActiveBlue = combineRgb(43, 90, 188) 12 | 13 | export const NormalGray = combineRgb(211, 211, 211) 14 | export const WarningOrange = combineRgb(255, 171, 51) 15 | export const DangerRed = combineRgb(237, 51, 51) 16 | -------------------------------------------------------------------------------- /src/assets/icons.ts: -------------------------------------------------------------------------------- 1 | export const PlaybackPause = 2 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACbSURBVHgB7dhBDcMwEETRdQk0EAIhEAIhUAuhEAIhEFIE7p6t2tJUU/WQ/y6WNqv4nx0B4FpKiGqtax5zMz5LKY/O/pbH1Iz33N/jV/LSqfatH/a3wf6k3H0Lzejns7h/D4Ea+jeEuhHqRqgboW6EuhHqRqgboW6EuhHqRqgboW5q6Cl+Owb7rxB880i25LE0Y/WR7Mj9ZwBA1xvuI1StPRVgBwAAAABJRU5ErkJggg==' 3 | export const PlaybackStart = 4 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEdSURBVHgB7ZjRDYJADIaLcQBHYATdwFEYQTZxA9iAEXAD3YARZIN6F4+ABLgDer2a3Jf0hfThS/8GSAEikQgviHjSBVJRcqmqGnsK/QykYcTGNKoykIKJe4lGxHRN7C4UQYVXiHbTvUEIVooOhc9AzAHoSVU9Wddh40T514FAdCi8ax18RD9FCj7XgXCi4+nSroMn0aGw8zpwRT9FCv06WH92Qop2ZKoqW5MEUc3VNlUpolakiJZJkrRLDRJES1W5rekI4XipytUkHy7NISaqI9aCF1dJDfdES/hKtkAJ4ZdJv9iv4AsC0Tf+wW9egVx3gI2ifmMmEOWJeafoHUOee9B+gKjZY54Df+9O4WOew8RfiYnZBko/O0YiETc+wxQ2rR+3zSsAAAAASUVORK5CYII=' 5 | export const PlaybackPrevious = 6 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAFMSURBVHgB7ZjRbcJAEETHUf6TEpIKknRAOkgJpIIoHdABlAAVUAIlQAe4A6CCY1echCUO7LNn1z/3pJWRbI6nnbWNDigUCj6EEP6kNlJvICLrvWqBgcqFKzMQiILzxrrrwU1gi8Z0DuGWTdt3n+CAiEyizEIqFfekbQyeYUj88bnUtMPlL1LHeyfNOqoxy2GPbpKt0DuqMePSxU8QoYlmxpwNJXp2zCkGddQq5hS9RK1jTpEdvUfMKXI6+iGSWzjEnCJH9Acj4vIKZVBE2RRRNjmiK6kaI5EjWldV9S7Hf4wgnB29yOq/9G9cOuxGrxkVWe3uVD5+wam7g24mkd15jQPlrvcYB9rjyXoc6M9Rq3Ewe+Czx8H0zdQYB+1w3XL56dFJl1doFFbZX6SFl3L+iL4YbZLpmsvmvlNg7BSqIGXH7XZd3rZjoVDozhl6iN12Uss4EQAAAABJRU5ErkJggg==' 7 | export const PlaybackNext = 8 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAFISURBVHgB7Zj/TcMwEIVfEAOwAR4BRugmYQOYADaADcgGsEGzATBBVugG5qzaEFVu7cTvnH/8SadI/XH5dO+kKAYajUZdrLU3rkBE+hmpD6lnlOKb7e0/7+4zEJA+L7O+tyjBi50ySfUohCbq477EVDLdJaJXiV6pnTRSE3MdzpESzaWX2ovsI5RgiTqM1KtfhzuQYYoGjNQXex00RAM9iOugKeowIK2DtmjAoHAdaokGeqxch9qiDgO/DnLNfhptIRowOE44iy1FF9FE2TRRNluKfkt95v54C9GD1FPXdfdy/cn90zXqMuAoecBCaom6mJ3giJVoR/8Xc4mkQ3OiA1bGHENDtDjmGMzoaTHHYE10ADHmGKmJpm48Su1E8EFT0nFR1N98jHwVYt5pxLyK2Ylb4M2STvUs85Bs1lTr2JH2St1oNDL4BUn9GkhJu2j0AAAAAElFTkSuQmCC' 9 | export const PlaybackReload = 10 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAInSURBVHgB7ZjhdcIgEMcvfR3AEdig7QTNCG6gG9QNkk5gN9ANaifQTtB2At1AN7jeCfQRPBNIiM2H/N679zRw8M9BgANgZCQtiDiBhGTQESNoSvZMlpNNjFlOZN/GPrIs28EtIYE52TvZEePYk72QqZj+MkEARyOnN9+ALJDLC7KFUHwg23nPFNkjVKNs675SP2tog3ljphDKHp1yy5Zs0TQnje9a8F9hm/nsNVI4z2fCEObQAvIrhbZUTBu+0LNYsqn37A06wsKwGt19VGRRxv1gSkgECyP7ctrexjjXUUJiBLGLUMc6CugBMw2OzuipEKcm+hI7d/pYhTiE0JfYrdPHpKlyKMnFot7tLGVT5RjCJn64UP6w7FytrAB3Qv0ThJP2hJRl3PfO/M3d4b8X6j+BPgWFNLyG9HyCPo0xCvSpa3hg9eu3gsWh/28Ozu+/oR+iUJEhCvWzgzNDFKqc3wcYKqjTmzOhDss+FvWAfsUFv87BHr2OmDj1relzHr3rYTVdKOEGYPXEr0Kd3H2396hi7DHPc3ajuoSewMv8ScX426i6DfRxtPNTkRLagDofP/YhFrskd1cadOdPErEop8sKuiKI5YanLdrhKBbeKKUR6XRy7Upn3tQR6jTDF8jwTtS4okRfOxpBJdlMKOZD7gGqWcL50g0uswGuw5dknW9dajFzbI3xcERLjFyXU1zkKtARY3sAfcXoYi9yf8g2N7/IHRm5Eb/Ktnjp/UmNnwAAAABJRU5ErkJggg==' 11 | export const PlaybackStop = 12 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACMSURBVHgB7djLCYAwEEXRiRVZgpZkCVamHcSOxogbP1koPIyBeyAEhkDuesyACrl7l87k34vb328iS7vFhkxoTFdrZc0hhP44yIW6/UAKPbU1VglC1QhVI1SNUDVC1QhVI1SNUDVC1QhVI1SNUDVC1XKhs5W3XAe50NHKGx698n1HGv17kz9d5AI/swL4g4zsxjFdIQAAAABJRU5ErkJggg==' 13 | export const PlaybackRoll = 14 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJGSURBVHgB7ZjRccIwDIZFr++wQb0BbNB0gtIJygaFCcoG0AloJ0iZIHQDmAA6AXQCV7rYV6MKHDvBx0O+O11ysaWIP8aWDdDSct10oAZaa4WXIdo92gBNsS5rtAPaEu2z0+nsICWYYIZW6HAKtBFEEKQovoRUm6FlrIlU20GpoKVnLBNCUd+HiyiMSb4ICuVG3Z7Hl/q8C/6v0CQYcCYkqCAQ8hESbiZZCuQE3fvGmPkRi3MqUwwTyzKDBpPc+lTE9qHTP/P0VSamZQyRSSqmpKrgM6qaqPMOV9nBqb43Z+IUzv3kEv9QE3PiPAobAkyZRaRfFuCXx/iRY+E4qgC/2ETdYVbEOFVWs06ixtdV9Y63S2N06Nx/QDrenPsn3igl+mhvcLCvIB22gCEy3iglaifqFSQERbH1AtHn7VKidi77gfRszFXxhnPz6B6uiBu4HIU+zVZ7Ki5O04nuKvZTaF3hefeUwy3IL1PwN1YrQ7MEKkVLYt/TdYN9v4XnVuU1+HAm3n3o56nLudVJ+vRf5kpJBqsaC1vJllUc3CU0h0TostgOqy90ZFESS1RRYhyzlKoyNUchvlzVZ7gQ+rji2kIo6DRwAlTaikS8g29FFMSAjmP31zaZrP6/uZtCHTDAnCk7gprQUGJKTqEJKJA+ZqHjDyByFmsOTSIkaxPOPH49Xc4iueBfeS8fekimoNxGK6F5BWWFfnCe2aPIntCXtuD+Nb0OupxSCh1OoQM3fZamDnJpnyXVBjsoKyGqH9If5La0JOQXsDS9WDSXD0kAAAAASUVORK5CYII=' 15 | export const OnAir = 16 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAHOSURBVHgB7ZjtTcMwEIbfIP7TDZoRugFmAzZomQBG6AaECdoN6AbxBmknaJgAmODwtY56sahiOx+tII90iuX64tf2+WoHGBn5BxBRauzV2J5OcHnFv+EaMELmxj7pPPzbHJfECJiRPwqXwlnqJva4BDxDFI5CJDeIRyGcGSJpI3SCcGJ8DgwtNJo2QgdlFNo1t74NTWrhHSvjcopwpm6KSpJEoyvo+J/dFxsfDb5Ln6I/7nwahcbo1tiTNY1wNsL/K8QxeDOZmFqzmeIO4eyEf69C5WYK6qgtvkI/7HNigr8SqxGOFuXUPr/hga/QrX2yyMPBwqaVEv6UVSpyUpRGV1D9SPd+pr6JhfBbifroE9U5sbl4uRL1Sw+RS9E+FfU5usaZvT2dYrXqPP9FYO4MakL1W8Ej+sC8OBOdFOTcMK0QHtBMDkQMphD+GfrCCimcmV14+ClnJgt3IE0kCMR2wLElN0Fp7A3H7FDautTYPY5XFiXacpsHkwGGycPkt4lc+lvuBrEcd2tq/gCR0RV9MVFWdAXPuMI1whtLCFXokPHO1DXB6anCpqln1E//XFa2rFE/tPAJLBssLVVQXHp6QSRtll4jDJ7JEiMjf5QfNy9cX3/Jd2AAAAAASUVORK5CYII=' 17 | export const MessagePublic = 18 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAsCAYAAAATmipGAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMJSURBVHgB7VjRcdswDIVyGUAbVBvEmSDKBHU30AbJBrEncDKB3QncTiB3ArsTSBs4nQAFLNCGWVISJTr9iN8dThQJQk8gSIIEuOKTIoEIQMSUHplIKtXvIrskSd7hf4HI5SQLkgq7sRXdHD4K9LGCpMThqNgGBKL30JPxCT0WJLnVVJNsSH5L2QyzCYcHkomU7X6PFBY1xAKRfLK8sid5DRlKCZWVw8PPEANk6MUyvCbJYCC4r4PwC4yBRXI/JLZabD+LzXFkLZKVxGhUsE08XzW8YZB4DGT0qOSVJ8e9CXppK6EdtfTjCfaD+u58iuIAtmfW3/s2fbuz/svCasswHCW2xDU2S57BFnqS1J2WjvYhRBlVB1k9wQroQbRqMzyCKKNs+W6Kp8lVdZGcKqNLj46LaCn1LBOxs/aQzVu+P+ujx4pLpZiFEPXolg5d78yWnzR41W03lm4uz12kre3NUXfnU5bZbmb8V912JIqnVI3xC+JgSHpnvp0JpwO0R/WCXkMcZI66P9COnau/Jpp6lAdBvPHSQcQFPQpH592qyhSGI7Um35TkCdwe3UA7nOFyC3HAf1710HsbOkn10Os/ySA+eCXpk3tmqlybgiaqYydmpsQOmJM89tTPrL4HHIeeh4TijBs4Vu8gDObEqd/5xzl7WgWeQh+MDW8WZW17qUen984UCmz2e6dNe2f6qcpxzjJhmKryd68Wnmcwe5dXL+zRCj25xplHJZbM/swkP8yr2OSgmbyuOpexLq9ewqNi0+tNhh2jxqtzeWWSBVwenPtmUp67vHnj6Th6r+8LbBL03HyXSM5cej6ievZt4AKQEGOShVTVJN8gBNjcvjH67N/BwOZ6R8dkhaG3L9ZkWTo+wDccMxxwa4LNUWNpTcRwkmKsUEamMkRMrsR/wavCWvpMHLZSIefrzxdtw9JLPN9GtxiOSokPJY691EX/Je3xqhEbL4Ve6O7HEEwcRHk3WqiqDTS71caVBWEzdDk0qeEXOK2HJqPiDOpwuox+l688NuZ4csUVMfAXO6aOl6UD9cMAAAAASUVORK5CYII=' 19 | export const MessageLower = 20 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAsCAYAAAATmipGAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAKPSURBVHgB7ViLcdswDIU8gTYoN6g3iDpBPYI3qDeIO4HTCeROkHYCqRM4nYDewN4ABStQhhlSMiVGSi55dzjRIgg+ATA/APjAO0UGCYCIOT0US86vzyxPWZadYS4QuYJkR6KxHwfWLWAq0GRrkgqHQxsbEImbQ0/Gl/TYkRRO15GkJvnLbRtmmw53JEtuu+O+UFocIRWI5DfHKyeSh5hQcqrsPR7eQAqQoXvH8COJgoEwYz2E72EMHJKnIbnVYXvDNseRdUhqztGkMDbxetWISwMOj/SkCuhoR0qIBJOVnr3dIc5XrqH/YywqGABsljyLw5BBZYdeMqJsb9/nHHeAZmWNHf/uFyCaixTQbv/CUV7BZWGuky7GPeDzwA/+aZxQyP6Fo/9VtL/D9Pgl2quglgh7b0KnDr2we/CFfyEU7N5s8Afmg51bMaf/kKGX69cR5sOTaCvbkETzgPLUkIfs1nkhonPCextYwBuBJCq/RMF8UKJ9tA1JVOZl8pNSBJRot85rifIuZDs+w3y44+eZOLXOc3O05mch17CpwHMW/PNq5XGJ/hbtNHeZOMht82dQyznBnLq8+hJbKF6fg5Xsu/Koc4IxJCfzKp9BFf/c957cbvVqwKM3IWBLh7zZRXYjBm0mIFqJ7q1vvtDONNlej811p7DzUsi3MYMfxBcuAzqjPIpNipXitcbYwgYGDq+pgE15R48lKT1VeiYw+bvFAVUTbO7wpePgeJJsbC2MrDhEhlyFz2FWhUces/TYyplcaLxJsWE7IE9sccB4aCEhVDi2qIvhIm1basTGS7EF3dMYgpmHqFk3d+JVDc1uVftq8Xg5SJjQf4LL7mJr+KbAa5a79LV84bHXcj35wPvFP/pYW0uGRwmTAAAAAElFTkSuQmCC' 21 | export const MessageSpeaker = 22 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAsCAYAAAATmipGAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgB7Vj/ldowDBY3QTaouwEbNDdBrxM0naBscGwAnSDXCWgnCJ3g6AShE4QNVBkroPjJP5Lw2j+O7z09ApHkz7KwZAPc8UaxgBsAEQv6MCwF/3xiOSwWixP8LxC5kmRD0mIar6xbwr8CDVaRNDgdrfUBI5G99OR8SR8bktJ7dSTZk/zm536Z+3T4QLLkZ9/ukdLiCLcCkfzqRaUj2Y5ZSk6VFyXCK7gFyNGz53hHYmAirK1C+BnmwCPZTcmtiO8V+5xH1iPZco6mbAqOmMmJuvWJw11jXBrwQDKSBuLk7KQa1GG3phoD+cxkZWSTAZHGrTCsInp+RFJotUmj2/IuE4NMktKojugZLxJZRCP+XnKCIw3a2OyFXh0g07GtNokq4q8QNm2K5BPmRbMIkCw9PRv1vpLFB3f665AvX7EWiiaiVyok6wSJAtJEl8LfVr578HRL/jxMKG1/Yi9zOijSOdix+etH+e5CFK+tmsUviEMb9DPOqFgC/dhGroKMqNy/jhABz9wna0ganF+9Dp7PMyTRIqAcwjflN0NS48RWjiEDcAleiGgSFNU1uPZOg4HphNVcfoAZILKPoEe2hwFHuM7518cgiZ68AbJAZG0j8QnieV2By98cskY8H/sHSVTmZX5jAGeyP0je0+MXCBO2PnNaOSOe9S1NlLAGZoCrUYs6ioRtw3pdTGmX6zCDrAmQfYrYyNI8CJb/Z/opnmedZbiyaX+0WADkJL4HtXDYwXShqKKr9QYSQHfcGBPRVuiZlHPZwawTOg26fDSKTmjpTcBnVh8sDZJR9SbT45WJN/ysoQmMaUZFUxjKJVtlEk0hePbC4XlrremEKlNOrR8Dux+qtyLolrnsx+XSnAd0tyA9lgGdvnOPnZtajr6WPjbFak/XwBjgNceSxwfWt515KWSJkX2Yddq5JOWZvlYGWHGUKhgJnkCtRN3AWOBwm3jiJVqhfsFgl33HNkvFV8HkQvZbnFoBcVhGQ9tMDK2QEBqce6mL4auZy1UjuihVOO5Ct5tDcKEQtfvmRvy0B1ez99pJEt3SleDauHdwbdP6O3x7wXs+Xd78Ll9EbFYHdccdN8BfbmMAyfxQu/oAAAAASUVORK5CYII=' 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { SomeCompanionConfigField } from '@companion-module/base' 2 | 3 | export interface OntimeConfig { 4 | host: string 5 | port: string | null //TODO: remove 6 | ssl: boolean 7 | version: string //TODO: remove 8 | refetchEvents: boolean 9 | customToVariable: boolean 10 | reconnect: boolean 11 | reconnectInterval: number 12 | } 13 | 14 | export function GetConfigFields(): SomeCompanionConfigField[] { 15 | return [ 16 | { 17 | label: 'Information', 18 | id: 'info', 19 | type: 'static-text', 20 | value: 21 | 'This module will establish a connection to Ontime v3. If you are upgrading from V2 to V3 we suggest backing up your configuration file.', 22 | width: 12, 23 | }, 24 | { 25 | label: 'Ontime server address', 26 | id: 'host', 27 | type: 'textinput', 28 | default: '127.0.0.1:4001', 29 | width: 9, 30 | required: true, 31 | tooltip: 'Ontime server address. eg. http://127.0.0.1:4001', 32 | }, 33 | { 34 | label: 'Use SSL', 35 | id: 'ssl', 36 | type: 'checkbox', 37 | default: false, 38 | width: 3, 39 | tooltip: 'Use SSL to connect to the Ontime server.', 40 | }, 41 | //New line 42 | { 43 | label: 'Reconnect', 44 | id: 'reconnect', 45 | type: 'checkbox', 46 | default: true, 47 | width: 4, 48 | tooltip: 'Chose if you want Companion to try to reconnect to ontime when the connection is lost.', 49 | }, 50 | { 51 | label: 'Reconnect interval (seconds)', 52 | id: 'reconnectInterval', 53 | type: 'number', 54 | min: 1, 55 | max: 60, 56 | default: 8, 57 | width: 4, 58 | isVisible: (config) => config.reconnect === true, 59 | tooltip: 'The interval in seconds between each reconnect attempt.', 60 | }, 61 | //New line 62 | { 63 | label: 'Refetch events', 64 | id: 'refetchEvents', 65 | type: 'checkbox', 66 | default: true, 67 | width: 12, 68 | tooltip: 'Whether Companion should keep the rundown updated with Ontime by refetching on change.', 69 | }, 70 | //New line 71 | { 72 | label: 'Custom variables', 73 | id: 'customToVariable', 74 | type: 'checkbox', 75 | default: true, 76 | width: 12, 77 | tooltip: 'Whether Ontime custom fields should be written to variables.', 78 | }, 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * maximum allowed time 23:59:59 as seconds 3 | */ 4 | export const MAX_TIME_SECONDS = 23 * 60 * 60 + 59 * 60 + 59 5 | 6 | export enum ActionId { 7 | Start = 'start', 8 | Load = 'load', 9 | Pause = 'pause', 10 | Stop = 'stop', 11 | Reload = 'reload', 12 | Roll = 'roll', 13 | 14 | Add = 'add', 15 | 16 | Change = 'change', 17 | 18 | TimerBlackout = 'TimerBlackout', 19 | TimerBlink = 'TimerBlink', 20 | MessageVisibility = 'setMessageVisibility', 21 | MessageVisibilityAndText = 'setMessageVisibilityAndText', 22 | MessageSecondarySource = 'setMessageSecondarySource', 23 | MessageText = 'setMessage', 24 | 25 | AuxTimerDuration = 'auxTimerDuration', 26 | AuxTimerPlayState = 'auxTimerPlayState', 27 | AuxTimerDirection = 'auxTimerDirection', 28 | AuxTimerAdd = 'auxTimerAdd', 29 | } 30 | 31 | export enum deprecatedActionId { 32 | Next = 'next', 33 | Previous = 'previous', 34 | SetOnAir = 'setOnAir', 35 | SetTimerMessageVisibility = 'setTimerMessageVisibility', 36 | SetTimerMessage = 'setTimerMessage', 37 | SetPublicMessageVisibility = 'setPublicMessageVisibility', 38 | SetPublicMessage = 'setPublicMessage', 39 | SetLowerMessageVisibility = 'setLowerMessageVisibility', 40 | SetLowerMessage = 'setLowerMessage', 41 | StartId = 'startId', 42 | StartSelect = 'startSelect', 43 | StartIndex = 'startIndex', 44 | StartNext = 'startNext', 45 | StartCue = 'startCue', 46 | LoadId = 'loadId', 47 | LoadSelect = 'loadSelect', 48 | LoadIndex = 'loadIndex', 49 | LoadCue = 'loadCue', 50 | SetTimerBlackout = 'setTimerBlackout', 51 | SetTimerBlink = 'setTimerBlink', 52 | } 53 | 54 | export enum feedbackId { 55 | ColorPlayback = 'colorPlayback', 56 | ColorAddRemove = 'state_color_add_remove', 57 | OnAir = 'onAir', 58 | 59 | MessageVisible = 'messageVisible', 60 | MessageSecondarySourceVisible = 'messageSecondarySourceVisible', 61 | TimerBlink = 'timerBlink', 62 | TimerBlackout = 'timerBlackout', 63 | TimerPhase = 'timerPhase', 64 | 65 | TimerProgressBar = 'timerProgressBar', 66 | TimerProgressBarMulti = 'timerProgressBarMulti', 67 | 68 | RundownOffset = 'rundownOffset', 69 | 70 | CustomFieldsValue = 'customFieldsValue', 71 | 72 | AuxTimerPlayback = 'auxTimerPlayback', 73 | AuxTimerNegative = 'auxTimerNegativePlayback', 74 | } 75 | 76 | export enum deprecatedFeedbackId { 77 | ThisMessageVisible = 'thisMessageVisible', 78 | TimerMessageVisible = 'timerMessageVisible', 79 | ThisTimerMessageVisible = 'thisTimerMessageVisible', 80 | PublicMessageVisible = 'publicMessageVisible', 81 | LowerMessageVisible = 'lowerMessageVisible', 82 | ColorRunning = 'state_color_running', 83 | ColorPaused = 'state_color_paused', 84 | ColorStopped = 'state_color_stopped', 85 | ColorRoll = 'state_color_roll', 86 | ColorNegative = 'timer_negative', 87 | TimerZone = 'timerZone', 88 | } 89 | 90 | export enum variableId { 91 | PlayState = 'playState', 92 | 93 | Clock = 'clock', 94 | 95 | TimerStart = 'timer_start', 96 | TimerFinish = 'timer_finish', 97 | TimerAdded = 'timer_added', 98 | TimerAddedNice = 'timer_added_nice', 99 | TimerTotalMs = 'timer_total_ms', 100 | TimerPhase = 'timer_phase', 101 | Time = 'time', 102 | TimeHM = 'time_hm', 103 | TimeH = 'time_h', 104 | TimeM = 'time_m', 105 | TimeS = 'time_s', 106 | TimeN = 'time_sign', 107 | 108 | IdPrevious = 'idPrevious', 109 | TitlePrevious = 'titlePrevious', 110 | NotePrevious = 'notePrevious', 111 | CuePrevious = 'cuePrevious', 112 | 113 | IdNow = 'idNow', 114 | TitleNow = 'titleNow', 115 | NoteNow = 'noteNow', 116 | CueNow = 'cueNow', 117 | 118 | IdNext = 'idNext', 119 | TitleNext = 'titleNext', 120 | NoteNext = 'noteNext', 121 | CueNext = 'cueNext', 122 | 123 | CurrentBlockTitle = 'currentBlockTitle', 124 | CurrentBlockStartedAt = 'currentBlockStartedAt_hms', 125 | CurrentBlockStartedAtMs = 'currentBlockStartedAt_ms', 126 | 127 | TimerMessage = 'timerMessage', 128 | TimerMessageVisible = 'timerMessageVisible', 129 | TimerBlink = 'timerBlink', 130 | TimerBlackout = 'timerBlackout', 131 | ExternalMessage = 'externalMessage', 132 | TimerSecondarySource = 'timerSecondarySource', 133 | 134 | AuxTimerDurationMs = 'auxTimer_duration_ms', 135 | AuxTimerPlayback = 'auxTimer_playback', 136 | AuxTimerCurrentMs = 'auxTimer_current_ms', 137 | AuxTimerCurrent = 'auxTimer_current_hms', 138 | AuxTimerDirection = 'auxTimer_direction', 139 | 140 | NumberOfEvents = 'numEvents', 141 | SelectedEventIndex = 'selectedEventIndex', 142 | RundownOffset = 'rundown_offset_hms', 143 | PlannedStart = 'plannedStart_hms', 144 | ActualStart = 'actualStart_hms', 145 | PlannedEnd = 'plannedEnd_hms', 146 | ExpectedEnd = 'expectedEnd_hms', 147 | } 148 | 149 | export enum OffsetState { 150 | On = 'on', 151 | Behind = 'behind', 152 | Ahead = 'ahead', 153 | Both = 'both', 154 | } 155 | 156 | export enum deprecatedVariableId { 157 | SubtitleNow = 'subtitleNow', 158 | SpeakerNow = 'speakerNow', 159 | SubtitleNext = 'subtitleNext', 160 | SpeakerNext = 'speakerNext', 161 | } 162 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SomeCompanionConfigField, 3 | CompanionActionDefinitions, 4 | CompanionPresetDefinitions, 5 | CompanionVariableDefinition, 6 | CompanionFeedbackDefinitions, 7 | } from '@companion-module/base' 8 | import { runEntrypoint, InstanceBase, InstanceStatus } from '@companion-module/base' 9 | 10 | import type { OntimeConfig } from './config.js' 11 | import { GetConfigFields } from './config.js' 12 | import { OntimeV3 } from './v3/ontimev3.js' 13 | import { UpgradeScripts } from './upgrades.js' 14 | 15 | export interface OntimeClient { 16 | instance: OnTimeInstance 17 | 18 | connect(): void 19 | disconnectSocket(): void 20 | 21 | getVariables(): CompanionVariableDefinition[] 22 | getActions(): CompanionActionDefinitions 23 | getFeedbacks(self: OnTimeInstance): CompanionFeedbackDefinitions 24 | getPresets(): CompanionPresetDefinitions 25 | } 26 | 27 | export class OnTimeInstance extends InstanceBase { 28 | public config!: OntimeConfig 29 | private ontime!: OntimeClient 30 | 31 | /** 32 | * Main initialization function called once the module 33 | * is OK to start doing things. 34 | */ 35 | async init(config: OntimeConfig): Promise { 36 | this.config = config 37 | 38 | this.log('debug', 'Initializing module') 39 | this.updateStatus(InstanceStatus.Disconnected) 40 | 41 | this.ontime = new OntimeV3(this) 42 | this.updateStatus(InstanceStatus.Connecting, 'starting V3') 43 | 44 | this.initConnection() 45 | this.init_variables() 46 | this.init_actions() 47 | this.init_feedbacks() 48 | this.init_presets() 49 | this.checkFeedbacks() 50 | } 51 | 52 | async destroy(): Promise { 53 | this.ontime.disconnectSocket() 54 | this.updateStatus(InstanceStatus.Disconnected) 55 | this.log('debug', 'destroy ' + this.id) 56 | } 57 | 58 | getConfigFields(): SomeCompanionConfigField[] { 59 | return GetConfigFields() 60 | } 61 | 62 | async configUpdated(config: OntimeConfig): Promise { 63 | this.config = config 64 | this.ontime.disconnectSocket() 65 | this.updateStatus(InstanceStatus.Disconnected) 66 | 67 | this.initConnection() 68 | this.init_variables() 69 | this.init_actions() 70 | this.init_feedbacks() 71 | this.init_presets() 72 | this.checkFeedbacks() 73 | } 74 | 75 | initConnection(): void { 76 | this.log('debug', 'Initializing connection') 77 | this.ontime.connect() 78 | } 79 | 80 | init_variables(): void { 81 | this.log('debug', 'Initializing variables') 82 | this.setVariableDefinitions(this.ontime.getVariables()) 83 | } 84 | 85 | init_actions(): void { 86 | this.log('debug', 'Initializing actions') 87 | this.setActionDefinitions(this.ontime.getActions()) 88 | } 89 | 90 | init_feedbacks(): void { 91 | this.log('debug', 'Initializing feedbacks') 92 | this.setFeedbackDefinitions(this.ontime.getFeedbacks(this)) 93 | } 94 | 95 | init_presets(): void { 96 | this.log('debug', 'Initializing presets') 97 | this.setPresetDefinitions(this.ontime.getPresets()) 98 | } 99 | } 100 | 101 | runEntrypoint(OnTimeInstance, UpgradeScripts) 102 | -------------------------------------------------------------------------------- /src/upgrades.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ 2 | import type { 3 | // CreateConvertToBooleanFeedbackUpgradeScript, 4 | CompanionStaticUpgradeProps, 5 | CompanionStaticUpgradeResult, 6 | CompanionUpgradeContext, 7 | // type InputValue, 8 | CompanionStaticUpgradeScript, 9 | CompanionMigrationAction, 10 | CompanionMigrationFeedback, 11 | } from '@companion-module/base' 12 | import type { OntimeConfig } from './config.js' 13 | import { feedbackId, ActionId, deprecatedActionId, deprecatedFeedbackId } from './enums.js' 14 | import { TimerPhase } from './v3/ontime-types.js' 15 | 16 | function update2x4x0( 17 | _context: CompanionUpgradeContext, 18 | props: CompanionStaticUpgradeProps, 19 | ): CompanionStaticUpgradeResult { 20 | const result = { 21 | updatedConfig: null, 22 | updatedActions: new Array(), 23 | updatedFeedbacks: new Array(), 24 | } 25 | if (_context.currentConfig.version === 'v1') { 26 | for (const action of props.actions) { 27 | if (action.actionId === 'setSpeakerMessageVisibility') { 28 | action.actionId = deprecatedActionId.SetTimerMessageVisibility 29 | result.updatedActions.push(action) 30 | } else if (action.actionId === 'setSpeakerMessage') { 31 | action.actionId = deprecatedActionId.SetTimerMessage 32 | result.updatedActions.push(action) 33 | } else if (action.actionId === 'delay') { 34 | action.actionId = ActionId.Add 35 | result.updatedActions.push(action) 36 | } 37 | } 38 | for (const feedback of props.feedbacks) { 39 | if (feedback.feedbackId === 'speakerMessageVisible') { 40 | feedback.feedbackId = deprecatedFeedbackId.TimerMessageVisible 41 | result.updatedFeedbacks.push(feedback) 42 | } 43 | } 44 | } else if (_context.currentConfig.version === 'v2') { 45 | for (const action of props.actions) { 46 | if (action.actionId === 'setSpeakerMessageVisibility') { 47 | action.actionId = deprecatedActionId.SetTimerMessageVisibility 48 | result.updatedActions.push(action) 49 | } else if (action.actionId === 'setSpeakerMessage') { 50 | action.actionId = deprecatedActionId.SetTimerMessage 51 | result.updatedActions.push(action) 52 | } else if (action.actionId === 'delay') { 53 | action.actionId = ActionId.Add 54 | action.options.addremove = (action.options.value as number) >= 0 ? 'add' : 'remove' 55 | action.options.minutes = Math.abs(action.options.value as number) 56 | action.options.hours = 0 57 | action.options.seconds = 0 58 | result.updatedActions.push(action) 59 | } 60 | } 61 | for (const feedback of props.feedbacks) { 62 | if (feedback.feedbackId === 'speakerMessageVisible') { 63 | feedback.feedbackId = deprecatedFeedbackId.TimerMessageVisible 64 | result.updatedFeedbacks.push(feedback) 65 | } 66 | } 67 | } 68 | 69 | return result 70 | } 71 | 72 | function update3x4x0( 73 | _context: CompanionUpgradeContext, 74 | props: CompanionStaticUpgradeProps, 75 | ): CompanionStaticUpgradeResult { 76 | const result = { 77 | updatedConfig: null, 78 | updatedActions: new Array(), 79 | updatedFeedbacks: new Array(), 80 | } 81 | if (_context.currentConfig.version === 'v2') { 82 | for (const action of props.actions) { 83 | if (action.actionId === ActionId.Change) { 84 | if ('val' in action.options) { 85 | action.options.properties = [String(action.options.property)] 86 | action.options[String(action.options.property)] = action.options.val 87 | if (action.options.eventId === 'selected') { 88 | action.options.method = 'loaded' 89 | } else if (action.options.eventId === 'next') { 90 | action.options.method = 'next' 91 | } else { 92 | action.options.method = 'list' 93 | action.options.eventList = action.options.eventId 94 | } 95 | delete action.options.eventId 96 | delete action.options.property 97 | delete action.options.val 98 | result.updatedActions.push(action) 99 | } 100 | } else if (action.actionId === deprecatedActionId.LoadIndex) { 101 | action.actionId = ActionId.Load 102 | action.options.method = 'index' 103 | action.options.eventIndex = action.options.value 104 | delete action.options.value 105 | result.updatedActions.push(action) 106 | } else if (action.actionId === deprecatedActionId.Previous) { 107 | action.actionId = ActionId.Load 108 | action.options.method = 'previous' 109 | result.updatedActions.push(action) 110 | } else if (action.actionId === deprecatedActionId.Next) { 111 | action.actionId = ActionId.Load 112 | action.options.method = 'next' 113 | result.updatedActions.push(action) 114 | } else if (action.actionId === deprecatedActionId.LoadSelect) { 115 | action.actionId = ActionId.Load 116 | action.options.method = 'list' 117 | action.options.eventList = action.options.value 118 | delete action.options.value 119 | result.updatedActions.push(action) 120 | } else if (action.actionId === deprecatedActionId.LoadCue) { 121 | action.actionId = ActionId.Load 122 | action.options.method = 'cue' 123 | action.options.eventCue = action.options.value 124 | delete action.options.value 125 | result.updatedActions.push(action) 126 | } else if (action.actionId === deprecatedActionId.LoadId) { 127 | action.actionId = ActionId.Load 128 | action.options.method = 'id' 129 | action.options.eventId = action.options.value 130 | delete action.options.value 131 | result.updatedActions.push(action) 132 | } else if (action.actionId === deprecatedActionId.StartIndex) { 133 | action.actionId = ActionId.Start 134 | action.options.method = 'index' 135 | action.options.eventIndex = action.options.value 136 | delete action.options.value 137 | result.updatedActions.push(action) 138 | } else if (action.actionId === deprecatedActionId.StartSelect) { 139 | action.actionId = ActionId.Start 140 | action.options.method = 'list' 141 | action.options.eventList = action.options.value 142 | delete action.options.value 143 | result.updatedActions.push(action) 144 | } else if (action.actionId === deprecatedActionId.StartCue) { 145 | action.actionId = ActionId.Start 146 | action.options.method = 'cue' 147 | action.options.eventCue = action.options.value 148 | delete action.options.value 149 | result.updatedActions.push(action) 150 | } else if (action.actionId === deprecatedActionId.StartId) { 151 | action.actionId = ActionId.Start 152 | action.options.method = 'id' 153 | action.options.eventId = action.options.value 154 | delete action.options.value 155 | result.updatedActions.push(action) 156 | } else if (action.actionId === deprecatedActionId.StartNext) { 157 | action.actionId = ActionId.Start 158 | action.options.method = 'next' 159 | result.updatedActions.push(action) 160 | } else if (action.actionId === ActionId.Start) { 161 | if (!('method' in action.options)) { 162 | action.options.method = 'loaded' 163 | result.updatedActions.push(action) 164 | } 165 | } else if (action.actionId === deprecatedActionId.SetTimerMessageVisibility) { 166 | action.actionId = ActionId.MessageVisibility 167 | action.options.destination = 'timer' 168 | result.updatedActions.push(action) 169 | } else if (action.actionId === deprecatedActionId.SetPublicMessageVisibility) { 170 | action.actionId = ActionId.MessageVisibility 171 | action.options.destination = 'public' 172 | result.updatedActions.push(action) 173 | } else if (action.actionId === deprecatedActionId.SetLowerMessageVisibility) { 174 | action.actionId = ActionId.MessageVisibility 175 | action.options.destination = 'lower' 176 | result.updatedActions.push(action) 177 | } else if (action.actionId === deprecatedActionId.SetTimerMessage) { 178 | action.actionId = ActionId.MessageText 179 | action.options.destination = 'timer' 180 | result.updatedActions.push(action) 181 | } else if (action.actionId === deprecatedActionId.SetPublicMessage) { 182 | action.actionId = ActionId.MessageText 183 | action.options.destination = 'public' 184 | result.updatedActions.push(action) 185 | } else if (action.actionId === deprecatedActionId.SetLowerMessage) { 186 | action.actionId = ActionId.MessageText 187 | action.options.destination = 'lower' 188 | result.updatedActions.push(action) 189 | } else if (action.actionId === deprecatedActionId.SetTimerBlackout) { 190 | action.actionId = ActionId.TimerBlackout 191 | result.updatedActions.push(action) 192 | } else if (action.actionId === deprecatedActionId.SetTimerBlink) { 193 | action.actionId = ActionId.TimerBlink 194 | result.updatedActions.push(action) 195 | } 196 | } 197 | for (const feedback of props.feedbacks) { 198 | if (feedback.feedbackId === deprecatedFeedbackId.ColorRunning) { 199 | feedback.feedbackId = feedbackId.ColorPlayback 200 | feedback.options.state = 'play' 201 | result.updatedFeedbacks.push(feedback) 202 | } else if (feedback.feedbackId === deprecatedFeedbackId.ColorPaused) { 203 | feedback.feedbackId = feedbackId.ColorPlayback 204 | feedback.options.state = 'pause' 205 | result.updatedFeedbacks.push(feedback) 206 | } else if (feedback.feedbackId === deprecatedFeedbackId.ColorStopped) { 207 | feedback.feedbackId = feedbackId.ColorPlayback 208 | feedback.options.state = 'stop' 209 | result.updatedFeedbacks.push(feedback) 210 | } else if (feedback.feedbackId === deprecatedFeedbackId.ColorRoll) { 211 | feedback.feedbackId = feedbackId.ColorPlayback 212 | feedback.options.state = 'roll' 213 | result.updatedFeedbacks.push(feedback) 214 | } else if (feedback.feedbackId === feedbackId.ColorAddRemove) { 215 | if (!('direction' in feedback.options)) { 216 | feedback.options.direction = 'both' 217 | result.updatedFeedbacks.push(feedback) 218 | } 219 | } else if (feedback.feedbackId === deprecatedFeedbackId.LowerMessageVisible) { 220 | feedback.feedbackId = feedbackId.MessageVisible 221 | feedback.options.source = 'lower' 222 | feedback.options.reqText = false 223 | result.updatedFeedbacks.push(feedback) 224 | } else if (feedback.feedbackId === deprecatedFeedbackId.PublicMessageVisible) { 225 | feedback.feedbackId = feedbackId.MessageVisible 226 | feedback.options.source = 'public' 227 | feedback.options.reqText = false 228 | result.updatedFeedbacks.push(feedback) 229 | } else if (feedback.feedbackId === deprecatedFeedbackId.ThisTimerMessageVisible) { 230 | feedback.feedbackId = feedbackId.MessageVisible 231 | feedback.options.source = 'timer' 232 | feedback.options.reqText = true 233 | feedback.options.text = feedback.options.msg 234 | delete feedback.options.msg 235 | result.updatedFeedbacks.push(feedback) 236 | } else if (feedback.feedbackId === deprecatedFeedbackId.TimerMessageVisible) { 237 | feedback.feedbackId = feedbackId.MessageVisible 238 | feedback.options.source = 'timer' 239 | feedback.options.reqText = false 240 | result.updatedFeedbacks.push(feedback) 241 | } else if (feedback.feedbackId === deprecatedFeedbackId.ColorNegative) { 242 | feedback.feedbackId = feedbackId.TimerPhase 243 | feedback.options.state = [TimerPhase.Overtime] 244 | result.updatedFeedbacks.push(feedback) 245 | } 246 | } 247 | } 248 | 249 | return result 250 | } 251 | 252 | function update4xx( 253 | _context: CompanionUpgradeContext, 254 | props: CompanionStaticUpgradeProps, 255 | ): CompanionStaticUpgradeResult { 256 | const result = { 257 | updatedConfig: null, 258 | updatedActions: new Array(), 259 | updatedFeedbacks: new Array(), 260 | } 261 | 262 | for (const feedback of props.feedbacks) { 263 | if (feedback.feedbackId === deprecatedFeedbackId.TimerZone) { 264 | feedback.feedbackId = feedbackId.TimerPhase 265 | 266 | switch (feedback.options.zone) { 267 | case '': { 268 | feedback.options.phase = [TimerPhase.None] 269 | break 270 | } 271 | case 'normal': { 272 | feedback.options.phase = [TimerPhase.Default] 273 | break 274 | } 275 | case 'warning': { 276 | feedback.options.phase = [TimerPhase.Warning] 277 | break 278 | } 279 | case 'danger': { 280 | feedback.options.phase = [TimerPhase.Danger] 281 | break 282 | } 283 | case 'overtime': { 284 | feedback.options.phase = [TimerPhase.Overtime] 285 | break 286 | } 287 | } 288 | delete feedback.options.zone 289 | result.updatedFeedbacks.push(feedback) 290 | } 291 | } 292 | return result 293 | } 294 | 295 | function update46x( 296 | _context: CompanionUpgradeContext, 297 | props: CompanionStaticUpgradeProps, 298 | ): CompanionStaticUpgradeResult { 299 | const result: CompanionStaticUpgradeResult = { 300 | updatedConfig: null, 301 | updatedActions: new Array(), 302 | updatedFeedbacks: new Array(), 303 | } 304 | 305 | if (props.config === null) { 306 | return result 307 | } 308 | 309 | const { host, port } = props.config 310 | if (port === null) { 311 | return result 312 | } 313 | 314 | const newAddress = `${host}:${port}` 315 | result.updatedConfig = { ...props.config, host: newAddress, port: null } 316 | return result 317 | } 318 | 319 | export const UpgradeScripts: CompanionStaticUpgradeScript[] = [ 320 | update2x4x0, 321 | update3x4x0, 322 | update4xx, 323 | update46x, 324 | ] 325 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionVariableValues, DropdownChoice } from '@companion-module/base' 2 | import type { EventCustomFields, OntimeEvent, RuntimeStore, SimpleTimerState } from './v3/ontime-types.js' 3 | import { OntimeV3 } from './v3/ontimev3.js' 4 | 5 | export const joinTime = (...args: string[]): string => args.join(':') 6 | 7 | function padTo2Digits(number: number) { 8 | return number.toString().padStart(2, '0') 9 | } 10 | 11 | const defaultTimerObject = { 12 | hours: '--', 13 | minutes: '--', 14 | seconds: '--', 15 | hoursMinutes: '--:--', 16 | hoursMinutesSeconds: '--:--:--', 17 | delayString: '0', 18 | negative: '', 19 | } 20 | 21 | function ensureTrailingSlash(url: URL): URL { 22 | if (!url.pathname.endsWith('/')) { 23 | url.pathname += '/' 24 | } 25 | return url 26 | } 27 | 28 | export function makeURL(host: string, path = '', ssl = false, ws = false): URL | undefined { 29 | let url: URL | undefined 30 | 31 | if (URL.canParse(host)) { 32 | url = new URL(host) 33 | } else if (URL.canParse(`http://${host}`)) { 34 | url = new URL(`http://${host}`) 35 | } 36 | 37 | if (url === undefined) return 38 | 39 | url = ensureTrailingSlash(url) 40 | url.pathname += path 41 | 42 | if (ssl) { 43 | url.protocol = ws ? 'wss' : 'https' 44 | } else { 45 | url.protocol = ws ? 'ws' : 'http' 46 | } 47 | 48 | return url 49 | } 50 | 51 | type SplitTime = typeof defaultTimerObject 52 | 53 | export function msToSplitTime(time: number | null): SplitTime { 54 | if (time === null) { 55 | return defaultTimerObject 56 | } 57 | let negative = false 58 | if (time < 0) { 59 | time = time * -1 60 | negative = true 61 | } else { 62 | negative = false 63 | } 64 | const s = Math.floor((time / 1000) % 60) 65 | const m = Math.floor((time / (1000 * 60)) % 60) 66 | const h = Math.floor((time / (1000 * 60 * 60)) % 24) 67 | 68 | const seconds = padTo2Digits(s) 69 | const minutes = padTo2Digits(m) 70 | const hours = padTo2Digits(h) 71 | const negativeSign = negative ? '-' : '' 72 | 73 | const hoursMinutes = `${hours}:${minutes}` 74 | const hoursMinutesSeconds = `${negativeSign}${hoursMinutes}:${seconds}` 75 | 76 | let delayString = '00' 77 | 78 | if (h && !m && !s) { 79 | delayString = `${negativeSign}${h}h` 80 | } else if (!h && m && !s) { 81 | delayString = `${negativeSign}${m}m` 82 | } else if (!h && !m && s) { 83 | delayString = `${negativeSign}${s}s` 84 | } 85 | 86 | return { 87 | hours, 88 | minutes, 89 | seconds, 90 | hoursMinutes, 91 | hoursMinutesSeconds, 92 | delayString, 93 | negative: negativeSign, 94 | } 95 | } 96 | 97 | export function eventsToChoices(events: OntimeEvent[]): DropdownChoice[] { 98 | return events.map(({ id, cue, title }) => { 99 | return { id, label: `${cue} | ${title}` } 100 | }) 101 | } 102 | 103 | export function getAuxTimerState(ontime: OntimeV3, index = 'auxtimer1'): SimpleTimerState { 104 | return ontime.state[index as keyof RuntimeStore] as unknown as SimpleTimerState 105 | } 106 | 107 | export function findPreviousPlayableEvent(ontime: OntimeV3): OntimeEvent | null { 108 | if (ontime.state.eventNow === null) { 109 | return null 110 | } 111 | 112 | const nowId = ontime.state.eventNow.id 113 | let now = false 114 | 115 | for (let i = ontime.events.length - 1; i >= 0; i--) { 116 | if (!now && ontime.events[i].id === nowId) { 117 | now = true 118 | continue 119 | } 120 | if (now && !ontime.events[i].skip) { 121 | return ontime.events[i] 122 | } 123 | } 124 | 125 | return null 126 | } 127 | 128 | export function variablesFromCustomFields( 129 | ontime: OntimeV3, 130 | postFix: string, 131 | val: EventCustomFields | undefined, 132 | ): CompanionVariableValues { 133 | const companionVariableValues: CompanionVariableValues = {} 134 | if (typeof val === 'undefined') { 135 | Object.keys(ontime.customFields).forEach((key) => { 136 | companionVariableValues[`${key}_Custom${postFix}`] = undefined 137 | }) 138 | return companionVariableValues 139 | } 140 | 141 | Object.keys(ontime.customFields).forEach((key) => { 142 | companionVariableValues[`${key}_Custom${postFix}`] = val[key] 143 | }) 144 | return companionVariableValues 145 | } 146 | 147 | export function strictTimerStringToSeconds(str: string): string | number { 148 | const [hh, mm, ss] = str.split(':') 149 | 150 | if (hh === undefined || mm === undefined || ss === undefined) { 151 | return 'undefined' 152 | } 153 | 154 | const isNegative = hh.startsWith('-') ? -1 : 1 155 | hh.replace('-', '') 156 | 157 | return isNegative * (Number(ss) + Number(mm) * 60 + Number(hh) * 60 * 60) 158 | } 159 | -------------------------------------------------------------------------------- /src/v3/actions/auxTimer.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionActionDefinition, CompanionActionEvent } from '@companion-module/base' 2 | import { socketSendJson } from '../connection.js' 3 | import { ActionId } from '../../enums.js' 4 | import { OntimeV3 } from '../ontimev3.js' 5 | import { ActionCommand } from './commands.js' 6 | import { SimpleDirection, SimplePlayback } from '../ontime-types.js' 7 | import { getAuxTimerState } from '../../utilities.js' 8 | 9 | export function createAuxTimerActions(ontime: OntimeV3): { [id: string]: CompanionActionDefinition } { 10 | function togglePlayState(action: CompanionActionEvent): void { 11 | const id = (action.options.destination ?? '1') as string 12 | const timer = getAuxTimerState(ontime) 13 | if (action.options.value === 'toggleSS') { 14 | socketSendJson(ActionCommand.AuxTimer, { 15 | [id]: timer.playback === SimplePlayback.Start ? SimplePlayback.Stop : SimplePlayback.Start, 16 | }) 17 | return 18 | } 19 | 20 | if (action.options.value === 'toggleSP') { 21 | socketSendJson(ActionCommand.AuxTimer, { 22 | [id]: timer.playback === SimplePlayback.Start ? SimplePlayback.Pause : SimplePlayback.Start, 23 | }) 24 | return 25 | } 26 | 27 | socketSendJson(ActionCommand.AuxTimer, { [id]: action.options.value }) 28 | } 29 | 30 | function duration(action: CompanionActionEvent): void { 31 | const id = action.options.destination as string 32 | const { hours, minutes, seconds } = action.options 33 | const val = (Number(hours) * 60 + Number(minutes)) * 60 + Number(seconds) 34 | socketSendJson(ActionCommand.AuxTimer, { [id]: { duration: val } }) 35 | } 36 | 37 | function addTime(action: CompanionActionEvent): void { 38 | const id = action.options.destination as string 39 | const { hours, minutes, seconds, addremove } = action.options 40 | const val = ((Number(hours) * 60 + Number(minutes)) * 60 + Number(seconds)) * (addremove == 'remove' ? -1 : 1) 41 | socketSendJson(ActionCommand.AuxTimer, { [id]: { addtime: val } }) 42 | } 43 | 44 | return { 45 | [ActionId.AuxTimerPlayState]: { 46 | name: 'Start/Stop/Pause the aux timer', 47 | options: [ 48 | { 49 | type: 'dropdown', 50 | choices: [{ id: '1', label: 'Aux Timer 1' }], 51 | default: '1', 52 | id: 'destination', 53 | label: 'Select Aux Timer', 54 | isVisible: () => false, //This Stays hidden for now 55 | }, 56 | { 57 | type: 'dropdown', 58 | choices: [ 59 | { id: 'toggleSS', label: 'Toggle Start/Stop' }, 60 | { id: 'toggleSP', label: 'Toggle Start/Pause' }, 61 | { id: SimplePlayback.Start, label: 'Start' }, 62 | { id: SimplePlayback.Stop, label: 'Stop' }, 63 | { id: SimplePlayback.Pause, label: 'Pause' }, 64 | ], 65 | default: SimplePlayback.Start, 66 | id: 'value', 67 | label: 'Action', 68 | }, 69 | ], 70 | callback: togglePlayState, 71 | }, 72 | [ActionId.AuxTimerDuration]: { 73 | name: 'Set the aux timer duration', 74 | options: [ 75 | { 76 | type: 'dropdown', 77 | choices: [{ id: '1', label: 'Aux Timer 1' }], 78 | default: '1', 79 | id: 'destination', 80 | label: 'Select Aux Timer', 81 | isVisible: () => false, //This Stays hidden for now 82 | }, 83 | { 84 | type: 'number', 85 | id: 'hours', 86 | label: 'Hours', 87 | default: 0, 88 | step: 1, 89 | min: 0, 90 | max: 24, 91 | required: true, 92 | }, 93 | { 94 | type: 'number', 95 | id: 'minutes', 96 | label: 'Minutes', 97 | default: 1, 98 | step: 1, 99 | min: 0, 100 | max: 1440, 101 | required: true, 102 | }, 103 | { 104 | type: 'number', 105 | id: 'seconds', 106 | label: 'Seconds', 107 | default: 0, 108 | min: 0, 109 | max: 86400, 110 | step: 1, 111 | required: true, 112 | }, 113 | ], 114 | callback: duration, 115 | }, 116 | [ActionId.AuxTimerDirection]: { 117 | name: 'Set the aux timer direction', 118 | options: [ 119 | { 120 | type: 'dropdown', 121 | choices: [{ id: '1', label: 'Aux Timer 1' }], 122 | default: '1', 123 | id: 'destination', 124 | label: 'Select Aux Timer', 125 | isVisible: () => false, //This Stays hidden for now 126 | }, 127 | { 128 | type: 'dropdown', 129 | choices: [ 130 | { id: SimpleDirection.CountDown, label: 'Count Down' }, 131 | { id: SimpleDirection.CountUp, label: 'Count Up' }, 132 | ], 133 | default: SimpleDirection.CountDown, 134 | id: 'direction', 135 | label: 'Direction', 136 | }, 137 | ], 138 | callback: ({ options }) => 139 | socketSendJson(ActionCommand.AuxTimer, { [options.destination as string]: { direction: options.direction } }), 140 | }, 141 | [ActionId.AuxTimerAdd]: { 142 | name: 'Add / remove time to aux timer', 143 | options: [ 144 | { 145 | type: 'dropdown', 146 | choices: [{ id: '1', label: 'Aux Timer 1' }], 147 | default: '1', 148 | id: 'destination', 149 | label: 'Select Aux Timer', 150 | isVisible: () => false, //This Stays hidden for now 151 | }, 152 | { 153 | id: 'addremove', 154 | type: 'dropdown', 155 | choices: [ 156 | { id: 'add', label: 'Add Time' }, 157 | { id: 'remove', label: 'Remove Time' }, 158 | ], 159 | label: 'Add or Remove', 160 | default: 'add', 161 | }, 162 | { 163 | type: 'number', 164 | id: 'hours', 165 | label: 'Hours', 166 | default: 0, 167 | step: 1, 168 | min: 0, 169 | max: 24, 170 | required: true, 171 | }, 172 | { 173 | type: 'number', 174 | id: 'minutes', 175 | label: 'Minutes', 176 | default: 1, 177 | step: 1, 178 | min: 0, 179 | max: 1440, 180 | required: true, 181 | }, 182 | { 183 | type: 'number', 184 | id: 'seconds', 185 | label: 'Seconds', 186 | default: 0, 187 | min: 0, 188 | max: 86400, 189 | step: 1, 190 | required: true, 191 | }, 192 | ], 193 | callback: addTime, 194 | }, 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/v3/actions/change.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionActionDefinition, CompanionActionEvent, CompanionActionContext } from '@companion-module/base' 2 | import { splitHex } from '@companion-module/base' 3 | import { socketSendJson } from '../connection.js' 4 | import { ActionId } from '../../enums.js' 5 | import { ActionCommand } from './commands.js' 6 | import { changePicker } from './changePicker.js' 7 | import { eventPicker } from './eventPicker.js' 8 | import { OntimeV3 } from '../ontimev3.js' 9 | import { strictTimerStringToSeconds } from '../../utilities.js' 10 | 11 | export function createChangeActions(ontime: OntimeV3): { [id: string]: CompanionActionDefinition } { 12 | async function changeEvent(action: CompanionActionEvent, context: CompanionActionContext): Promise { 13 | const { properties, method, eventList, eventId } = action.options 14 | let id: string | null = null 15 | switch (method) { 16 | case 'list': { 17 | id = eventList ? String(eventList) : null 18 | break 19 | } 20 | case 'loaded': { 21 | id = ontime.state.eventNow?.id ?? null 22 | break 23 | } 24 | case 'next': { 25 | id = ontime.state.eventNext?.id ?? null 26 | break 27 | } 28 | case 'id': { 29 | id = eventId ? String(eventId) : null 30 | break 31 | } 32 | } 33 | //if no ID skip 34 | if (id === null) { 35 | return 36 | } 37 | const patch = {} 38 | if (properties && Array.isArray(properties)) { 39 | //remove unwanted placeholder value if present 40 | if (properties.includes('pickOne')) { 41 | properties.splice(properties.indexOf('pickOne'), 1) 42 | } 43 | 44 | for (const property of properties) { 45 | const value = action.options[property] 46 | //return early if propval is empty 47 | if (!value || typeof property !== 'string') { 48 | continue 49 | } 50 | // converts companion color value to hex 51 | if (property === 'colour') { 52 | const colour = splitHex(value as string) 53 | Object.assign(patch, { colour }) 54 | continue 55 | } 56 | 57 | // converts companion time variable (hh:mm:ss) to ontime seconds 58 | if (property.endsWith('_hhmmss')) { 59 | const timeString = await context.parseVariablesInString(value as string) 60 | const seconds = strictTimerStringToSeconds(timeString) 61 | const propertyName = property.split('_hhmmss')[0] 62 | Object.assign(patch, { [propertyName]: seconds }) 63 | continue 64 | } 65 | 66 | Object.assign(patch, { [property]: value }) 67 | } 68 | socketSendJson(ActionCommand.Change, { [id]: patch }) 69 | } 70 | } 71 | 72 | return { 73 | [ActionId.Change]: { 74 | name: 'Change event property', 75 | options: [...eventPicker(ontime.events, ['list', 'loaded', 'next', 'id']), ...changePicker(ontime)], 76 | callback: changeEvent, 77 | }, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/v3/actions/changePicker.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CompanionInputFieldCheckbox, 3 | CompanionInputFieldColor, 4 | CompanionInputFieldDropdown, 5 | CompanionInputFieldMultiDropdown, 6 | CompanionInputFieldNumber, 7 | CompanionInputFieldStaticText, 8 | CompanionInputFieldTextInput, 9 | } from '@companion-module/base' 10 | import { combineRgb } from '@companion-module/base' 11 | import { OntimeV3 } from '../ontimev3.js' 12 | import { MAX_TIME_SECONDS } from '../../enums.js' 13 | 14 | const throttledEndpointText = 15 | 'This property will cause a recalculation of the rundwon\nand id therfor throttled by ontime' 16 | 17 | export function changePicker( 18 | ontime: OntimeV3, 19 | ): Array< 20 | | CompanionInputFieldNumber 21 | | CompanionInputFieldCheckbox 22 | | CompanionInputFieldDropdown 23 | | CompanionInputFieldMultiDropdown 24 | | CompanionInputFieldColor 25 | | CompanionInputFieldTextInput 26 | | CompanionInputFieldStaticText 27 | > { 28 | const allProps: ReturnType = [ 29 | { 30 | type: 'textinput', 31 | label: 'Title', 32 | id: 'title', 33 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('title'), 34 | }, 35 | { 36 | type: 'textinput', 37 | label: 'Note', 38 | id: 'note', 39 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('note'), 40 | }, 41 | { 42 | type: 'textinput', 43 | label: 'Cue', 44 | id: 'cue', 45 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('cue'), 46 | }, 47 | { 48 | type: 'checkbox', 49 | label: 'Is Public', 50 | id: 'isPublic', 51 | default: false, 52 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('isPublic'), 53 | }, 54 | { 55 | type: 'checkbox', 56 | label: 'Skip', 57 | tooltip: throttledEndpointText, 58 | id: 'skip', 59 | default: false, 60 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('skip'), 61 | }, 62 | { 63 | type: 'colorpicker', 64 | label: 'Colour', 65 | id: 'colour', 66 | default: combineRgb(255, 255, 255), 67 | returnType: 'string', 68 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('colour'), 69 | }, 70 | { 71 | type: 'number', 72 | label: 'Duration', 73 | tooltip: 'In Seconds\n' + throttledEndpointText, 74 | id: 'duration', 75 | default: 0, 76 | min: 0, 77 | max: MAX_TIME_SECONDS, 78 | step: 1, 79 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('duration'), 80 | }, 81 | { 82 | type: 'textinput', 83 | label: 'Duration (hh:mm:ss)', 84 | tooltip: throttledEndpointText, 85 | id: 'duration_hhmmss', 86 | default: '00:00:00', 87 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('duration_hhmmss'), 88 | useVariables: true, 89 | }, 90 | { 91 | type: 'number', 92 | label: 'Start Time', 93 | tooltip: 'In Seconds\n' + throttledEndpointText, 94 | id: 'timeStart', 95 | default: 0, 96 | min: 0, 97 | max: MAX_TIME_SECONDS, 98 | step: 1, 99 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeStart'), 100 | }, 101 | { 102 | type: 'textinput', 103 | label: 'Start Time (hh:mm:ss)', 104 | tooltip: 'The variable should result in (hh:mm:ss)\n' + throttledEndpointText, 105 | id: 'timeStart_hhmmss', 106 | default: '00:00:00', 107 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeStart_hhmmss'), 108 | useVariables: true, 109 | }, 110 | { 111 | type: 'number', 112 | label: 'End Time', 113 | tooltip: 'In Seconds\n' + throttledEndpointText, 114 | id: 'timeEnd', 115 | default: 0, 116 | min: 0, 117 | max: MAX_TIME_SECONDS, 118 | step: 1, 119 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeEnd'), 120 | }, 121 | { 122 | type: 'textinput', 123 | label: 'End Time (hh:mm:ss)', 124 | tooltip: throttledEndpointText, 125 | id: 'timeEnd_hhmmss', 126 | default: '00:00:00', 127 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeEnd_hhmmss'), 128 | useVariables: true, 129 | }, 130 | { 131 | type: 'number', 132 | label: 'Warning Time', 133 | id: 'timeWarning', 134 | default: 0, 135 | min: 0, 136 | max: MAX_TIME_SECONDS, 137 | step: 1, 138 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeWarning'), 139 | }, 140 | { 141 | type: 'textinput', 142 | label: 'Warning Time (hh:mm:ss)', 143 | id: 'timeWarning_hhmmss', 144 | default: '00:00:00', 145 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeWarning_hhmmss'), 146 | useVariables: true, 147 | }, 148 | { 149 | type: 'number', 150 | label: 'Danger Time', 151 | id: 'timeDanger', 152 | default: 0, 153 | min: 0, 154 | max: MAX_TIME_SECONDS, 155 | step: 1, 156 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeDanger'), 157 | }, 158 | { 159 | type: 'textinput', 160 | label: 'Danger Time (hh:mm:ss)', 161 | id: 'timeDanger_hhmmss', 162 | default: '00:00:00', 163 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timeDanger_hhmmss'), 164 | useVariables: true, 165 | }, 166 | { 167 | type: 'dropdown', 168 | label: 'End Action', 169 | id: 'endAction', 170 | choices: [ 171 | { id: 'load-next', label: 'Load Next' }, 172 | { id: 'none', label: 'None' }, 173 | { id: 'stop', label: 'Stop' }, 174 | { id: 'play-next', label: 'Play Next' }, 175 | ], 176 | default: 'none', 177 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('endAction'), 178 | }, 179 | { 180 | type: 'dropdown', 181 | label: 'Timer Type', 182 | id: 'timerType', 183 | choices: [ 184 | { id: 'count-down', label: 'Count Down' }, 185 | { id: 'count-up', label: 'Count Up' }, 186 | { id: 'time-to-end', label: 'Time To End' }, 187 | { id: 'clock', label: 'Clock' }, 188 | ], 189 | default: 'count-down', 190 | isVisible: (opts) => Array.isArray(opts.properties) && opts.properties.includes('timerType'), 191 | }, 192 | ...generateCustomFieldsOptions(ontime), 193 | ] 194 | 195 | return [ 196 | { 197 | type: 'multidropdown', 198 | id: 'properties', 199 | label: 'Properties', 200 | minSelection: 1, 201 | default: [], 202 | choices: allProps.map((p) => ({ id: p.id, label: p.label })), 203 | }, 204 | ...allProps, 205 | ] 206 | } 207 | 208 | function generateCustomFieldsOptions(ontime: OntimeV3): Array { 209 | const { customFields } = ontime 210 | const customProps: ReturnType = [] 211 | 212 | for (const field in customFields) { 213 | const id = `custom:${field}` 214 | customProps.push({ 215 | type: 'textinput', 216 | id, 217 | label: `Custom: ${customFields[field].label}`, 218 | isVisibleExpression: `arrayIncludes($(options:properties), '${id}')`, 219 | }) 220 | } 221 | 222 | return customProps 223 | } 224 | -------------------------------------------------------------------------------- /src/v3/actions/commands.ts: -------------------------------------------------------------------------------- 1 | export enum ActionCommand { 2 | Start = 'start', 3 | Load = 'load', 4 | Reload = 'reload', 5 | Pause = 'pause', 6 | Stop = 'stop', 7 | Roll = 'roll', 8 | Add = 'addtime', 9 | Message = 'message', 10 | Change = 'change', 11 | AuxTimer = 'auxtimer', 12 | } 13 | -------------------------------------------------------------------------------- /src/v3/actions/eventPicker.ts: -------------------------------------------------------------------------------- 1 | import type { SomeCompanionActionInputField, DropdownChoice } from '@companion-module/base' 2 | import { eventsToChoices } from '../../utilities.js' 3 | import type { OntimeEvent } from '../ontime-types.js' 4 | 5 | type SelectOptions = 'list' | 'loaded' | 'previous' | 'next' | 'cue' | 'id' | 'index' | 'go' 6 | const selectOptions: DropdownChoice[] = [ 7 | { id: 'list', label: 'From list' }, 8 | { id: 'loaded', label: 'Loaded Event' }, 9 | { id: 'next', label: 'Next Event' }, 10 | { id: 'go', label: 'GO (Next/Loaded Event)' }, 11 | { id: 'previous', label: 'Previous Event' }, 12 | { id: 'cue', label: 'CUE' }, 13 | { id: 'id', label: 'ID' }, 14 | { id: 'index', label: 'Index' }, 15 | ] 16 | 17 | export function eventPicker( 18 | events: OntimeEvent[], 19 | options: SelectOptions[] = ['list', 'next', 'previous', 'loaded', 'cue', 'id', 'index'], 20 | ): SomeCompanionActionInputField[] { 21 | const selectChoices = new Array() 22 | selectOptions.forEach((choice) => { 23 | if (options.includes(choice.id as SelectOptions)) { 24 | selectChoices.push(choice) 25 | } 26 | }) 27 | return [ 28 | { 29 | type: 'dropdown', 30 | id: 'method', 31 | label: 'Event Selection', 32 | choices: selectChoices, 33 | default: 'loaded', 34 | }, 35 | { 36 | type: 'dropdown', 37 | choices: eventsToChoices(events), 38 | id: 'eventList', 39 | label: 'Event', 40 | default: events[0]?.id ?? '', 41 | isVisible: (options) => options['method'] === 'list', 42 | }, 43 | { 44 | type: 'static-text', 45 | value: '', 46 | id: 'cuenote', 47 | label: 'NB! this will target the first event with a matching CUE name', 48 | isVisible: (options) => options['method'] === 'cue', 49 | }, 50 | { 51 | type: 'textinput', 52 | id: 'eventCue', 53 | label: 'Event Cue', 54 | default: '', 55 | isVisible: (options) => options['method'] === 'cue', 56 | }, 57 | { 58 | type: 'textinput', 59 | id: 'eventId', 60 | label: 'Event Id', 61 | default: '', 62 | isVisible: (options) => options['method'] === 'id', 63 | }, 64 | { 65 | type: 'number', 66 | id: 'eventIndex', 67 | label: 'Event Index', 68 | default: 1, 69 | min: 1, 70 | max: events.length, 71 | isVisible: (options) => options['method'] === 'index', 72 | }, 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/v3/actions/index.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionActionDefinition, CompanionActionDefinitions } from '@companion-module/base' 2 | 3 | import { createPlaybackActions } from './playback.js' 4 | import { createMessageActions } from './message.js' 5 | import { createChangeActions } from './change.js' 6 | import { OntimeV3 } from '../ontimev3.js' 7 | import { createAuxTimerActions } from './auxTimer.js' 8 | 9 | /** 10 | * Returns all implemented actions. 11 | * @param ontime reference to the Ontime version 12 | * @constructor 13 | * @returns CompanionActions 14 | */ 15 | export function actions(ontime: OntimeV3): CompanionActionDefinitions { 16 | const actions: { [id: string]: CompanionActionDefinition } = { 17 | ...createMessageActions(ontime), 18 | ...createPlaybackActions(ontime), 19 | ...createChangeActions(ontime), 20 | ...createAuxTimerActions(ontime), 21 | } 22 | return actions 23 | } 24 | -------------------------------------------------------------------------------- /src/v3/actions/message.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionActionDefinition, CompanionActionEvent } from '@companion-module/base' 2 | import { socketSendJson } from '../connection.js' 3 | import { ActionId } from '../../enums.js' 4 | import { ActionCommand } from './commands.js' 5 | import { OntimeV3 } from '../ontimev3.js' 6 | 7 | enum ToggleOnOff { 8 | Off = 0, 9 | On = 1, 10 | Toggle = 2, 11 | } 12 | 13 | export function createMessageActions(ontime: OntimeV3): { [id: string]: CompanionActionDefinition } { 14 | function messageVisibility(action: CompanionActionEvent): void { 15 | const value = action.options.value as ToggleOnOff 16 | const visible = value === ToggleOnOff.Toggle ? !ontime.state.message.timer.visible : value 17 | socketSendJson('message', { timer: { visible } }) 18 | } 19 | 20 | function messageVisibilityAndText(action: CompanionActionEvent): void { 21 | const value = action.options.value as ToggleOnOff 22 | const text = action.options.text as string 23 | const textIsDifferent = text !== ontime.state.message.timer.text 24 | const thisTextIsVisible = ontime.state.message.timer.visible && !textIsDifferent 25 | switch (value) { 26 | case ToggleOnOff.Off: 27 | if (thisTextIsVisible) { 28 | socketSendJson('message', { timer: { visible: false } }) 29 | } 30 | break 31 | case ToggleOnOff.On: 32 | socketSendJson('message', { timer: { visible: true, text } }) 33 | break 34 | case ToggleOnOff.Toggle: 35 | if (thisTextIsVisible) { 36 | socketSendJson('message', { timer: { visible: false, text } }) 37 | } else { 38 | socketSendJson('message', { timer: { visible: true, text } }) 39 | } 40 | break 41 | } 42 | } 43 | 44 | function timerBlackout(action: CompanionActionEvent): void { 45 | const value = action.options.value as ToggleOnOff 46 | const blackout = value === ToggleOnOff.Toggle ? !ontime.state.message.timer.blackout : value 47 | socketSendJson(ActionCommand.Message, { timer: { blackout } }) 48 | } 49 | 50 | function timerBlink(action: CompanionActionEvent): void { 51 | const value = action.options.value as ToggleOnOff 52 | const blink = value === ToggleOnOff.Toggle ? !ontime.state.message.timer.blink : value 53 | socketSendJson(ActionCommand.Message, { timer: { blink } }) 54 | } 55 | 56 | function setSecondarySource(action: CompanionActionEvent): void { 57 | const value = action.options.value as ToggleOnOff 58 | const source = action.options.source 59 | const isActive = ontime.state.message.timer.secondarySource === source 60 | const shouldShow = value === ToggleOnOff.Toggle ? !isActive : value 61 | const secondarySource = shouldShow ? source : 'off' 62 | socketSendJson(ActionCommand.Message, { timer: { secondarySource } }) 63 | } 64 | 65 | return { 66 | [ActionId.MessageVisibility]: { 67 | name: 'Toggle/On/Off visibility of message', 68 | options: [ 69 | { 70 | type: 'dropdown', 71 | choices: [ 72 | { id: ToggleOnOff.Toggle, label: 'Toggle' }, 73 | { id: ToggleOnOff.On, label: 'On' }, 74 | { id: ToggleOnOff.Off, label: 'Off' }, 75 | ], 76 | default: 2, 77 | id: 'value', 78 | label: 'Action', 79 | }, 80 | ], 81 | callback: messageVisibility, 82 | }, 83 | [ActionId.MessageText]: { 84 | name: 'Set text for message', 85 | options: [ 86 | { 87 | type: 'textinput', 88 | label: 'Timer message', 89 | id: 'value', 90 | required: true, 91 | }, 92 | ], 93 | callback: ({ options }) => 94 | socketSendJson(ActionCommand.Message, { 95 | timer: { text: options.value }, 96 | }), 97 | }, 98 | [ActionId.MessageVisibilityAndText]: { 99 | name: 'Toggle/On/Off visibility and text for message', 100 | description: 101 | 'Combined action for setting the text and visibility. "Toggle" will replace the current message. "Off" will disable the message visibility', 102 | options: [ 103 | { 104 | type: 'textinput', 105 | label: 'Timer message', 106 | id: 'text', 107 | required: true, 108 | }, 109 | { 110 | type: 'dropdown', 111 | choices: [ 112 | { id: ToggleOnOff.Toggle, label: 'Toggle' }, 113 | { id: ToggleOnOff.On, label: 'On' }, 114 | { id: ToggleOnOff.Off, label: 'Off' }, 115 | ], 116 | default: 2, 117 | id: 'value', 118 | label: 'Action', 119 | }, 120 | ], 121 | callback: messageVisibilityAndText, 122 | }, 123 | [ActionId.TimerBlackout]: { 124 | name: 'Toggle/On/Off blackout timer', 125 | options: [ 126 | { 127 | type: 'dropdown', 128 | choices: [ 129 | { id: ToggleOnOff.Toggle, label: 'Toggle' }, 130 | { id: ToggleOnOff.On, label: 'On' }, 131 | { id: ToggleOnOff.Off, label: 'Off' }, 132 | ], 133 | default: 2, 134 | id: 'value', 135 | label: 'Blackout of timer', 136 | }, 137 | ], 138 | callback: timerBlackout, 139 | }, 140 | [ActionId.TimerBlink]: { 141 | name: 'Toggle/On/Off blinking of timer', 142 | options: [ 143 | { 144 | type: 'dropdown', 145 | choices: [ 146 | { id: ToggleOnOff.Toggle, label: 'Toggle' }, 147 | { id: ToggleOnOff.On, label: 'On' }, 148 | { id: ToggleOnOff.Off, label: 'Off' }, 149 | ], 150 | default: 2, 151 | id: 'value', 152 | label: 'Blink timer', 153 | }, 154 | ], 155 | callback: timerBlink, 156 | }, 157 | [ActionId.MessageSecondarySource]: { 158 | name: 'Toggle/On/Off visibility of secondary source', 159 | options: [ 160 | { 161 | type: 'dropdown', 162 | choices: [ 163 | { id: 'external', label: 'External' }, 164 | { id: 'aux', label: 'Aux timer' }, 165 | ], 166 | default: 'external', 167 | id: 'source', 168 | label: 'Source', 169 | }, 170 | { 171 | type: 'dropdown', 172 | choices: [ 173 | { id: ToggleOnOff.Toggle, label: 'Toggle' }, 174 | { id: ToggleOnOff.On, label: 'On' }, 175 | { id: ToggleOnOff.Off, label: 'Off' }, 176 | ], 177 | default: 2, 178 | id: 'value', 179 | label: 'Action', 180 | }, 181 | ], 182 | callback: setSecondarySource, 183 | }, 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/v3/actions/playback.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionActionDefinition, CompanionActionEvent } from '@companion-module/base' 2 | import { socketSendJson } from '../connection.js' 3 | import { ActionId } from '../../enums.js' 4 | import { ActionCommand } from './commands.js' 5 | import { eventPicker } from './eventPicker.js' 6 | import { OntimeV3 } from '../ontimev3.js' 7 | import { Playback } from '../ontime-types.js' 8 | 9 | export function createPlaybackActions(ontime: OntimeV3): { [id: string]: CompanionActionDefinition } { 10 | function start(action: CompanionActionEvent): void { 11 | const { method, eventList, eventCue, eventId, eventIndex } = action.options 12 | switch (method) { 13 | case 'loaded': { 14 | socketSendJson(ActionCommand.Start) 15 | break 16 | } 17 | case 'next': { 18 | socketSendJson(ActionCommand.Start, 'next') 19 | break 20 | } 21 | case 'go': { 22 | if (ontime.state.timer.playback === Playback.Armed || ontime.state.timer.playback === Playback.Pause) { 23 | socketSendJson(ActionCommand.Start) 24 | } else { 25 | socketSendJson(ActionCommand.Start, 'next') 26 | } 27 | break 28 | } 29 | case 'previous': { 30 | socketSendJson(ActionCommand.Start, 'previous') 31 | break 32 | } 33 | case 'list': { 34 | socketSendJson(ActionCommand.Start, { id: eventList }) 35 | break 36 | } 37 | case 'cue': { 38 | socketSendJson(ActionCommand.Start, { cue: eventCue }) 39 | break 40 | } 41 | case 'id': { 42 | socketSendJson(ActionCommand.Start, { id: eventId }) 43 | break 44 | } 45 | case 'index': { 46 | socketSendJson(ActionCommand.Start, { index: eventIndex }) 47 | break 48 | } 49 | } 50 | } 51 | 52 | function load(action: CompanionActionEvent): void { 53 | const { method, eventList, eventCue, eventId, eventIndex } = action.options 54 | switch (method) { 55 | case 'loaded': { 56 | socketSendJson(ActionCommand.Reload) 57 | break 58 | } 59 | case 'next': { 60 | socketSendJson(ActionCommand.Load, 'next') 61 | break 62 | } 63 | case 'previous': { 64 | socketSendJson(ActionCommand.Load, 'previous') 65 | break 66 | } 67 | case 'list': { 68 | socketSendJson(ActionCommand.Load, { id: eventList }) 69 | break 70 | } 71 | case 'cue': { 72 | socketSendJson(ActionCommand.Load, { cue: eventCue }) 73 | break 74 | } 75 | case 'id': { 76 | socketSendJson(ActionCommand.Load, { id: eventId }) 77 | break 78 | } 79 | case 'index': { 80 | socketSendJson(ActionCommand.Load, { index: eventIndex }) 81 | break 82 | } 83 | } 84 | } 85 | 86 | function addTime(action: CompanionActionEvent): void { 87 | const { hours, minutes, seconds, addremove } = action.options 88 | const val = ((Number(hours) * 60 + Number(minutes)) * 60 + Number(seconds)) * (addremove == 'remove' ? -1 : 1) 89 | socketSendJson(ActionCommand.Add, val) 90 | } 91 | 92 | return { 93 | [ActionId.Start]: { 94 | name: 'Start an event', 95 | options: [...eventPicker(ontime.events, ['list', 'next', 'previous', 'loaded', 'cue', 'id', 'index', 'go'])], 96 | callback: start, 97 | }, 98 | [ActionId.Load]: { 99 | name: 'Load an event', 100 | options: [...eventPicker(ontime.events)], 101 | callback: load, 102 | }, 103 | 104 | [ActionId.Pause]: { 105 | name: 'Pause running timer', 106 | options: [], 107 | callback: () => socketSendJson(ActionCommand.Pause), 108 | }, 109 | [ActionId.Stop]: { 110 | name: 'Stop running timer', 111 | options: [], 112 | callback: () => socketSendJson(ActionCommand.Stop), 113 | }, 114 | [ActionId.Reload]: { 115 | name: 'Reload selected event', 116 | options: [], 117 | callback: () => socketSendJson(ActionCommand.Reload), 118 | }, 119 | 120 | [ActionId.Roll]: { 121 | name: 'Start roll mode', 122 | options: [], 123 | callback: () => socketSendJson(ActionCommand.Roll), 124 | }, 125 | [ActionId.Add]: { 126 | name: 'Add / remove time to running timer', 127 | options: [ 128 | { 129 | id: 'addremove', 130 | type: 'dropdown', 131 | choices: [ 132 | { id: 'add', label: 'Add Time' }, 133 | { id: 'remove', label: 'Remove Time' }, 134 | ], 135 | label: 'Add or Remove', 136 | default: 'add', 137 | }, 138 | { 139 | type: 'number', 140 | id: 'hours', 141 | label: 'Hours', 142 | default: 0, 143 | step: 1, 144 | min: 0, 145 | max: 24, 146 | required: true, 147 | }, 148 | { 149 | type: 'number', 150 | id: 'minutes', 151 | label: 'Minutes', 152 | default: 1, 153 | step: 1, 154 | min: 0, 155 | max: 1440, 156 | required: true, 157 | }, 158 | { 159 | type: 'number', 160 | id: 'seconds', 161 | label: 'Seconds', 162 | default: 0, 163 | min: 0, 164 | max: 86400, 165 | step: 1, 166 | required: true, 167 | }, 168 | ], 169 | callback: addTime, 170 | }, 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/v3/connection.ts: -------------------------------------------------------------------------------- 1 | import { type InputValue, InstanceStatus } from '@companion-module/base' 2 | import { OnTimeInstance } from '../index.js' 3 | import Websocket from 'ws' 4 | import { findPreviousPlayableEvent, msToSplitTime, makeURL, variablesFromCustomFields } from '../utilities.js' 5 | import { feedbackId, variableId } from '../enums.js' 6 | import type { 7 | CurrentBlockState, 8 | MessageState, 9 | OntimeBaseEvent, 10 | OntimeEvent, 11 | Runtime, 12 | SimpleTimerState, 13 | TimerState, 14 | CustomFields, 15 | } from './ontime-types.js' 16 | import { SupportedEvent } from './ontime-types.js' 17 | import { OntimeV3 } from './ontimev3.js' 18 | 19 | let ws: Websocket | null = null 20 | let reconnectionTimeout: NodeJS.Timeout | null = null 21 | let versionTimeout: NodeJS.Timeout | null = null 22 | let reconnectInterval: number 23 | let shouldReconnect = false 24 | 25 | export function connect(self: OnTimeInstance, ontime: OntimeV3): void { 26 | reconnectInterval = self.config.reconnectInterval * 1000 27 | shouldReconnect = self.config.reconnect 28 | 29 | const wsUrls = makeURL(self.config.host, 'ws', self.config.ssl, true) 30 | 31 | if (!wsUrls) { 32 | self.updateStatus(InstanceStatus.BadConfig, `host format error`) 33 | return 34 | } 35 | 36 | self.updateStatus(InstanceStatus.Connecting, 'Trying WS connection') 37 | 38 | if (ws) { 39 | ws.close() 40 | } 41 | 42 | ws = new Websocket(wsUrls) 43 | 44 | self.log('info', `connection to server with: ${wsUrls}`) 45 | 46 | ws.onopen = () => { 47 | clearTimeout(reconnectionTimeout as NodeJS.Timeout) 48 | clearTimeout(versionTimeout as NodeJS.Timeout) 49 | self.updateStatus(InstanceStatus.Connecting) 50 | socketSendJson('version') 51 | versionTimeout = setTimeout(() => { 52 | self.updateStatus(InstanceStatus.ConnectionFailure, 'Unsupported version: see log') 53 | self.log( 54 | 'error', 55 | 'The version request timed out, this is most likely do to an old ontime version. You can download the latest version of Ontime through the website https://www.getontime.no/', 56 | ) 57 | ws?.close() 58 | }, 500) 59 | } 60 | 61 | ws.onclose = (event) => { 62 | self.log('debug', `Connection closed with code ${event.code}`) 63 | if (shouldReconnect) { 64 | reconnectionTimeout = setTimeout(() => { 65 | if (ws && ws.readyState === Websocket.CLOSED) { 66 | void connect(self, ontime) 67 | } 68 | }, reconnectInterval) 69 | } 70 | } 71 | 72 | ws.onerror = (event) => { 73 | self.log('debug', `WebSocket error: ${event.message}`) 74 | self.updateStatus(InstanceStatus.ConnectionFailure, `WebSocket error: ${event.message}`) 75 | } 76 | 77 | const updateClock = (val: number) => { 78 | ontime.state.clock = val 79 | const clock = msToSplitTime(val) 80 | self.setVariableValues({ [variableId.Clock]: clock.hoursMinutesSeconds }) 81 | } 82 | 83 | const updateTimer = (val: TimerState) => { 84 | ontime.state.timer = val 85 | const timer = msToSplitTime(val.current) 86 | const timer_start = msToSplitTime(val.startedAt) 87 | const timer_finish = msToSplitTime(val.expectedFinish) 88 | const added = msToSplitTime(val.addedTime) 89 | 90 | self.setVariableValues({ 91 | [variableId.TimerTotalMs]: val.current ?? 0, 92 | [variableId.TimeN]: timer.negative, 93 | [variableId.Time]: timer.hoursMinutesSeconds, 94 | [variableId.TimeHM]: timer.hoursMinutes, 95 | [variableId.TimeH]: timer.hours, 96 | [variableId.TimeM]: timer.minutes, 97 | [variableId.TimeS]: timer.seconds, 98 | [variableId.TimerPhase]: val.phase, 99 | [variableId.TimerStart]: timer_start.hoursMinutesSeconds, 100 | [variableId.TimerFinish]: timer_finish.hoursMinutesSeconds, 101 | [variableId.TimerAdded]: added.hoursMinutesSeconds, 102 | [variableId.TimerAddedNice]: added.delayString, 103 | [variableId.PlayState]: val.playback, 104 | }) 105 | 106 | self.checkFeedbacks( 107 | feedbackId.ColorPlayback, 108 | feedbackId.ColorAddRemove, 109 | feedbackId.TimerPhase, 110 | feedbackId.TimerProgressBar, 111 | feedbackId.TimerProgressBarMulti, 112 | ) 113 | } 114 | 115 | const updateMessage = (val: MessageState) => { 116 | ontime.state.message = val 117 | self.setVariableValues({ 118 | [variableId.TimerMessage]: val.timer.text, 119 | [variableId.ExternalMessage]: val.external, 120 | [variableId.TimerMessageVisible]: val.timer.visible, 121 | [variableId.TimerBlackout]: val.timer.blackout, 122 | [variableId.TimerBlink]: val.timer.blink, 123 | [variableId.TimerSecondarySource]: val.timer.secondarySource as string, 124 | }) 125 | 126 | self.checkFeedbacks( 127 | feedbackId.MessageVisible, 128 | feedbackId.TimerBlackout, 129 | feedbackId.TimerBlink, 130 | feedbackId.MessageSecondarySourceVisible, 131 | ) 132 | } 133 | 134 | const updateRuntime = (val: Runtime) => { 135 | ontime.state.runtime = val 136 | const offset = msToSplitTime(ontime.state.runtime.offset) 137 | const plannedStart = msToSplitTime(val.plannedStart) 138 | const actualStart = msToSplitTime(val.actualStart) 139 | const plannedEnd = msToSplitTime(val.plannedEnd) 140 | const expectedEnd = msToSplitTime(val.expectedEnd) 141 | const selectedEventIndex = val.selectedEventIndex === null ? undefined : val.selectedEventIndex + 1 142 | self.setVariableValues({ 143 | [variableId.NumberOfEvents]: val.numEvents, 144 | [variableId.SelectedEventIndex]: selectedEventIndex, 145 | [variableId.RundownOffset]: offset.hoursMinutesSeconds, 146 | [variableId.PlannedStart]: plannedStart.hoursMinutesSeconds, 147 | [variableId.ActualStart]: actualStart.hoursMinutesSeconds, 148 | [variableId.PlannedEnd]: plannedEnd.hoursMinutesSeconds, 149 | [variableId.ExpectedEnd]: expectedEnd.hoursMinutesSeconds, 150 | }) 151 | self.checkFeedbacks(feedbackId.RundownOffset) 152 | } 153 | 154 | const updateEventNow = (val: OntimeEvent | null) => { 155 | ontime.state.eventNow = val 156 | self.setVariableValues({ 157 | [variableId.TitleNow]: val?.title, 158 | [variableId.NoteNow]: val?.note, 159 | [variableId.CueNow]: val?.cue, 160 | [variableId.IdNow]: val?.id, 161 | }) 162 | if (self.config.customToVariable) { 163 | self.setVariableValues(variablesFromCustomFields(ontime, 'Now', val?.custom)) 164 | self.checkFeedbacks(feedbackId.CustomFieldsValue) 165 | } 166 | } 167 | 168 | const updateEventPrevious = (val: OntimeEvent | null) => { 169 | self.setVariableValues({ 170 | [variableId.TitlePrevious]: val?.title ?? '', 171 | [variableId.NotePrevious]: val?.note ?? '', 172 | [variableId.CuePrevious]: val?.cue ?? '', 173 | [variableId.IdPrevious]: val?.id ?? '', 174 | }) 175 | if (self.config.customToVariable) { 176 | self.setVariableValues(variablesFromCustomFields(ontime, 'Previous', val?.custom)) 177 | } 178 | } 179 | 180 | const updateEventNext = (val: OntimeEvent | null) => { 181 | ontime.state.eventNext = val 182 | self.setVariableValues({ 183 | [variableId.TitleNext]: val?.title ?? '', 184 | [variableId.NoteNext]: val?.note ?? '', 185 | [variableId.CueNext]: val?.cue ?? '', 186 | [variableId.IdNext]: val?.id ?? '', 187 | }) 188 | if (self.config.customToVariable) { 189 | self.setVariableValues(variablesFromCustomFields(ontime, 'Next', val?.custom)) 190 | self.checkFeedbacks(feedbackId.CustomFieldsValue) 191 | } 192 | } 193 | 194 | const updateCurrentBlock = (val: CurrentBlockState) => { 195 | ontime.state.currentBlock = val 196 | const startedAt = msToSplitTime(val.startedAt) 197 | self.setVariableValues({ 198 | [variableId.CurrentBlockStartedAt]: startedAt.hoursMinutesSeconds, 199 | [variableId.CurrentBlockStartedAtMs]: val.startedAt ?? 0, 200 | [variableId.CurrentBlockTitle]: val.block?.title ?? '', 201 | }) 202 | } 203 | 204 | const updateAuxTimer1 = (val: SimpleTimerState) => { 205 | ontime.state.auxtimer1 = val 206 | const duration = msToSplitTime(val.duration) 207 | const current = msToSplitTime(val.current) 208 | 209 | self.setVariableValues({ 210 | [variableId.AuxTimerDurationMs + '-1']: val.duration, 211 | [variableId.AuxTimerCurrentMs + '-1']: val.current, 212 | [variableId.AuxTimerDirection + '-1']: duration.hoursMinutesSeconds, 213 | [variableId.AuxTimerCurrent + '-1']: current.hoursMinutesSeconds, 214 | [variableId.AuxTimerPlayback + '-1']: val.playback, 215 | [variableId.AuxTimerDirection + '-1']: val.direction, 216 | }) 217 | self.checkFeedbacks(feedbackId.AuxTimerNegative, feedbackId.AuxTimerPlayback) 218 | } 219 | 220 | // eslint-disable-next-line @typescript-eslint/no-misused-promises -- TODO: not sure how to fix this 221 | ws.onmessage = async (event: any) => { 222 | try { 223 | const data = JSON.parse(event.data) 224 | const { type, payload } = data 225 | 226 | if (!type) { 227 | return 228 | } 229 | //https://docs.getontime.no/api/runtime-data/ 230 | switch (type) { 231 | case 'ontime-clock': { 232 | updateClock(payload) 233 | break 234 | } 235 | case 'ontime-timer': { 236 | updateTimer(payload) 237 | break 238 | } 239 | case 'ontime-message': { 240 | updateMessage(payload) 241 | break 242 | } 243 | case 'ontime-runtime': { 244 | updateRuntime(payload) 245 | break 246 | } 247 | 248 | case 'ontime-eventNow': { 249 | updateEventNow(payload) 250 | const prev = findPreviousPlayableEvent(ontime) 251 | updateEventPrevious(prev) 252 | break 253 | } 254 | case 'ontime-eventNext': { 255 | updateEventNext(payload) 256 | break 257 | } 258 | case 'ontime-auxtimer1': { 259 | updateAuxTimer1(payload) 260 | break 261 | } 262 | case 'ontime-currentBlock': { 263 | updateCurrentBlock(payload) 264 | break 265 | } 266 | case 'ontime': { 267 | updateTimer(payload.timer) 268 | updateClock(payload.clock) 269 | updateMessage(payload.message) 270 | updateEventNow(payload.eventNow) 271 | updateEventNext(payload.eventNext) 272 | 273 | // currentBlock dons't exist in ontime prior to v3.5.0 274 | if ('currentBlock' in payload) { 275 | updateCurrentBlock(payload.currentBlock) 276 | } 277 | break 278 | } 279 | case 'version': { 280 | clearTimeout(versionTimeout as NodeJS.Timeout) 281 | const version = payload.split('.') 282 | self.log('info', `Ontime version "${payload}"`) 283 | self.log('debug', version) 284 | if (version.at(0) === '3') { 285 | if (Number(version.at(1)) < 6) { 286 | self.updateStatus( 287 | InstanceStatus.BadConfig, 288 | 'Ontime version is too old (required >3.6.0) some features are not available', 289 | ) 290 | } else { 291 | self.updateStatus(InstanceStatus.Ok, payload) 292 | } 293 | await fetchCustomFields(self, ontime) 294 | await fetchAllEvents(self, ontime) 295 | self.init_actions() 296 | self.init_feedbacks() 297 | const prev = findPreviousPlayableEvent(ontime) 298 | updateEventPrevious(prev) 299 | if (self.config.customToVariable) { 300 | self.setVariableDefinitions(ontime.getVariables(true)) 301 | } 302 | } else { 303 | self.updateStatus(InstanceStatus.ConnectionFailure, 'Unsupported version: see log') 304 | self.log( 305 | 'error', 306 | `Unsupported version "${payload}" You can download the latest version of Ontime through the website https://www.getontime.no/`, 307 | ) 308 | ws?.close() 309 | } 310 | break 311 | } 312 | case 'ontime-refetch': { 313 | if (self.config.refetchEvents) { 314 | await fetchAllEvents(self, ontime) 315 | const prev = findPreviousPlayableEvent(ontime) 316 | updateEventPrevious(prev) 317 | self.init_actions() 318 | } 319 | const change = await fetchCustomFields(self, ontime) 320 | if (change && self.config.customToVariable) { 321 | self.setVariableDefinitions(ontime.getVariables(true)) 322 | self.init_feedbacks() 323 | } 324 | break 325 | } 326 | } 327 | } catch (_) { 328 | // ignore unhandled 329 | } 330 | } 331 | } 332 | 333 | export function disconnectSocket(): void { 334 | shouldReconnect = false 335 | if (reconnectionTimeout) { 336 | clearTimeout(reconnectionTimeout) 337 | } 338 | ws?.close() 339 | } 340 | 341 | export function socketSendJson(type: string, payload?: InputValue | object): void { 342 | if (ws && ws.readyState === ws.OPEN) { 343 | ws.send( 344 | JSON.stringify({ 345 | type, 346 | payload, 347 | }), 348 | ) 349 | } 350 | } 351 | 352 | let rundownEtag: string = '' 353 | 354 | async function fetchAllEvents(self: OnTimeInstance, ontime: OntimeV3): Promise { 355 | const serverHttp = makeURL(self.config.host, 'data/rundown', self.config.ssl) 356 | if (!serverHttp) { 357 | return 358 | } 359 | self.log('debug', 'fetching events from ontime') 360 | try { 361 | const response = await fetch(serverHttp.href, { 362 | method: 'GET', 363 | headers: { 'if-none-match': rundownEtag, 'cache-control': '3600', pragma: '' }, 364 | }) 365 | if (response.status === 304) { 366 | self.log('debug', '304 -> nothing change in rundown') 367 | return 368 | } 369 | if (!response.ok) { 370 | ontime.events = [] 371 | self.log('error', `uable to fetch events: ${response.statusText}`) 372 | return 373 | } 374 | rundownEtag = response.headers.get('Etag') ?? '' 375 | const data = (await response.json()) as OntimeBaseEvent[] 376 | ontime.events = data.filter((entry) => entry.type === SupportedEvent.Event) as OntimeEvent[] 377 | self.log('debug', `fetched ${ontime.events.length} events`) 378 | } catch (e: any) { 379 | ontime.events = [] 380 | self.log('error', 'failed to fetch events from ontime') 381 | self.log('error', e.message) 382 | } 383 | } 384 | 385 | let customFieldsEtag: string = '' 386 | 387 | //TODO: this might need to be updated on an interval 388 | async function fetchCustomFields(self: OnTimeInstance, ontime: OntimeV3): Promise { 389 | const serverHttp = makeURL(self.config.host, 'data/custom-fields', self.config.ssl) 390 | if (!serverHttp) { 391 | return false 392 | } 393 | self.log('debug', 'fetching custom-fields from ontime') 394 | try { 395 | const response = await fetch(serverHttp, { 396 | method: 'GET', 397 | headers: { 'if-none-match': customFieldsEtag, 'cache-control': '3600', pragma: '' }, 398 | }) 399 | if (response.status === 304) { 400 | self.log('debug', '304 -> nothing change custom fields') 401 | return false 402 | } 403 | if (!response.ok) { 404 | ontime.events = [] 405 | self.log('error', `uable to fetch events: ${response.statusText}`) 406 | return false 407 | } 408 | customFieldsEtag = response.headers.get('Etag') ?? '' 409 | const data = (await response.json()) as CustomFields 410 | ontime.customFields = data 411 | return true 412 | } catch (e: any) { 413 | ontime.events = [] 414 | self.log('error', `unable to fetch custom fields: ${e}`) 415 | return false 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/v3/feedbacks/auxTimer.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { feedbackId } from '../../enums.js' 4 | import { SimplePlayback } from '../ontime-types.js' 5 | import { getAuxTimerState } from '../../utilities.js' 6 | import { DangerRed, PlaybackGreen, White } from '../../assets/colours.js' 7 | 8 | export function createAuxTimerFeedbacks(ontime: OntimeV3): { [id: string]: CompanionFeedbackDefinition } { 9 | return { 10 | [feedbackId.AuxTimerPlayback]: { 11 | type: 'boolean', 12 | name: 'Aux Timer Playback state', 13 | description: 'Indicator colour for playback state', 14 | defaultStyle: { 15 | color: White, 16 | bgcolor: PlaybackGreen, 17 | }, 18 | options: [ 19 | { 20 | type: 'dropdown', 21 | choices: [{ id: 'auxtimer1', label: 'Aux Timer 1' }], 22 | default: 'auxtimer1', 23 | id: 'destination', 24 | label: 'Select Aux Timer', 25 | isVisible: () => false, //This Stays hidden for now 26 | }, 27 | { 28 | type: 'dropdown', 29 | label: 'State', 30 | id: 'state', 31 | choices: [ 32 | { id: SimplePlayback.Start, label: 'Play' }, 33 | { id: SimplePlayback.Pause, label: 'Pause' }, 34 | { id: SimplePlayback.Stop, label: 'Stop' }, 35 | ], 36 | default: SimplePlayback.Start, 37 | }, 38 | ], 39 | callback: (feedback) => 40 | getAuxTimerState(ontime, feedback.options.id as string).playback === feedback.options.state, 41 | }, 42 | [feedbackId.AuxTimerNegative]: { 43 | type: 'boolean', 44 | name: 'Aux Timer negative', 45 | description: 'Indicator colour for Aux Timer negative', 46 | defaultStyle: { 47 | color: White, 48 | bgcolor: DangerRed, 49 | }, 50 | options: [ 51 | { 52 | type: 'dropdown', 53 | choices: [{ id: 'auxtimer1', label: 'Aux Timer 1' }], 54 | default: 'auxtimer1', 55 | id: 'destination', 56 | label: 'Select Aux Timer', 57 | isVisible: () => false, //This Stays hidden for now 58 | }, 59 | ], 60 | callback: (feedback) => getAuxTimerState(ontime, feedback.options.id as string).current < 0, 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/v3/feedbacks/customFields.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { feedbackId } from '../../enums.js' 4 | import { ActiveBlue, White } from '../../assets/colours.js' 5 | 6 | export function createCustomFieldsFeedbacks(ontime: OntimeV3): { [id: string]: CompanionFeedbackDefinition } { 7 | const { customFields } = ontime 8 | return { 9 | [feedbackId.CustomFieldsValue]: { 10 | type: 'boolean', 11 | name: 'Custom Field', 12 | description: 'Colour of indicator for rundown offset state', 13 | defaultStyle: { 14 | color: White, 15 | bgcolor: ActiveBlue, 16 | }, 17 | options: [ 18 | { 19 | type: 'dropdown', 20 | id: 'target', 21 | label: 'Target', 22 | choices: [ 23 | { id: 'now', label: 'Current Event' }, 24 | { id: 'next', label: 'Next Event' }, 25 | ], 26 | default: 'now', 27 | }, 28 | { 29 | type: 'dropdown', 30 | id: 'field', 31 | label: 'Custom Field', 32 | choices: Object.entries(customFields).map(([key, field]) => { 33 | return { id: key, label: field.label } 34 | }), 35 | default: '', 36 | }, 37 | { 38 | type: 'checkbox', 39 | id: 'requireValue', 40 | label: 'Match specific value', 41 | default: false, 42 | }, 43 | { 44 | type: 'textinput', 45 | id: 'match', 46 | label: 'Value to match', 47 | isVisible: (opts) => opts.requireValue === true, 48 | }, 49 | ], 50 | learn: (action) => { 51 | const target = action.options.target as string 52 | const field = action.options.field as string 53 | const fieldValue = 54 | target === 'now' ? ontime.state.eventNow?.custom[field] : ontime.state.eventNext?.custom[field] 55 | return { ...action.options, requireValue: true, match: fieldValue ?? '' } 56 | }, 57 | callback: (feedback) => { 58 | const target = feedback.options.target as string 59 | const field = feedback.options.field as string 60 | const requireValue = feedback.options.requireValue as string 61 | const match = feedback.options.match as string 62 | const fieldValue = 63 | target === 'now' ? ontime.state.eventNow?.custom[field] : ontime.state.eventNext?.custom[field] 64 | 65 | if (fieldValue === undefined || fieldValue === '') { 66 | return false 67 | } 68 | 69 | if (!requireValue) { 70 | return true 71 | } 72 | 73 | if (match === fieldValue) { 74 | return true 75 | } 76 | return false 77 | }, 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/v3/feedbacks/index.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackDefinition, CompanionFeedbackDefinitions } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { createPlaybackFeedbacks } from './playback.js' 4 | import { createMessageFeedbacks } from './message.js' 5 | import { createTimerPhaseFeedback } from './timerPhase.js' 6 | import { createOffsetFeedbacks } from './offset.js' 7 | import { createAuxTimerFeedbacks } from './auxTimer.js' 8 | import { createProgressFeedbacks } from './progress.js' 9 | import { createCustomFieldsFeedbacks } from './customFields.js' 10 | 11 | export function feedbacks(ontime: OntimeV3): CompanionFeedbackDefinitions { 12 | const feedbacks: { [id: string]: CompanionFeedbackDefinition | undefined } = { 13 | ...createPlaybackFeedbacks(ontime), 14 | ...createMessageFeedbacks(ontime), 15 | ...createTimerPhaseFeedback(ontime), 16 | ...createProgressFeedbacks(ontime), 17 | ...createOffsetFeedbacks(ontime), 18 | ...createAuxTimerFeedbacks(ontime), 19 | ...createCustomFieldsFeedbacks(ontime), 20 | } 21 | 22 | return feedbacks 23 | } 24 | -------------------------------------------------------------------------------- /src/v3/feedbacks/message.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackBooleanEvent, CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { feedbackId } from '../../enums.js' 4 | import { ActiveBlue, White } from '../../assets/colours.js' 5 | 6 | export function createMessageFeedbacks(ontime: OntimeV3): { [id: string]: CompanionFeedbackDefinition } { 7 | function messageVisible(feedback: CompanionFeedbackBooleanEvent): boolean { 8 | const { text, visible } = ontime.state.message.timer as { text: string; visible: boolean } 9 | return feedback.options.reqText ? visible && text === feedback.options.text : visible 10 | } 11 | 12 | function secondaryVisible(feedback: CompanionFeedbackBooleanEvent): boolean { 13 | const secondarySource = ontime.state.message.timer.secondarySource as string 14 | 15 | return ( 16 | (feedback.options.source === 'any' && secondarySource !== null) || secondarySource === feedback.options.source 17 | ) 18 | } 19 | 20 | return { 21 | [feedbackId.MessageVisible]: { 22 | type: 'boolean', 23 | name: 'Message visibility', 24 | description: 'Change the colors if message is visible', 25 | defaultStyle: { 26 | color: White, 27 | bgcolor: ActiveBlue, 28 | }, 29 | options: [ 30 | { type: 'checkbox', id: 'reqText', default: false, label: 'Require matching text' }, 31 | { type: 'textinput', id: 'text', label: 'Text', isVisible: (options) => options.reqText == true }, 32 | ], 33 | callback: messageVisible, 34 | }, 35 | [feedbackId.MessageSecondarySourceVisible]: { 36 | type: 'boolean', 37 | name: 'Message secondary source visibility', 38 | description: 'Change the colors if secondary source is visible', 39 | defaultStyle: { 40 | color: White, 41 | bgcolor: ActiveBlue, 42 | }, 43 | options: [ 44 | { 45 | type: 'dropdown', 46 | id: 'source', 47 | label: 'Source', 48 | default: 'external', 49 | choices: [ 50 | { id: 'external', label: 'External' }, 51 | { id: 'aux', label: 'Aux timer' }, 52 | { id: 'any', label: 'Any' }, 53 | ], 54 | }, 55 | ], 56 | callback: secondaryVisible, 57 | }, 58 | [feedbackId.TimerBlink]: { 59 | type: 'boolean', 60 | name: 'Timer is blinking', 61 | description: 'Change the colors of a button if timer is blinking', 62 | defaultStyle: { 63 | color: White, 64 | bgcolor: ActiveBlue, 65 | }, 66 | options: [], 67 | callback: () => ontime.state.message.timer.blink, 68 | }, 69 | [feedbackId.TimerBlackout]: { 70 | type: 'boolean', 71 | name: 'Timer is blacked out', 72 | description: 'Change the colors of a button if timer is blacked out', 73 | defaultStyle: { 74 | bgcolor: ActiveBlue, 75 | }, 76 | options: [], 77 | callback: () => ontime.state.message.timer.blackout, 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/v3/feedbacks/offset.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackBooleanEvent, CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { feedbackId, OffsetState } from '../../enums.js' 4 | import { DangerRed, White } from '../../assets/colours.js' 5 | 6 | export function createOffsetFeedbacks(ontime: OntimeV3): { [id: string]: CompanionFeedbackDefinition } { 7 | function offset(feedback: CompanionFeedbackBooleanEvent): boolean { 8 | const state = feedback.options.state as OffsetState | undefined 9 | if (!state) return false 10 | if (ontime.state.runtime.offset === null || ontime.state.runtime.offset === undefined) return false 11 | const margin = Number(feedback.options.margin) 12 | const offset = ontime.state.runtime.offset / 1000 13 | switch (state) { 14 | case OffsetState.On: 15 | return offset > -margin && offset < margin 16 | case OffsetState.Both: 17 | return offset < -margin || offset > margin 18 | case OffsetState.Behind: 19 | return offset < -margin 20 | case OffsetState.Ahead: 21 | return offset > margin 22 | } 23 | 24 | return false 25 | } 26 | 27 | return { 28 | [feedbackId.RundownOffset]: { 29 | type: 'boolean', 30 | name: 'Rundown Offset', 31 | description: 'Colour of indicator for rundown offset state', 32 | defaultStyle: { 33 | color: White, 34 | bgcolor: DangerRed, 35 | }, 36 | options: [ 37 | { 38 | type: 'dropdown', 39 | label: 'State', 40 | id: 'state', 41 | choices: [ 42 | { id: OffsetState.On, label: 'On time' }, 43 | { id: OffsetState.Behind, label: 'Behind schedule' }, 44 | { id: OffsetState.Ahead, label: 'Ahead of schedule' }, 45 | { id: OffsetState.Both, label: 'Behind or Ahead of schedule' }, 46 | ], 47 | default: 'behind', 48 | }, 49 | { 50 | type: 'number', 51 | label: 'Margin', 52 | id: 'margin', 53 | default: 10, 54 | min: 0, 55 | max: 120, 56 | tooltip: 'How many seconds in offset time to allow before triggering', 57 | }, 58 | ], 59 | callback: offset, 60 | }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/v3/feedbacks/playback.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackBooleanEvent, CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { feedbackId } from '../../enums.js' 4 | import { Playback } from '../ontime-types.js' 5 | import { PauseOrange, PlaybackGreen, White } from '../../assets/colours.js' 6 | 7 | export function createPlaybackFeedbacks(ontime: OntimeV3): { [id: string]: CompanionFeedbackDefinition } { 8 | function addTime(feedback: CompanionFeedbackBooleanEvent): boolean { 9 | const { direction } = feedback.options 10 | 11 | if (direction === 'add') { 12 | return ontime.state.timer.addedTime > 0 13 | } 14 | if (direction === 'remove') { 15 | return ontime.state.timer.addedTime < 0 16 | } 17 | if (direction === 'both') { 18 | return ontime.state.timer.addedTime != 0 19 | } 20 | if (direction === 'none') { 21 | return ontime.state.timer.addedTime == 0 22 | } 23 | return false 24 | } 25 | 26 | return { 27 | [feedbackId.ColorPlayback]: { 28 | type: 'boolean', 29 | name: 'Playback state', 30 | description: 'Indicator colour for playback state', 31 | defaultStyle: { 32 | color: White, 33 | bgcolor: PlaybackGreen, 34 | }, 35 | options: [ 36 | { 37 | type: 'dropdown', 38 | label: 'State', 39 | id: 'state', 40 | choices: [ 41 | { id: Playback.Play, label: 'Play' }, 42 | { id: Playback.Pause, label: 'Pause' }, 43 | { id: Playback.Stop, label: 'Stop' }, 44 | { id: Playback.Armed, label: 'Armed' }, 45 | { id: Playback.Roll, label: 'Roll' }, 46 | ], 47 | default: Playback.Play, 48 | }, 49 | ], 50 | callback: (feedback) => ontime.state.timer.playback === feedback.options.state, 51 | }, 52 | [feedbackId.ColorAddRemove]: { 53 | type: 'boolean', 54 | name: 'Added/removed time', 55 | description: 'Indicator colour for whether timer has user added time', 56 | defaultStyle: { 57 | color: White, 58 | bgcolor: PauseOrange, 59 | }, 60 | options: [ 61 | { 62 | type: 'dropdown', 63 | label: 'Direction', 64 | id: 'direction', 65 | choices: [ 66 | { id: 'add', label: 'Only Added' }, 67 | { id: 'remove', label: 'Only Removed' }, 68 | { id: 'none', label: 'No change' }, 69 | ], 70 | default: 'both', 71 | }, 72 | ], 73 | callback: addTime, 74 | }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/v3/feedbacks/progress.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackAdvancedEvent, CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { OntimeV3 } from '../ontimev3.js' 3 | import { feedbackId } from '../../enums.js' 4 | import { graphics } from 'companion-module-utils' 5 | import { TimerPhase } from '../ontime-types.js' 6 | import { DangerRed, NormalGray, WarningOrange } from '../../assets/colours.js' 7 | 8 | export function createProgressFeedbacks(ontime: OntimeV3): { [id: string]: CompanionFeedbackDefinition } { 9 | function progressbar(feedback: CompanionFeedbackAdvancedEvent) { 10 | if (!feedback.image) { 11 | return {} 12 | } 13 | 14 | const { current, duration } = ontime.state.timer 15 | const { phase } = ontime.state.timer 16 | const { normal, warning, danger } = feedback.options as { [key: string]: number } 17 | 18 | const amount = (feedback.options.amount as number) ?? 1 19 | const index = ((feedback.options.index as number) ?? 1) - 1 20 | const big = feedback.options.index as boolean 21 | 22 | const val = current !== null && duration !== null ? (1 - current / duration) * 100 : 0 23 | 24 | const scaledVal = val * amount - 100 * index 25 | 26 | let colour = 0 27 | 28 | switch (phase) { 29 | case TimerPhase.None: 30 | case TimerPhase.Default: 31 | case TimerPhase.Pending: 32 | colour = normal 33 | break 34 | case TimerPhase.Warning: 35 | colour = warning 36 | break 37 | case TimerPhase.Danger: 38 | case TimerPhase.Overtime: 39 | colour = danger 40 | break 41 | } 42 | 43 | const options: graphics.OptionsBar = { 44 | width: feedback.image.width, 45 | height: feedback.image.height, 46 | colors: [ 47 | { 48 | size: 100, 49 | color: colour, 50 | background: colour, 51 | backgroundOpacity: 150, 52 | }, 53 | ], 54 | barLength: feedback.image.width, 55 | barWidth: big ? feedback.image.height - 40 : 10, 56 | value: scaledVal, 57 | type: 'horizontal', 58 | offsetX: 0, 59 | offsetY: big ? 20 : feedback.image.height - 10, 60 | opacity: 255, 61 | } 62 | 63 | return { 64 | imageBuffer: graphics.bar(options), 65 | } 66 | } 67 | return { 68 | [feedbackId.TimerProgressBar]: { 69 | type: 'advanced', 70 | name: 'Progressbar', 71 | description: 'Progressbar indicating the main timer progression', 72 | options: [ 73 | { type: 'checkbox', id: 'big', label: 'Big graphic', default: false }, 74 | { type: 'colorpicker', id: 'normal', label: 'Normal', default: NormalGray }, 75 | { type: 'colorpicker', id: 'warning', label: 'Warning', default: WarningOrange }, 76 | { type: 'colorpicker', id: 'danger', label: 'Danger', default: DangerRed }, 77 | ], 78 | callback: (feedback) => progressbar(feedback), 79 | }, 80 | [feedbackId.TimerProgressBarMulti]: { 81 | type: 'advanced', 82 | name: 'Multi Progressbar', 83 | description: 'Progressbar across multiple buttons indicating the main timer progression', 84 | options: [ 85 | { type: 'checkbox', id: 'big', label: 'Big graphic', default: true }, 86 | { type: 'colorpicker', id: 'normal', label: 'Normal', default: NormalGray }, 87 | { type: 'colorpicker', id: 'warning', label: 'Warning', default: WarningOrange }, 88 | { type: 'colorpicker', id: 'danger', label: 'Danger', default: DangerRed }, 89 | { type: 'number', id: 'amount', label: 'Amount', default: 3, min: 1, max: 8 }, 90 | { 91 | type: 'number', 92 | id: 'index', 93 | label: 'Index', 94 | tooltip: 'this buttons index in the array', 95 | default: 1, 96 | min: 1, 97 | max: 8, 98 | }, 99 | ], 100 | callback: (feedback) => progressbar(feedback), 101 | }, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/v3/feedbacks/timerPhase.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionFeedbackDefinition } from '@companion-module/base' 2 | import { TimerPhase } from '../ontime-types.js' 3 | import { feedbackId } from '../../enums.js' 4 | import { OntimeV3 } from '../ontimev3.js' 5 | import { DangerRed, White } from '../../assets/colours.js' 6 | 7 | export function createTimerPhaseFeedback(ontime: OntimeV3): { 8 | [id: string]: CompanionFeedbackDefinition 9 | } { 10 | return { 11 | [feedbackId.TimerPhase]: { 12 | type: 'boolean', 13 | name: 'Timer phase', 14 | description: 'Timer phase use Ontimes warn and danger times to change colour depending on timer progress', 15 | defaultStyle: { 16 | color: White, 17 | bgcolor: DangerRed, 18 | }, 19 | options: [ 20 | { 21 | type: 'multidropdown', 22 | label: 'State', 23 | id: 'phase', 24 | choices: [ 25 | { id: TimerPhase.None, label: 'None' }, 26 | { id: TimerPhase.Default, label: 'Default' }, 27 | { id: TimerPhase.Warning, label: 'Warning' }, 28 | { id: TimerPhase.Danger, label: 'Danger' }, 29 | { id: TimerPhase.Overtime, label: 'Overtime' }, 30 | { id: TimerPhase.Pending, label: 'Pending Roll' }, 31 | ], 32 | default: [TimerPhase.Danger], 33 | }, 34 | ], 35 | callback: (feedback) => { 36 | if (typeof feedback.options.phase === 'string') { 37 | return feedback.options.phase === ontime.state.timer.phase // eslint-disable-line @typescript-eslint/no-unsafe-enum-comparison 38 | } 39 | return (feedback.options.phase as TimerPhase[]).some((value) => value === ontime.state.timer.phase) 40 | }, 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/v3/ontime-types.ts: -------------------------------------------------------------------------------- 1 | //Playbeck 2 | 3 | export enum Playback { 4 | Roll = 'roll', 5 | Play = 'play', 6 | Pause = 'pause', 7 | Stop = 'stop', 8 | Armed = 'armed', 9 | } 10 | 11 | // Message 12 | 13 | export type Message = { 14 | text: string 15 | visible: boolean 16 | } 17 | 18 | // Event 19 | 20 | export enum SupportedEvent { 21 | Event = 'event', 22 | Delay = 'delay', 23 | Block = 'block', 24 | } 25 | 26 | /** 27 | * {@link https://github.com/cpvalente/ontime/blob/8f249b9d515fb0d799514d3a67de6713f5029faf/packages/types/src/definitions/core/OntimeEvent.type.ts GitHub}. 28 | */ 29 | export type OntimeBaseEvent = { 30 | type: SupportedEvent 31 | id: string 32 | after?: string // used when creating an event to indicate its position in rundown 33 | } 34 | 35 | export enum EndAction { 36 | None = 'none', 37 | Stop = 'stop', 38 | LoadNext = 'load-next', 39 | PlayNext = 'play-next', 40 | } 41 | 42 | export enum TimerType { 43 | CountDown = 'count-down', 44 | CountUp = 'count-up', 45 | TimeToEnd = 'time-to-end', 46 | Clock = 'clock', 47 | } 48 | 49 | // Main runtime store 50 | 51 | export type RuntimeStore = { 52 | // timer data 53 | clock: number 54 | timer: TimerState 55 | onAir: boolean 56 | 57 | // messages service 58 | message: MessageState 59 | 60 | // rundown data 61 | runtime: Runtime 62 | eventNow: OntimeEvent | null 63 | // publicEventNow: OntimeEvent | null 64 | eventNext: OntimeEvent | null 65 | // publicEventNext: OntimeEvent | null 66 | currentBlock: CurrentBlockState 67 | // extra timers 68 | auxtimer1: SimpleTimerState 69 | } 70 | 71 | /** 72 | * {@link https://github.com/cpvalente/ontime/blob/master/packages/types/src/definitions/runtime/TimerState.type.ts GitHub} 73 | */ 74 | export enum TimerPhase { 75 | None = 'none', 76 | Default = 'default', 77 | Warning = 'warning', 78 | Danger = 'danger', 79 | Overtime = 'overtime', 80 | Pending = 'pending', // used for waiting to roll 81 | } 82 | 83 | /** 84 | * {@link https://github.com/cpvalente/ontime/blob/master/packages/types/src/definitions/runtime/TimerState.type.ts GitHub} 85 | */ 86 | export type TimerState = { 87 | addedTime: number // time added by user, can be negative 88 | current: number | null // running countdown 89 | duration: number | null // normalised duration of current event 90 | elapsed: number | null // elapsed time in current timer 91 | expectedFinish: number | null // time we expect timer to finish 92 | finishedAt: number | null // only if timer has already finished 93 | phase: TimerPhase 94 | playback: Playback 95 | secondaryTimer: number | null // used for roll mode 96 | startedAt: number | null // only if timer has already started 97 | } 98 | 99 | /** 100 | * {@link https://github.com/cpvalente/ontime/blob/master/packages/types/src/definitions/runtime/Runtime.type.ts GitHub} 101 | */ 102 | export type Runtime = { 103 | numEvents: number 104 | selectedEventIndex: number | null 105 | offset: number | null 106 | plannedStart: number | null 107 | actualStart: number | null 108 | plannedEnd: number | null 109 | expectedEnd: number | null 110 | } 111 | 112 | // Event 113 | 114 | export type OntimeEvent = OntimeBaseEvent & { 115 | type: SupportedEvent.Event 116 | cue: string 117 | title: string 118 | note: string 119 | endAction: EndAction 120 | timerType: TimerType 121 | linkStart: string | null // ID of event to link to 122 | timeStrategy: TimeStrategy 123 | timeStart: number 124 | timeEnd: number 125 | duration: number 126 | isPublic: boolean 127 | skip: boolean 128 | colour: string 129 | revision: number 130 | delay: number // calculated at runtime 131 | timeWarning: number 132 | timeDanger: number 133 | custom: EventCustomFields 134 | } 135 | 136 | export enum TimeStrategy { 137 | LockEnd = 'lock-end', 138 | LockDuration = 'lock-duration', 139 | } 140 | 141 | // Message 142 | 143 | export type MessageState = { 144 | timer: TimerMessage 145 | external: string 146 | } 147 | 148 | export type TimerMessage = { 149 | text: string 150 | visible: boolean 151 | blink: boolean 152 | blackout: boolean 153 | secondarySource: 'aux' | 'external' | null 154 | } 155 | 156 | // Custom fields 157 | 158 | export type CustomField = { 159 | type: 'string' 160 | colour: string 161 | label: string 162 | } 163 | 164 | export type CustomFields = Record 165 | export type EventCustomFields = Record 166 | 167 | //Extra timer 168 | 169 | export enum SimplePlayback { 170 | Start = 'start', 171 | Pause = 'pause', 172 | Stop = 'stop', 173 | } 174 | 175 | export enum SimpleDirection { 176 | CountUp = 'count-up', 177 | CountDown = 'count-down', 178 | } 179 | 180 | export type SimpleTimerState = { 181 | duration: number 182 | current: number 183 | playback: SimplePlayback 184 | direction: SimpleDirection 185 | } 186 | 187 | export type OntimeBlock = OntimeBaseEvent & { 188 | type: SupportedEvent.Block 189 | title: string 190 | } 191 | 192 | export type CurrentBlockState = { 193 | block: OntimeBlock | null 194 | startedAt: number | null 195 | } 196 | -------------------------------------------------------------------------------- /src/v3/ontimev3.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CompanionActionDefinitions, 3 | CompanionFeedbackDefinitions, 4 | CompanionPresetDefinitions, 5 | CompanionVariableDefinition, 6 | } from '@companion-module/base' 7 | import { OnTimeInstance, type OntimeClient } from '../index.js' 8 | 9 | import { connect, disconnectSocket } from './connection.js' 10 | import type { CustomFields, OntimeEvent } from './ontime-types.js' 11 | import { stateobj } from './state.js' 12 | 13 | import { actions } from './actions/index.js' 14 | import { feedbacks } from './feedbacks/index.js' 15 | import { variables } from './variables.js' 16 | import { presets } from './presets.js' 17 | 18 | export class OntimeV3 implements OntimeClient { 19 | instance: OnTimeInstance 20 | public events: OntimeEvent[] = [] 21 | public customFields: CustomFields = {} 22 | public state = stateobj 23 | 24 | constructor(instance: OnTimeInstance) { 25 | this.instance = instance 26 | } 27 | 28 | connect(): void { 29 | connect(this.instance, this) 30 | } 31 | 32 | disconnectSocket(): void { 33 | disconnectSocket() 34 | } 35 | 36 | getVariables(includeCustom: boolean = false): CompanionVariableDefinition[] { 37 | if (includeCustom) { 38 | const customVariables = Object.entries(this.customFields).map((value) => { 39 | const name = value[1].label 40 | const variableId = value[0] 41 | return [ 42 | { name: `Custom "${name}" value of previous event`, variableId: `${variableId}_CustomPrevious` }, 43 | { name: `Custom "${name}" value of current event`, variableId: `${variableId}_CustomNow` }, 44 | { name: `Custom "${name}" value of next event`, variableId: `${variableId}_CustomNext` }, 45 | ] 46 | }) 47 | 48 | return variables().concat(...customVariables) 49 | } 50 | return variables() 51 | } 52 | 53 | getActions(): CompanionActionDefinitions { 54 | return actions(this) 55 | } 56 | 57 | getFeedbacks(): CompanionFeedbackDefinitions { 58 | return feedbacks(this) 59 | } 60 | 61 | getPresets(): CompanionPresetDefinitions { 62 | return presets() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/v3/presets.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CompanionButtonPresetDefinition, 3 | CompanionButtonStyleProps, 4 | CompanionPresetDefinitions, 5 | } from '@companion-module/base' 6 | import * as icons from '../assets/icons.js' 7 | import { ActionId, feedbackId, OffsetState } from '../enums.js' 8 | import { TimerPhase } from './ontime-types.js' 9 | import { graphics } from 'companion-module-utils' 10 | import { 11 | ActiveBlue, 12 | Black, 13 | DangerRed, 14 | NormalGray, 15 | PauseOrange, 16 | PlaybackGreen, 17 | PlaybackRed, 18 | RollBlue, 19 | WarningOrange, 20 | White, 21 | } from '../assets/colours.js' 22 | 23 | export function presets(): CompanionPresetDefinitions { 24 | return { ...playbackPresets, ...timerPresets, ...auxTimerPresets, ...rundownPresets, ...messagePresets } 25 | } 26 | 27 | const defaultStyle: CompanionButtonStyleProps = { 28 | size: '24', 29 | color: White, 30 | bgcolor: Black, 31 | text: '', 32 | alignment: 'center:center', 33 | // show_topbar: false, 34 | } 35 | 36 | const defaultWithIconStyle: CompanionButtonStyleProps = { 37 | pngalignment: 'center:top', 38 | size: '14', 39 | color: White, 40 | bgcolor: Black, 41 | text: '', 42 | alignment: 'center:bottom', 43 | // show_topbar: false, 44 | } 45 | 46 | const canPlayFeedback = [ 47 | { 48 | feedbackId: feedbackId.ColorPlayback, 49 | options: { 50 | state: 'play', 51 | }, 52 | style: { 53 | color: White, 54 | bgcolor: PlaybackGreen, 55 | }, 56 | }, 57 | { 58 | feedbackId: feedbackId.ColorPlayback, 59 | options: { 60 | state: 'armed', 61 | }, 62 | style: { 63 | color: PlaybackGreen, 64 | }, 65 | }, 66 | { 67 | feedbackId: feedbackId.ColorPlayback, 68 | options: { 69 | state: 'pause', 70 | }, 71 | style: { 72 | color: PlaybackGreen, 73 | }, 74 | }, 75 | ] 76 | 77 | const playbackPresets: { [id: string]: CompanionButtonPresetDefinition } = { 78 | select_previous_event: { 79 | type: 'button', 80 | category: 'Playback', 81 | name: 'Selects previous event', 82 | style: { 83 | ...defaultWithIconStyle, 84 | png64: icons.PlaybackPrevious, 85 | }, 86 | steps: [ 87 | { 88 | down: [ 89 | { 90 | actionId: ActionId.Load, 91 | options: { method: 'previous' }, 92 | }, 93 | ], 94 | up: [], 95 | }, 96 | ], 97 | feedbacks: [], 98 | }, 99 | select_next_event: { 100 | type: 'button', 101 | category: 'Playback', 102 | name: 'Selects next event', 103 | style: { 104 | ...defaultWithIconStyle, 105 | png64: icons.PlaybackNext, 106 | }, 107 | steps: [ 108 | { 109 | down: [ 110 | { 111 | actionId: ActionId.Load, 112 | options: { method: 'next' }, 113 | }, 114 | ], 115 | up: [], 116 | }, 117 | ], 118 | feedbacks: [], 119 | }, 120 | stop_selected_event: { 121 | type: 'button', 122 | category: 'Playback', 123 | name: 'Stops running event', 124 | style: { 125 | ...defaultWithIconStyle, 126 | png64: icons.PlaybackStop, 127 | text: 'STOP', 128 | color: PlaybackRed, 129 | }, 130 | previewStyle: { 131 | ...defaultWithIconStyle, 132 | png64: icons.PlaybackStop, 133 | text: 'STOP', 134 | bgcolor: PlaybackRed, 135 | }, 136 | steps: [ 137 | { 138 | down: [ 139 | { 140 | actionId: ActionId.Stop, 141 | options: {}, 142 | }, 143 | ], 144 | up: [], 145 | }, 146 | ], 147 | feedbacks: [ 148 | { 149 | feedbackId: feedbackId.ColorPlayback, 150 | options: { 151 | state: 'stop', 152 | }, 153 | style: { 154 | bgcolor: PlaybackRed, 155 | color: White, 156 | }, 157 | }, 158 | ], 159 | }, 160 | pause_selected_event: { 161 | type: 'button', 162 | category: 'Playback', 163 | name: 'Pauses running event', 164 | style: { 165 | ...defaultWithIconStyle, 166 | png64: icons.PlaybackPause, 167 | text: 'PAUSE', 168 | }, 169 | previewStyle: { 170 | ...defaultWithIconStyle, 171 | png64: icons.PlaybackPause, 172 | text: 'PAUSE', 173 | bgcolor: PauseOrange, 174 | }, 175 | steps: [ 176 | { 177 | down: [ 178 | { 179 | actionId: ActionId.Pause, 180 | options: {}, 181 | }, 182 | ], 183 | up: [], 184 | }, 185 | ], 186 | feedbacks: [ 187 | { 188 | feedbackId: feedbackId.ColorPlayback, 189 | options: { 190 | state: 'pause', 191 | }, 192 | style: { 193 | color: White, 194 | bgcolor: PauseOrange, 195 | }, 196 | }, 197 | { 198 | feedbackId: feedbackId.ColorPlayback, 199 | options: { 200 | state: 'play', 201 | }, 202 | style: { 203 | color: PauseOrange, 204 | }, 205 | }, 206 | ], 207 | }, 208 | start_selected_event: { 209 | type: 'button', 210 | category: 'Playback', 211 | name: 'Starts selected event', 212 | style: { 213 | ...defaultWithIconStyle, 214 | png64: icons.PlaybackStart, 215 | text: 'START', 216 | }, 217 | previewStyle: { 218 | ...defaultWithIconStyle, 219 | png64: icons.PlaybackStart, 220 | text: 'START', 221 | bgcolor: PlaybackGreen, 222 | }, 223 | steps: [ 224 | { 225 | down: [ 226 | { 227 | actionId: ActionId.Start, 228 | options: { 229 | method: 'loaded', 230 | }, 231 | }, 232 | ], 233 | up: [], 234 | }, 235 | ], 236 | feedbacks: canPlayFeedback, 237 | }, 238 | start_next_event: { 239 | type: 'button', 240 | category: 'Playback', 241 | name: 'Start next event', 242 | style: { 243 | ...defaultWithIconStyle, 244 | png64: icons.PlaybackStart, 245 | text: 'NEXT', 246 | }, 247 | previewStyle: { 248 | ...defaultWithIconStyle, 249 | png64: icons.PlaybackStart, 250 | text: 'NEXT', 251 | bgcolor: PlaybackGreen, 252 | }, 253 | steps: [ 254 | { 255 | down: [ 256 | { 257 | actionId: ActionId.Start, 258 | options: { method: 'next' }, 259 | }, 260 | ], 261 | up: [], 262 | }, 263 | ], 264 | feedbacks: canPlayFeedback, 265 | }, 266 | start_selected_or_next_event: { 267 | type: 'button', 268 | category: 'Playback', 269 | name: 'Start selected/next event', 270 | style: { 271 | ...defaultWithIconStyle, 272 | png64: icons.PlaybackStart, 273 | text: 'GO', 274 | }, 275 | previewStyle: { 276 | ...defaultWithIconStyle, 277 | png64: icons.PlaybackStart, 278 | text: 'GO', 279 | bgcolor: PlaybackGreen, 280 | }, 281 | steps: [ 282 | { 283 | down: [ 284 | { 285 | actionId: ActionId.Start, 286 | options: { method: 'go' }, 287 | }, 288 | ], 289 | up: [], 290 | }, 291 | ], 292 | feedbacks: canPlayFeedback, 293 | }, 294 | reload_selected_event: { 295 | type: 'button', 296 | category: 'Playback', 297 | name: 'Reload selected event', 298 | style: { 299 | ...defaultWithIconStyle, 300 | png64: icons.PlaybackReload, 301 | text: 'RELOAD', 302 | }, 303 | steps: [ 304 | { 305 | down: [ 306 | { 307 | actionId: ActionId.Reload, 308 | options: {}, 309 | }, 310 | ], 311 | up: [], 312 | }, 313 | ], 314 | feedbacks: [], 315 | }, 316 | start_roll_mode: { 317 | type: 'button', 318 | category: 'Playback', 319 | name: 'Starts Roll Mode', 320 | style: { 321 | ...defaultWithIconStyle, 322 | png64: icons.PlaybackRoll, 323 | text: 'ROLL', 324 | color: RollBlue, 325 | }, 326 | previewStyle: { 327 | ...defaultWithIconStyle, 328 | png64: icons.PlaybackRoll, 329 | text: 'ROLL', 330 | bgcolor: RollBlue, 331 | }, 332 | steps: [ 333 | { 334 | down: [ 335 | { 336 | actionId: ActionId.Roll, 337 | options: {}, 338 | }, 339 | ], 340 | up: [], 341 | }, 342 | ], 343 | feedbacks: [ 344 | { 345 | feedbackId: feedbackId.ColorPlayback, 346 | options: { state: 'roll' }, 347 | style: { 348 | color: White, 349 | bgcolor: RollBlue, 350 | }, 351 | }, 352 | ], 353 | }, 354 | } 355 | 356 | const timerPhaseFeedback = [ 357 | { 358 | feedbackId: feedbackId.TimerPhase, 359 | options: { phase: [TimerPhase.Default] }, 360 | style: { bgcolor: NormalGray, color: Black }, 361 | }, 362 | { 363 | feedbackId: feedbackId.TimerPhase, 364 | options: { phase: [TimerPhase.Warning] }, 365 | style: { bgcolor: WarningOrange, color: Black }, 366 | }, 367 | { 368 | feedbackId: feedbackId.TimerPhase, 369 | options: { phase: [TimerPhase.Danger] }, 370 | style: { bgcolor: DangerRed, color: Black }, 371 | }, 372 | { 373 | feedbackId: feedbackId.TimerPhase, 374 | options: { phase: [TimerPhase.Overtime] }, 375 | style: { bgcolor: Black, color: DangerRed }, 376 | }, 377 | ] 378 | 379 | const timerPhaseAndPauseFeedback = [ 380 | ...timerPhaseFeedback, 381 | { 382 | feedbackId: feedbackId.ColorPlayback, 383 | options: { state: 'pause' }, 384 | style: { color: PauseOrange }, 385 | }, 386 | ] 387 | 388 | const rundownPresets: { [id: string]: CompanionButtonPresetDefinition } = { 389 | currentBlock: { 390 | type: 'button', 391 | category: 'Rundown', 392 | name: 'Title of current block', 393 | style: { 394 | ...defaultStyle, 395 | size: 'auto', 396 | text: '$(ontime:currentBlockTitle)', 397 | }, 398 | previewStyle: { 399 | ...defaultStyle, 400 | size: 'auto', 401 | text: 'Current Block', 402 | }, 403 | steps: [ 404 | { 405 | down: [], 406 | up: [], 407 | }, 408 | ], 409 | feedbacks: [], 410 | }, 411 | offset: { 412 | type: 'button', 413 | category: 'Rundown', 414 | name: 'Current offset', 415 | style: { 416 | ...defaultStyle, 417 | size: '14', 418 | text: '$(ontime:rundown_offset_hms)', 419 | }, 420 | previewStyle: { 421 | ...defaultStyle, 422 | size: '14', 423 | text: '00:00:05', 424 | }, 425 | feedbacks: [ 426 | { 427 | feedbackId: feedbackId.RundownOffset, 428 | options: { 429 | state: OffsetState.Ahead, 430 | margin: 10, 431 | }, 432 | style: { 433 | bgcolor: PlaybackGreen, 434 | }, 435 | }, 436 | { 437 | feedbackId: feedbackId.RundownOffset, 438 | options: { 439 | state: OffsetState.Behind, 440 | margin: 10, 441 | }, 442 | style: { 443 | bgcolor: PlaybackRed, 444 | }, 445 | }, 446 | ], 447 | steps: [], 448 | }, 449 | } 450 | 451 | const messagePresets: { [id: string]: CompanionButtonPresetDefinition } = { 452 | showMessage: { 453 | type: 'button', 454 | category: 'Message', 455 | name: 'Show Message', 456 | style: { 457 | ...defaultStyle, 458 | size: '18', 459 | text: "Time's up", 460 | }, 461 | previewStyle: { 462 | ...defaultStyle, 463 | size: '18', 464 | text: "Time's up", 465 | }, 466 | steps: [ 467 | { 468 | down: [ 469 | { actionId: ActionId.MessageText, options: { value: 'Your time is up' } }, 470 | { actionId: ActionId.MessageVisibility, options: { value: 2 } }, 471 | ], 472 | up: [], 473 | }, 474 | ], 475 | feedbacks: [ 476 | { 477 | feedbackId: feedbackId.MessageVisible, 478 | options: { reqText: true, text: 'Your time is up' }, 479 | style: { 480 | bgcolor: ActiveBlue, 481 | }, 482 | }, 483 | ], 484 | }, 485 | showSelectedMessage: { 486 | type: 'button', 487 | category: 'Message', 488 | name: 'Show Selected Message', 489 | style: { 490 | ...defaultStyle, 491 | size: 'auto', 492 | text: 'Show\n$(ontime:timerMessage)', 493 | }, 494 | previewStyle: { 495 | ...defaultStyle, 496 | size: 'auto', 497 | text: 'Show\nSelected Message', 498 | }, 499 | steps: [ 500 | { 501 | down: [{ actionId: ActionId.MessageVisibility, options: { value: 2 } }], 502 | up: [], 503 | }, 504 | ], 505 | feedbacks: [ 506 | { 507 | feedbackId: feedbackId.MessageVisible, 508 | options: {}, 509 | style: { 510 | bgcolor: ActiveBlue, 511 | }, 512 | }, 513 | ], 514 | }, 515 | selectMessage1: { 516 | type: 'button', 517 | category: 'Message', 518 | name: 'Selected Message 1', 519 | style: { 520 | ...defaultStyle, 521 | size: 'auto', 522 | text: 'Select Msg 1', 523 | }, 524 | previewStyle: { 525 | ...defaultStyle, 526 | size: 'auto', 527 | text: 'Select Msg 1', 528 | }, 529 | steps: [ 530 | { 531 | down: [{ actionId: ActionId.MessageText, options: { value: 'Message 1' } }], 532 | up: [], 533 | }, 534 | ], 535 | feedbacks: [ 536 | { 537 | feedbackId: feedbackId.MessageVisible, 538 | options: { reqText: true, text: 'Message 1' }, 539 | style: { 540 | bgcolor: ActiveBlue, 541 | }, 542 | }, 543 | ], 544 | }, 545 | selectMessage2: { 546 | type: 'button', 547 | category: 'Message', 548 | name: 'Selected Message 2', 549 | style: { 550 | ...defaultStyle, 551 | size: 'auto', 552 | text: 'Select Msg 2', 553 | }, 554 | previewStyle: { 555 | ...defaultStyle, 556 | size: 'auto', 557 | text: 'Select Msg 2', 558 | }, 559 | steps: [ 560 | { 561 | down: [{ actionId: ActionId.MessageText, options: { value: 'Message 2' } }], 562 | up: [], 563 | }, 564 | ], 565 | feedbacks: [ 566 | { 567 | feedbackId: feedbackId.MessageVisible, 568 | options: { reqText: true, text: 'Message 2' }, 569 | style: { 570 | bgcolor: ActiveBlue, 571 | }, 572 | }, 573 | ], 574 | }, 575 | } 576 | 577 | const timerPresets: { [id: string]: CompanionButtonPresetDefinition } = { 578 | add_1_min: { 579 | type: 'button', 580 | category: 'Timer Management', 581 | name: 'Add 1 minute to running timer', 582 | style: { 583 | ...defaultStyle, 584 | text: '+1', 585 | color: PauseOrange, 586 | alignment: 'center:center', 587 | }, 588 | steps: [ 589 | { 590 | down: [ 591 | { 592 | actionId: ActionId.Add, 593 | options: { addremove: 'add', minutes: 1, hours: 0, seconds: 0 }, 594 | }, 595 | ], 596 | up: [], 597 | }, 598 | ], 599 | feedbacks: [], 600 | }, 601 | remove_1_min: { 602 | type: 'button', 603 | category: 'Timer Management', 604 | name: 'Remove 1 minute to running timer', 605 | style: { ...defaultStyle, text: '-1', color: PauseOrange, alignment: 'center:center' }, 606 | steps: [ 607 | { 608 | down: [ 609 | { 610 | actionId: ActionId.Add, 611 | options: { addremove: 'remove', minutes: 1, hours: 0, seconds: 0 }, 612 | }, 613 | ], 614 | up: [], 615 | }, 616 | ], 617 | feedbacks: [], 618 | }, 619 | add_5_min: { 620 | type: 'button', 621 | category: 'Timer Management', 622 | name: 'Add 5 minute to running timer', 623 | style: { 624 | ...defaultStyle, 625 | text: '+5', 626 | color: PauseOrange, 627 | alignment: 'center:center', 628 | }, 629 | steps: [ 630 | { 631 | down: [ 632 | { 633 | actionId: ActionId.Add, 634 | options: { addremove: 'add', minutes: 5, hours: 0, seconds: 0 }, 635 | }, 636 | ], 637 | up: [], 638 | }, 639 | ], 640 | feedbacks: [], 641 | }, 642 | remove_5_min: { 643 | type: 'button', 644 | category: 'Timer Management', 645 | name: 'Remove 5 minute to running timer', 646 | style: { 647 | ...defaultStyle, 648 | text: '-5', 649 | color: PauseOrange, 650 | alignment: 'center:center', 651 | }, 652 | steps: [ 653 | { 654 | down: [ 655 | { 656 | actionId: ActionId.Add, 657 | options: { addremove: 'remove', minutes: 5, hours: 0, seconds: 0 }, 658 | }, 659 | ], 660 | up: [], 661 | }, 662 | ], 663 | feedbacks: [], 664 | }, 665 | current_added: { 666 | type: 'button', 667 | category: 'Timer Management', 668 | name: 'Amount of time added/removed from running timer', 669 | style: { 670 | ...defaultStyle, 671 | text: `Total added\n$(ontime:timer_added_nice)`, 672 | size: 'auto', 673 | alignment: 'center:center', 674 | }, 675 | previewStyle: { 676 | ...defaultStyle, 677 | text: 'Total added\n00', 678 | size: 'auto', 679 | alignment: 'center:center', 680 | bgcolor: PauseOrange, 681 | }, 682 | steps: [ 683 | { 684 | down: [], 685 | up: [], 686 | }, 687 | ], 688 | feedbacks: [ 689 | { feedbackId: feedbackId.ColorAddRemove, options: { direction: 'both' }, style: { bgcolor: PauseOrange } }, 690 | ], 691 | }, 692 | current_time_hms: { 693 | type: 'button', 694 | category: 'Timer Management', 695 | name: 'Current time', 696 | style: { 697 | ...defaultStyle, 698 | size: 14, 699 | text: `$(ontime:time)`, 700 | alignment: 'center:center', 701 | }, 702 | previewStyle: { 703 | ...defaultStyle, 704 | size: 14, 705 | text: 'HH:MM:SS', 706 | alignment: 'center:center', 707 | png64: graphics.toPNG64({ 708 | image: graphics.bar({ 709 | width: 72, 710 | height: 72, 711 | colors: [ 712 | { 713 | size: 100, 714 | color: NormalGray, 715 | background: NormalGray, 716 | backgroundOpacity: 150, 717 | }, 718 | ], 719 | barLength: 72, 720 | barWidth: 10, 721 | value: 50, 722 | type: 'horizontal', 723 | offsetX: 0, 724 | offsetY: 72 - 10, 725 | opacity: 255, 726 | }), 727 | width: 72, 728 | height: 72, 729 | }), 730 | }, 731 | steps: [ 732 | { 733 | down: [], 734 | up: [], 735 | }, 736 | ], 737 | feedbacks: [ 738 | { 739 | feedbackId: feedbackId.TimerProgressBar, 740 | options: { 741 | normal: NormalGray, 742 | warning: WarningOrange, 743 | danger: DangerRed, 744 | }, 745 | }, 746 | ], 747 | }, 748 | current_time_h: { 749 | type: 'button', 750 | category: 'Timer Management', 751 | name: 'Current timer hour', 752 | style: { 753 | ...defaultStyle, 754 | size: 44, 755 | text: `$(ontime:time_sign)$(ontime:time_h)`, 756 | alignment: 'center:center', 757 | }, 758 | previewStyle: { 759 | ...defaultStyle, 760 | size: 'auto', 761 | text: '+/-HH', 762 | alignment: 'center:center', 763 | bgcolor: NormalGray, 764 | color: Black, 765 | }, 766 | steps: [ 767 | { 768 | down: [], 769 | up: [], 770 | }, 771 | ], 772 | feedbacks: timerPhaseAndPauseFeedback, 773 | }, 774 | current_time_m: { 775 | type: 'button', 776 | category: 'Timer Management', 777 | name: 'Current timer minutes', 778 | style: { 779 | ...defaultStyle, 780 | size: 44, 781 | text: `$(ontime:time_m)`, 782 | alignment: 'center:center', 783 | }, 784 | previewStyle: { 785 | ...defaultStyle, 786 | size: 28, 787 | text: 'MM', 788 | alignment: 'center:center', 789 | bgcolor: NormalGray, 790 | color: Black, 791 | }, 792 | steps: [ 793 | { 794 | down: [], 795 | up: [], 796 | }, 797 | ], 798 | feedbacks: timerPhaseAndPauseFeedback, 799 | }, 800 | current_time_s: { 801 | type: 'button', 802 | category: 'Timer Management', 803 | name: 'Current timer seconds', 804 | style: { 805 | ...defaultStyle, 806 | size: 44, 807 | text: `$(ontime:time_s)`, 808 | alignment: 'center:center', 809 | }, 810 | previewStyle: { 811 | ...defaultStyle, 812 | size: 28, 813 | text: 'SS', 814 | alignment: 'center:center', 815 | bgcolor: NormalGray, 816 | color: Black, 817 | }, 818 | steps: [ 819 | { 820 | down: [], 821 | up: [], 822 | }, 823 | ], 824 | feedbacks: timerPhaseAndPauseFeedback, 825 | }, 826 | } 827 | 828 | const auxTimerNegative = [ 829 | { 830 | feedbackId: feedbackId.AuxTimerNegative, 831 | options: {}, 832 | style: { 833 | color: DangerRed, 834 | }, 835 | }, 836 | ] 837 | 838 | const auxTimerPresets: { [id: string]: CompanionButtonPresetDefinition } = { 839 | current_auxtime_hms: { 840 | type: 'button', 841 | category: 'Aux Timer', 842 | name: 'Current aux time', 843 | style: { 844 | ...defaultStyle, 845 | text: `$(ontime:auxTimer_current_hms-1)`, 846 | size: '14', 847 | alignment: 'center:center', 848 | }, 849 | previewStyle: { 850 | ...defaultStyle, 851 | text: 'HH:MM:SS', 852 | size: '14', 853 | alignment: 'center:center', 854 | }, 855 | steps: [ 856 | { 857 | down: [], 858 | up: [], 859 | }, 860 | ], 861 | feedbacks: auxTimerNegative, 862 | }, 863 | start_stop_auxtimer: { 864 | type: 'button', 865 | category: 'Aux Timer', 866 | name: 'Start/Stop Aux Timer', 867 | style: { 868 | ...defaultWithIconStyle, 869 | png64: icons.PlaybackStart, 870 | text: 'START', 871 | color: PlaybackGreen, 872 | }, 873 | previewStyle: { 874 | ...defaultWithIconStyle, 875 | png64: icons.PlaybackStart, 876 | text: 'START/STOP', 877 | bgcolor: PlaybackGreen, 878 | }, 879 | steps: [ 880 | { 881 | down: [ 882 | { 883 | actionId: ActionId.AuxTimerPlayState, 884 | options: { value: 'toggleSS', destination: '1' }, 885 | }, 886 | ], 887 | up: [], 888 | }, 889 | ], 890 | feedbacks: [ 891 | { 892 | feedbackId: feedbackId.AuxTimerPlayback, 893 | options: { 894 | state: 'start', 895 | }, 896 | style: { 897 | color: PlaybackRed, 898 | png64: icons.PlaybackStop, 899 | text: 'STOP', 900 | }, 901 | }, 902 | ], 903 | }, 904 | pause_auxtimer: { 905 | type: 'button', 906 | category: 'Aux Timer', 907 | name: 'Pause Aux Timer', 908 | style: { 909 | ...defaultWithIconStyle, 910 | png64: icons.PlaybackPause, 911 | text: 'PAUSE', 912 | color: PauseOrange, 913 | }, 914 | previewStyle: { 915 | ...defaultWithIconStyle, 916 | png64: icons.PlaybackPause, 917 | text: 'PAUSE', 918 | bgcolor: PauseOrange, 919 | }, 920 | steps: [ 921 | { 922 | down: [ 923 | { 924 | actionId: ActionId.AuxTimerPlayState, 925 | options: { value: 'pause', destination: '1' }, 926 | }, 927 | ], 928 | up: [], 929 | }, 930 | ], 931 | feedbacks: [ 932 | { 933 | feedbackId: feedbackId.AuxTimerPlayback, 934 | options: { 935 | state: 'pause', 936 | }, 937 | style: { 938 | bgcolor: PauseOrange, 939 | color: White, 940 | }, 941 | }, 942 | ], 943 | }, 944 | add_auxtimer: { 945 | type: 'button', 946 | category: 'Aux Timer', 947 | name: 'Add timer to Aux Timer', 948 | style: { 949 | ...defaultStyle, 950 | text: '+5m', 951 | }, 952 | previewStyle: { 953 | ...defaultStyle, 954 | text: '+5m', 955 | }, 956 | steps: [ 957 | { 958 | down: [ 959 | { 960 | actionId: ActionId.AuxTimerAdd, 961 | options: { hours: 0, minutes: 5, seconds: 0, addremove: 'add', destination: '1' }, 962 | }, 963 | ], 964 | up: [], 965 | }, 966 | ], 967 | feedbacks: [], 968 | }, 969 | remove_auxtimer: { 970 | type: 'button', 971 | category: 'Aux Timer', 972 | name: 'Remove timer to Aux Timer', 973 | style: { 974 | ...defaultStyle, 975 | text: '-5m', 976 | }, 977 | previewStyle: { 978 | ...defaultStyle, 979 | text: '-5m', 980 | }, 981 | steps: [ 982 | { 983 | down: [ 984 | { 985 | actionId: ActionId.AuxTimerAdd, 986 | options: { hours: 0, minutes: 5, seconds: 0, addremove: 'remove', destination: '1' }, 987 | }, 988 | ], 989 | up: [], 990 | }, 991 | ], 992 | feedbacks: [], 993 | }, 994 | } 995 | -------------------------------------------------------------------------------- /src/v3/state.ts: -------------------------------------------------------------------------------- 1 | import { Playback, TimerPhase, SimpleDirection, SimplePlayback } from './ontime-types.js' 2 | import type { RuntimeStore } from './ontime-types.js' 3 | 4 | const stateobj: RuntimeStore = { 5 | clock: 0, 6 | timer: { 7 | current: null, 8 | elapsed: null, 9 | expectedFinish: null, 10 | addedTime: 0, 11 | startedAt: null, 12 | finishedAt: null, 13 | secondaryTimer: null, 14 | duration: null, 15 | phase: TimerPhase.None, 16 | playback: Playback.Stop, 17 | }, 18 | onAir: false, 19 | message: { 20 | timer: { text: '', visible: false, blink: false, blackout: false, secondarySource: null }, 21 | external: '', 22 | }, 23 | runtime: { 24 | numEvents: 0, 25 | selectedEventIndex: 0, 26 | offset: 0, 27 | plannedStart: 0, 28 | actualStart: 0, 29 | plannedEnd: 0, 30 | expectedEnd: 0, 31 | }, 32 | eventNow: null, 33 | eventNext: null, 34 | currentBlock: { block: null, startedAt: null }, 35 | auxtimer1: { 36 | duration: 0, 37 | current: 0, 38 | playback: SimplePlayback.Stop, 39 | direction: SimpleDirection.CountDown, 40 | }, 41 | } 42 | 43 | export { stateobj } 44 | -------------------------------------------------------------------------------- /src/v3/variables.ts: -------------------------------------------------------------------------------- 1 | import type { CompanionVariableDefinition } from '@companion-module/base' 2 | import { variableId } from '../enums.js' 3 | 4 | export function variables(): CompanionVariableDefinition[] { 5 | return [ 6 | //clock 7 | { 8 | name: 'Clock (hh:mm:ss)', 9 | variableId: variableId.Clock, 10 | }, 11 | //timer.addedTime 12 | { 13 | name: 'User added time to current event (hh:mm:ss)', 14 | variableId: variableId.TimerAdded, 15 | }, 16 | { 17 | name: 'User added time to current event (smallest unit)', 18 | variableId: variableId.TimerAddedNice, 19 | }, 20 | //timer.current 21 | { 22 | name: 'Current timer progress (Default/Warning/Danger/Overtime)', 23 | variableId: variableId.TimerPhase, 24 | }, 25 | { 26 | name: 'Current timer (milliseconds)', 27 | variableId: variableId.TimerTotalMs, 28 | }, 29 | { 30 | name: 'Current timer (hh:mm:ss)', 31 | variableId: variableId.Time, 32 | }, 33 | { 34 | name: 'Current time of event (hh:mm)', 35 | variableId: variableId.TimeHM, 36 | }, 37 | { 38 | name: 'Current event state Hours', 39 | variableId: variableId.TimeH, 40 | }, 41 | { 42 | name: 'Current event state Minutes', 43 | variableId: variableId.TimeM, 44 | }, 45 | { 46 | name: 'Current event state Seconds', 47 | variableId: variableId.TimeS, 48 | }, 49 | { 50 | name: 'Current event timer Sign', 51 | variableId: variableId.TimeN, 52 | }, 53 | //timer.duration 54 | //timer.elapsed 55 | //timer.expectedFinish 56 | { 57 | name: 'Expected finish of event (hh:mm:ss)', 58 | variableId: variableId.TimerFinish, 59 | }, 60 | //timer.finishedAt 61 | //timer.playback 62 | { 63 | name: 'Playback state (Running, Paused, Stopped, Roll)', 64 | variableId: variableId.PlayState, 65 | }, 66 | //timer.secondaryTimer 67 | //timer.startedAt 68 | { 69 | name: 'Start time of current timer (hh:mm:ss)', 70 | variableId: variableId.TimerStart, 71 | }, 72 | //message.timer.text 73 | { 74 | name: 'Timer Message', 75 | variableId: variableId.TimerMessage, 76 | }, 77 | //message.timer.visible 78 | { 79 | name: 'Timer Message Visible', 80 | variableId: variableId.TimerMessageVisible, 81 | }, 82 | //message.timer.blackout 83 | { 84 | name: 'Timer Blackout', 85 | variableId: variableId.TimerBlackout, 86 | }, 87 | //message.timer.blink 88 | { 89 | name: 'Timer Blinking', 90 | variableId: variableId.TimerBlink, 91 | }, 92 | { 93 | name: 'External Message', 94 | variableId: variableId.ExternalMessage, 95 | }, 96 | { 97 | name: 'Timer Message Secondary Source', 98 | variableId: variableId.TimerSecondarySource, 99 | }, 100 | { 101 | name: 'Number of events', 102 | variableId: variableId.NumberOfEvents, 103 | }, 104 | { 105 | name: 'Selected event index', 106 | variableId: variableId.SelectedEventIndex, 107 | }, 108 | { 109 | name: 'Rundown offset (hh:mm:ss)', 110 | variableId: variableId.RundownOffset, 111 | }, 112 | { 113 | name: 'Rundown planned start (hh:mm:ss)', 114 | variableId: variableId.PlannedStart, 115 | }, 116 | { 117 | name: 'Rundown planned end (hh:mm:ss)', 118 | variableId: variableId.PlannedEnd, 119 | }, 120 | { 121 | name: 'Rundown actual start (hh:mm:ss)', 122 | variableId: variableId.ActualStart, 123 | }, 124 | { 125 | name: 'Rundown expected end (hh:mm:ss)', 126 | variableId: variableId.ExpectedEnd, 127 | }, 128 | { 129 | name: 'Title of current block', 130 | variableId: variableId.CurrentBlockTitle, 131 | }, 132 | { 133 | name: 'Start time of current block (hh:mm:ss)', 134 | variableId: variableId.CurrentBlockStartedAt, 135 | }, 136 | { 137 | name: 'Start time of current block (milliseconds)', 138 | variableId: variableId.CurrentBlockStartedAtMs, 139 | }, 140 | { 141 | name: 'ID of previous event', 142 | variableId: variableId.IdPrevious, 143 | }, 144 | { 145 | name: 'Title of previous event', 146 | variableId: variableId.TitlePrevious, 147 | }, 148 | { 149 | name: 'Note of previous event', 150 | variableId: variableId.NotePrevious, 151 | }, 152 | { 153 | name: 'Cue of previous event', 154 | variableId: variableId.CuePrevious, 155 | }, 156 | { 157 | name: 'ID of current event', 158 | variableId: variableId.IdNow, 159 | }, 160 | //eventNow.title 161 | { 162 | name: 'Title of current event', 163 | variableId: variableId.TitleNow, 164 | }, 165 | //eventNow.note 166 | { 167 | name: 'Note of current event', 168 | variableId: variableId.NoteNow, 169 | }, 170 | //eventNow.cue 171 | { 172 | name: 'Cue of current event', 173 | variableId: variableId.CueNow, 174 | }, 175 | //eventNext.di 176 | { 177 | name: 'ID of next event', 178 | variableId: variableId.IdNext, 179 | }, 180 | //eventNext.title 181 | { 182 | name: 'Title of next event', 183 | variableId: variableId.TitleNext, 184 | }, 185 | //eventNext.note 186 | { 187 | name: 'Note of next event', 188 | variableId: variableId.NoteNext, 189 | }, 190 | //eventNext.cue 191 | { 192 | name: 'Cue of next event', 193 | variableId: variableId.CueNext, 194 | }, 195 | //aux timer 196 | { 197 | name: 'Aux timer 1 duration (milliseconds)', 198 | variableId: variableId.AuxTimerDurationMs + '-1', 199 | }, 200 | { 201 | name: 'Aux timer 1 current (milliseconds)', 202 | variableId: variableId.AuxTimerDurationMs + '-1', 203 | }, 204 | { 205 | name: 'Aux timer 1 current (hh:mm:ss)', 206 | variableId: variableId.AuxTimerCurrent + '-1', 207 | }, 208 | { 209 | name: 'Aux timer 1 playback', 210 | variableId: variableId.AuxTimerPlayback + '-1', 211 | }, 212 | { 213 | name: 'Aux timer 1 direction (count-up/count-down)', 214 | variableId: variableId.AuxTimerDirection + '-1', 215 | }, 216 | ] 217 | } 218 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@companion-module/tools/tsconfig/node22/recommended", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"], 5 | "compilerOptions": { 6 | "outDir": "./dist", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["./node_modules/*"] 10 | }, 11 | "module": "es2020", 12 | "verbatimModuleSyntax": true 13 | } 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "include": ["src/**/*.ts", "dump-model.ts"], 4 | "exclude": ["node_modules/**"], 5 | "compilerOptions": { 6 | // "types": ["jest", "node"] 7 | } 8 | } -------------------------------------------------------------------------------- /tsconfig.preset.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@companion-module/tools/tsconfig/node18/recommended", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "paths": { 6 | "*": ["../node_modules/*"] 7 | }, 8 | "declaration": true, 9 | "importHelpers": true, 10 | "types": ["node"], 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitAny": false 16 | }, 17 | "compileOnSave": false 18 | } 19 | --------------------------------------------------------------------------------