Merge pull request #5 from bitfocus/companion-v3-compatibility

Companion v3 compatibility
This commit is contained in:
Peter
2023-06-29 13:12:12 +01:00
committed by GitHub
10 changed files with 604 additions and 419 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
node_modules/
package-lock.json
.nova
archive
.DS_Store

51
actions.js Normal file
View File

@@ -0,0 +1,51 @@
export function updateActions() {
let actions = {}
actions['startNewTimer'] = {
name: 'Start New Timer',
options: [
{
type: 'textinput',
label: 'Description',
id: 'description',
default: '',
},
{
type: 'dropdown',
label: 'Project',
id: 'project',
default: '0',
choices: this.projects,
},
],
callback: ({ options }) => {
this.startTimer(options.project, options.description)
},
}
actions['getCurrentTimer'] = {
name: 'Get Current Timer',
options: [],
callback: (action) => {
this.getCurrentTimer()
},
}
actions['stopCurrentTimer'] = {
name: 'Stop Current Timer',
options: [],
callback: (action) => {
this.stopTimer()
},
}
actions['refreshProjects'] = {
name: 'Refresh Project List',
options: [],
callback: (action) => {
this.getWorkspace()
},
}
this.setActionDefinitions(actions)
}

View File

@@ -26,7 +26,7 @@ Attempt to stop the current timer. This will fail if Companion doesn't know the
**Refresh Project List**
Retrives the current list of projects from the toggl server. This action runs automatically when Companion starts and when the module is enabled.
Retrieves the current list of projects from the toggl server. This action runs automatically when Companion starts and when the module is enabled.
## Presets
@@ -49,3 +49,8 @@ Add 'Always start' configuration option
### Version 1.0.3
Add variables for timerId and timerDescription
### Version 2.0.0
Updated for Companion version 3
Updated for toggl API version 9

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

31
companion/manifest.json Normal file
View File

@@ -0,0 +1,31 @@
{
"id": "toggl-track",
"name": "toggl-track",
"shortname": "toggl",
"description": "Companion module for toggltrack timers",
"version": "2.0.0",
"license": "MIT",
"repository": "git+https://github.com/bitfocus/companion-module-toggl-track.git",
"bugs": "https://github.com/bitfocus/companion-module-toggl-track/issues",
"maintainers": [
{
"name": "Peter Daniel"
}
],
"legacyIds": [
"toggl-track"
],
"runtime": {
"type": "node18",
"api": "nodejs-ipc",
"apiVersion": "0.0.0",
"entrypoint": "../index.js"
},
"manufacturer": "Toggl",
"products": [
"Track"
],
"keywords": [
"Logging", "Timer", "Task Tracking", "Time Tracking", "Project Management"
]
}

702
index.js
View File

