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/lib/mqtt/factory.d.ts
CHANGED
|
@@ -1,12 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a configured MQTT client for a Yoto device
|
|
3
|
+
* @param {YotoMqttOptions} options - MQTT connection options
|
|
4
|
+
* @returns {YotoMqttClient} Configured Yoto MQTT client
|
|
5
|
+
* @throws {Error} If required options are missing
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```javascript
|
|
9
|
+
* import { createYotoMqttClient } from 'yoto-nodejs-client/lib/mqtt'
|
|
10
|
+
*
|
|
11
|
+
* const client = createYotoMqttClient({
|
|
12
|
+
* deviceId: 'abc123',
|
|
13
|
+
* accessToken: 'eyJhbGc...'
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* client.on('events', (message) => {
|
|
17
|
+
* console.log('Playing:', message.trackTitle)
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* await client.connect()
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```javascript
|
|
25
|
+
* // Disable automatic reconnection
|
|
26
|
+
* const client = createYotoMqttClient({
|
|
27
|
+
* deviceId: 'abc123',
|
|
28
|
+
* accessToken: 'token',
|
|
29
|
+
* mqttOptions: {
|
|
30
|
+
* reconnectPeriod: 0, // Disable auto-reconnect (default is 5000ms)
|
|
31
|
+
* connectTimeout: 30000 // 30 second connection timeout
|
|
32
|
+
* }
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
1
36
|
export function createYotoMqttClient(options: YotoMqttOptions): YotoMqttClient;
|
|
37
|
+
/**
|
|
38
|
+
* MQTT.js client options that can be passed to createYotoMqttClient
|
|
39
|
+
*/
|
|
40
|
+
export type MqttClientOptions = Partial<mqtt.IClientOptions>;
|
|
2
41
|
export type YotoMqttOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* - Device ID to connect to
|
|
44
|
+
*/
|
|
3
45
|
deviceId: string;
|
|
46
|
+
/**
|
|
47
|
+
* - JWT access token for authentication
|
|
48
|
+
*/
|
|
4
49
|
accessToken: string;
|
|
50
|
+
/**
|
|
51
|
+
* - Prefix for MQTT client ID (default: 'DASH')
|
|
52
|
+
*/
|
|
5
53
|
clientIdPrefix?: string;
|
|
54
|
+
/**
|
|
55
|
+
* - MQTT broker URL
|
|
56
|
+
*/
|
|
6
57
|
brokerUrl?: string;
|
|
7
|
-
|
|
58
|
+
/**
|
|
59
|
+
* - MQTT broker port
|
|
60
|
+
*/
|
|
8
61
|
port?: number;
|
|
62
|
+
/**
|
|
63
|
+
* - Auto-subscribe to device topics on connect
|
|
64
|
+
*/
|
|
9
65
|
autoSubscribe?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* - Additional MQTT.js client options (defaults: reconnectPeriod=5000, keepalive=300; cannot override: clientId, username, password, protocol, ALPNProtocols)
|
|
68
|
+
*/
|
|
69
|
+
mqttOptions?: MqttClientOptions;
|
|
10
70
|
};
|
|
11
71
|
import { YotoMqttClient } from './client.js';
|
|
72
|
+
import mqtt from 'mqtt';
|
|
12
73
|
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["factory.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["factory.js"],"names":[],"mappings":"AA0CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,8CAjCW,eAAe,GACb,cAAc,CAmF1B;;;;gCA/GY,OAAO,CAAC,mBAAc,CAAC;;;;;cAKtB,MAAM;;;;iBACN,MAAM;;;;qBACN,MAAM;;;;gBACN,MAAM;;;;WACN,MAAM;;;;oBACN,OAAO;;;;kBACP,iBAAiB;;+BAIA,aAAa;iBAD3B,MAAM"}
|
package/lib/mqtt/factory.js
CHANGED
|
@@ -13,15 +13,20 @@
|
|
|
13
13
|
// MQTT Factory: Create properly configured MQTT clients for Yoto devices
|
|
14
14
|
// ============================================================================
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* MQTT.js client options that can be passed to createYotoMqttClient
|
|
18
|
+
* @typedef {Partial<IClientOptions>} MqttClientOptions
|
|
19
|
+
*/
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* @typedef {Object} YotoMqttOptions
|
|
18
23
|
* @property {string} deviceId - Device ID to connect to
|
|
19
24
|
* @property {string} accessToken - JWT access token for authentication
|
|
20
25
|
* @property {string} [clientIdPrefix='DASH'] - Prefix for MQTT client ID (default: 'DASH')
|
|
21
26
|
* @property {string} [brokerUrl='wss://aqrphjqbp3u2z-ats.iot.eu-west-2.amazonaws.com'] - MQTT broker URL
|
|
22
|
-
* @property {number} [keepalive=300] - Keepalive interval in seconds
|
|
23
27
|
* @property {number} [port=443] - MQTT broker port
|
|
24
28
|
* @property {boolean} [autoSubscribe=true] - Auto-subscribe to device topics on connect
|
|
29
|
+
* @property {MqttClientOptions} [mqttOptions] - Additional MQTT.js client options (defaults: reconnectPeriod=5000, keepalive=300; cannot override: clientId, username, password, protocol, ALPNProtocols)
|
|
25
30
|
*/
|
|
26
31
|
|
|
27
32
|
import mqtt from 'mqtt'
|
|
@@ -56,6 +61,19 @@ import {
|
|
|
56
61
|
*
|
|
57
62
|
* await client.connect()
|
|
58
63
|
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```javascript
|
|
67
|
+
* // Disable automatic reconnection
|
|
68
|
+
* const client = createYotoMqttClient({
|
|
69
|
+
* deviceId: 'abc123',
|
|
70
|
+
* accessToken: 'token',
|
|
71
|
+
* mqttOptions: {
|
|
72
|
+
* reconnectPeriod: 0, // Disable auto-reconnect (default is 5000ms)
|
|
73
|
+
* connectTimeout: 30000 // 30 second connection timeout
|
|
74
|
+
* }
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
59
77
|
*/
|
|
60
78
|
export function createYotoMqttClient (options) {
|
|
61
79
|
// Validate required options
|
|
@@ -73,9 +91,9 @@ export function createYotoMqttClient (options) {
|
|
|
73
91
|
accessToken,
|
|
74
92
|
clientIdPrefix = 'DASH',
|
|
75
93
|
brokerUrl = MQTT_BROKER_URL,
|
|
76
|
-
keepalive = MQTT_KEEPALIVE,
|
|
77
94
|
port = MQTT_PORT,
|
|
78
|
-
autoSubscribe = true
|
|
95
|
+
autoSubscribe = true,
|
|
96
|
+
mqttOptions: additionalMqttOptions = {}
|
|
79
97
|
} = options
|
|
80
98
|
|
|
81
99
|
// Build MQTT client ID
|
|
@@ -85,15 +103,19 @@ export function createYotoMqttClient (options) {
|
|
|
85
103
|
const username = `${deviceId}?x-amz-customauthorizer-name=${MQTT_AUTH_NAME}`
|
|
86
104
|
|
|
87
105
|
// Create MQTT connection options
|
|
106
|
+
// Merge additional options first, then override with required Yoto settings
|
|
88
107
|
/** @type {IClientOptions} */
|
|
89
108
|
const mqttOptions = {
|
|
109
|
+
// Defaults (can be overridden by additionalMqttOptions)
|
|
110
|
+
reconnectPeriod: 5000, // Default: auto-reconnect after 5 seconds
|
|
111
|
+
keepalive: MQTT_KEEPALIVE, // Default: 300 seconds (5 minutes)
|
|
112
|
+
...additionalMqttOptions, // Allow overriding defaults
|
|
113
|
+
// Required Yoto-specific settings (cannot be overridden)
|
|
90
114
|
clientId,
|
|
91
115
|
username,
|
|
92
116
|
password: accessToken,
|
|
93
|
-
keepalive,
|
|
94
117
|
port,
|
|
95
118
|
protocol: MQTT_PROTOCOL,
|
|
96
|
-
reconnectPeriod: 0, // Disable auto-reconnect, handle manually
|
|
97
119
|
ALPNProtocols: MQTT_ALPN_PROTOCOLS
|
|
98
120
|
}
|
|
99
121
|
|
package/lib/mqtt/mqtt.test.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
3
|
import { getDevices } from '../api-endpoints/devices.js'
|
|
4
|
-
import { loadTestTokens, logResponse } from '../api-endpoints/test-helpers.js'
|
|
4
|
+
import { loadTestTokens, logResponse } from '../api-endpoints/endpoint-test-helpers.js'
|
|
5
5
|
import { createYotoMqttClient } from './index.js'
|
|
6
6
|
|
|
7
7
|
const { accessToken } = loadTestTokens()
|
|
8
8
|
|
|
9
9
|
test('MQTT client', async (t) => {
|
|
10
|
-
await t.test('should connect, request status/events, and receive response and events messages', async () => {
|
|
10
|
+
await t.test('should connect, request status/events, and receive response and events messages (status-legacy is optional)', async () => {
|
|
11
11
|
// Get an online device
|
|
12
12
|
const response = await getDevices({ accessToken })
|
|
13
13
|
assert.ok(response.devices.length > 0, 'Should have at least one device')
|
|
@@ -28,23 +28,26 @@ test('MQTT client', async (t) => {
|
|
|
28
28
|
let eventsResponseReceived = false
|
|
29
29
|
let eventsMessageReceived = false
|
|
30
30
|
let statusMessageReceived = false
|
|
31
|
+
let statusLegacyMessageReceived = false
|
|
31
32
|
/** @type {Error[]} */
|
|
32
33
|
const errors = []
|
|
33
34
|
|
|
34
35
|
// Setup message handlers
|
|
35
|
-
mqttClient.on('events', (
|
|
36
|
-
logResponse('MQTT events message',
|
|
36
|
+
mqttClient.on('events', (topic, payload) => {
|
|
37
|
+
logResponse('MQTT events message', { topic, payload })
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
40
|
// Validate events message structure
|
|
40
|
-
assert.ok(
|
|
41
|
+
assert.ok(payload, 'Events message should exist')
|
|
42
|
+
assert.ok(typeof topic === 'string', 'Topic should be a string')
|
|
43
|
+
assert.ok(topic.includes('/data/events'), 'Topic should contain /data/events')
|
|
41
44
|
|
|
42
45
|
// Events messages are partial - not all fields are always present
|
|
43
|
-
if (
|
|
44
|
-
assert.ok(typeof
|
|
46
|
+
if (payload.playbackStatus !== undefined) {
|
|
47
|
+
assert.ok(typeof payload.playbackStatus === 'string', 'playbackStatus should be string')
|
|
45
48
|
}
|
|
46
|
-
if (
|
|
47
|
-
assert.ok(typeof
|
|
49
|
+
if (payload.cardId !== undefined) {
|
|
50
|
+
assert.ok(typeof payload.cardId === 'string', 'cardId should be string')
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
eventsMessageReceived = true
|
|
@@ -53,18 +56,20 @@ test('MQTT client', async (t) => {
|
|
|
53
56
|
}
|
|
54
57
|
})
|
|
55
58
|
|
|
56
|
-
mqttClient.on('status', (
|
|
57
|
-
logResponse('MQTT status message',
|
|
59
|
+
mqttClient.on('status', (topic, payload) => {
|
|
60
|
+
logResponse('MQTT status message', { topic, payload })
|
|
58
61
|
|
|
59
62
|
try {
|
|
60
63
|
// Validate status message structure
|
|
61
|
-
assert.ok(
|
|
62
|
-
assert.ok(
|
|
63
|
-
assert.ok(
|
|
64
|
-
assert.ok(
|
|
65
|
-
assert.ok(typeof
|
|
66
|
-
assert.ok(typeof
|
|
67
|
-
assert.ok(typeof
|
|
64
|
+
assert.ok(payload, 'Status message should exist')
|
|
65
|
+
assert.ok(typeof topic === 'string', 'Topic should be a string')
|
|
66
|
+
assert.ok(topic.includes('/data/status'), 'Topic should contain /data/status')
|
|
67
|
+
assert.ok(payload.status, 'Status message should have status object')
|
|
68
|
+
assert.ok(typeof payload.status.statusVersion === 'number', 'Should have statusVersion number')
|
|
69
|
+
assert.ok(typeof payload.status.fwVersion === 'string', 'Should have fwVersion string')
|
|
70
|
+
assert.ok(typeof payload.status.productType === 'string', 'Should have productType string')
|
|
71
|
+
assert.ok(typeof payload.status.batteryLevel === 'number', 'Should have batteryLevel number')
|
|
72
|
+
assert.ok(typeof payload.status.volume === 'number', 'Should have volume number')
|
|
68
73
|
|
|
69
74
|
statusMessageReceived = true
|
|
70
75
|
} catch (err) {
|
|
@@ -72,28 +77,71 @@ test('MQTT client', async (t) => {
|
|
|
72
77
|
}
|
|
73
78
|
})
|
|
74
79
|
|
|
75
|
-
mqttClient.on('
|
|
76
|
-
logResponse('MQTT
|
|
80
|
+
mqttClient.on('status-legacy', (topic, payload) => {
|
|
81
|
+
logResponse('MQTT status-legacy message', { topic, payload })
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Validate legacy status message structure
|
|
85
|
+
assert.ok(payload, 'Legacy status message should exist')
|
|
86
|
+
assert.ok(typeof topic === 'string', 'Topic should be a string')
|
|
87
|
+
assert.ok(!topic.includes('/data/'), 'Legacy topic should NOT contain /data/')
|
|
88
|
+
assert.ok(payload.status, 'Legacy status message should have status object')
|
|
89
|
+
|
|
90
|
+
// Validate lifecycle fields unique to legacy format
|
|
91
|
+
assert.ok(typeof payload.status.statusVersion === 'number', 'Should have statusVersion number')
|
|
92
|
+
assert.ok(typeof payload.status.fwVersion === 'string', 'Should have fwVersion string')
|
|
93
|
+
|
|
94
|
+
// shutDown field is the key field for lifecycle events
|
|
95
|
+
if (payload.status.shutDown !== undefined) {
|
|
96
|
+
assert.ok(typeof payload.status.shutDown === 'string', 'shutDown should be string')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// upTime and utcTime are key for startup detection
|
|
100
|
+
if (payload.status.upTime !== undefined) {
|
|
101
|
+
assert.ok(typeof payload.status.upTime === 'number', 'upTime should be number')
|
|
102
|
+
}
|
|
103
|
+
if (payload.status.utcTime !== undefined) {
|
|
104
|
+
assert.ok(typeof payload.status.utcTime === 'number', 'utcTime should be number')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Hardware diagnostic fields unique to legacy
|
|
108
|
+
if (payload.status.battery !== undefined) {
|
|
109
|
+
assert.ok(typeof payload.status.battery === 'number', 'battery should be number')
|
|
110
|
+
}
|
|
111
|
+
if (payload.status.wifiStrength !== undefined) {
|
|
112
|
+
assert.ok(typeof payload.status.wifiStrength === 'number', 'wifiStrength should be number')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
statusLegacyMessageReceived = true
|
|
116
|
+
} catch (err) {
|
|
117
|
+
errors.push(/** @type {Error} */ (err))
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
mqttClient.on('response', (topic, payload) => {
|
|
122
|
+
logResponse('MQTT response message', { topic, payload })
|
|
77
123
|
|
|
78
124
|
try {
|
|
79
125
|
// Validate response message structure
|
|
80
|
-
assert.ok(
|
|
81
|
-
assert.ok(
|
|
82
|
-
assert.ok(
|
|
126
|
+
assert.ok(payload, 'Response message should exist')
|
|
127
|
+
assert.ok(typeof topic === 'string', 'Topic should be a string')
|
|
128
|
+
assert.ok(topic.includes('/response'), 'Topic should contain /response')
|
|
129
|
+
assert.ok(payload.status, 'Response should have status object')
|
|
130
|
+
assert.ok(typeof payload.status.req_body === 'string', 'Response should have req_body string')
|
|
83
131
|
|
|
84
132
|
// Check if status request was acknowledged (field name is 'status/request')
|
|
85
|
-
if (
|
|
133
|
+
if (payload.status['status/request']) {
|
|
86
134
|
assert.ok(
|
|
87
|
-
|
|
135
|
+
payload.status['status/request'] === 'OK' || payload.status['status/request'] === 'FAIL',
|
|
88
136
|
'Status request field should be OK or FAIL'
|
|
89
137
|
)
|
|
90
138
|
statusResponseReceived = true
|
|
91
139
|
}
|
|
92
140
|
|
|
93
141
|
// Check if events request was acknowledged
|
|
94
|
-
if (
|
|
142
|
+
if (payload.status.events) {
|
|
95
143
|
assert.ok(
|
|
96
|
-
|
|
144
|
+
payload.status.events === 'OK' || payload.status.events === 'FAIL',
|
|
97
145
|
'Events request field should be OK or FAIL'
|
|
98
146
|
)
|
|
99
147
|
eventsResponseReceived = true
|
|
@@ -125,7 +173,7 @@ test('MQTT client', async (t) => {
|
|
|
125
173
|
// Wait for messages (with timeout)
|
|
126
174
|
await new Promise((resolve, reject) => {
|
|
127
175
|
timeoutId = setTimeout(() => {
|
|
128
|
-
reject(new Error(`Timeout waiting for messages. statusResponse=${statusResponseReceived}, eventsResponse=${eventsResponseReceived}, eventsMessage=${eventsMessageReceived}, statusMessage=${statusMessageReceived}`))
|
|
176
|
+
reject(new Error(`Timeout waiting for messages. statusResponse=${statusResponseReceived}, eventsResponse=${eventsResponseReceived}, eventsMessage=${eventsMessageReceived}, statusMessage=${statusMessageReceived}, statusLegacy=${statusLegacyMessageReceived} (optional)`))
|
|
129
177
|
}, 5000) // 5 second timeout
|
|
130
178
|
|
|
131
179
|
checkIntervalId = setInterval(() => {
|
|
@@ -134,6 +182,8 @@ test('MQTT client', async (t) => {
|
|
|
134
182
|
clearInterval(checkIntervalId)
|
|
135
183
|
reject(errors[0])
|
|
136
184
|
}
|
|
185
|
+
// Note: status-legacy is NOT required - it doesn't respond to requestStatus()
|
|
186
|
+
// It only emits on real-time lifecycle events (shutdown/startup) or 5-minute periodic updates
|
|
137
187
|
if (statusResponseReceived && eventsResponseReceived && eventsMessageReceived && statusMessageReceived) {
|
|
138
188
|
clearTimeout(timeoutId)
|
|
139
189
|
clearInterval(checkIntervalId)
|
|
@@ -147,6 +197,13 @@ test('MQTT client', async (t) => {
|
|
|
147
197
|
assert.ok(eventsResponseReceived, 'Should have received events request response')
|
|
148
198
|
assert.ok(eventsMessageReceived, 'Should have received events data message')
|
|
149
199
|
assert.ok(statusMessageReceived, 'Should have received status data message')
|
|
200
|
+
|
|
201
|
+
// Note: status-legacy message is optional in this test because it does NOT respond to requestStatus()
|
|
202
|
+
// It only emits on real-time lifecycle events (shutdown/startup) or 5-minute periodic updates
|
|
203
|
+
// If we happened to catch one during the test window, validate it was received correctly
|
|
204
|
+
if (statusLegacyMessageReceived) {
|
|
205
|
+
console.log('✓ Received optional status-legacy message with lifecycle events')
|
|
206
|
+
}
|
|
150
207
|
} catch (err) {
|
|
151
208
|
// Clean up timers on error
|
|
152
209
|
if (timeoutId) clearTimeout(timeoutId)
|
package/lib/mqtt/topics.d.ts
CHANGED
|
@@ -1,34 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the events topics for a device
|
|
3
|
+
*
|
|
4
|
+
* NOTE: Old undocumented format device/{id}/events exists but returns different low-level
|
|
5
|
+
* hardware data. Do not use. Only the documented /data/ path is supported.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} deviceId - Device ID
|
|
8
|
+
* @returns {string[]} Events topics
|
|
9
|
+
*/
|
|
1
10
|
export function getEventsTopic(deviceId: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Get the status topics for a device
|
|
13
|
+
*
|
|
14
|
+
* Subscribes to BOTH documented and legacy status topics:
|
|
15
|
+
* - device/{id}/data/status: Documented format - responds to requestStatus(), auto-publishes every 5 min
|
|
16
|
+
* - device/{id}/status: Legacy format - does NOT respond to requests, emits on lifecycle events + every 5 min
|
|
17
|
+
*
|
|
18
|
+
* The legacy topic is the ONLY source for:
|
|
19
|
+
* - shutDown field: 'nA' = device running, any other value = device shutting down/shut down
|
|
20
|
+
* - Startup detection: shutDown='nA' + low upTime values + utcTime: 0 after power on
|
|
21
|
+
* - Full hardware diagnostics: battery voltage, memory stats, temperatures
|
|
22
|
+
*
|
|
23
|
+
* Key behavior differences:
|
|
24
|
+
* - Documented topic: Responds immediately to requestStatus() command
|
|
25
|
+
* - Legacy topic: Emits real-time on shutdown/startup events, plus periodic updates every 5 minutes
|
|
26
|
+
* - Legacy topic does NOT respond to requestStatus() - it's passive/event-driven only
|
|
27
|
+
*
|
|
28
|
+
* Both topics are necessary - the documented /data/ path does not include lifecycle events.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} deviceId - Device ID
|
|
31
|
+
* @returns {string[]} Status topics (both documented and legacy)
|
|
32
|
+
*/
|
|
2
33
|
export function getStatusTopic(deviceId: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Get the response topic for a device (subscribe)
|
|
36
|
+
* @param {string} deviceId - Device ID
|
|
37
|
+
* @returns {string} Response topic
|
|
38
|
+
*/
|
|
3
39
|
export function getResponseTopic(deviceId: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Get all subscription topics for a device
|
|
42
|
+
*
|
|
43
|
+
* Subscribes to documented formats PLUS legacy status topic:
|
|
44
|
+
* - device/{id}/data/events - Event messages (documented)
|
|
45
|
+
* - device/{id}/data/status - Status messages (documented)
|
|
46
|
+
* - device/{id}/status - Legacy status (REQUIRED for shutdown/startup lifecycle events)
|
|
47
|
+
* - device/{id}/response - Command responses (documented)
|
|
48
|
+
*
|
|
49
|
+
* The legacy status topic is necessary because the documented /data/ topics do not
|
|
50
|
+
* include device lifecycle events (shutdown, startup) which are critical for proper
|
|
51
|
+
* device state management.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} deviceId - Device ID
|
|
54
|
+
* @returns {string[]} Array of topics to subscribe to
|
|
55
|
+
*/
|
|
4
56
|
export function getSubscriptionTopics(deviceId: string): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Get a command topic for a device (publish)
|
|
59
|
+
* @param {string} deviceId - Device ID
|
|
60
|
+
* @param {string} resource - Command resource (e.g., 'volume', 'ambients', 'card')
|
|
61
|
+
* @param {string} [action] - Command action (e.g., 'set', 'start', 'stop')
|
|
62
|
+
* @returns {string} Command topic
|
|
63
|
+
*/
|
|
5
64
|
export function getCommandTopic(deviceId: string, resource: string, action?: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Parse a topic string to extract device ID and message type
|
|
67
|
+
*
|
|
68
|
+
* Distinguishes between documented and legacy status topics:
|
|
69
|
+
* - device/{id}/data/status -> messageType: 'status' (documented)
|
|
70
|
+
* - device/{id}/status -> messageType: 'status-legacy' (for lifecycle events)
|
|
71
|
+
*
|
|
72
|
+
* @param {string} topic - Full MQTT topic string
|
|
73
|
+
* @returns {{ deviceId: string, messageType: YotoMqttTopicType | 'unknown' }} Parsed topic info
|
|
74
|
+
*/
|
|
6
75
|
export function parseTopic(topic: string): {
|
|
7
76
|
deviceId: string;
|
|
8
77
|
messageType: YotoMqttTopicType | "unknown";
|
|
9
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* Get the events request command topic
|
|
81
|
+
* @param {string} deviceId - Device ID
|
|
82
|
+
* @returns {string} Events request topic
|
|
83
|
+
*/
|
|
10
84
|
export function getEventsRequestTopic(deviceId: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Get the status request command topic
|
|
87
|
+
* @param {string} deviceId - Device ID
|
|
88
|
+
* @returns {string} Status request topic
|
|
89
|
+
*/
|
|
11
90
|
export function getStatusRequestTopic(deviceId: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Get the volume set command topic
|
|
93
|
+
* @param {string} deviceId - Device ID
|
|
94
|
+
* @returns {string} Volume set topic
|
|
95
|
+
*/
|
|
12
96
|
export function getVolumeSetTopic(deviceId: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Get the ambients set command topic
|
|
99
|
+
* @param {string} deviceId - Device ID
|
|
100
|
+
* @returns {string} Ambients set topic
|
|
101
|
+
*/
|
|
13
102
|
export function getAmbientsSetTopic(deviceId: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Get the sleep timer set command topic
|
|
105
|
+
* @param {string} deviceId - Device ID
|
|
106
|
+
* @returns {string} Sleep timer set topic
|
|
107
|
+
*/
|
|
14
108
|
export function getSleepTimerSetTopic(deviceId: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* Get the reboot command topic
|
|
111
|
+
* @param {string} deviceId - Device ID
|
|
112
|
+
* @returns {string} Reboot topic
|
|
113
|
+
*/
|
|
15
114
|
export function getRebootTopic(deviceId: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* Get the card start command topic
|
|
117
|
+
* @param {string} deviceId - Device ID
|
|
118
|
+
* @returns {string} Card start topic
|
|
119
|
+
*/
|
|
16
120
|
export function getCardStartTopic(deviceId: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Get the card stop command topic
|
|
123
|
+
* @param {string} deviceId - Device ID
|
|
124
|
+
* @returns {string} Card stop topic
|
|
125
|
+
*/
|
|
17
126
|
export function getCardStopTopic(deviceId: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* Get the card pause command topic
|
|
129
|
+
* @param {string} deviceId - Device ID
|
|
130
|
+
* @returns {string} Card pause topic
|
|
131
|
+
*/
|
|
18
132
|
export function getCardPauseTopic(deviceId: string): string;
|
|
133
|
+
/**
|
|
134
|
+
* Get the card resume command topic
|
|
135
|
+
* @param {string} deviceId - Device ID
|
|
136
|
+
* @returns {string} Card resume topic
|
|
137
|
+
*/
|
|
19
138
|
export function getCardResumeTopic(deviceId: string): string;
|
|
139
|
+
/**
|
|
140
|
+
* Get the bluetooth on command topic
|
|
141
|
+
* @param {string} deviceId - Device ID
|
|
142
|
+
* @returns {string} Bluetooth on topic
|
|
143
|
+
*/
|
|
20
144
|
export function getBluetoothOnTopic(deviceId: string): string;
|
|
145
|
+
/**
|
|
146
|
+
* Get the bluetooth off command topic
|
|
147
|
+
* @param {string} deviceId - Device ID
|
|
148
|
+
* @returns {string} Bluetooth off topic
|
|
149
|
+
*/
|
|
21
150
|
export function getBluetoothOffTopic(deviceId: string): string;
|
|
151
|
+
/**
|
|
152
|
+
* Get the bluetooth delete bonds command topic
|
|
153
|
+
* @param {string} deviceId - Device ID
|
|
154
|
+
* @returns {string} Bluetooth delete bonds topic
|
|
155
|
+
*/
|
|
22
156
|
export function getBluetoothDeleteBondsTopic(deviceId: string): string;
|
|
157
|
+
/**
|
|
158
|
+
* Get the bluetooth connect command topic
|
|
159
|
+
* @param {string} deviceId - Device ID
|
|
160
|
+
* @returns {string} Bluetooth connect topic
|
|
161
|
+
*/
|
|
23
162
|
export function getBluetoothConnectTopic(deviceId: string): string;
|
|
163
|
+
/**
|
|
164
|
+
* Get the bluetooth disconnect command topic
|
|
165
|
+
* @param {string} deviceId - Device ID
|
|
166
|
+
* @returns {string} Bluetooth disconnect topic
|
|
167
|
+
*/
|
|
24
168
|
export function getBluetoothDisconnectTopic(deviceId: string): string;
|
|
169
|
+
/**
|
|
170
|
+
* Get the bluetooth state command topic
|
|
171
|
+
* @param {string} deviceId - Device ID
|
|
172
|
+
* @returns {string} Bluetooth state topic
|
|
173
|
+
*/
|
|
25
174
|
export function getBluetoothStateTopic(deviceId: string): string;
|
|
175
|
+
/**
|
|
176
|
+
* Get the display preview command topic
|
|
177
|
+
* @param {string} deviceId - Device ID
|
|
178
|
+
* @returns {string} Display preview topic
|
|
179
|
+
*/
|
|
26
180
|
export function getDisplayPreviewTopic(deviceId: string): string;
|
|
181
|
+
/**
|
|
182
|
+
* MQTT Topics for Yoto Players
|
|
183
|
+
*
|
|
184
|
+
* Topic builders and constants for Yoto MQTT communication
|
|
185
|
+
* @see https://yoto.dev/players-mqtt/
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* MQTT topic type for subscriptions
|
|
189
|
+
* @typedef {'events' | 'status' | 'status-legacy' | 'response'} YotoMqttTopicType
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* MQTT broker URL for Yoto devices
|
|
193
|
+
*/
|
|
27
194
|
export const MQTT_BROKER_URL: "wss://aqrphjqbp3u2z-ats.iot.eu-west-2.amazonaws.com";
|
|
195
|
+
/**
|
|
196
|
+
* MQTT authorizer name for Yoto authentication
|
|
197
|
+
*/
|
|
28
198
|
export const MQTT_AUTH_NAME: "PublicJWTAuthorizer";
|
|
199
|
+
/**
|
|
200
|
+
* MQTT connection port
|
|
201
|
+
*/
|
|
29
202
|
export const MQTT_PORT: 443;
|
|
203
|
+
/**
|
|
204
|
+
* MQTT protocol
|
|
205
|
+
*/
|
|
30
206
|
export const MQTT_PROTOCOL: "wss";
|
|
207
|
+
/**
|
|
208
|
+
* MQTT keepalive interval in seconds
|
|
209
|
+
*/
|
|
31
210
|
export const MQTT_KEEPALIVE: 300;
|
|
211
|
+
/**
|
|
212
|
+
* ALPN protocols for AWS IoT
|
|
213
|
+
*/
|
|
32
214
|
export const MQTT_ALPN_PROTOCOLS: string[];
|
|
33
|
-
|
|
215
|
+
/**
|
|
216
|
+
* MQTT topic type for subscriptions
|
|
217
|
+
*/
|
|
218
|
+
export type YotoMqttTopicType = "events" | "status" | "status-legacy" | "response";
|
|
34
219
|
//# sourceMappingURL=topics.d.ts.map
|
package/lib/mqtt/topics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["topics.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["topics.js"],"names":[],"mappings":"AA8CA;;;;;;;;GAQG;AACH,yCAHW,MAAM,GACJ,MAAM,EAAE,CAOpB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,yCAHW,MAAM,GACJ,MAAM,EAAE,CAOpB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,gDAHW,MAAM,GACJ,MAAM,EAAE,CAQpB;AAED;;;;;;GAMG;AACH,0CALW,MAAM,YACN,MAAM,WACN,MAAM,GACJ,MAAM,CAKlB;AAED;;;;;;;;;GASG;AACH,kCAHW,MAAM,GACJ;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAA;CAAE,CA6B5E;AAID;;;;GAIG;AACH,gDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,gDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,4CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,8CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,gDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,yCAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,4CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,4CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,6CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,8CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,+CAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,uDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,mDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,sDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,iDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,iDAHW,MAAM,GACJ,MAAM,CAIlB;AAxUD;;;;;GAKG;AAMH;;;GAGG;AAEH;;GAEG;AACH,8BAA+B,qDAAqD,CAAA;AAEpF;;GAEG;AACH,6BAA8B,qBAAqB,CAAA;AAEnD;;GAEG;AACH,wBAAyB,GAAG,CAAA;AAE5B;;GAEG;AACH,4BAA6B,KAAK,CAAA;AAElC;;GAEG;AACH,6BAA8B,GAAG,CAAA;AAEjC;;GAEG;AACH,2CAAqD;;;;gCA/BxC,QAAQ,GAAG,QAAQ,GAAG,eAAe,GAAG,UAAU"}
|