yoto-nodejs-client 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +523 -30
- package/bin/auth.js +36 -46
- package/bin/content.js +0 -0
- package/bin/device-model.d.ts +3 -0
- package/bin/device-model.d.ts.map +1 -0
- package/bin/device-model.js +360 -0
- package/bin/device-tui.TODO.md +125 -0
- package/bin/device-tui.d.ts +31 -0
- package/bin/device-tui.d.ts.map +1 -0
- package/bin/device-tui.js +1123 -0
- package/bin/devices.js +166 -28
- package/bin/groups.js +0 -0
- package/bin/icons.js +0 -0
- package/bin/lib/cli-helpers.d.ts +33 -1
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +5 -5
- package/bin/lib/token-helpers.d.ts +32 -0
- package/bin/lib/token-helpers.d.ts.map +1 -1
- package/bin/refresh-token.js +6 -6
- package/bin/token-info.js +3 -3
- package/index.d.ts +4 -217
- package/index.d.ts.map +1 -1
- package/index.js +11 -689
- package/lib/api-client.d.ts +576 -0
- package/lib/api-client.d.ts.map +1 -0
- package/lib/api-client.js +681 -0
- package/lib/api-endpoints/auth.d.ts +280 -4
- package/lib/api-endpoints/auth.d.ts.map +1 -1
- package/lib/api-endpoints/auth.js +224 -7
- package/lib/api-endpoints/auth.test.js +54 -2
- package/lib/api-endpoints/constants.d.ts +30 -2
- package/lib/api-endpoints/constants.d.ts.map +1 -1
- package/lib/api-endpoints/constants.js +17 -10
- package/lib/api-endpoints/content.d.ts +760 -0
- package/lib/api-endpoints/content.d.ts.map +1 -1
- package/lib/api-endpoints/content.test.js +1 -1
- package/lib/api-endpoints/devices.d.ts +917 -48
- package/lib/api-endpoints/devices.d.ts.map +1 -1
- package/lib/api-endpoints/devices.js +114 -52
- package/lib/api-endpoints/devices.test.js +1 -1
- package/lib/api-endpoints/endpoint-test-helpers.d.ts +28 -0
- package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.d.ts +187 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
- package/lib/api-endpoints/family-library-groups.test.js +1 -1
- package/lib/api-endpoints/family.d.ts +88 -0
- package/lib/api-endpoints/family.d.ts.map +1 -1
- package/lib/api-endpoints/family.test.js +1 -1
- package/lib/api-endpoints/helpers.d.ts +37 -3
- package/lib/api-endpoints/helpers.d.ts.map +1 -1
- package/lib/api-endpoints/icons.d.ts +196 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -1
- package/lib/api-endpoints/icons.test.js +1 -1
- package/lib/api-endpoints/media.d.ts +83 -0
- package/lib/api-endpoints/media.d.ts.map +1 -1
- package/lib/helpers/power-state.d.ts +53 -0
- package/lib/helpers/power-state.d.ts.map +1 -0
- package/lib/helpers/power-state.js +73 -0
- package/lib/helpers/power-state.test.js +100 -0
- package/lib/helpers/temperature.d.ts +24 -0
- package/lib/helpers/temperature.d.ts.map +1 -0
- package/lib/helpers/temperature.js +61 -0
- package/lib/helpers/temperature.test.js +58 -0
- package/lib/helpers/typed-keys.d.ts +7 -0
- package/lib/helpers/typed-keys.d.ts.map +1 -0
- package/lib/helpers/typed-keys.js +8 -0
- package/lib/mqtt/client.d.ts +610 -7
- package/lib/mqtt/client.d.ts.map +1 -1
- package/lib/mqtt/client.js +213 -31
- package/lib/mqtt/commands.d.ts +195 -0
- package/lib/mqtt/commands.d.ts.map +1 -1
- package/lib/mqtt/factory.d.ts +62 -1
- package/lib/mqtt/factory.d.ts.map +1 -1
- package/lib/mqtt/factory.js +27 -5
- package/lib/mqtt/mqtt.test.js +85 -28
- package/lib/mqtt/topics.d.ts +186 -1
- package/lib/mqtt/topics.d.ts.map +1 -1
- package/lib/mqtt/topics.js +54 -20
- package/lib/pkg.d.cts +9 -0
- package/lib/token.d.ts +106 -3
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +30 -23
- package/lib/yoto-account.d.ts +163 -0
- package/lib/yoto-account.d.ts.map +1 -0
- package/lib/yoto-account.js +340 -0
- package/lib/yoto-device.d.ts +656 -0
- package/lib/yoto-device.d.ts.map +1 -0
- package/lib/yoto-device.js +2850 -0
- package/package.json +22 -15
- package/lib/api-endpoints/test-helpers.d.ts +0 -7
- package/lib/api-endpoints/test-helpers.d.ts.map +0 -1
- /package/lib/api-endpoints/{test-helpers.js → endpoint-test-helpers.js} +0 -0
package/bin/auth.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseArgs } from 'node:util'
|
|
|
9
9
|
import { YotoClient } from '../index.js'
|
|
10
10
|
import { pkg } from '../lib/pkg.cjs'
|
|
11
11
|
import { DEFAULT_CLIENT_ID } from '../lib/api-endpoints/constants.js'
|
|
12
|
-
import {
|
|
12
|
+
import { saveTokensToEnv } from './lib/token-helpers.js'
|
|
13
13
|
import {
|
|
14
14
|
getCommonOptions,
|
|
15
15
|
handleCliError,
|
|
@@ -72,56 +72,46 @@ async function main () {
|
|
|
72
72
|
console.log(` Code expires in: ${expiresInMinutes} minute(s)`)
|
|
73
73
|
console.log(` Polling every: ${deviceAuth.interval} second(s)\n`)
|
|
74
74
|
|
|
75
|
-
// Step 3:
|
|
75
|
+
// Step 3: Wait for authorization (with automatic polling)
|
|
76
76
|
console.log('Waiting for authorization...')
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
try {
|
|
79
|
+
const tokens = await YotoClient.waitForDeviceAuthorization({
|
|
80
|
+
deviceCode: deviceAuth.device_code,
|
|
81
|
+
clientId,
|
|
82
|
+
initialInterval: deviceAuth.interval * 1000,
|
|
83
|
+
expiresIn: deviceAuth.expires_in,
|
|
84
|
+
onPoll: (result) => {
|
|
85
|
+
if (result.status === 'pending') {
|
|
86
|
+
process.stdout.write('.')
|
|
87
|
+
} else if (result.status === 'slow_down') {
|
|
88
|
+
console.log(`\n⚠️ Slowing down polling to ${result.interval / 1000}s...`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Success! Save tokens to .env file
|
|
94
|
+
console.log('\n✅ Authorization successful!\n')
|
|
95
|
+
|
|
96
|
+
await saveTokensToEnv(outputFile, tokens, clientId)
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
console.log(`✨ Tokens saved to ${outputFile}`)
|
|
99
|
+
console.log('\nYou can now use these environment variables:')
|
|
100
|
+
console.log(' - YOTO_ACCESS_TOKEN')
|
|
101
|
+
console.log(' - YOTO_REFRESH_TOKEN')
|
|
102
|
+
console.log(' - YOTO_CLIENT_ID')
|
|
103
|
+
|
|
104
|
+
process.exit(0)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const error = /** @type {any} */ (err)
|
|
107
|
+
if (error.message === 'Device code has expired') {
|
|
85
108
|
console.error('\n❌ Device code has expired. Please run the command again.')
|
|
86
109
|
process.exit(1)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
deviceCode: deviceAuth.device_code,
|
|
93
|
-
clientId
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
// Success! Save tokens to .env file
|
|
97
|
-
console.log('\n✅ Authorization successful!\n')
|
|
98
|
-
|
|
99
|
-
await saveTokensToEnv(outputFile, tokens, clientId)
|
|
100
|
-
|
|
101
|
-
console.log(`✨ Tokens saved to ${outputFile}`)
|
|
102
|
-
console.log('\nYou can now use these environment variables:')
|
|
103
|
-
console.log(' - YOTO_ACCESS_TOKEN')
|
|
104
|
-
console.log(' - YOTO_REFRESH_TOKEN')
|
|
105
|
-
console.log(' - YOTO_CLIENT_ID')
|
|
106
|
-
|
|
107
|
-
process.exit(0)
|
|
108
|
-
} catch (err) {
|
|
109
|
-
const error = /** @type {any} */ (err)
|
|
110
|
-
if (error.body?.error === 'authorization_pending') {
|
|
111
|
-
process.stdout.write('.')
|
|
112
|
-
await sleep(pollInterval)
|
|
113
|
-
continue
|
|
114
|
-
} else if (error.body?.error === 'slow_down') {
|
|
115
|
-
pollInterval += 5000
|
|
116
|
-
console.log(`\n⚠️ Slowing down polling to ${pollInterval / 1000}s...`)
|
|
117
|
-
await sleep(pollInterval)
|
|
118
|
-
continue
|
|
119
|
-
} else if (error.body?.error === 'expired_token') {
|
|
120
|
-
console.error('\n❌ Device code has expired. Please run the command again.')
|
|
121
|
-
process.exit(1)
|
|
122
|
-
} else {
|
|
123
|
-
handleCliError(error)
|
|
124
|
-
}
|
|
110
|
+
} else if (error.body?.error === 'expired_token') {
|
|
111
|
+
console.error('\n❌ Device code has expired. Please run the command again.')
|
|
112
|
+
process.exit(1)
|
|
113
|
+
} else {
|
|
114
|
+
handleCliError(error)
|
|
125
115
|
}
|
|
126
116
|
}
|
|
127
117
|
}
|
package/bin/content.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-model.d.ts","sourceRoot":"","sources":["device-model.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { printHelpText } from 'argsclopts'
|
|
8
|
+
import { parseArgs } from 'node:util'
|
|
9
|
+
import { pkg } from '../lib/pkg.cjs'
|
|
10
|
+
import {
|
|
11
|
+
getCommonOptions,
|
|
12
|
+
loadTokensFromEnv,
|
|
13
|
+
createYotoClient,
|
|
14
|
+
handleCliError,
|
|
15
|
+
printHeader
|
|
16
|
+
} from './lib/cli-helpers.js'
|
|
17
|
+
import { YotoDeviceModel } from '../lib/yoto-device.js'
|
|
18
|
+
|
|
19
|
+
/** @type {ArgscloptsParseArgsOptionsConfig} */
|
|
20
|
+
const options = {
|
|
21
|
+
...getCommonOptions(),
|
|
22
|
+
'poll-interval': {
|
|
23
|
+
type: 'string',
|
|
24
|
+
short: 'p',
|
|
25
|
+
help: 'HTTP polling interval in milliseconds (default: 600000 / 10 minutes)'
|
|
26
|
+
},
|
|
27
|
+
duration: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
short: 't',
|
|
30
|
+
help: 'How long to run in seconds (default: run indefinitely)'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args = parseArgs({ options, strict: false, allowPositionals: true })
|
|
35
|
+
|
|
36
|
+
if (args.values['help']) {
|
|
37
|
+
await printHelpText({
|
|
38
|
+
options,
|
|
39
|
+
name: 'yoto-device-model',
|
|
40
|
+
version: pkg.version,
|
|
41
|
+
exampleFn: ({ name }) => ` Yoto Device Model tester - Stateful device client with HTTP + MQTT
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
${name} abc123 # Start device model (runs indefinitely)
|
|
45
|
+
${name} abc123 --duration 30 # Run for 30 seconds
|
|
46
|
+
${name} abc123 --poll-interval 300000 # Poll every 5 minutes
|
|
47
|
+
`
|
|
48
|
+
})
|
|
49
|
+
process.exit(0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load tokens from environment
|
|
53
|
+
const { clientId, refreshToken, accessToken, envFile } = loadTokensFromEnv(args)
|
|
54
|
+
|
|
55
|
+
const deviceId = args.positionals[0]
|
|
56
|
+
const pollInterval = args.values['poll-interval'] ? Number(args.values['poll-interval']) : 600000
|
|
57
|
+
const duration = args.values['duration'] ? Number(args.values['duration']) : null
|
|
58
|
+
|
|
59
|
+
if (!deviceId) {
|
|
60
|
+
console.error('❌ deviceId is required as first positional argument')
|
|
61
|
+
console.error('Usage: yoto-device-model <deviceId> [options]')
|
|
62
|
+
console.error('Example: yoto-device-model abc123 --duration 30')
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main () {
|
|
67
|
+
printHeader('Yoto Device Model Tester')
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Create API client
|
|
71
|
+
console.log('\n📡 Creating YotoClient...')
|
|
72
|
+
const client = createYotoClient({
|
|
73
|
+
clientId,
|
|
74
|
+
refreshToken,
|
|
75
|
+
accessToken,
|
|
76
|
+
outputFile: envFile
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Get device info
|
|
80
|
+
console.log(`\n🔍 Fetching device info for: ${deviceId}`)
|
|
81
|
+
const { devices } = await client.getDevices()
|
|
82
|
+
const device = devices.find(d => d.deviceId === deviceId)
|
|
83
|
+
|
|
84
|
+
if (!device) {
|
|
85
|
+
console.error(`❌ Device with ID '${deviceId}' not found`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`✅ Found device: ${device.name} (${device.deviceType})`)
|
|
90
|
+
|
|
91
|
+
// Create device model
|
|
92
|
+
console.log('\n🎛️ Creating YotoDeviceModel...')
|
|
93
|
+
const deviceModel = new YotoDeviceModel(client, device, {
|
|
94
|
+
httpPollIntervalMs: pollInterval
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
console.log(` HTTP polling interval: ${pollInterval}ms (${pollInterval / 1000}s)`)
|
|
98
|
+
|
|
99
|
+
// Setup event handlers with timer-based display logic
|
|
100
|
+
console.log('\n📡 Setting up event handlers...')
|
|
101
|
+
|
|
102
|
+
// Timer state for each event type
|
|
103
|
+
let lastStatusFullDisplay = 0
|
|
104
|
+
let lastConfigFullDisplay = 0
|
|
105
|
+
let lastPlaybackFullDisplay = 0
|
|
106
|
+
const FULL_DISPLAY_INTERVAL_MS = 30000 // 30 seconds
|
|
107
|
+
|
|
108
|
+
deviceModel.on('started', () => {
|
|
109
|
+
console.log('✅ Device model started')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
deviceModel.on('stopped', () => {
|
|
113
|
+
console.log('🛑 Device model stopped')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
deviceModel.on('statusUpdate', (status, source, changedFields) => {
|
|
117
|
+
const timestamp = new Date().toISOString()
|
|
118
|
+
const capabilities = deviceModel.capabilities
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
const showFullState = (now - lastStatusFullDisplay) >= FULL_DISPLAY_INTERVAL_MS
|
|
121
|
+
|
|
122
|
+
if (showFullState) {
|
|
123
|
+
// Show full state
|
|
124
|
+
console.log(`\n📊 STATUS UPDATE [${timestamp}] via ${source}: (FULL STATE)`)
|
|
125
|
+
console.log(` Battery: ${status.batteryLevelPercentage}%`)
|
|
126
|
+
console.log(` Charging: ${status.isCharging}`)
|
|
127
|
+
console.log(` Online: ${status.isOnline}`)
|
|
128
|
+
console.log(` Max Volume: ${status.maxVolume}/16`)
|
|
129
|
+
console.log(` Volume: ${status.volume}/16`)
|
|
130
|
+
if (capabilities.hasTemperatureSensor) {
|
|
131
|
+
console.log(` Temperature: ${status.temperatureCelsius}°C`)
|
|
132
|
+
}
|
|
133
|
+
if (capabilities.hasAmbientLightSensor) {
|
|
134
|
+
console.log(` Ambient Light: ${status.ambientLightSensorReading}`)
|
|
135
|
+
}
|
|
136
|
+
console.log(` WiFi: ${status.wifiStrength} dBm`)
|
|
137
|
+
console.log(` Active Card: ${status.activeCardId || 'none'}`)
|
|
138
|
+
console.log(` Power Source: ${status.powerSource}`)
|
|
139
|
+
console.log(` Day Mode: ${status.dayMode}`)
|
|
140
|
+
console.log(` Card Insertion: ${status.cardInsertionState}`)
|
|
141
|
+
if (capabilities.hasColoredNightlight) {
|
|
142
|
+
const nightlight = deviceModel.nightlight
|
|
143
|
+
console.log(` Nightlight: ${nightlight.name} (${nightlight.value})`)
|
|
144
|
+
}
|
|
145
|
+
console.log(` Firmware: ${status.firmwareVersion}`)
|
|
146
|
+
console.log(` Uptime: ${status.uptime}s`)
|
|
147
|
+
console.log(` Updated At: ${status.updatedAt}`)
|
|
148
|
+
lastStatusFullDisplay = now
|
|
149
|
+
} else if (changedFields && changedFields.size > 0) {
|
|
150
|
+
// Show only changed fields
|
|
151
|
+
console.log(`\n📊 STATUS UPDATE [${timestamp}] via ${source}: (${changedFields.size} change${changedFields.size === 1 ? '' : 's'})`)
|
|
152
|
+
for (const field of changedFields) {
|
|
153
|
+
let value = status[field]
|
|
154
|
+
if (field === 'volume') {
|
|
155
|
+
value = `${value}/16 (max: ${status.maxVolume}/16)`
|
|
156
|
+
} else if (field === 'maxVolume') {
|
|
157
|
+
value = `${value}/16`
|
|
158
|
+
} else if (field === 'temperatureCelsius') {
|
|
159
|
+
value = `${value}°C`
|
|
160
|
+
} else if (field === 'wifiStrength') {
|
|
161
|
+
value = `${value} dBm`
|
|
162
|
+
} else if (field === 'batteryLevelPercentage') {
|
|
163
|
+
value = `${value}%`
|
|
164
|
+
} else if (field === 'uptime') {
|
|
165
|
+
value = `${value}s`
|
|
166
|
+
} else if (field === 'nightlightMode' && typeof value === 'string') {
|
|
167
|
+
const colorName = YotoDeviceModel.getNightlightColorName(value)
|
|
168
|
+
value = `${colorName} (${value})`
|
|
169
|
+
}
|
|
170
|
+
console.log(` ${field}: ${value}`)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// No changes (shouldn't happen, but handle it)
|
|
174
|
+
console.log(`\n📊 STATUS UPDATE [${timestamp}] via ${source}: (no changes)`)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
deviceModel.on('configUpdate', (config, changedFields) => {
|
|
179
|
+
const timestamp = new Date().toISOString()
|
|
180
|
+
const now = Date.now()
|
|
181
|
+
const showFullState = (now - lastConfigFullDisplay) >= FULL_DISPLAY_INTERVAL_MS
|
|
182
|
+
|
|
183
|
+
if (showFullState) {
|
|
184
|
+
// Show full config state
|
|
185
|
+
console.log(`\n⚙️ CONFIG UPDATE [${timestamp}]: (FULL STATE)`)
|
|
186
|
+
console.log(` Max Volume Limit: ${config.maxVolumeLimit}`)
|
|
187
|
+
console.log(` Day Time: ${config.dayTime}`)
|
|
188
|
+
console.log(` Night Time: ${config.nightTime}`)
|
|
189
|
+
console.log(` Timezone: ${config.timezone}`)
|
|
190
|
+
console.log(` Repeat All: ${config.repeatAll}`)
|
|
191
|
+
console.log(` Bluetooth Enabled: ${config.bluetoothEnabled}`)
|
|
192
|
+
console.log(` BT Headphones Enabled: ${config.btHeadphonesEnabled}`)
|
|
193
|
+
console.log(` Clock Face: ${config.clockFace}`)
|
|
194
|
+
console.log(` Day Display Brightness: ${config.dayDisplayBrightness}`)
|
|
195
|
+
console.log(` Night Display Brightness: ${config.nightDisplayBrightness}`)
|
|
196
|
+
console.log(` Shutdown Timeout: ${config.shutdownTimeout}`)
|
|
197
|
+
console.log(` Volume Level: ${config.volumeLevel}`)
|
|
198
|
+
console.log(` Ambient Colour: ${config.ambientColour}`)
|
|
199
|
+
console.log(` Night Ambient Colour: ${config.nightAmbientColour}`)
|
|
200
|
+
lastConfigFullDisplay = now
|
|
201
|
+
} else if (changedFields && changedFields.size > 0) {
|
|
202
|
+
// Show only changed fields
|
|
203
|
+
console.log(`\n⚙️ CONFIG UPDATE [${timestamp}]: (${changedFields.size} change${changedFields.size === 1 ? '' : 's'})`)
|
|
204
|
+
for (const field of changedFields) {
|
|
205
|
+
console.log(` ${field}: ${config[field]}`)
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`\n⚙️ CONFIG UPDATE [${timestamp}]: (no changes)`)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
deviceModel.on('playbackUpdate', (playback, changedFields) => {
|
|
213
|
+
const timestamp = new Date().toISOString()
|
|
214
|
+
const now = Date.now()
|
|
215
|
+
const showFullState = (now - lastPlaybackFullDisplay) >= FULL_DISPLAY_INTERVAL_MS
|
|
216
|
+
|
|
217
|
+
// Get playback icon based on state
|
|
218
|
+
let playbackIcon = '⏏️' // Default: ejected/no card
|
|
219
|
+
const hasCard = playback.cardId !== null
|
|
220
|
+
if (hasCard || playback.playbackStatus === 'playing' || playback.playbackStatus === 'paused') {
|
|
221
|
+
if (playback.playbackStatus === 'playing') {
|
|
222
|
+
playbackIcon = '▶️'
|
|
223
|
+
} else if (playback.playbackStatus === 'paused') {
|
|
224
|
+
playbackIcon = '⏸️'
|
|
225
|
+
} else {
|
|
226
|
+
playbackIcon = '⏹️' // Stopped but card present
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (showFullState) {
|
|
231
|
+
// Show full playback state
|
|
232
|
+
console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (FULL STATE)`)
|
|
233
|
+
console.log(` Status: ${playback.playbackStatus}`)
|
|
234
|
+
console.log(` Track: ${playback.trackTitle}`)
|
|
235
|
+
console.log(` Track Key: ${playback.trackKey}`)
|
|
236
|
+
console.log(` Chapter: ${playback.chapterTitle}`)
|
|
237
|
+
console.log(` Chapter Key: ${playback.chapterKey}`)
|
|
238
|
+
console.log(` Position: ${playback.position}/${playback.trackLength}s`)
|
|
239
|
+
console.log(` Card ID: ${playback.cardId}`)
|
|
240
|
+
console.log(` Source: ${playback.source}`)
|
|
241
|
+
console.log(` Streaming: ${playback.streaming}`)
|
|
242
|
+
console.log(` Sleep Timer Active: ${playback.sleepTimerActive}`)
|
|
243
|
+
console.log(` Sleep Timer Seconds: ${playback.sleepTimerSeconds}`)
|
|
244
|
+
console.log(` Updated At: ${playback.updatedAt}`)
|
|
245
|
+
lastPlaybackFullDisplay = now
|
|
246
|
+
} else if (changedFields && changedFields.size > 0) {
|
|
247
|
+
// Show only changed fields
|
|
248
|
+
console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (${changedFields.size} change${changedFields.size === 1 ? '' : 's'})`)
|
|
249
|
+
for (const field of changedFields) {
|
|
250
|
+
let value = playback[field]
|
|
251
|
+
if (field === 'position' || field === 'trackLength') {
|
|
252
|
+
// Show position/trackLength together if either changed
|
|
253
|
+
value = `${playback.position}/${playback.trackLength}s`
|
|
254
|
+
console.log(` position/trackLength: ${value}`)
|
|
255
|
+
// Skip if we already printed this combo
|
|
256
|
+
if (field === 'trackLength') continue
|
|
257
|
+
} else {
|
|
258
|
+
console.log(` ${field}: ${value}`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (no changes)`)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
deviceModel.on('online', (metadata) => {
|
|
267
|
+
const timestamp = new Date().toISOString()
|
|
268
|
+
console.log(`\n🟢 DEVICE ONLINE [${timestamp}]:`)
|
|
269
|
+
console.log(` Reason: ${metadata.reason}`)
|
|
270
|
+
if (metadata.upTime !== undefined) {
|
|
271
|
+
console.log(` Uptime: ${metadata.upTime}s`)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
deviceModel.on('offline', (metadata) => {
|
|
276
|
+
const timestamp = new Date().toISOString()
|
|
277
|
+
console.log(`\n🔴 DEVICE OFFLINE [${timestamp}]:`)
|
|
278
|
+
console.log(` Reason: ${metadata.reason}`)
|
|
279
|
+
if (metadata.shutDownReason) {
|
|
280
|
+
console.log(` Shutdown Reason: ${metadata.shutDownReason}`)
|
|
281
|
+
}
|
|
282
|
+
if (metadata.timeSinceLastSeen !== undefined) {
|
|
283
|
+
console.log(` Time Since Last Seen: ${metadata.timeSinceLastSeen}ms`)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
deviceModel.on('mqttConnected', () => {
|
|
288
|
+
const timestamp = new Date().toISOString()
|
|
289
|
+
console.log(`\n🔌 MQTT CONNECTED [${timestamp}]`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
deviceModel.on('mqttDisconnected', () => {
|
|
293
|
+
const timestamp = new Date().toISOString()
|
|
294
|
+
console.log(`\n🔌 MQTT DISCONNECTED [${timestamp}]`)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
deviceModel.on('error', (error) => {
|
|
298
|
+
const timestamp = new Date().toISOString()
|
|
299
|
+
console.error(`\n⚠️ ERROR [${timestamp}]:`)
|
|
300
|
+
console.error(` ${error.message}`)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Handle graceful shutdown
|
|
304
|
+
const cleanup = async () => {
|
|
305
|
+
console.log('\n\n🛑 Shutting down...')
|
|
306
|
+
try {
|
|
307
|
+
await deviceModel.stop()
|
|
308
|
+
console.log('✅ Device model stopped cleanly')
|
|
309
|
+
process.exit(0)
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error('❌ Error during shutdown:', err)
|
|
312
|
+
process.exit(1)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
process.on('SIGINT', cleanup)
|
|
317
|
+
process.on('SIGTERM', cleanup)
|
|
318
|
+
|
|
319
|
+
// Start device model
|
|
320
|
+
console.log('\n🚀 Starting device model...')
|
|
321
|
+
await deviceModel.start()
|
|
322
|
+
|
|
323
|
+
console.log('\n✅ Device model is running!')
|
|
324
|
+
console.log('\n📊 Current State:')
|
|
325
|
+
console.log(` Device: ${deviceModel.device.name}`)
|
|
326
|
+
console.log(` Type: ${deviceModel.device.deviceType}`)
|
|
327
|
+
console.log(` Online: ${deviceModel.deviceOnline}`)
|
|
328
|
+
console.log(` MQTT Connected: ${deviceModel.mqttConnected}`)
|
|
329
|
+
console.log(` Initialized: ${deviceModel.initialized}`)
|
|
330
|
+
console.log(` Running: ${deviceModel.running}`)
|
|
331
|
+
|
|
332
|
+
console.log('\n🔧 Capabilities:')
|
|
333
|
+
console.log(` Temperature Sensor: ${deviceModel.capabilities.hasTemperatureSensor}`)
|
|
334
|
+
console.log(` Ambient Light Sensor: ${deviceModel.capabilities.hasAmbientLightSensor}`)
|
|
335
|
+
console.log(` Colored Nightlight: ${deviceModel.capabilities.hasColoredNightlight}`)
|
|
336
|
+
console.log(` Supported: ${deviceModel.capabilities.supported}`)
|
|
337
|
+
|
|
338
|
+
console.log('\n👂 Listening for events... (Ctrl+C to stop)')
|
|
339
|
+
|
|
340
|
+
if (duration) {
|
|
341
|
+
console.log(`⏱️ Will auto-stop after ${duration} seconds\n`)
|
|
342
|
+
setTimeout(async () => {
|
|
343
|
+
console.log(`\n⏰ ${duration} seconds elapsed, stopping...`)
|
|
344
|
+
await cleanup()
|
|
345
|
+
}, duration * 1000)
|
|
346
|
+
} else {
|
|
347
|
+
console.log(' (Running indefinitely)\n')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Keep process alive (unless duration is set)
|
|
351
|
+
if (!duration) {
|
|
352
|
+
await new Promise(() => {}) // Keep alive indefinitely
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
handleCliError(error)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Run the main function
|
|
360
|
+
await main().catch(handleCliError)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Device TUI - Next Steps
|
|
2
|
+
|
|
3
|
+
## Known Limitations to Address
|
|
4
|
+
|
|
5
|
+
### P1 - High Priority
|
|
6
|
+
|
|
7
|
+
- [ ] **Grid doesn't resize on terminal resize**
|
|
8
|
+
- `columns` is calculated once at startup
|
|
9
|
+
- Should listen for `screen.on('resize')` and reflow cards
|
|
10
|
+
- May need to recreate card widgets or just reposition them
|
|
11
|
+
|
|
12
|
+
- [ ] **No scroll in overview**
|
|
13
|
+
- If more devices than fit on screen, they overflow/clip
|
|
14
|
+
- Consider using a scrollable container or List widget for overview
|
|
15
|
+
- Alternative: pagination with page indicators
|
|
16
|
+
|
|
17
|
+
### P2 - Medium Priority
|
|
18
|
+
|
|
19
|
+
- [ ] **No device hot-add/remove**
|
|
20
|
+
- If devices are added/removed from account, TUI doesn't update
|
|
21
|
+
- Could poll `account.refreshDevices()` periodically
|
|
22
|
+
- Or add a manual refresh key (`R` to refresh device list?)
|
|
23
|
+
|
|
24
|
+
- [ ] **Fixed card dimensions**
|
|
25
|
+
- Cards are hardcoded at 28x8 chars
|
|
26
|
+
- May clip content on very small terminals
|
|
27
|
+
- Consider adaptive sizing based on terminal dimensions
|
|
28
|
+
|
|
29
|
+
## Feature Ideas
|
|
30
|
+
|
|
31
|
+
### Device Interaction (not just monitoring)
|
|
32
|
+
|
|
33
|
+
- [ ] **Volume control**
|
|
34
|
+
- `+`/`-` keys to adjust volume in detail view
|
|
35
|
+
- Show visual feedback when changing
|
|
36
|
+
|
|
37
|
+
- [ ] **Playback controls**
|
|
38
|
+
- Space to play/pause
|
|
39
|
+
- `n`/`p` for next/previous track
|
|
40
|
+
- `s` to stop playback
|
|
41
|
+
|
|
42
|
+
- [ ] **Sleep timer**
|
|
43
|
+
- Set sleep timer from TUI
|
|
44
|
+
- Show countdown in detail view
|
|
45
|
+
|
|
46
|
+
- [ ] **Day/Night mode toggle**
|
|
47
|
+
- Quick toggle between day/night mode
|
|
48
|
+
|
|
49
|
+
### Display Enhancements
|
|
50
|
+
|
|
51
|
+
- [ ] **Progress bar for playback**
|
|
52
|
+
- Visual progress bar instead of just time display
|
|
53
|
+
- `[████████░░░░░░░░] 3:45 / 8:20`
|
|
54
|
+
|
|
55
|
+
- [ ] **Card content info**
|
|
56
|
+
- Show card title/artwork info if available
|
|
57
|
+
- Maybe fetch card metadata from API
|
|
58
|
+
|
|
59
|
+
- [ ] **Alarms display**
|
|
60
|
+
- Show configured alarms in detail view
|
|
61
|
+
- Ability to enable/disable alarms
|
|
62
|
+
|
|
63
|
+
- [ ] **Config editing**
|
|
64
|
+
- Edit device config values from detail view
|
|
65
|
+
- Form-based UI for settings
|
|
66
|
+
|
|
67
|
+
### UX Improvements
|
|
68
|
+
|
|
69
|
+
- [ ] **Loading spinner during initialization**
|
|
70
|
+
- Replace console.log with proper TUI loading screen
|
|
71
|
+
- Show progress as each device connects
|
|
72
|
+
|
|
73
|
+
- [ ] **Connection status in footer**
|
|
74
|
+
- Show overall connection health
|
|
75
|
+
- "3/3 devices online" or similar
|
|
76
|
+
|
|
77
|
+
- [ ] **Notification/toast system**
|
|
78
|
+
- Brief popup notifications for events
|
|
79
|
+
- "Device X went offline" toast
|
|
80
|
+
|
|
81
|
+
- [ ] **Help overlay**
|
|
82
|
+
- `?` key to show all keyboard shortcuts
|
|
83
|
+
- Dismissable overlay with command reference
|
|
84
|
+
|
|
85
|
+
- [ ] **Search/filter devices**
|
|
86
|
+
- `/` to search by name
|
|
87
|
+
- Filter by online/offline status
|
|
88
|
+
|
|
89
|
+
## Technical Improvements
|
|
90
|
+
|
|
91
|
+
- [ ] **Extract TUI components**
|
|
92
|
+
- Move `createDeviceCard`, `createDetailView` to separate files
|
|
93
|
+
- Create reusable component pattern
|
|
94
|
+
|
|
95
|
+
- [ ] **Add YotoAccount device event forwarding**
|
|
96
|
+
- Update `YotoAccount` to forward all device events (statusUpdate, configUpdate, etc.)
|
|
97
|
+
- Would simplify TUI event subscription code
|
|
98
|
+
|
|
99
|
+
- [ ] **Configuration file**
|
|
100
|
+
- Support config file for TUI preferences
|
|
101
|
+
- Colors, refresh intervals, default view, etc.
|
|
102
|
+
|
|
103
|
+
- [ ] **Logging to file**
|
|
104
|
+
- Option to log events to file for debugging
|
|
105
|
+
- `--log-file` CLI option
|
|
106
|
+
|
|
107
|
+
## Refactoring
|
|
108
|
+
|
|
109
|
+
- [ ] **Consider blessed-contrib widgets**
|
|
110
|
+
- Gauge widget for battery
|
|
111
|
+
- Sparkline for historical data
|
|
112
|
+
- LCD-style numbers for volume
|
|
113
|
+
|
|
114
|
+
- [ ] **State management**
|
|
115
|
+
- Currently state is scattered (selectedIndex, currentView, etc.)
|
|
116
|
+
- Consider a simple state object with update functions
|
|
117
|
+
|
|
118
|
+
## Testing Notes
|
|
119
|
+
|
|
120
|
+
- Test with 1 device
|
|
121
|
+
- Test with many devices (5+) to verify grid layout
|
|
122
|
+
- Test device going offline/online during session
|
|
123
|
+
- Test token refresh during long session
|
|
124
|
+
- Test on different terminal sizes
|
|
125
|
+
- Test vim key navigation (hjkl)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export type DeviceCard = {
|
|
3
|
+
box: Box;
|
|
4
|
+
nameText: Text;
|
|
5
|
+
statusText: Text;
|
|
6
|
+
batteryText: Text;
|
|
7
|
+
playbackText: Text;
|
|
8
|
+
model: YotoDeviceModel;
|
|
9
|
+
index: number;
|
|
10
|
+
};
|
|
11
|
+
export type DetailView = {
|
|
12
|
+
container: Box;
|
|
13
|
+
deviceBox: Box;
|
|
14
|
+
connectionText: Text;
|
|
15
|
+
batteryText: Text;
|
|
16
|
+
volumeText: Text;
|
|
17
|
+
modeText: Text;
|
|
18
|
+
tempText: Text;
|
|
19
|
+
playbackHeader: Text;
|
|
20
|
+
trackText: Text;
|
|
21
|
+
chapterText: Text;
|
|
22
|
+
progressText: Text;
|
|
23
|
+
cardText: Text;
|
|
24
|
+
logBox: Box;
|
|
25
|
+
logLines: string[];
|
|
26
|
+
model: YotoDeviceModel | null;
|
|
27
|
+
};
|
|
28
|
+
import { Box } from '@unblessed/node';
|
|
29
|
+
import { Text } from '@unblessed/node';
|
|
30
|
+
import type { YotoDeviceModel } from '../lib/yoto-device.js';
|
|
31
|
+
//# sourceMappingURL=device-tui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-tui.d.ts","sourceRoot":"","sources":["device-tui.js"],"names":[],"mappings":";;SAuOc,GAAG;cACH,IAAI;gBACJ,IAAI;iBACJ,IAAI;kBACJ,IAAI;WACJ,eAAe;WACf,MAAM;;;eAmHN,GAAG;eACH,GAAG;oBACH,IAAI;iBACJ,IAAI;gBACJ,IAAI;cACJ,IAAI;cACJ,IAAI;oBACJ,IAAI;eACJ,IAAI;iBACJ,IAAI;kBACJ,IAAI;cACJ,IAAI;YACJ,GAAG;cACH,MAAM,EAAE;WACR,eAAe,GAAG,IAAI;;oBArW6B,iBAAiB;qBAAjB,iBAAiB;qCAH9C,uBAAuB"}
|