Compare commits
14 Commits
8cd5a65542
...
mk/add_web
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d5a3c1e51 | |||
| 0a327b247c | |||
| 8cd7127af8 | |||
| b9060b683f | |||
| 34a2814c19 | |||
|
|
2f9fa1cb0b | ||
| 0e8d7ffa32 | |||
| 168d938435 | |||
| 6b8b0a9097 | |||
| 8faa0443a5 | |||
| 2e885a890d | |||
| 96e1d0f10a | |||
| 3aa54c4474 | |||
| c62a74a635 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: daniep01 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
github: [daniep01, krombel] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
|||||||
76
.github/workflows/node.yaml
vendored
Normal file
76
.github/workflows/node.yaml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: Node CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js 22.x
|
||||||
|
# This should match the version of Node.js you have defined in the manifest.json runtime field
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22.x
|
||||||
|
- name: Prepare Environment
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
- name: Prepare Environment (For template repository)
|
||||||
|
# Only run this step if the repository is a template repository
|
||||||
|
# If you are using this in a module, you can remove this step
|
||||||
|
if: ${{ contains(github.repository, 'companion-module-template-') }}
|
||||||
|
run: |
|
||||||
|
# Perform an install to generate the lockfile
|
||||||
|
yarn install
|
||||||
|
env:
|
||||||
|
CI: false
|
||||||
|
- name: Prepare module
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build and check types
|
||||||
|
run: |
|
||||||
|
yarn build
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Run lint
|
||||||
|
run: |
|
||||||
|
yarn lint
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
# Uncomment this to enable running unit tests
|
||||||
|
# test:
|
||||||
|
# name: Test
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# timeout-minutes: 15
|
||||||
|
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
# - name: Use Node.js 22.x
|
||||||
|
# uses: actions/setup-node@v4
|
||||||
|
# with:
|
||||||
|
# node-version: 22.x
|
||||||
|
# - name: Prepare Environment
|
||||||
|
# run: |
|
||||||
|
# corepack enable
|
||||||
|
# yarn install
|
||||||
|
# env:
|
||||||
|
# CI: true
|
||||||
|
# - name: Run tests
|
||||||
|
# run: |
|
||||||
|
# yarn test
|
||||||
|
# env:
|
||||||
|
# CI: true
|
||||||
|
# - name: Send coverage
|
||||||
|
# uses: codecov/codecov-action@v5
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,7 +2,8 @@ node_modules/
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/pkg
|
/pkg
|
||||||
/pkg.tgz
|
/*.tgz
|
||||||
/dist
|
/dist
|
||||||
DEBUG-*
|
DEBUG-*
|
||||||
/.yarn
|
/.yarn
|
||||||
|
/.vscode
|
||||||
|
|||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lint-staged
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Bitfocus AS - Open Source
|
Copyright (c) 2022 Bitfocus AS - Open Source
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1,3 +1,11 @@
|
|||||||
# companion-module-toggl-track
|
# companion-module-toggl-track
|
||||||
|
|
||||||
See [HELP.md](./HELP.md) and [LICENSE](./LICENSE)
|
See [HELP.md](./companion/HELP.md) and [LICENSE](./LICENSE)
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Executing a `yarn` command should perform all necessary steps to develop the module, if it does not then follow the steps below.
|
||||||
|
|
||||||
|
The module can be built once with `yarn build`. This should be enough to get the module to be loadable by companion.
|
||||||
|
|
||||||
|
While developing the module, by using `yarn dev` the compiler will be run in watch mode to recompile the files on change.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Start a new timer running with the description set in the action and store the I
|
|||||||
|
|
||||||
**Get Current Timer**
|
**Get Current Timer**
|
||||||
|
|
||||||
Companion only knows the ID of timers it has started, if a timer is started from another application or the toggle website then this action will get the ID so Companion knows about it.
|
Companion only knows the ID of timers it has started. If you did not enable time entry poller you can use this action to poll the current time entry, if a timer is started from another application or the toggle website so Companion knows about it.
|
||||||
|
|
||||||
**Stop Current Timer**
|
**Stop Current Timer**
|
||||||
|
|
||||||
@@ -36,32 +36,51 @@ Presets are available for **Start Timer** and **Stop Timer**.
|
|||||||
|
|
||||||
### Version 1.0.0
|
### Version 1.0.0
|
||||||
|
|
||||||
First release
|
- First release
|
||||||
|
|
||||||
### Version 1.0.1
|
### Version 1.0.1
|
||||||
|
|
||||||
Fix broken link
|
- Fix broken link
|
||||||
|
|
||||||
### Version 1.0.2
|
### Version 1.0.2
|
||||||
|
|
||||||
Allow a project to be specified when starting a new timer button
|
- Allow a project to be specified when starting a new timer button
|
||||||
|
- Add an action to refresh the project list
|
||||||
Add an action to refresh the project list
|
- Add 'Always start' configuration option
|
||||||
|
|
||||||
Add 'Always start' configuration option
|
|
||||||
|
|
||||||
### Version 1.0.3
|
### Version 1.0.3
|
||||||
|
|
||||||
Add variables for timerId and timerDescription
|
- Add variables for timerId and timerDescription
|
||||||
|
|
||||||
### Version 2.0.0
|
### Version 2.0.0
|
||||||
|
|
||||||
Updated for Companion version 3
|
- Updated for Companion version 3
|
||||||
|
- Updated for toggl API version 9
|
||||||
Updated for toggl API version 9
|
|
||||||
|
|
||||||
### Version 2.0.1
|
### Version 2.0.1
|
||||||
|
|
||||||
Make the API token config field required
|
- Make the API token config field required
|
||||||
|
- Fix manifest file
|
||||||
|
|
||||||
Fix manifest file
|
### Version 2.1.0
|
||||||
|
|
||||||
|
- Rewrite module in typescript
|
||||||
|
- Use module toggl-track instead of implementing api on our own
|
||||||
|
- Add status reports for some failure cases in connections dashboard
|
||||||
|
- Add configurable time entry poller
|
||||||
|
- Add feedback for currently running project and client
|
||||||
|
- Update timerDuration to contain the correct duration formatted as time string
|
||||||
|
|
||||||
|
### Version 2.1.1
|
||||||
|
|
||||||
|
- Prevent module crash if user has no Clients
|
||||||
|
|
||||||
|
### Version 2.1.2
|
||||||
|
|
||||||
|
- Update node runtime to node22
|
||||||
|
- make polling interval configurable as toggl is updating their [API usage limits](https://support.toggl.com/en/articles/11484112-api-webhook-limits)
|
||||||
|
|
||||||
|
### Version 2.1.3
|
||||||
|
|
||||||
|
- update dependencies (fix CVE-2025-58754)
|
||||||
|
- handle rate limiting of toggl
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"name": "toggl-track",
|
"name": "toggl-track",
|
||||||
"shortname": "toggl-track",
|
"shortname": "toggl-track",
|
||||||
"description": "Companion module for toggltrack timers",
|
"description": "Companion module for toggltrack timers",
|
||||||
"version": "2.0.1",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "git+https://github.com/bitfocus/companion-module-toggl-track.git",
|
"repository": "git+https://github.com/bitfocus/companion-module-toggl-track.git",
|
||||||
"bugs": "https://github.com/bitfocus/companion-module-toggl-track/issues",
|
"bugs": "https://github.com/bitfocus/companion-module-toggl-track/issues",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
],
|
],
|
||||||
"legacyIds": [],
|
"legacyIds": [],
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"type": "node18",
|
"type": "node22",
|
||||||
"api": "nodejs-ipc",
|
"api": "nodejs-ipc",
|
||||||
"apiVersion": "0.0.0",
|
"apiVersion": "0.0.0",
|
||||||
"entrypoint": "../dist/main.js"
|
"entrypoint": "../dist/main.js"
|
||||||
|
|||||||
40
package.json
40
package.json
@@ -1,35 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "toggl-track",
|
"name": "toggl-track",
|
||||||
"version": "2.1.0",
|
"version": "2.1.3",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "husky",
|
||||||
"format": "prettier -w .",
|
"format": "prettier -w .",
|
||||||
"package": "yarn run build && companion-module-build",
|
"package": "run build && companion-module-build",
|
||||||
"build": "rimraf dist && yarn run build:main",
|
"build": "rimraf dist && run build:main",
|
||||||
"build:main": "tsc -p tsconfig.build.json",
|
"build:main": "tsc -p tsconfig.build.json",
|
||||||
"dev": "tsc -p tsconfig.build.json --watch",
|
"dev": "tsc -p tsconfig.build.json --watch",
|
||||||
"lint:raw": "eslint",
|
"lint:raw": "eslint",
|
||||||
"lint": "yarn run lint:raw .",
|
"lint": "run lint:raw .",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
|
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"engines": {
|
||||||
|
"node": "^22.14",
|
||||||
|
"yarn": "^4"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@companion-module/base": "~1.11",
|
"@companion-module/base": "~1.13.4",
|
||||||
"toggl-track": "^0.8.0"
|
"toggl-track": "https://github.com/krombel/toggl-track#v0.8.0-2",
|
||||||
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@companion-module/tools": "~2.1.1",
|
"@companion-module/tools": "~2.4.2",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.18.12",
|
||||||
"eslint": "^9.17.0",
|
"@types/ws": "^8",
|
||||||
"prettier": "^3.4.2",
|
"eslint": "^9.38.0",
|
||||||
"rimraf": "^5.0.10",
|
"husky": "^9.1.7",
|
||||||
"typescript": "~5.5.4",
|
"lint-staged": "^16.2.6",
|
||||||
"typescript-eslint": "^8.18.1"
|
"prettier": "^3.6.2",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.2"
|
||||||
},
|
},
|
||||||
"prettier": "@companion-module/tools/.prettierrc.json",
|
"prettier": "@companion-module/tools/.prettierrc.json",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
@@ -39,5 +48,6 @@
|
|||||||
"*.{ts,tsx,js,jsx}": [
|
"*.{ts,tsx,js,jsx}": [
|
||||||
"yarn lint:raw --fix"
|
"yarn lint:raw --fix"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@4.10.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TogglTrack } from './main.js'
|
import { TogglTrack } from './main.js'
|
||||||
|
|
||||||
export default function (self: TogglTrack): void {
|
export function UpdateActions(self: TogglTrack): void {
|
||||||
self.setActionDefinitions({
|
self.setActionDefinitions({
|
||||||
startNewTimer: {
|
startNewTimer: {
|
||||||
name: 'Start New Timer',
|
name: 'Start New Timer',
|
||||||
@@ -16,7 +16,7 @@ export default function (self: TogglTrack): void {
|
|||||||
label: 'Project',
|
label: 'Project',
|
||||||
id: 'project',
|
id: 'project',
|
||||||
default: '0',
|
default: '0',
|
||||||
choices: self.projects!,
|
choices: self.projects ?? [{ id: -1, label: 'None' }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
callback: async ({ options }) => {
|
callback: async ({ options }) => {
|
||||||
@@ -44,7 +44,15 @@ export default function (self: TogglTrack): void {
|
|||||||
name: 'Refresh Project List',
|
name: 'Refresh Project List',
|
||||||
options: [],
|
options: [],
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await self.getWorkspace()
|
await self.getProjects()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshStaticData: {
|
||||||
|
name: 'Refresh Workspace, Project and Client List',
|
||||||
|
options: [],
|
||||||
|
callback: async () => {
|
||||||
|
await self.loadStaticData()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export interface ModuleConfig {
|
|||||||
workspaceName: string
|
workspaceName: string
|
||||||
alwaysStart: boolean
|
alwaysStart: boolean
|
||||||
startTimerPoller: boolean
|
startTimerPoller: boolean
|
||||||
|
timerPollerInterval: number
|
||||||
|
startTogglWebsocket: boolean
|
||||||
|
wsToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetConfigFields(): SomeCompanionConfigField[] {
|
export function GetConfigFields(): SomeCompanionConfigField[] {
|
||||||
@@ -36,9 +39,42 @@ export function GetConfigFields(): SomeCompanionConfigField[] {
|
|||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
id: 'startTimerPoller',
|
id: 'startTimerPoller',
|
||||||
label: 'Poll for current time entry every 30 seconds',
|
label: 'Poll for current time entry every n seconds',
|
||||||
width: 12,
|
width: 12,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
id: 'timerPollerInterval',
|
||||||
|
label: 'Poll interval in seconds',
|
||||||
|
width: 12,
|
||||||
|
default: 60,
|
||||||
|
min: 30,
|
||||||
|
max: 3600,
|
||||||
|
isVisible: (opt) => {
|
||||||
|
return opt.startTimerPoller === true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
id: 'startTogglWebsocket',
|
||||||
|
label: 'EXPERIMENTAL: Use background websocket connection for updates',
|
||||||
|
width: 12,
|
||||||
|
default: false,
|
||||||
|
isVisible: (opt) => {
|
||||||
|
return !opt.startTimerPoller
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'textinput',
|
||||||
|
id: 'wsToken',
|
||||||
|
label: 'Personal API Token from your Toggl user profile (required)',
|
||||||
|
width: 12,
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
isVisible: (opt) => {
|
||||||
|
return !opt.startTimerPoller && opt.startTogglWebsocket === true
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/feedbacks.ts
Normal file
50
src/feedbacks.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { combineRgb } from '@companion-module/base'
|
||||||
|
import type { TogglTrack } from './main.js'
|
||||||
|
|
||||||
|
export function UpdateFeedbacks(self: TogglTrack): void {
|
||||||
|
self.setFeedbackDefinitions({
|
||||||
|
ProjectRunningState: {
|
||||||
|
name: 'Project Counting',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultStyle: {
|
||||||
|
bgcolor: combineRgb(255, 0, 0),
|
||||||
|
color: combineRgb(0, 0, 0),
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'project',
|
||||||
|
type: 'dropdown',
|
||||||
|
label: 'Project',
|
||||||
|
default: -1,
|
||||||
|
choices: self.projects ?? [{ id: -1, label: 'None' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
callback: (feedback) => {
|
||||||
|
//self.log('debug', 'check project counting ' + feedback.options.project)
|
||||||
|
return feedback.options.project == self.currentEntry?.project_id
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ClientRunningState: {
|
||||||
|
name: 'Client Counting',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultStyle: {
|
||||||
|
bgcolor: combineRgb(255, 0, 0),
|
||||||
|
color: combineRgb(0, 0, 0),
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'client',
|
||||||
|
type: 'dropdown',
|
||||||
|
label: 'Client',
|
||||||
|
default: -1,
|
||||||
|
choices: self.clients ?? [{ id: -1, label: 'None' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
callback: (feedback) => {
|
||||||
|
//self.log('debug', 'check client counting ' + feedback.options.client)
|
||||||
|
// find the project that matches the project_id of the current entry and compare its client_id with the configured one
|
||||||
|
return feedback.options.client == self.projects?.find((p) => p.id == self.currentEntry?.project_id)?.clientID
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
311
src/main.ts
311
src/main.ts
@@ -1,69 +1,85 @@
|
|||||||
// toggltrack module
|
// toggltrack module
|
||||||
// Peter Daniel
|
// Peter Daniel, Matthias Kesler
|
||||||
|
|
||||||
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
|
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField, LogLevel } from '@companion-module/base'
|
||||||
import { GetConfigFields, type ModuleConfig } from './config.js'
|
import { GetConfigFields, type ModuleConfig } from './config.js'
|
||||||
import UpdateActions from './actions.js'
|
import { UpdatePresets } from './presets.js'
|
||||||
import UpdatePresets from './presets.js'
|
import { UpdateVariableDefinitions } from './variables.js'
|
||||||
import UpdateVariableDefinitions from './variables.js'
|
import { UpgradeScripts } from './upgrades.js'
|
||||||
import UpgradeScripts from './upgrades.js'
|
import { UpdateActions } from './actions.js'
|
||||||
import { Toggl, ITimeEntry, IWorkspaceProject } from 'toggl-track'
|
import { UpdateFeedbacks } from './feedbacks.js'
|
||||||
|
import { Toggl, ITimeEntry, IWorkspaceProject, IClient, isRatelimitError } from 'toggl-track'
|
||||||
import { togglGetWorkspaces } from './toggl-extend.js'
|
import { togglGetWorkspaces } from './toggl-extend.js'
|
||||||
|
import { timecodeSince } from './utils.js'
|
||||||
|
import { TogglStream } from './toggl-stream.js'
|
||||||
|
|
||||||
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||||
config!: ModuleConfig // Setup in init()
|
config!: ModuleConfig // Setup in init()
|
||||||
|
|
||||||
toggl?: Toggl
|
toggl?: Toggl
|
||||||
|
stream?: TogglStream
|
||||||
|
|
||||||
workspaceId?: number // current active workspace id
|
workspaceId?: number // current active workspace id
|
||||||
workspaceName: string = '' // name of workspace
|
workspaceName: string = '' // name of workspace
|
||||||
projects?: { id: number; label: string }[]
|
projects?: { id: number; label: string; clientID?: number }[]
|
||||||
intervalId?: NodeJS.Timeout
|
clients?: { id: number; label: string }[]
|
||||||
|
currentEntry?: ITimeEntry
|
||||||
|
|
||||||
|
intervalId?: NodeJS.Timeout // used for time entry poller and to postpone init on ratelimit
|
||||||
|
currentTimerUpdaterIntervalId?: NodeJS.Timeout // used to update the timer duration variable every second
|
||||||
|
|
||||||
constructor(internal: unknown) {
|
constructor(internal: unknown) {
|
||||||
super(internal)
|
super(internal)
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfigFields(): SomeCompanionConfigField[] {
|
|
||||||
return GetConfigFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
|
||||||
this.log('info', 'destroy ' + this.id)
|
|
||||||
if (this.config.startTimerPoller) {
|
|
||||||
this.stopTimeEntryPoller()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(config: ModuleConfig): Promise<void> {
|
async init(config: ModuleConfig): Promise<void> {
|
||||||
this.log('info', '--- init toggltrack ' + this.id + ' ---')
|
this.log('info', '--- init toggltrack ' + this.id + ' ---')
|
||||||
|
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
this.projects = [{ id: 0, label: 'None' }]
|
|
||||||
|
|
||||||
this.updateVariableDefinitions()
|
this.updateVariableDefinitions()
|
||||||
this.updatePresets()
|
this.updatePresets()
|
||||||
|
|
||||||
this.setVariableValues({
|
|
||||||
timerId: undefined,
|
|
||||||
timerDuration: undefined,
|
|
||||||
timerDescription: undefined,
|
|
||||||
lastTimerDuration: undefined,
|
|
||||||
workspace: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.initToggleConnection()
|
await this.initToggleConnection()
|
||||||
|
if (this.toggl && this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Retry init in ' + this.toggl.getRateLimitedUntil() + ' seconds')
|
||||||
|
this.intervalId = setTimeout(() => {
|
||||||
|
void this.init(this.config)
|
||||||
|
}, this.toggl.getRateLimitedUntil() * 1000)
|
||||||
|
// skip further init until ratelimit is over
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadStaticData()
|
||||||
|
|
||||||
this.updateActions()
|
this.updateActions()
|
||||||
|
this.updateFeedbacks()
|
||||||
|
|
||||||
if (this.toggl && this.workspaceId) {
|
if (this.toggl && this.workspaceId) {
|
||||||
this.updateStatus(InstanceStatus.Ok)
|
this.updateStatus(InstanceStatus.Ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.getCurrentTimer()
|
||||||
if (this.config.startTimerPoller) {
|
if (this.config.startTimerPoller) {
|
||||||
this.startTimeEntryPoller()
|
this.startTimeEntryPoller()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.startTogglWebsocket) {
|
||||||
|
await this.startToggleStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When module gets deleted
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
this.log('debug', 'destroy ' + this.id)
|
||||||
|
if (this.config.startTimerPoller) {
|
||||||
|
this.stopTimeEntryPoller()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||||
|
if (this.config.startTogglWebsocket && this.stream) {
|
||||||
|
await this.stream.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async configUpdated(config: ModuleConfig): Promise<void> {
|
async configUpdated(config: ModuleConfig): Promise<void> {
|
||||||
@@ -73,15 +89,30 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
|
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
|
||||||
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
|
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
|
||||||
|
|
||||||
|
const oldConfig = this.config
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
if (apiTokenChanged) {
|
if (apiTokenChanged) {
|
||||||
this.log('debug', 'api token changed. init new toggle connection')
|
this.log('debug', 'api token changed. init new toggle connection')
|
||||||
this.toggl = undefined
|
this.toggl = undefined
|
||||||
await this.initToggleConnection()
|
await this.initToggleConnection()
|
||||||
|
await this.loadStaticData()
|
||||||
} else if (workSpaceDefaultChanged) {
|
} else if (workSpaceDefaultChanged) {
|
||||||
this.log('debug', 'workspace default changed. reload workspaces')
|
this.log('debug', 'workspace default changed. reload workspaces')
|
||||||
await this.getWorkspace()
|
await this.loadStaticData()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.toggl && this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Retry init in ' + this.toggl.getRateLimitedUntil() + ' seconds')
|
||||||
|
this.config = oldConfig // revert to old config until ratelimit is over
|
||||||
|
this.intervalId = setTimeout(() => {
|
||||||
|
// this harms the linter (handle unawaited promise in an non-async context)
|
||||||
|
void (async () => {
|
||||||
|
await this.configUpdated(config)
|
||||||
|
})()
|
||||||
|
}, this.toggl.getRateLimitedUntil() * 1000)
|
||||||
|
// skip further init until ratelimit is over
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeEntryPollerChanged) {
|
if (timeEntryPollerChanged) {
|
||||||
@@ -91,6 +122,14 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.stopTimeEntryPoller()
|
this.stopTimeEntryPoller()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.config.startTogglWebsocket) {
|
||||||
|
await this.startToggleStream()
|
||||||
|
} else {
|
||||||
|
if (this.stream) {
|
||||||
|
await this.stream.destroy()
|
||||||
|
delete this.stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.updateActions()
|
this.updateActions()
|
||||||
//this.updateVariables()
|
//this.updateVariables()
|
||||||
@@ -98,6 +137,11 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.updateStatus(InstanceStatus.Ok)
|
this.updateStatus(InstanceStatus.Ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConfigFields(): SomeCompanionConfigField[] {
|
||||||
|
return GetConfigFields()
|
||||||
|
}
|
||||||
|
|
||||||
updateVariables(): void {
|
updateVariables(): void {
|
||||||
this.log('error', 'updateVariables not implemented')
|
this.log('error', 'updateVariables not implemented')
|
||||||
//throw new Error('Method not implemented.')
|
//throw new Error('Method not implemented.')
|
||||||
@@ -106,6 +150,10 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
UpdateActions(this)
|
UpdateActions(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFeedbacks(): void {
|
||||||
|
UpdateFeedbacks(this)
|
||||||
|
}
|
||||||
|
|
||||||
updatePresets(): void {
|
updatePresets(): void {
|
||||||
UpdatePresets(this)
|
UpdatePresets(this)
|
||||||
}
|
}
|
||||||
@@ -122,15 +170,25 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const resp = await this.toggl.me.logged()
|
try {
|
||||||
if (resp !== '') {
|
const resp = await this.toggl.me.logged()
|
||||||
this.log('warn', 'error during token check: ' + resp)
|
if (resp !== '') {
|
||||||
|
this.log('warn', 'error during token check: ' + resp)
|
||||||
|
this.toggl = undefined
|
||||||
|
this.updateStatus(InstanceStatus.AuthenticationFailure, resp as string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this.log('warn', 'error during token check: ' + (e as Error).message)
|
||||||
|
if (isRatelimitError(e)) {
|
||||||
|
this.log('warn', 'ratelimited. Will reset in' + e.resetAfterSeconds)
|
||||||
|
this.updateStatus(InstanceStatus.ConnectionFailure, `Ratelimited. Retry after ${e.resetAfterSeconds} seconds`)
|
||||||
|
return
|
||||||
|
}
|
||||||
this.toggl = undefined
|
this.toggl = undefined
|
||||||
this.updateStatus(InstanceStatus.AuthenticationFailure, resp)
|
this.updateStatus(InstanceStatus.AuthenticationFailure, (e as Error).message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.getWorkspace()
|
|
||||||
await this.getCurrentTimer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,13 +200,87 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
await this.getCurrentTimer()
|
await this.getCurrentTimer()
|
||||||
})()
|
})()
|
||||||
}, 30 * 1000)
|
}, this.config.timerPollerInterval * 1000)
|
||||||
}
|
}
|
||||||
private stopTimeEntryPoller(): void {
|
private stopTimeEntryPoller(): void {
|
||||||
this.log('info', 'Stopping TimeEntry-Poller')
|
this.log('info', 'Stopping TimeEntry-Poller')
|
||||||
clearInterval(this.intervalId)
|
clearInterval(this.intervalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async startToggleStream(): Promise<void> {
|
||||||
|
if (this.stream) {
|
||||||
|
await this.stream.destroy()
|
||||||
|
delete this.stream
|
||||||
|
}
|
||||||
|
this.stream = new TogglStream({
|
||||||
|
apiToken: this.config.wsToken!,
|
||||||
|
reconnect: true,
|
||||||
|
log: (level: LogLevel, message: string) => {
|
||||||
|
this.log(level, 'ws: ' + message)
|
||||||
|
},
|
||||||
|
onTimeEntryCreated: (e: ITimeEntry) => {
|
||||||
|
if (!e.stop) {
|
||||||
|
this.setCurrentlyRunningTimeEntry(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTimeEntryUpdate: (e: ITimeEntry) => {
|
||||||
|
if (e.id == this.currentEntry?.id && e.stop) {
|
||||||
|
// currently running timer got a stop entry
|
||||||
|
this.setCurrentlyRunningTimeEntry(undefined)
|
||||||
|
} else {
|
||||||
|
// store update on this time entry (e.g. rename)
|
||||||
|
this.setCurrentlyRunningTimeEntry(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.stream.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set variables to this time entry
|
||||||
|
* @param entry running entry or undefined
|
||||||
|
*/
|
||||||
|
private setCurrentlyRunningTimeEntry(entry: ITimeEntry | undefined): void {
|
||||||
|
this.currentEntry = entry
|
||||||
|
if (entry) {
|
||||||
|
const project = this.projects?.find((p) => p.id == entry.project_id)
|
||||||
|
this.setVariableValues({
|
||||||
|
timerId: entry.id,
|
||||||
|
timerDescription: entry.description,
|
||||||
|
timerDuration: timecodeSince(new Date(entry.start)),
|
||||||
|
timerProject: project?.label,
|
||||||
|
timerProjectID: entry.project_id,
|
||||||
|
timerClient: this.clients?.find((c) => c.id == project?.clientID)?.label ?? 'None',
|
||||||
|
timerClientID: project?.clientID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// in case there is on update thread running clear it
|
||||||
|
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||||
|
|
||||||
|
// Update timerDuration once per second
|
||||||
|
this.currentTimerUpdaterIntervalId = setInterval(() => {
|
||||||
|
// this harms the linter (handle unawaited promise in an non-async context)
|
||||||
|
void (async () => {
|
||||||
|
this.setVariableValues({
|
||||||
|
timerDuration: timecodeSince(new Date(entry.start)),
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
}, 1000) // update every second
|
||||||
|
} else {
|
||||||
|
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||||
|
this.setVariableValues({
|
||||||
|
timerId: undefined,
|
||||||
|
timerDescription: undefined,
|
||||||
|
timerDuration: undefined,
|
||||||
|
timerProject: undefined,
|
||||||
|
timerProjectID: undefined,
|
||||||
|
timerClient: undefined,
|
||||||
|
timerClientID: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.checkFeedbacks('ProjectRunningState', 'ClientRunningState')
|
||||||
|
}
|
||||||
|
|
||||||
async getCurrentTimer(): Promise<number | null> {
|
async getCurrentTimer(): Promise<number | null> {
|
||||||
this.log('debug', 'function: getCurrentTimer')
|
this.log('debug', 'function: getCurrentTimer')
|
||||||
|
|
||||||
@@ -156,36 +288,50 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.log('warn', 'Not authorized')
|
this.log('warn', 'Not authorized')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Skipping get current timer')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const entry: ITimeEntry = await this.toggl.timeEntry.current()
|
const entry: ITimeEntry = await this.toggl.timeEntry.current()
|
||||||
this.log('debug', 'response for timer id ' + JSON.stringify(entry))
|
this.log('debug', 'response for timer id ' + JSON.stringify(entry))
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
this.log('info', 'Current timer id: ' + entry.id)
|
this.log('info', 'Current timer id: ' + entry.id)
|
||||||
this.setVariableValues({
|
this.setCurrentlyRunningTimeEntry(entry)
|
||||||
timerId: entry.id,
|
|
||||||
timerDescription: entry.description,
|
|
||||||
timerDuration: entry.duration,
|
|
||||||
})
|
|
||||||
|
|
||||||
return entry.id
|
return entry.id
|
||||||
} else {
|
} else {
|
||||||
this.log('info', 'No current timer')
|
this.log('info', 'No current timer')
|
||||||
this.setVariableValues({
|
this.setCurrentlyRunningTimeEntry(undefined)
|
||||||
timerId: undefined,
|
|
||||||
timerDescription: undefined,
|
|
||||||
timerDuration: undefined,
|
|
||||||
})
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspace(): Promise<void> {
|
async loadStaticData(): Promise<void> {
|
||||||
|
if (!this.toggl) {
|
||||||
|
this.log('warn', 'loadStaticData: toggle connection not set up')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Skipping get current timer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.getWorkspace()
|
||||||
|
await this.getProjects()
|
||||||
|
await this.getClients()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getWorkspace(): Promise<void> {
|
||||||
this.log('debug', 'function: getWorkspace')
|
this.log('debug', 'function: getWorkspace')
|
||||||
if (!this.toggl) {
|
if (!this.toggl) {
|
||||||
this.log('warn', 'Not authorized')
|
this.log('warn', 'Not authorized')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Skipping get current timer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
this.workspaceId = undefined
|
this.workspaceId = undefined
|
||||||
@@ -210,7 +356,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
if (this.workspaceId == undefined) {
|
if (this.workspaceId == undefined) {
|
||||||
// no workspace found
|
// no workspace found
|
||||||
this.log('debug', 'workspace not found. Response: ' + JSON.stringify(workspaces))
|
this.log('debug', 'workspace not found. Response: ' + JSON.stringify(workspaces))
|
||||||
this.updateStatus(InstanceStatus.BadConfig, 'No workspace found')
|
this.updateStatus(InstanceStatus.BadConfig, 'Available Workspaces: ' + workspaces.map((ws) => ws.name).join(','))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +366,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await this.getProjects()
|
await this.getProjects()
|
||||||
|
await this.getClients()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(): Promise<void> {
|
async getProjects(): Promise<void> {
|
||||||
@@ -232,11 +379,9 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
|
|
||||||
const projects: IWorkspaceProject[] = await this.toggl!.projects.list(this.workspaceId)
|
const projects: IWorkspaceProject[] = await this.toggl!.projects.list(this.workspaceId)
|
||||||
|
|
||||||
//const projects: IWorkspaceProject[] = await togglGetProjects(this.toggl!, this.workspaceId!)
|
|
||||||
|
|
||||||
if (typeof projects === 'string' || projects.length == 0) {
|
if (typeof projects === 'string' || projects.length == 0) {
|
||||||
this.log('debug', 'No projects found')
|
this.log('debug', 'No projects found')
|
||||||
this.projects = [{ id: 0, label: 'None' }]
|
this.projects = undefined
|
||||||
this.log('debug', 'projects response' + JSON.stringify(projects))
|
this.log('debug', 'projects response' + JSON.stringify(projects))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -247,6 +392,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
return {
|
return {
|
||||||
id: p.id,
|
id: p.id,
|
||||||
label: p.name,
|
label: p.name,
|
||||||
|
clientID: p.client_id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@@ -265,11 +411,56 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.log('debug', 'Projects: ' + JSON.stringify(this.projects))
|
this.log('debug', 'Projects: ' + JSON.stringify(this.projects))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getClients(): Promise<void> {
|
||||||
|
this.log('debug', 'function: getClients ' + this.workspaceId)
|
||||||
|
|
||||||
|
if (!this.workspaceId) {
|
||||||
|
this.log('warn', 'workspaceId undefined')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const clients: IClient[] = await this.toggl!.me.clients()
|
||||||
|
|
||||||
|
if (typeof clients === 'string' || clients.length == 0) {
|
||||||
|
this.log('debug', 'No clients found')
|
||||||
|
this.clients = undefined
|
||||||
|
this.log('debug', 'clients response' + JSON.stringify(clients))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clients = clients
|
||||||
|
.filter((c) => c.wid == this.workspaceId)
|
||||||
|
.map((c) => {
|
||||||
|
return {
|
||||||
|
id: c.id,
|
||||||
|
label: c.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const fa = a.label.toLowerCase()
|
||||||
|
const fb = b.label.toLowerCase()
|
||||||
|
|
||||||
|
if (fa < fb) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (fa > fb) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
this.log('debug', 'Clients: ' + JSON.stringify(this.clients))
|
||||||
|
}
|
||||||
|
|
||||||
async startTimer(project: number, description: string): Promise<void> {
|
async startTimer(project: number, description: string): Promise<void> {
|
||||||
if (!this.toggl || !this.workspaceId) {
|
if (!this.toggl || !this.workspaceId) {
|
||||||
this.log('error', 'toggle not initialized. Do not start time')
|
this.log('error', 'toggle not initialized. Do not start time')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Skipping get current timer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const currentId = await this.getCurrentTimer()
|
const currentId = await this.getCurrentTimer()
|
||||||
let newEntry: ITimeEntry
|
let newEntry: ITimeEntry
|
||||||
@@ -284,11 +475,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
project_id: project != 0 ? project : undefined,
|
project_id: project != 0 ? project : undefined,
|
||||||
})
|
})
|
||||||
this.log('info', 'New timer started ' + newEntry.id + ' ' + newEntry.description)
|
this.log('info', 'New timer started ' + newEntry.id + ' ' + newEntry.description)
|
||||||
this.setVariableValues({
|
this.setCurrentlyRunningTimeEntry(newEntry)
|
||||||
timerId: newEntry.id,
|
|
||||||
timerDescription: newEntry.description,
|
|
||||||
timerDuration: newEntry.duration,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
this.log('info', 'A timer is already running ' + currentId + ' not starting a new one!')
|
this.log('info', 'A timer is already running ' + currentId + ' not starting a new one!')
|
||||||
}
|
}
|
||||||
@@ -301,6 +488,10 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.log('error', 'toggle not initialized. Do not start time')
|
this.log('error', 'toggle not initialized. Do not start time')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.toggl.getRateLimitedUntil() > 0) {
|
||||||
|
this.log('warn', 'Ratelimited. Skipping get current timer')
|
||||||
|
return
|
||||||
|
}
|
||||||
const currentId = await this.getCurrentTimer()
|
const currentId = await this.getCurrentTimer()
|
||||||
this.log('info', 'Trying to stop current timer id: ' + currentId)
|
this.log('info', 'Trying to stop current timer id: ' + currentId)
|
||||||
|
|
||||||
@@ -308,10 +499,8 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
const updated: ITimeEntry = await this.toggl.timeEntry.stop(currentId, this.workspaceId)
|
const updated: ITimeEntry = await this.toggl.timeEntry.stop(currentId, this.workspaceId)
|
||||||
this.log('info', 'Stopped ' + updated.id + ', duration ' + updated.duration)
|
this.log('info', 'Stopped ' + updated.id + ', duration ' + updated.duration)
|
||||||
|
|
||||||
|
this.setCurrentlyRunningTimeEntry(undefined)
|
||||||
this.setVariableValues({
|
this.setVariableValues({
|
||||||
timerId: undefined,
|
|
||||||
timerDescription: undefined,
|
|
||||||
timerDuration: undefined,
|
|
||||||
lastTimerDuration: updated.duration,
|
lastTimerDuration: updated.duration,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { combineRgb } from '@companion-module/base'
|
import { combineRgb } from '@companion-module/base'
|
||||||
import { TogglTrack } from './main.js'
|
import { TogglTrack } from './main.js'
|
||||||
|
|
||||||
export default function (self: TogglTrack): void {
|
export function UpdatePresets(self: TogglTrack): void {
|
||||||
self.setPresetDefinitions({
|
self.setPresetDefinitions({
|
||||||
Start: {
|
Start: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|||||||
143
src/toggl-stream.ts
Normal file
143
src/toggl-stream.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { LogLevel } from '@companion-module/base'
|
||||||
|
import { ITimeEntry } from 'toggl-track'
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
|
||||||
|
interface IToggleStreamConfig {
|
||||||
|
apiToken: string
|
||||||
|
reconnect?: boolean
|
||||||
|
onMessage?: (body: string) => void
|
||||||
|
log: (level: LogLevel, message: string) => void
|
||||||
|
onTimeEntryCreated: (entry: ITimeEntry) => void
|
||||||
|
onTimeEntryUpdate: (entry: ITimeEntry) => void
|
||||||
|
onTimeEntryDelete?: (entry: ITimeEntry) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = 'wss://doh.krombel.de/toggl-websocket'
|
||||||
|
|
||||||
|
export class TogglStream {
|
||||||
|
ws?: WebSocket
|
||||||
|
apiToken: string
|
||||||
|
reconnect: boolean
|
||||||
|
ping_timer?: NodeJS.Timeout
|
||||||
|
reconnect_timer?: NodeJS.Timeout
|
||||||
|
|
||||||
|
log: (level: LogLevel, message: string) => void
|
||||||
|
onTimeEntryCreated: (entry: ITimeEntry) => void
|
||||||
|
onTimeEntryUpdate: (entry: ITimeEntry) => void
|
||||||
|
onTimeEntryDelete?: (entry: ITimeEntry) => void
|
||||||
|
|
||||||
|
constructor({ apiToken, reconnect, log, onTimeEntryCreated, onTimeEntryUpdate }: IToggleStreamConfig) {
|
||||||
|
this.apiToken = apiToken
|
||||||
|
this.reconnect = Boolean(reconnect)
|
||||||
|
this.log = log
|
||||||
|
this.onTimeEntryCreated = onTimeEntryCreated
|
||||||
|
this.onTimeEntryUpdate = onTimeEntryUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
clearInterval(this.ping_timer)
|
||||||
|
this.ping_timer = undefined
|
||||||
|
if (this.reconnect_timer) {
|
||||||
|
clearTimeout(this.reconnect_timer)
|
||||||
|
this.reconnect_timer = undefined
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000)
|
||||||
|
delete this.ws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
this.log('debug', 'startToggleStream')
|
||||||
|
if (this.ping_timer) {
|
||||||
|
clearInterval(this.ping_timer)
|
||||||
|
this.ping_timer = undefined
|
||||||
|
}
|
||||||
|
if (this.reconnect_timer) {
|
||||||
|
clearTimeout(this.reconnect_timer)
|
||||||
|
this.reconnect_timer = undefined
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000)
|
||||||
|
delete this.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws = new WebSocket(url)
|
||||||
|
this.ws.addEventListener('open', () => {
|
||||||
|
this.log('debug', 'websocket connection opened')
|
||||||
|
|
||||||
|
this.ws?.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'authenticate',
|
||||||
|
api_token: this.apiToken,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
this.ws.on('close', (code) => {
|
||||||
|
this.log('debug', `websocket connection closed with code ${code}`)
|
||||||
|
})
|
||||||
|
this.ws.on('error', (err) => {
|
||||||
|
this.log('debug', `websocket connection errored with code ${err.message}`)
|
||||||
|
this.maybeReconnect()
|
||||||
|
})
|
||||||
|
this.ws.on('message', this.onMessageReceived.bind(this))
|
||||||
|
this.ping_timer = setInterval(() => {
|
||||||
|
this.ws?.ping()
|
||||||
|
}, 25000) // send ping every 25 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
private maybeReconnect(): void {
|
||||||
|
if (this.reconnect) {
|
||||||
|
clearTimeout(this.reconnect_timer)
|
||||||
|
this.reconnect_timer = setTimeout(() => {
|
||||||
|
this.start()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessageReceived(data: WebSocket.RawData, isBinary: boolean): void {
|
||||||
|
this.log('debug', 'Got ' + (isBinary ? 'binary' : 'string') + ' message: ' + JSON.stringify(data))
|
||||||
|
|
||||||
|
const message = (data as Buffer).toString()
|
||||||
|
let msgValue // IToggleStreamPing | string
|
||||||
|
try {
|
||||||
|
msgValue = JSON.parse(message)
|
||||||
|
} catch (e) {
|
||||||
|
this.log('warn', 'Failed parsing message' + JSON.stringify(e))
|
||||||
|
msgValue = data
|
||||||
|
}
|
||||||
|
this.log('debug', 'parsed message: ' + JSON.stringify(msgValue))
|
||||||
|
if ('type' in msgValue && msgValue.type == 'ping') {
|
||||||
|
this.ws?.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'pong',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('action' in msgValue) {
|
||||||
|
switch (msgValue.action) {
|
||||||
|
case 'update': // stop:
|
||||||
|
// {"action":"update","data":{"id":3766092771,"wid":3516429,"duration":4115,"duration_diff":4115,"pid":193750968,"project_name":"Notizen","project_color":"#c7af14","project_active":true,"project_billable":false,"tid":null,"tag_ids":null,"tags":[],"description":"StreamDeck","billable":false,"duronly":true,"start":"2025-01-17T14:14:31Z","stop":"2025-01-17T15:23:06Z","shared_with":null,"at":"2025-01-17T15:23:06.279165Z"},"model":"time_entry"}
|
||||||
|
if ('model' in msgValue && msgValue.model == 'time_entry') {
|
||||||
|
this.onTimeEntryUpdate(msgValue.data as ITimeEntry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'insert': // start
|
||||||
|
// {"action":"insert","data":{"id":3766232279,"wid":3516429,"duration":-1,"pid":193750968,"project_name":"Notizen","project_color":"#c7af14","project_active":true,"project_billable":false,"tid":null,"tag_ids":null,"tags":[],"description":"StreamDeck","billable":false,"duronly":true,"start":"2025-01-17T15:29:32Z","stop":null,"shared_with":null,"at":"2025-01-17T15:29:32.533896Z"},"model":"time_entry"}
|
||||||
|
if ('model' in msgValue && msgValue.model == 'time_entry') {
|
||||||
|
this.onTimeEntryCreated(msgValue.data as ITimeEntry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
if ('model' in msgValue && msgValue.model == 'time_entry') {
|
||||||
|
this.onTimeEntryDelete!(msgValue.data as ITimeEntry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
export default [
|
import type { CompanionStaticUpgradeScript } from '@companion-module/base'
|
||||||
|
import type { ModuleConfig } from './config.js'
|
||||||
|
|
||||||
|
export const UpgradeScripts: CompanionStaticUpgradeScript<ModuleConfig>[] = [
|
||||||
/*
|
/*
|
||||||
* Place your upgrade scripts here
|
* Place your upgrade scripts here
|
||||||
* Remember that once it has been added it cannot be removed!
|
* Remember that once it has been added it cannot be removed!
|
||||||
|
|||||||
12
src/utils.ts
Normal file
12
src/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* returns formatted timecode from date until now
|
||||||
|
* @param date start date
|
||||||
|
* @returns formatted string 0:00:00 - where hours and minutes are hidden if 0
|
||||||
|
*/
|
||||||
|
export function timecodeSince(date: Date): string {
|
||||||
|
const dateObj = new Date(Date.now() - date.getTime())
|
||||||
|
const hours = dateObj.getUTCHours()
|
||||||
|
const minutes = `0${dateObj.getUTCMinutes()}`.slice(-2)
|
||||||
|
const seconds = `0${dateObj.getSeconds()}`.slice(-2)
|
||||||
|
return (hours > 0 ? hours + ':' : '') + (hours > 0 || minutes !== '00' ? minutes + ':' : '') + seconds
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TogglTrack } from './main.js'
|
import { TogglTrack } from './main.js'
|
||||||
|
|
||||||
export default function (self: TogglTrack): void {
|
export function UpdateVariableDefinitions(self: TogglTrack): void {
|
||||||
self.setVariableDefinitions([
|
self.setVariableDefinitions([
|
||||||
{
|
{
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
@@ -22,5 +22,21 @@ export default function (self: TogglTrack): void {
|
|||||||
name: 'Current Timer Description',
|
name: 'Current Timer Description',
|
||||||
variableId: 'timerDescription',
|
variableId: 'timerDescription',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Current Timer Project ID',
|
||||||
|
variableId: 'timerProjectID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Current Timer Project',
|
||||||
|
variableId: 'timerProject',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Current Timer Client ID',
|
||||||
|
variableId: 'timerClientID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Current Timer Client',
|
||||||
|
variableId: 'timerClient',
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@companion-module/tools/tsconfig/node18/recommended",
|
"extends": "@companion-module/tools/tsconfig/node22/recommended",
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"],
|
"exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules/**"],
|
"exclude": ["node_modules/**"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["jest", "node"]
|
"types": ["node" /* , "jest" ] // uncomment this if using jest */]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user