Compare commits
10 Commits
8cd5a65542
...
b0e40e66a7
| Author | SHA1 | Date | |
|---|---|---|---|
| b0e40e66a7 | |||
| 0ab9fd495f | |||
| 0e8d7ffa32 | |||
| 168d938435 | |||
| 6b8b0a9097 | |||
| 8faa0443a5 | |||
| 2e885a890d | |||
| 96e1d0f10a | |||
| 3aa54c4474 | |||
| c62a74a635 |
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
|
||||||
|
|||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lint-staged
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# 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)
|
||||||
|
|||||||
@@ -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,37 @@ 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
|
||||||
|
|||||||
29
package.json
29
package.json
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "toggl-track",
|
"name": "toggl-track",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"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": "yarn run build && companion-module-build",
|
||||||
"build": "rimraf dist && yarn run build:main",
|
"build": "rimraf dist && yarn run build:main",
|
||||||
@@ -17,19 +18,27 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.14",
|
||||||
|
"yarn": "^4"
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@companion-module/base": "~1.11",
|
"@companion-module/base": "~1.11.3",
|
||||||
"toggl-track": "^0.8.0"
|
"toggl-track": "^0.8.0",
|
||||||
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@companion-module/tools": "~2.1.1",
|
"@companion-module/tools": "~2.3.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.14.1",
|
||||||
"eslint": "^9.17.0",
|
"@types/ws": "^8",
|
||||||
"prettier": "^3.4.2",
|
"eslint": "^9.24.0",
|
||||||
"rimraf": "^5.0.10",
|
"husky": "^9.1.7",
|
||||||
"typescript": "~5.5.4",
|
"lint-staged": "^15.5.1",
|
||||||
"typescript-eslint": "^8.18.1"
|
"prettier": "^3.5.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.30.1"
|
||||||
},
|
},
|
||||||
"prettier": "@companion-module/tools/.prettierrc.json",
|
"prettier": "@companion-module/tools/.prettierrc.json",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
startWebSocket: boolean
|
||||||
|
websocketSecret: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetConfigFields(): SomeCompanionConfigField[] {
|
export function GetConfigFields(): SomeCompanionConfigField[] {
|
||||||
@@ -36,9 +39,32 @@ 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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
id: 'startWebSocket',
|
||||||
|
label: 'start Websocket (krombel testing)',
|
||||||
|
width: 12,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'textinput',
|
||||||
|
id: 'websocketSecret',
|
||||||
|
label: 'secret to authenticate against Websocket server (krombel testing)',
|
||||||
|
width: 12,
|
||||||
|
default: 'secret',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
260
src/main.ts
260
src/main.ts
@@ -1,5 +1,5 @@
|
|||||||
// toggltrack module
|
// toggltrack module
|
||||||
// Peter Daniel
|
// Peter Daniel, Matthias Kesler
|
||||||
|
|
||||||
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
|
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
|
||||||
import { GetConfigFields, type ModuleConfig } from './config.js'
|
import { GetConfigFields, type ModuleConfig } from './config.js'
|
||||||
@@ -7,8 +7,11 @@ 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 { Toggl, ITimeEntry, IWorkspaceProject } from 'toggl-track'
|
import { UpdateFeedbacks } from './feedbacks.js'
|
||||||
import { togglGetWorkspaces } from './toggl-extend.js'
|
import { Toggl, ITimeEntry, IWorkspaceProject, IClient } from 'toggl-track'
|
||||||
|
import { isITimeEntryWebhookPayload, togglGetWorkspaces } from './toggl-extend.js'
|
||||||
|
import { timecodeSince } from './utils.js'
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
|
||||||
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||||
config!: ModuleConfig // Setup in init()
|
config!: ModuleConfig // Setup in init()
|
||||||
@@ -17,8 +20,15 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
|
|
||||||
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 }[]
|
||||||
|
clients?: { id: number; label: string }[]
|
||||||
|
currentEntry?: ITimeEntry
|
||||||
|
|
||||||
intervalId?: NodeJS.Timeout
|
intervalId?: NodeJS.Timeout
|
||||||
|
currentTimerUpdaterIntervalId?: NodeJS.Timeout
|
||||||
|
wsReconnectTimer?: NodeJS.Timeout
|
||||||
|
|
||||||
|
ws?: WebSocket
|
||||||
|
|
||||||
constructor(internal: unknown) {
|
constructor(internal: unknown) {
|
||||||
super(internal)
|
super(internal)
|
||||||
@@ -33,6 +43,12 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
if (this.config.startTimerPoller) {
|
if (this.config.startTimerPoller) {
|
||||||
this.stopTimeEntryPoller()
|
this.stopTimeEntryPoller()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.startWebSocket) {
|
||||||
|
this.stopWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(config: ModuleConfig): Promise<void> {
|
async init(config: ModuleConfig): Promise<void> {
|
||||||
@@ -40,30 +56,27 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
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.startWebSocket) {
|
||||||
|
this.startWebSocket()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async configUpdated(config: ModuleConfig): Promise<void> {
|
async configUpdated(config: ModuleConfig): Promise<void> {
|
||||||
@@ -72,6 +85,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
|
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
|
||||||
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 webSocketChanged: boolean = this.config.startWebSocket != config.startWebSocket
|
||||||
|
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
@@ -79,9 +93,10 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
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 (timeEntryPollerChanged) {
|
if (timeEntryPollerChanged) {
|
||||||
@@ -91,6 +106,13 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.stopTimeEntryPoller()
|
this.stopTimeEntryPoller()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (webSocketChanged) {
|
||||||
|
if (this.config.startWebSocket) {
|
||||||
|
this.startWebSocket()
|
||||||
|
} else {
|
||||||
|
this.stopWebSocket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.updateActions()
|
this.updateActions()
|
||||||
//this.updateVariables()
|
//this.updateVariables()
|
||||||
@@ -105,6 +127,9 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
updateActions(): void {
|
updateActions(): void {
|
||||||
UpdateActions(this)
|
UpdateActions(this)
|
||||||
}
|
}
|
||||||
|
updateFeedbacks(): void {
|
||||||
|
UpdateFeedbacks(this)
|
||||||
|
}
|
||||||
|
|
||||||
updatePresets(): void {
|
updatePresets(): void {
|
||||||
UpdatePresets(this)
|
UpdatePresets(this)
|
||||||
@@ -129,8 +154,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.updateStatus(InstanceStatus.AuthenticationFailure, resp)
|
this.updateStatus(InstanceStatus.AuthenticationFailure, resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.getWorkspace()
|
|
||||||
await this.getCurrentTimer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,13 +165,131 @@ 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 startWebSocket(): void {
|
||||||
|
this.log('info', 'start websocket')
|
||||||
|
|
||||||
|
this.updateStatus(InstanceStatus.Connecting)
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000)
|
||||||
|
delete this.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws = new WebSocket('wss://doh.krombel.de/toggl-websocket')
|
||||||
|
this.ws.addEventListener('open', () => {
|
||||||
|
this.updateStatus(InstanceStatus.Ok)
|
||||||
|
this.ws?.send(this.config.websocketSecret)
|
||||||
|
})
|
||||||
|
this.ws.addEventListener('close', (code) => {
|
||||||
|
this.log('debug', `Connection closed with code ${code.code}`)
|
||||||
|
this.updateStatus(InstanceStatus.Disconnected, `WS-Connection closed with code ${code.code}`)
|
||||||
|
this.maybeReconnectWebSocket()
|
||||||
|
})
|
||||||
|
this.ws.addEventListener('message', this.websocketMessageHandler.bind(this))
|
||||||
|
this.ws.addEventListener('error', (data) => {
|
||||||
|
this.log('error', `WebSocket error: ${data.error}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopWebSocket(): void {
|
||||||
|
this.log('info', 'stop websocket')
|
||||||
|
|
||||||
|
if (this.wsReconnectTimer) {
|
||||||
|
clearTimeout(this.wsReconnectTimer)
|
||||||
|
this.wsReconnectTimer = undefined
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000)
|
||||||
|
delete this.ws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private maybeReconnectWebSocket() {
|
||||||
|
if (this.wsReconnectTimer) {
|
||||||
|
clearTimeout(this.wsReconnectTimer)
|
||||||
|
}
|
||||||
|
this.wsReconnectTimer = setTimeout(() => {
|
||||||
|
this.startWebSocket()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private websocketMessageHandler(data: WebSocket.MessageEvent) {
|
||||||
|
this.log('info', 'Websocket message received')
|
||||||
|
|
||||||
|
if (typeof data.data === 'string') {
|
||||||
|
this.log('debug', `data: ${data.data}`)
|
||||||
|
const event = JSON.parse(data.data)
|
||||||
|
if (isITimeEntryWebhookPayload(event)) {
|
||||||
|
const entry = event.payload
|
||||||
|
if (entry.stop === '' && event.metadata.action != 'deleted') {
|
||||||
|
this.log('info', `update time entry to ${entry.description}`)
|
||||||
|
this.setCurrentlyRunningTimeEntry(entry)
|
||||||
|
} else if (entry.id == this.currentEntry?.id) {
|
||||||
|
// only unset value if update is for current entry
|
||||||
|
this.log('info', 'stop time entry')
|
||||||
|
this.setCurrentlyRunningTimeEntry(undefined)
|
||||||
|
} else {
|
||||||
|
this.log('warn', `unhandled case for time entry ${JSON.stringify(entry)}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.log('warn', `unhandled event ${data.data}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.log('warn', `unhandled websocket event '${JSON.stringify(data)}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
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')
|
||||||
|
|
||||||
@@ -162,25 +303,27 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
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')
|
||||||
@@ -210,7 +353,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 +363,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 +376,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 +389,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,6 +408,47 @@ 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')
|
||||||
@@ -284,11 +468,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!')
|
||||||
}
|
}
|
||||||
@@ -308,10 +488,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,4 +1,4 @@
|
|||||||
import { Toggl } from 'toggl-track'
|
import { ITimeEntry, Toggl } from 'toggl-track'
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
id: number
|
id: number
|
||||||
@@ -9,3 +9,55 @@ export async function togglGetWorkspaces(toggl: Toggl): Promise<IWorkspace[]> {
|
|||||||
const resp: IWorkspace[] = await toggl.request<IWorkspace[]>('workspaces', {})
|
const resp: IWorkspace[] = await toggl.request<IWorkspace[]>('workspaces', {})
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWebhookMetadata {
|
||||||
|
action: 'created' | 'updated' | 'deleted'
|
||||||
|
event_user_id: number
|
||||||
|
entity_id: number
|
||||||
|
model: 'time_entry'
|
||||||
|
workspace_id: number
|
||||||
|
project_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimeEntryWebhookPayload {
|
||||||
|
event_id: number
|
||||||
|
created_at: string
|
||||||
|
creator_id: number
|
||||||
|
metadata: IWebhookMetadata
|
||||||
|
payload: ITimeEntry
|
||||||
|
subscription_id: number
|
||||||
|
timestamp: string
|
||||||
|
url_callback: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isITimeEntryWebhookPayload(event: unknown): event is ITimeEntryWebhookPayload {
|
||||||
|
return (
|
||||||
|
typeof event === 'object' &&
|
||||||
|
event != null &&
|
||||||
|
'metadata' in event &&
|
||||||
|
typeof event.metadata === 'object' &&
|
||||||
|
event.metadata !== null &&
|
||||||
|
'model' in event.metadata &&
|
||||||
|
typeof event.metadata.model === 'string' &&
|
||||||
|
event.metadata.model === 'time_entry' &&
|
||||||
|
'payload' in event &&
|
||||||
|
typeof event.payload === 'object' &&
|
||||||
|
event.payload !== null &&
|
||||||
|
isITimeEntry(event.payload)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isITimeEntry(entry: unknown): entry is ITimeEntry {
|
||||||
|
return (
|
||||||
|
typeof entry === 'object' &&
|
||||||
|
entry != null &&
|
||||||
|
'id' in entry &&
|
||||||
|
typeof entry.id === 'number' &&
|
||||||
|
'description' in entry &&
|
||||||
|
typeof entry.description === 'string' &&
|
||||||
|
'project_id' in entry &&
|
||||||
|
typeof entry.project_id === 'number' &&
|
||||||
|
'start' in entry &&
|
||||||
|
typeof entry.start === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user