@@ -1,72 +1,26 @@
var instance_skel = require('../../instance_skel')
// toggltrack module
// Peter Daniel
var debug
var log
import { InstanceBase, Regex, runEntrypoint, InstanceStatus } from '@companion-module/base'
import { updateActions } from './actions.js'
import { updatePresets } from './presets.js'
import { updateVariables } from './variables.js'
import { upgradeScripts } from './upgrades.js'
import got from 'got'
function instance(system, id, config) {
var self = this
class toggltrack extends InstanceBase {
constructor(internal) {
super(internal)
// super-constructor
instance_skel.apply(this, arguments)
this.updateActions = updateActions.bind(this)
this.updatePresets = updatePresets.bind(this)
this.updateVariables = updateVariables.bind(this)
return self
}
instance.prototype.updateConfig = function (config) {
var self = this
self.config = config
self.auth()
self.getWorkspace()
self.actions()
}
instance.prototype.init = function () {
var self = this
debug = self.debug
log = self.log
self.workspace = null
self.workspaceName = null
self.projects = [{ id: '0', label: 'None' }]
self.init_presets()
self.update_variables()
self.auth()
self.getWorkspace()
self.getCurrentTimer().then((timerId) => {
self.setVariable('timerId', timerId.id)
self.setVariable('timerDescription', timerId.description)
})
self.actions()
}
instance.prototype.auth = function () {
var self = this
self.header = []
if (self.config.apiToken !== null && self.config.apiToken.length > 0) {
auth = Buffer.from(self.config.apiToken + ':' + 'api_token').toString('base64')
self.header['Content-Type'] = 'application/json'
self.header['Authorization'] = 'Basic ' + auth
} else {
self.log('warn', 'Please enter your toggl API token')
}
// console.log(self.header)
}
instance.prototype.config_fields = function () {
var self = this
getConfigFields() {
// console.log('config fields')
return [
{
type: 'text',
id: 'info',
width: 12,
label: 'Information',
value: 'This module is for the toggl track service',
},
{
type: 'textinput',
id: 'apiToken',
@@ -77,260 +31,198 @@ instance.prototype.config_fields = function () {
{
type: 'checkbox',
id: 'alwaysStart',
label: 'Enable',
width: 1,
default: false,
},
{
type: 'text',
id: 'alwaysStartTxt',
label: 'Always start a new timer even if there is one already running',
width: 11,
},
{
type: 'text',
id: 'break',
label: '',
width: 12,
default: false,
},
]
}
instance.prototype.destroy = function () {
var self = this
debug('destroy', self.id)
async destroy() {
console.log('destroy', this.id)
}
instance.prototype.update_variables = function (system) {
var self = this
var variables = []
async init(config) {
console.log('--- init toggltrack ---')
this.prefixUrl = 'https://api.track.toggl.com/api/v9/'
variables.push(
{
label: 'Current Timer Id',
name: 'timerId',
},
{
label: 'Current Timer Description',
name: 'timerDescription',
this.config = config
this.gotOptions = {
responseType: 'json',
throwHttpErrors: false
}
this.gotOptions.prefixUrl = this.prefixUrl
this.workspace = null
this.workspaceName = null
this.projects = [{ id: '0', label: 'None' }]
this.updateVariables()
this.updatePresets()
this.setVariableValues({
timerId: null,
timerDuration: null,
timerDescription: null,
lastTimerDuration: null,
workspace: null,
})
this.gotOptions.headers = this.auth()
if (this.gotOptions.headers != null) {
this.getWorkspace().then(
this.getCurrentTimer()
)
}
this.updateActions()
}
async configUpdated(config) {
console.log('config updated')
this.config = config
this.gotOptions.headers = this.auth()
if (this.gotOptions.headers != null) {
this.getWorkspace().then(
this.getCurrentTimer()
)
}
this.updateActions()
this.updateVariables()
}
auth() {
if (this.config.apiToken !== null && this.config.apiToken.length > 0) {
let auth = Buffer.from(this.config.apiToken + ':' + 'api_token').toString('base64')
let headers = {}
headers['Content-Type'] = 'application/json'
headers['authorization'] = 'Basic ' + auth
return headers
} else {
this.log('warn', 'Please enter your toggl API token')
return null
}
// console.log(this.gotOptions)
}
async getCurrentTimer() {
console.log('function: getCurrentTimer')
if (this.gotOptions.headers == null) {
this.log('warn', 'Not authorized')
return
}
let cmd = 'me/time_entries/current'
return new Promise((resolve, reject) => {
this.sendGetCommand(cmd).then(
(result) => {
if (typeof result === 'object' && result !== null) {
if ('id' in result) {
this.setVariableValues({
timerId: result.id,
timerDescription: result.description,
timerDuration: result.duration,
})
this.log('info', 'Current timer id: ' + result.id)
resolve(result.id)
} else {
this.log('info', 'No current timer (no id in data)')
this.setVariableValues({
timerId: null,
timerDescription: null,
timerDuration: null
})
resolve(null)
}
} else {
this.log('info', 'No current timer (no object)')
this.setVariableValues({
timerId: null,
timerDescription: null,
timerDuration: null
})
resolve(null)
}
}
)
self.setVariableDefinitions(variables)
self.setVariable('timerId', null)
self.setVariable('timerDescription', null)
}
instance.prototype.init_presets = function () {
var self = this
var presets = []
presets.push({
category: 'Timer',
label: 'Start',
bank: {
style: 'text',
text: 'Start Timer',
size: '18',
color: 16777215,
bgcolor: 0,
},
actions: [
{
action: 'startNewTimer',
options: {
description: '',
project: '0',
},
},
],
})
presets.push({
category: 'Timer',
label: 'Stop',
bank: {
style: 'text',
text: 'Stop Timer',
size: '18',
color: 16777215,
bgcolor: 0,
},
actions: [
{
action: 'stopCurrentTimer',
},
],
})
self.setPresetDefinitions(presets)
}
instance.prototype.actions = function (system) {
var self = this
self.setActions({
startNewTimer: {
label: 'Start New Timer',
options: [
{
type: 'textinput',
label: 'Description',
id: 'description',
default: '',
},
{
type: 'dropdown',
label: 'Project',
id: 'project',
default: '0',
choices: self.projects,
},
],
},
getCurrentTimer: {
label: 'Get Current Timer',
},
stopCurrentTimer: {
label: 'Stop Current Timer',
},
refreshProjects: {
label: 'Refresh Project List',
},
})
}
instance.prototype.action = function (action) {
var self = this
const opt = action.options
async getWorkspace() {
let cmd = 'workspaces'
console.log('function: getWorkspace')
switch (action.action) {
case 'startNewTimer': {
self.getCurrentTimer().then((timerId) => {
if (timerId === undefined || timerId === null || self.config.alwaysStart === true) {
// no timer currently running or we want to restart it
var cmd = 'https://api.track.toggl.com/api/v8/time_entries/start'
if (opt.project == '0') {
var body = '{"time_entry":{"description":"' + opt.description + '","created_with":"companion"}}'
} else {
var body =
'{"time_entry":{"description":"' +
opt.description +
'","created_with":"companion","pid":"' +
opt.project +
'"}}'
if (this.gotOptions.headers == null) {
this.log('warn', 'Not authorized')
return
}
self.sendCommand('rest', cmd, body).then((result) => {
if (typeof result === 'object' && result.data !== null && result.data !== undefined) {
self.log('debug', 'New timer started ' + result.data.id)
self.setVariable('timerId', result.data.id)
self.setVariable('timerDescription', result.data.description)
} else {
self.log('warn', 'Error starting timer')
}
})
} else {
self.log('debug', 'A timer is already running ' + timerId.id)
}
})
break
}
case 'stopCurrentTimer': {
self.getCurrentTimer().then((timerId) => {
self.log('debug', 'Current timer id ' + timerId.id)
if (timerId.id !== null && timerId.id !== undefined) {
var cmd = 'https://api.track.toggl.com/api/v8/time_entries/' + timerId.id + '/stop'
self.sendCommand('rest_put', cmd).then((result) => {
if (typeof result === 'object' && result.data !== null && result.data !== undefined) {
self.log('debug', 'Stopped ' + result.data.id + ', duration ' + result.data.duration)
self.setVariable('timerId', null)
self.setVariable('timerDescription', null)
} else {
self.log('warn', 'Error stopping timer')
}
})
} else {
self.log('warn', 'No running timer to stop or running timer id unknown')
}
})
break
}
case 'getCurrentTimer': {
self.getCurrentTimer().then((timerId) => {
self.log('debug', 'Current timer id ' + timerId.id + ' ' + timerId.description)
self.setVariable('timerId', timerId.id)
self.setVariable('timerDescription', timerId.description)
})
break
}
case 'refreshProjects': {
self.getWorkspace()
break
}
default:
break
}
}
instance.prototype.getWorkspace = function () {
var self = this
var cmd = 'https://api.track.toggl.com/api/v8/workspaces'
// console.log('getWorkspace')
// reset
self.workspace = null
this.workspace = null
this.setVariableValues({
workspace: null
})
// get workspace ID
self.sendCommand('rest_get', cmd).then(
this.sendGetCommand(cmd).then(
(result) => {
// console.log('result ' + JSON.stringify(result, null, 4))
if (typeof result === 'object' && result !== null) {
console.log('Found ' + result.length + ' workspace')
// only interested in first workspace
if ('id' in result[0]) {
self.workspace = result[0].id
self.workspaceName = result[0].name
console.log('Workspace ' + self.workspace + ' ' + self.workspaceName)
self.log('debug', 'Workspace ' + self.workspace + ':' + self.workspaceName)
self.getProjects()
this.workspace = result[0].id
this.workspaceName = result[0].name
this.log('info', 'Workspace: ' + this.workspace + ' - ' + this.workspaceName)
this.setVariableValues({
workspace: this.workspaceName
})
this.getProjects()
}
} else {
console.log('result ' + JSON.stringify(result, null, 4))
self.log('debug', 'No workspace')
this.log('debug', 'No workspace')
}
},
(error) => {
console.log('error ' + error)
self.log('debug', 'Error getting workspace')
}
)
}
instance.prototype.getProjects = function () {
var self = this
getProjects() {
console.log('function: getProjects')
if (self.workspace !== null) {
var cmd = 'https://api.track.toggl.com/api/v8/workspaces/' + self.workspace + '/projects'
self.sendCommand('rest_get', cmd).then(
if (this.workspace !== null) {
let cmd = 'workspaces/' + this.workspace + '/projects'
this.sendGetCommand(cmd).then(
(result) => {
// console.log('result ' + JSON.stringify(result, null, 4))
if (typeof result === 'object' && result !== null) {
// reset
self.projects = []
this.projects = []
for (p = 0; p < result.length; p++) {
for (let p = 0; p < result.length; p++) {
if ('id' in result[p]) {
self.projects.push({
if (result[p].active === true) {
// don't add archived projects
this.projects.push({
id: result[p].id.toString(),
label: result[p].name,
})
self.log('debug', 'Project ' + result[p].id + ':' + result[p].name)
}
// this.log('debug', 'Project ' + result[p].id + ':' + result[p].name)
}
}
self.projects.sort((a, b) => {
fa = a.label.toLowerCase()
fb = b.label.toLowerCase()
this.projects.sort((a, b) => {
let fa = a.label.toLowerCase()
let fb = b.label.toLowerCase()
if (fa < fb) {
return -1
@@ -341,103 +233,223 @@ instance.prototype.getProjects = function () {
return 0
})
self.projects.unshift({ id: '0', label: 'None' })
this.projects.unshift({ id: '0', label: 'None' })
console.log('Projects:')
console.log(self.projects)
self.actions()
console.log(this.projects)
this.updateActions()
} else {
console.log(result)
self.log('debug', 'No projects')
this.log('debug', 'No projects')
}
},
(error) => {
console.log('error ' + error)
self.log('debug', 'Error getting projects')
}
)
})
}
}
instance.prototype.getCurrentTimer = function () {
var self = this
var cmd = 'https://api.track.toggl.com/api/v8/time_entries/current'
// getTimerDuration(id) {
// let cmd = 'time_entries/' + id
//
// return new Promise((resolve, reject) => {
// self.sendCommand('rest_get', cmd).then(
// (result) => {
// if (typeof result === 'object' && result.data !== null && result.data !== undefined) {
// if ('duration' in result.data) {
// self.setVariable('timerDuration', result.data.duration)
// resolve(result.data.duration)
// } else {
// self.log('debug', 'Error getting current timer duration (no id in data)')
// self.setVariable('timerDuration', null)
// resolve(null)
// }
// } else {
// self.log('debug', 'Error getting current timer duration (no object)')
// self.setVariable('timerDuration', null)
// resolve(null)
// }
// },
// (error) => {
// console.log('error ' + error)
// self.log('debug', 'Error getting current timer duration')
// }
// )
// })
// }
return new Promise((resolve, reject) => {
self.sendCommand('rest_get', cmd).then(
(result) => {
if (typeof result === 'object' && result.data !== null && result.data !== undefined) {
if ('id' in result.data) {
resolve(result.data)
async startTimer(project, description) {
let body
let cmd
let timerId
const startTime = new Date()
this.getCurrentTimer().then((timerId) => {
console.log('timerId: ' + timerId)
if (timerId === null || this.config.alwaysStart === true) {
// no timer currently running or we want to restart it
cmd = 'workspaces/' + this.workspace + '/time_entries'
if (project == '0') {
body = '{"wid":' + this.workspace + ',"description":"' + description +
'","created_with":"companion",' + '"start":"' + startTime.toISOString() + '","duration":-1}'
} else {
self.log('debug', 'Error getting current timer (no id in data)')
self.setVariable('timerId', null)
self.setVariable('timerDescription', null)
resolve(null)
body =
'{"wid":' + this.workspace + ',"description":"' + description +
'","created_with":"companion","project_id":' + project +
',"start":"' + startTime.toISOString() + '","duration":-1}'
}
// console.log(body)
this.sendPostCommand(cmd, body).then((result) => {
if (typeof result === 'object' && result !== null) {
this.log('info', 'New timer started ' + result.id + " " + result.description)
this.setVariableValues({
timerId: result.id,
timerDescription: result.description,
timerDuration: result.duration,
})
} else {
self.log('debug', 'Error getting current timer (no object)')
self.setVariable('timerId', null)
self.setVariable('timerDescription', null)
resolve(null)
this.log('warn', 'Error starting timer')
}
},
(error) => {
console.log('error ' + error)
self.log('debug', 'Error getting current timer')
})
} else {
this.log('info', 'A timer is already running ' + timerId + ' not starting a new one!')
}
)
})
}
instance.prototype.sendCommand = function (mode, command, body = '') {
var self = this
console.log(mode + ' : ' + command)
async stopTimer() {
console.log('function: stopTimer')
switch (mode) {
case 'rest_get': {
return new Promise((resolve, reject) => {
self.system.emit(
mode,
command,
(err, { data, error, response }) => {
if (err) {
self.status(self.STATUS_ERROR)
console.log(error)
reject(error)
return
}
self.status(self.STATUS_OK)
resolve(data)
},
self.header
)
this.getCurrentTimer().then((timerId) => {
this.log('info', 'Trying to stop current timer id: ' + timerId)
// console.log(typeof timerId)
if (typeof timerId === 'number' && timerId > 0) {
let cmd = 'workspaces/' + this.workspace + '/time_entries/' + timerId + '/stop'
this.sendPatchCommand(cmd).then((result) => {
if (typeof result === 'object' && result !== null && result !== undefined) {
this.log('info', 'Stopped ' + result.id + ', duration ' + result.duration)
this.setVariableValues({
timerId: null,
timerDescription: null,
timerDuration: null,
lastTimerDuration: result.duration,
})
break
} else {
this.log('warn', 'Error stopping timer')
}
case 'rest':
case 'rest_put': {
return new Promise((resolve, reject) => {
self.system.emit(
mode,
command,
body,
(err, { data, error, response }) => {
if (err) {
self.status(self.STATUS_ERROR)
console.log(error)
reject(error)
return
}
self.status(self.STATUS_OK)
resolve(data)
},
self.header
)
})
break
} else {
this.log('warn', 'No running timer to stop or running timer id unknown')
}
})
}
async sendGetCommand(GetURL) {
console.log('get: ' + GetURL)
let response
try {
response = await got.get(GetURL, this.gotOptions)
if (response.statusCode == 200) {
this.updateStatus(InstanceStatus.Ok)
return response.body
} else {
this.updateStatus(
InstanceStatus.UnknownError,
`Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`
)
this.log('warn', `Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`)
return null
}
} catch (error) {
console.log(error.message)
this.processError(error)
return null
}
}
instance_skel.extendedBy(instance)
exports = module.exports = instance
async sendPutCommand(PutURL) {
console.log('put: ' + PutURL)
let response
try {
response = await got.put(PutURL, this.gotOptions)
console.log('status: ' + response.statusCode)
if (response.statusCode == 200) {
console.log(response.body)
return response.body
} else {
this.updateStatus(
InstanceStatus.UnknownError,
`Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`
)
this.log('warn', `Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`)
return null
}
} catch (error) {
console.log(error.message)
this.processError(error)
return null
}
}
async sendPatchCommand(PatchURL) {
console.log('patch: ' + PatchURL)
let response
try {
response = await got.patch(PatchURL, this.gotOptions)
// console.log('status: ' + response.statusCode)
if (response.statusCode == 200) {
// console.log(response.body)
return response.body
} else {
this.updateStatus(
InstanceStatus.UnknownError,
`Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`
)
this.log('warn', `Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`)
return null
}
} catch (error) {
console.log(error.message)
this.processError(error)
return null
}
}
async sendPostCommand(cmd, body) {
console.log(body)
let response
let postdata = {}
postdata.prefixUrl = this.prefixUrl
postdata.responseType = 'json',
postdata.throwHttpErrors = false
postdata.headers = this.auth()
postdata.json = JSON.parse(body)
// console.log(postdata)
try {
response = await got.post(cmd, postdata)
// console.log(response.request.requestUrl)
// console.log(response.statusCode)
if (response.statusCode == 200) {
return response.body
} else {
this.updateStatus(
InstanceStatus.UnknownError,
`Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`
)
this.log('warn', `Unexpected HTTP status code: ${response.statusCode} - ${response.body.error}`)
return null
}
} catch (error) {
console.log(error.message)
this.processError(error)
return null
}
}
processError(error) {
console.log('gotError: ' + error.code)
}
}
runEntrypoint(toggltrack, upgradeScripts)

View File

@@ -1,29 +1,21 @@
{
"name": "toggl-track",
"version": "1.0.3",
"api_version": "1.0.0",
"keywords": [
"Timer",
"Logging"
],
"manufacturer": "Toggl",
"product": [
"Track"
],
"shortname": "toggl",
"description": "Companion module for TogglTrack",
"version": "2.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Peter Daniel",
"license": "MIT",
"homepage": "https://github.com/bitfocus/companion-module-toggl-track#readme",
"bugs": {
"url": "https://github.com/bitfocus/companion-module-toggl-track/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
},
"license": "MIT",
"dependencies": {
"@companion-module/base": "~1.4",
"got": "~13.0.0"
},
"devDependencies": {
"@companion-module/tools": "~1.1"
}
}

56
presets.js Normal file
View File

@@ -0,0 +1,56 @@
import { combineRgb } from '@companion-module/base'
export function updatePresets() {
let presets = {}
presets['Start'] = {
type: 'button',
category: 'Timer',
name: 'Start',
style: {
text: 'Start Timer',
size: '18',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(0, 0, 0),
},
steps: [{
down: [{
actionId: 'startNewTimer',
options: {
description: '',
project: '0',
},
},
],
up: [],
},
],
feedbacks: [],
}
presets['Stop'] = {
type: 'button',
category: 'Timer',
name: 'Stop',
style: {
text: 'Stop Timer',
size: '18',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(0, 0, 0),
},
steps: [{
down: [
{
actionId: 'stopCurrentTimer',
options: {
},
},
],
up: [],
},
],
feedbacks: [],
}
this.setPresetDefinitions(presets)
}

7
upgrades.js Normal file
View File

@@ -0,0 +1,7 @@
export function upgradeScripts() {
return {
updatedConfig: null,
updatedActions: [],
updatedFeedbacks: [],
}
}

29
variables.js Normal file
View File

@@ -0,0 +1,29 @@
export function updateVariables() {
let variables = []
variables.push(
{
name: 'Workspace',
variableId: 'workspace',
},
{
name: 'Current Timer Id',
variableId: 'timerId',
},
{
name: 'Current Timer Duration',
variableId: 'timerDuration',
},
{
name: 'Last Timer Duration',
variableId: 'lastTimerDuration',
},
{
name: 'Current Timer Description',
variableId: 'timerDescription',
}
)
this.setVariableDefinitions(variables)
}