yoto-nodejs-client 0.0.1
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/LICENSE +21 -0
- package/README.md +736 -0
- package/bin/auth.d.ts +3 -0
- package/bin/auth.d.ts.map +1 -0
- package/bin/auth.js +130 -0
- package/bin/content.d.ts +3 -0
- package/bin/content.d.ts.map +1 -0
- package/bin/content.js +117 -0
- package/bin/devices.d.ts +3 -0
- package/bin/devices.d.ts.map +1 -0
- package/bin/devices.js +239 -0
- package/bin/groups.d.ts +3 -0
- package/bin/groups.d.ts.map +1 -0
- package/bin/groups.js +80 -0
- package/bin/icons.d.ts +3 -0
- package/bin/icons.d.ts.map +1 -0
- package/bin/icons.js +100 -0
- package/bin/lib/cli-helpers.d.ts +21 -0
- package/bin/lib/cli-helpers.d.ts.map +1 -0
- package/bin/lib/cli-helpers.js +140 -0
- package/bin/lib/token-helpers.d.ts +14 -0
- package/bin/lib/token-helpers.d.ts.map +1 -0
- package/bin/lib/token-helpers.js +151 -0
- package/bin/refresh-token.d.ts +3 -0
- package/bin/refresh-token.d.ts.map +1 -0
- package/bin/refresh-token.js +168 -0
- package/bin/token-info.d.ts +3 -0
- package/bin/token-info.d.ts.map +1 -0
- package/bin/token-info.js +351 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +689 -0
- package/lib/api-endpoints/auth.d.ts +56 -0
- package/lib/api-endpoints/auth.d.ts.map +1 -0
- package/lib/api-endpoints/auth.js +209 -0
- package/lib/api-endpoints/auth.test.js +27 -0
- package/lib/api-endpoints/constants.d.ts +6 -0
- package/lib/api-endpoints/constants.d.ts.map +1 -0
- package/lib/api-endpoints/constants.js +31 -0
- package/lib/api-endpoints/content.d.ts +275 -0
- package/lib/api-endpoints/content.d.ts.map +1 -0
- package/lib/api-endpoints/content.js +518 -0
- package/lib/api-endpoints/content.test.js +250 -0
- package/lib/api-endpoints/devices.d.ts +202 -0
- package/lib/api-endpoints/devices.d.ts.map +1 -0
- package/lib/api-endpoints/devices.js +404 -0
- package/lib/api-endpoints/devices.test.js +483 -0
- package/lib/api-endpoints/family-library-groups.d.ts +75 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.js +247 -0
- package/lib/api-endpoints/family-library-groups.test.js +272 -0
- package/lib/api-endpoints/family.d.ts +39 -0
- package/lib/api-endpoints/family.d.ts.map +1 -0
- package/lib/api-endpoints/family.js +166 -0
- package/lib/api-endpoints/family.test.js +184 -0
- package/lib/api-endpoints/helpers.d.ts +29 -0
- package/lib/api-endpoints/helpers.d.ts.map +1 -0
- package/lib/api-endpoints/helpers.js +104 -0
- package/lib/api-endpoints/icons.d.ts +62 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -0
- package/lib/api-endpoints/icons.js +201 -0
- package/lib/api-endpoints/icons.test.js +118 -0
- package/lib/api-endpoints/media.d.ts +37 -0
- package/lib/api-endpoints/media.d.ts.map +1 -0
- package/lib/api-endpoints/media.js +155 -0
- package/lib/api-endpoints/test-helpers.d.ts +7 -0
- package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/test-helpers.js +64 -0
- package/lib/mqtt/client.d.ts +124 -0
- package/lib/mqtt/client.d.ts.map +1 -0
- package/lib/mqtt/client.js +558 -0
- package/lib/mqtt/commands.d.ts +69 -0
- package/lib/mqtt/commands.d.ts.map +1 -0
- package/lib/mqtt/commands.js +238 -0
- package/lib/mqtt/factory.d.ts +12 -0
- package/lib/mqtt/factory.d.ts.map +1 -0
- package/lib/mqtt/factory.js +107 -0
- package/lib/mqtt/index.d.ts +5 -0
- package/lib/mqtt/index.d.ts.map +1 -0
- package/lib/mqtt/index.js +81 -0
- package/lib/mqtt/mqtt.test.js +168 -0
- package/lib/mqtt/topics.d.ts +34 -0
- package/lib/mqtt/topics.d.ts.map +1 -0
- package/lib/mqtt/topics.js +295 -0
- package/lib/pkg.cjs +3 -0
- package/lib/pkg.d.cts +70 -0
- package/lib/pkg.d.cts.map +1 -0
- package/lib/token.d.ts +29 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +240 -0
- package/package.json +91 -0
- package/yoto.png +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export class YotoMqttClient extends EventEmitter<any> {
|
|
2
|
+
constructor(mqttClient: MqttClient, deviceId: string, options?: {
|
|
3
|
+
autoSubscribe?: boolean | undefined;
|
|
4
|
+
});
|
|
5
|
+
mqttClient: MqttClient;
|
|
6
|
+
deviceId: string;
|
|
7
|
+
autoSubscribe: boolean;
|
|
8
|
+
commands: {
|
|
9
|
+
volume: typeof import("./commands.js").createVolumeCommand;
|
|
10
|
+
ambient: typeof import("./commands.js").createAmbientCommand;
|
|
11
|
+
ambientFromHex: typeof import("./commands.js").createAmbientCommandFromHex;
|
|
12
|
+
sleepTimer: typeof import("./commands.js").createSleepTimerCommand;
|
|
13
|
+
cardStart: typeof import("./commands.js").createCardStartCommand;
|
|
14
|
+
bluetoothOn: typeof import("./commands.js").createBluetoothOnCommand;
|
|
15
|
+
bluetoothSpeaker: typeof import("./commands.js").createBluetoothSpeakerCommand;
|
|
16
|
+
bluetoothAudioSource: typeof import("./commands.js").createBluetoothAudioSourceCommand;
|
|
17
|
+
displayPreview: typeof import("./commands.js").createDisplayPreviewCommand;
|
|
18
|
+
};
|
|
19
|
+
get state(): YotoMqttConnectionState;
|
|
20
|
+
get connected(): boolean;
|
|
21
|
+
connect(): Promise<void>;
|
|
22
|
+
disconnect(): Promise<void>;
|
|
23
|
+
requestEvents(): Promise<void>;
|
|
24
|
+
requestStatus(): Promise<void>;
|
|
25
|
+
setVolume(volume: number): Promise<void>;
|
|
26
|
+
setAmbient(r: number, g: number, b: number): Promise<void>;
|
|
27
|
+
setAmbientHex(hexColor: string): Promise<void>;
|
|
28
|
+
setSleepTimer(seconds: number): Promise<void>;
|
|
29
|
+
reboot(): Promise<void>;
|
|
30
|
+
startCard(options: {
|
|
31
|
+
uri: string;
|
|
32
|
+
chapterKey?: string | undefined;
|
|
33
|
+
trackKey?: string | undefined;
|
|
34
|
+
secondsIn?: number | undefined;
|
|
35
|
+
cutOff?: number | undefined;
|
|
36
|
+
anyButtonStop?: boolean | undefined;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
stopCard(): Promise<void>;
|
|
39
|
+
pauseCard(): Promise<void>;
|
|
40
|
+
resumeCard(): Promise<void>;
|
|
41
|
+
bluetoothOn(options?: {
|
|
42
|
+
action?: string | undefined;
|
|
43
|
+
mode?: string | boolean | undefined;
|
|
44
|
+
rssi?: number | undefined;
|
|
45
|
+
name?: string | undefined;
|
|
46
|
+
mac?: string | undefined;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
bluetoothOff(): Promise<void>;
|
|
49
|
+
bluetoothSpeakerMode(): Promise<void>;
|
|
50
|
+
bluetoothAudioSourceMode(): Promise<void>;
|
|
51
|
+
bluetoothDeleteBonds(): Promise<void>;
|
|
52
|
+
bluetoothConnect(): Promise<void>;
|
|
53
|
+
bluetoothDisconnect(): Promise<void>;
|
|
54
|
+
bluetoothGetState(): Promise<void>;
|
|
55
|
+
displayPreview(options: {
|
|
56
|
+
uri: string;
|
|
57
|
+
timeout: number;
|
|
58
|
+
animated: boolean;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
#private;
|
|
61
|
+
}
|
|
62
|
+
export type YotoEventsMessage = {
|
|
63
|
+
repeatAll?: boolean;
|
|
64
|
+
streaming?: boolean;
|
|
65
|
+
volume?: number;
|
|
66
|
+
volumeMax?: number;
|
|
67
|
+
playbackWait?: boolean;
|
|
68
|
+
sleepTimerActive?: boolean;
|
|
69
|
+
eventUtc?: number;
|
|
70
|
+
trackLength?: number;
|
|
71
|
+
position?: number;
|
|
72
|
+
cardId?: string;
|
|
73
|
+
source?: string;
|
|
74
|
+
cardUpdatedAt?: string;
|
|
75
|
+
chapterTitle?: string;
|
|
76
|
+
chapterKey?: string;
|
|
77
|
+
trackTitle?: string;
|
|
78
|
+
trackKey?: string;
|
|
79
|
+
playbackStatus?: string;
|
|
80
|
+
sleepTimerSeconds?: number;
|
|
81
|
+
};
|
|
82
|
+
export type YotoStatusMessage = {
|
|
83
|
+
statusVersion: number;
|
|
84
|
+
fwVersion: string;
|
|
85
|
+
productType: string;
|
|
86
|
+
batteryLevel: number;
|
|
87
|
+
als: number;
|
|
88
|
+
freeDisk: number;
|
|
89
|
+
shutdownTimeout: number;
|
|
90
|
+
dbatTimeout: number;
|
|
91
|
+
charging: number;
|
|
92
|
+
activeCard: string;
|
|
93
|
+
cardInserted: number;
|
|
94
|
+
playingStatus: number;
|
|
95
|
+
headphones: boolean;
|
|
96
|
+
dnowBrightness: number;
|
|
97
|
+
dayBright: number;
|
|
98
|
+
nightBright: number;
|
|
99
|
+
bluetoothHp: boolean;
|
|
100
|
+
volume: number;
|
|
101
|
+
userVolume: number;
|
|
102
|
+
timeFormat: "12" | "24";
|
|
103
|
+
nightlightMode: string;
|
|
104
|
+
temp: string;
|
|
105
|
+
day: number;
|
|
106
|
+
};
|
|
107
|
+
export type YotoResponseMessage = {
|
|
108
|
+
status: {
|
|
109
|
+
volume?: "OK" | "FAIL" | undefined;
|
|
110
|
+
ambients?: "OK" | "FAIL" | undefined;
|
|
111
|
+
card?: "OK" | "FAIL" | undefined;
|
|
112
|
+
events?: "OK" | "FAIL" | undefined;
|
|
113
|
+
status?: "OK" | "FAIL" | undefined;
|
|
114
|
+
bluetooth?: "OK" | "FAIL" | undefined;
|
|
115
|
+
display?: "OK" | "FAIL" | undefined;
|
|
116
|
+
reboot?: "OK" | "FAIL" | undefined;
|
|
117
|
+
req_body: string;
|
|
118
|
+
sleepTimer: "OK" | "FAIL";
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
export type YotoMqttConnectionState = "disconnected" | "connected" | "reconnecting";
|
|
122
|
+
import { EventEmitter } from 'events';
|
|
123
|
+
import type { MqttClient } from 'mqtt';
|
|
124
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["client.js"],"names":[],"mappings":"AAgIA;IAQE,wBALW,UAAU,YACV,MAAM,YAEd;QAA0B,aAAa;KACzC,EAaA;IATC,uBAA4B;IAC5B,iBAAwB;IACxB,uBAAoD;IAMpD;;;;;;;;;;MAAwB;IAO1B,aAFa,uBAAuB,CAMnC;IAMD,iBAFa,OAAO,CAInB;IA2GD,WAFa,OAAO,CAAC,IAAI,CAAC,CA8BzB;IAMD,cAFa,OAAO,CAAC,IAAI,CAAC,CAiBzB;IA4BD,iBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,iBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAOD,kBAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAMzB;IASD,cALW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAMzB;IAOD,wBAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAMzB;IAOD,uBAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAMzB;IAMD,UAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAaD,mBARG;QAAwB,GAAG,EAAnB,MAAM;QACW,UAAU;QACV,QAAQ;QACR,SAAS;QACT,MAAM;QACL,aAAa;KACvC,GAAU,OAAO,CAAC,IAAI,CAAC,CAMzB;IAMD,YAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,aAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,cAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAYD,sBAPG;QAAyB,MAAM;QACI,IAAI;QACd,IAAI;QACJ,IAAI;QACJ,GAAG;KAC5B,GAAU,OAAO,CAAC,IAAI,CAAC,CAMzB;IAMD,gBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,wBAFa,OAAO,CAAC,IAAI,CAAC,CAMzB;IAMD,4BAFa,OAAO,CAAC,IAAI,CAAC,CAMzB;IAMD,wBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,oBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,uBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAMD,qBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAUD,wBALG;QAAwB,GAAG,EAAnB,MAAM;QACU,OAAO,EAAvB,MAAM;QACW,QAAQ,EAAzB,OAAO;KACf,GAAU,OAAO,CAAC,IAAI,CAAC,CAMzB;;CACF;;gBAzhBa,OAAO;gBACP,OAAO;aACP,MAAM;gBACN,MAAM;mBACN,OAAO;uBACP,OAAO;eACP,MAAM;kBACN,MAAM;eACN,MAAM;aACN,MAAM;aACN,MAAM;oBACN,MAAM;mBACN,MAAM;iBACN,MAAM;iBACN,MAAM;eACN,MAAM;qBACN,MAAM;wBACN,MAAM;;;mBAON,MAAM;eACN,MAAM;iBACN,MAAM;kBACN,MAAM;SACN,MAAM;cACN,MAAM;qBACN,MAAM;iBACN,MAAM;cACN,MAAM;gBACN,MAAM;kBACN,MAAM;mBACN,MAAM;gBACN,OAAO;oBACP,MAAM;eACN,MAAM;iBACN,MAAM;iBACN,OAAO;YACP,MAAM;gBACN,MAAM;gBACN,IAAI,GAAG,IAAI;oBACX,MAAM;UACN,MAAM;SACN,MAAM;;;YAQjB;QAAkC,MAAM;QACN,QAAQ;QACR,IAAI;QACJ,MAAM;QACN,MAAM;QACN,SAAS;QACT,OAAO;QACP,MAAM;QACd,QAAQ,EAAvB,MAAM;QACgB,UAAU,EAAhC,IAAI,GAAG,MAAM;KAE1B;;sCAIY,cAAc,GAAG,WAAW,GAAG,cAAc;6BAG7B,QAAQ;gCAhFN,MAAM"}
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yoto MQTT Client
|
|
3
|
+
*
|
|
4
|
+
* Wrapper around mqtt.js with Yoto-specific functionality
|
|
5
|
+
* @see https://yoto.dev/players-mqtt/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// MQTT Client: Yoto-specific wrapper around mqtt.js
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @import { MqttClient } from 'mqtt'
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Events message from device
|
|
18
|
+
* Note: Messages are partial - only changed fields are included
|
|
19
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceiddataevents
|
|
20
|
+
* @typedef {Object} YotoEventsMessage
|
|
21
|
+
* @property {boolean} [repeatAll] - Repeat all tracks
|
|
22
|
+
* @property {boolean} [streaming] - Whether streaming
|
|
23
|
+
* @property {number} [volume] - Current volume level
|
|
24
|
+
* @property {number} [volumeMax] - Maximum volume level
|
|
25
|
+
* @property {boolean} [playbackWait] - Playback waiting
|
|
26
|
+
* @property {boolean} [sleepTimerActive] - Sleep timer active
|
|
27
|
+
* @property {number} [eventUtc] - Unix timestamp
|
|
28
|
+
* @property {number} [trackLength] - Track duration in seconds
|
|
29
|
+
* @property {number} [position] - Current position in seconds
|
|
30
|
+
* @property {string} [cardId] - Currently playing card ID
|
|
31
|
+
* @property {string} [source] - Source of playback (e.g., "card", "remote", "MQTT")
|
|
32
|
+
* @property {string} [cardUpdatedAt] - ISO8601 format timestamp
|
|
33
|
+
* @property {string} [chapterTitle] - Current chapter title
|
|
34
|
+
* @property {string} [chapterKey] - Current chapter key
|
|
35
|
+
* @property {string} [trackTitle] - Current track title
|
|
36
|
+
* @property {string} [trackKey] - Current track key
|
|
37
|
+
* @property {string} [playbackStatus] - Playback status (e.g., "playing", "paused", "stopped")
|
|
38
|
+
* @property {number} [sleepTimerSeconds] - Seconds remaining on sleep timer
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Status message from device
|
|
43
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceiddatastatus
|
|
44
|
+
* @typedef {Object} YotoStatusMessage
|
|
45
|
+
* @property {number} statusVersion - Status message version
|
|
46
|
+
* @property {string} fwVersion - Firmware version
|
|
47
|
+
* @property {string} productType - Product type identifier
|
|
48
|
+
* @property {number} batteryLevel - Battery level percentage
|
|
49
|
+
* @property {number} als - Ambient light sensor reading
|
|
50
|
+
* @property {number} freeDisk - Free disk space
|
|
51
|
+
* @property {number} shutdownTimeout - Shutdown timeout in seconds
|
|
52
|
+
* @property {number} dbatTimeout - DBAT timeout
|
|
53
|
+
* @property {number} charging - Charging state (0 or 1)
|
|
54
|
+
* @property {string} activeCard - Active card ID
|
|
55
|
+
* @property {number} cardInserted - Card insertion state
|
|
56
|
+
* @property {number} playingStatus - Playing status code
|
|
57
|
+
* @property {boolean} headphones - Headphones connected
|
|
58
|
+
* @property {number} dnowBrightness - Current display brightness
|
|
59
|
+
* @property {number} dayBright - Day brightness setting
|
|
60
|
+
* @property {number} nightBright - Night brightness setting
|
|
61
|
+
* @property {boolean} bluetoothHp - Bluetooth headphones enabled
|
|
62
|
+
* @property {number} volume - Current volume
|
|
63
|
+
* @property {number} userVolume - User volume setting
|
|
64
|
+
* @property {'12' | '24'} timeFormat - Time format preference
|
|
65
|
+
* @property {string} nightlightMode - Nightlight mode setting
|
|
66
|
+
* @property {string} temp - Temperature reading
|
|
67
|
+
* @property {number} day - Day mode (0 or 1)
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Response message from device
|
|
72
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidresponse
|
|
73
|
+
* @typedef {Object} YotoResponseMessage
|
|
74
|
+
* @property {Object} status - Status object with dynamic resource keys
|
|
75
|
+
* @property {'OK' | 'FAIL'} [status.volume] - Volume command result
|
|
76
|
+
* @property {'OK' | 'FAIL'} [status.ambients] - Ambients command result
|
|
77
|
+
* @property {'OK' | 'FAIL'} [status.card] - Card command result
|
|
78
|
+
* @property {'OK' | 'FAIL'} [status.events] - Events request result
|
|
79
|
+
* @property {'OK' | 'FAIL'} [status.status] - Status request result
|
|
80
|
+
* @property {'OK' | 'FAIL'} [status.bluetooth] - Bluetooth command result
|
|
81
|
+
* @property {'OK' | 'FAIL'} [status.display] - Display command result
|
|
82
|
+
* @property {'OK' | 'FAIL'} [status.reboot] - Reboot command result
|
|
83
|
+
* @property {string} status.req_body - Stringified JSON from original request
|
|
84
|
+
* @property {'OK' | 'FAIL'} status.sleepTimer - Sleep timer command result (note: actual field name is 'sleep-timer')
|
|
85
|
+
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* MQTT connection state
|
|
90
|
+
* @typedef {'disconnected' | 'connected' | 'reconnecting'} YotoMqttConnectionState
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
import { EventEmitter } from 'events'
|
|
94
|
+
import {
|
|
95
|
+
getSubscriptionTopics,
|
|
96
|
+
parseTopic,
|
|
97
|
+
getEventsRequestTopic,
|
|
98
|
+
getStatusRequestTopic,
|
|
99
|
+
getVolumeSetTopic,
|
|
100
|
+
getAmbientsSetTopic,
|
|
101
|
+
getSleepTimerSetTopic,
|
|
102
|
+
getRebootTopic,
|
|
103
|
+
getCardStartTopic,
|
|
104
|
+
getCardStopTopic,
|
|
105
|
+
getCardPauseTopic,
|
|
106
|
+
getCardResumeTopic,
|
|
107
|
+
getBluetoothOnTopic,
|
|
108
|
+
getBluetoothOffTopic,
|
|
109
|
+
getBluetoothDeleteBondsTopic,
|
|
110
|
+
getBluetoothConnectTopic,
|
|
111
|
+
getBluetoothDisconnectTopic,
|
|
112
|
+
getBluetoothStateTopic,
|
|
113
|
+
getDisplayPreviewTopic
|
|
114
|
+
} from './topics.js'
|
|
115
|
+
import { commands } from './commands.js'
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Yoto MQTT Client class
|
|
119
|
+
* @extends EventEmitter
|
|
120
|
+
*
|
|
121
|
+
* @fires YotoMqttClient#events
|
|
122
|
+
* @fires YotoMqttClient#status
|
|
123
|
+
* @fires YotoMqttClient#response
|
|
124
|
+
* @fires YotoMqttClient#connected
|
|
125
|
+
* @fires YotoMqttClient#disconnected
|
|
126
|
+
* @fires YotoMqttClient#reconnecting
|
|
127
|
+
* @fires YotoMqttClient#error
|
|
128
|
+
*/
|
|
129
|
+
export class YotoMqttClient extends EventEmitter {
|
|
130
|
+
/**
|
|
131
|
+
* Create a Yoto MQTT client
|
|
132
|
+
* @param {MqttClient} mqttClient - Underlying MQTT client
|
|
133
|
+
* @param {string} deviceId - Device ID
|
|
134
|
+
* @param {Object} [options] - Client options
|
|
135
|
+
* @param {boolean} [options.autoSubscribe=true] - Auto-subscribe to device topics on connect
|
|
136
|
+
*/
|
|
137
|
+
constructor (mqttClient, deviceId, options = {}) {
|
|
138
|
+
super()
|
|
139
|
+
|
|
140
|
+
this.mqttClient = mqttClient
|
|
141
|
+
this.deviceId = deviceId
|
|
142
|
+
this.autoSubscribe = options.autoSubscribe !== false
|
|
143
|
+
|
|
144
|
+
// Bind MQTT event handlers
|
|
145
|
+
this.#setupEventHandlers()
|
|
146
|
+
|
|
147
|
+
// Export command builders for convenience
|
|
148
|
+
this.commands = commands
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get current connection state
|
|
153
|
+
* @returns {YotoMqttConnectionState}
|
|
154
|
+
*/
|
|
155
|
+
get state () {
|
|
156
|
+
if (this.mqttClient.connected) return 'connected'
|
|
157
|
+
if (this.mqttClient.reconnecting) return 'reconnecting'
|
|
158
|
+
return 'disconnected'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if client is connected
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
get connected () {
|
|
166
|
+
return this.mqttClient.connected
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Setup MQTT event handlers
|
|
171
|
+
*/
|
|
172
|
+
#setupEventHandlers () {
|
|
173
|
+
// Connection established
|
|
174
|
+
this.mqttClient.on('connect', async () => {
|
|
175
|
+
// Auto-subscribe to device topics if enabled
|
|
176
|
+
if (this.autoSubscribe) {
|
|
177
|
+
await this.#subscribe()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Only emit connected after subscriptions are ready
|
|
181
|
+
this.emit('connected')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Connection lost
|
|
185
|
+
this.mqttClient.on('disconnect', () => {
|
|
186
|
+
this.emit('disconnected')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Connection closed
|
|
190
|
+
this.mqttClient.on('close', () => {
|
|
191
|
+
this.emit('disconnected')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Reconnecting
|
|
195
|
+
this.mqttClient.on('reconnect', () => {
|
|
196
|
+
this.emit('reconnecting')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Error occurred
|
|
200
|
+
this.mqttClient.on('error', (error) => {
|
|
201
|
+
this.emit('error', error)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Message received
|
|
205
|
+
this.mqttClient.on('message', (topic, message) => {
|
|
206
|
+
this.#handleMessage(topic, message)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Subscribe to device topics
|
|
212
|
+
* @returns {Promise<void>}
|
|
213
|
+
*/
|
|
214
|
+
async #subscribe () {
|
|
215
|
+
const topics = getSubscriptionTopics(this.deviceId)
|
|
216
|
+
|
|
217
|
+
// Wait for all subscriptions to complete
|
|
218
|
+
await Promise.all(
|
|
219
|
+
topics.map((topic) => {
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
this.mqttClient.subscribe(topic, (err) => {
|
|
222
|
+
if (err) {
|
|
223
|
+
const error = new Error(`Failed to subscribe to ${topic}: ${err.message}`)
|
|
224
|
+
this.emit('error', error)
|
|
225
|
+
reject(error)
|
|
226
|
+
} else {
|
|
227
|
+
resolve(undefined)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Handle incoming MQTT message
|
|
237
|
+
* @param {string} topic - MQTT topic
|
|
238
|
+
* @param {Buffer} message - Message payload
|
|
239
|
+
*/
|
|
240
|
+
#handleMessage (topic, message) {
|
|
241
|
+
const { deviceId, messageType } = parseTopic(topic)
|
|
242
|
+
|
|
243
|
+
// Ignore messages from other devices
|
|
244
|
+
if (deviceId !== this.deviceId) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Ignore unknown message types
|
|
249
|
+
if (messageType === 'unknown') {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const payload = JSON.parse(message.toString())
|
|
255
|
+
|
|
256
|
+
// Emit typed events based on message type
|
|
257
|
+
if (messageType === 'events') {
|
|
258
|
+
this.emit('events', /** @type {YotoEventsMessage} */ (payload))
|
|
259
|
+
} else if (messageType === 'status') {
|
|
260
|
+
this.emit('status', /** @type {YotoStatusMessage} */ (payload))
|
|
261
|
+
} else if (messageType === 'response') {
|
|
262
|
+
this.emit('response', /** @type {YotoResponseMessage} */ (payload))
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const error = /** @type {Error} */ (err)
|
|
266
|
+
this.emit('error', new Error(`Failed to parse message from ${topic}: ${error.message}`))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Connect to MQTT broker
|
|
272
|
+
* @returns {Promise<void>}
|
|
273
|
+
*/
|
|
274
|
+
async connect () {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
if (this.connected) {
|
|
277
|
+
resolve()
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const onConnect = () => {
|
|
282
|
+
cleanup()
|
|
283
|
+
resolve()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const onError = (/** @type {Error} */ error) => {
|
|
287
|
+
cleanup()
|
|
288
|
+
reject(error)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const cleanup = () => {
|
|
292
|
+
this.mqttClient.off('connect', onConnect)
|
|
293
|
+
this.mqttClient.off('error', onError)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.mqttClient.once('connect', onConnect)
|
|
297
|
+
this.mqttClient.once('error', onError)
|
|
298
|
+
|
|
299
|
+
// MQTT client should already be connecting from factory
|
|
300
|
+
// If not connected, it will connect automatically
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Disconnect from MQTT broker
|
|
306
|
+
* @returns {Promise<void>}
|
|
307
|
+
*/
|
|
308
|
+
async disconnect () {
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
if (!this.connected && !this.mqttClient.reconnecting) {
|
|
311
|
+
resolve()
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const onClose = () => {
|
|
316
|
+
this.mqttClient.off('close', onClose)
|
|
317
|
+
resolve()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.mqttClient.once('close', onClose)
|
|
321
|
+
this.mqttClient.end()
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Publish a message to a topic
|
|
327
|
+
* @param {string} topic - MQTT topic
|
|
328
|
+
* @param {Object | string} payload - Message payload (will be JSON stringified if object)
|
|
329
|
+
* @returns {Promise<void>}
|
|
330
|
+
*/
|
|
331
|
+
async #publish (topic, payload) {
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
const message = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
|
334
|
+
|
|
335
|
+
this.mqttClient.publish(topic, message, (err) => {
|
|
336
|
+
if (err) {
|
|
337
|
+
reject(new Error(`Failed to publish to ${topic}: ${err.message}`))
|
|
338
|
+
} else {
|
|
339
|
+
resolve()
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Command methods
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Request current events from device
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
*/
|
|
351
|
+
async requestEvents () {
|
|
352
|
+
const topic = getEventsRequestTopic(this.deviceId)
|
|
353
|
+
return this.#publish(topic, '')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Request current status from device
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
*/
|
|
360
|
+
async requestStatus () {
|
|
361
|
+
const topic = getStatusRequestTopic(this.deviceId)
|
|
362
|
+
return this.#publish(topic, '')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Set device volume
|
|
367
|
+
* @param {number} volume - Volume level [0-100]
|
|
368
|
+
* @returns {Promise<void>}
|
|
369
|
+
*/
|
|
370
|
+
async setVolume (volume) {
|
|
371
|
+
const command = commands.volume(volume)
|
|
372
|
+
const topic = getVolumeSetTopic(this.deviceId)
|
|
373
|
+
return this.#publish(topic, command)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Set ambient light color
|
|
378
|
+
* @param {number} r - Red intensity [0-255]
|
|
379
|
+
* @param {number} g - Green intensity [0-255]
|
|
380
|
+
* @param {number} b - Blue intensity [0-255]
|
|
381
|
+
* @returns {Promise<void>}
|
|
382
|
+
*/
|
|
383
|
+
async setAmbient (r, g, b) {
|
|
384
|
+
const command = commands.ambient(r, g, b)
|
|
385
|
+
const topic = getAmbientsSetTopic(this.deviceId)
|
|
386
|
+
return this.#publish(topic, command)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Set ambient light color from hex
|
|
391
|
+
* @param {string} hexColor - Hex color string (e.g., "#FF0000")
|
|
392
|
+
* @returns {Promise<void>}
|
|
393
|
+
*/
|
|
394
|
+
async setAmbientHex (hexColor) {
|
|
395
|
+
const command = commands.ambientFromHex(hexColor)
|
|
396
|
+
const topic = getAmbientsSetTopic(this.deviceId)
|
|
397
|
+
return this.#publish(topic, command)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Set sleep timer
|
|
402
|
+
* @param {number} seconds - Timer duration in seconds (0 to disable)
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async setSleepTimer (seconds) {
|
|
406
|
+
const command = commands.sleepTimer(seconds)
|
|
407
|
+
const topic = getSleepTimerSetTopic(this.deviceId)
|
|
408
|
+
return this.#publish(topic, command)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Reboot device
|
|
413
|
+
* @returns {Promise<void>}
|
|
414
|
+
*/
|
|
415
|
+
async reboot () {
|
|
416
|
+
const topic = getRebootTopic(this.deviceId)
|
|
417
|
+
return this.#publish(topic, '')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Start card playback
|
|
422
|
+
* @param {Object} options - Card start options
|
|
423
|
+
* @param {string} options.uri - Card URI (e.g., "https://yoto.io/<cardID>")
|
|
424
|
+
* @param {string} [options.chapterKey] - Chapter to start from
|
|
425
|
+
* @param {string} [options.trackKey] - Track to start from
|
|
426
|
+
* @param {number} [options.secondsIn] - Playback start offset in seconds
|
|
427
|
+
* @param {number} [options.cutOff] - Playback stop offset in seconds
|
|
428
|
+
* @param {boolean} [options.anyButtonStop] - Whether button press stops playback
|
|
429
|
+
* @returns {Promise<void>}
|
|
430
|
+
*/
|
|
431
|
+
async startCard (options) {
|
|
432
|
+
const command = commands.cardStart(options)
|
|
433
|
+
const topic = getCardStartTopic(this.deviceId)
|
|
434
|
+
return this.#publish(topic, command)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Stop card playback
|
|
439
|
+
* @returns {Promise<void>}
|
|
440
|
+
*/
|
|
441
|
+
async stopCard () {
|
|
442
|
+
const topic = getCardStopTopic(this.deviceId)
|
|
443
|
+
return this.#publish(topic, '')
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Pause card playback
|
|
448
|
+
* @returns {Promise<void>}
|
|
449
|
+
*/
|
|
450
|
+
async pauseCard () {
|
|
451
|
+
const topic = getCardPauseTopic(this.deviceId)
|
|
452
|
+
return this.#publish(topic, '')
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resume card playback
|
|
457
|
+
* @returns {Promise<void>}
|
|
458
|
+
*/
|
|
459
|
+
async resumeCard () {
|
|
460
|
+
const topic = getCardResumeTopic(this.deviceId)
|
|
461
|
+
return this.#publish(topic, '')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Turn Bluetooth on
|
|
466
|
+
* @param {Object} [options] - Bluetooth options
|
|
467
|
+
* @param {string} [options.action] - Bluetooth action
|
|
468
|
+
* @param {boolean | string} [options.mode] - Bluetooth mode
|
|
469
|
+
* @param {number} [options.rssi] - RSSI threshold
|
|
470
|
+
* @param {string} [options.name] - Target device name
|
|
471
|
+
* @param {string} [options.mac] - Target device MAC
|
|
472
|
+
* @returns {Promise<void>}
|
|
473
|
+
*/
|
|
474
|
+
async bluetoothOn (options) {
|
|
475
|
+
const command = commands.bluetoothOn(options)
|
|
476
|
+
const topic = getBluetoothOnTopic(this.deviceId)
|
|
477
|
+
return this.#publish(topic, command)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Turn Bluetooth off
|
|
482
|
+
* @returns {Promise<void>}
|
|
483
|
+
*/
|
|
484
|
+
async bluetoothOff () {
|
|
485
|
+
const topic = getBluetoothOffTopic(this.deviceId)
|
|
486
|
+
return this.#publish(topic, '')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Enable Bluetooth speaker mode
|
|
491
|
+
* @returns {Promise<void>}
|
|
492
|
+
*/
|
|
493
|
+
async bluetoothSpeakerMode () {
|
|
494
|
+
const command = commands.bluetoothSpeaker()
|
|
495
|
+
const topic = getBluetoothOnTopic(this.deviceId)
|
|
496
|
+
return this.#publish(topic, command)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Enable Bluetooth audio source mode
|
|
501
|
+
* @returns {Promise<void>}
|
|
502
|
+
*/
|
|
503
|
+
async bluetoothAudioSourceMode () {
|
|
504
|
+
const command = commands.bluetoothAudioSource()
|
|
505
|
+
const topic = getBluetoothOnTopic(this.deviceId)
|
|
506
|
+
return this.#publish(topic, command)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Delete all Bluetooth bonds
|
|
511
|
+
* @returns {Promise<void>}
|
|
512
|
+
*/
|
|
513
|
+
async bluetoothDeleteBonds () {
|
|
514
|
+
const topic = getBluetoothDeleteBondsTopic(this.deviceId)
|
|
515
|
+
return this.#publish(topic, '')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Connect to Bluetooth device
|
|
520
|
+
* @returns {Promise<void>}
|
|
521
|
+
*/
|
|
522
|
+
async bluetoothConnect () {
|
|
523
|
+
const topic = getBluetoothConnectTopic(this.deviceId)
|
|
524
|
+
return this.#publish(topic, '')
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Disconnect Bluetooth device
|
|
529
|
+
* @returns {Promise<void>}
|
|
530
|
+
*/
|
|
531
|
+
async bluetoothDisconnect () {
|
|
532
|
+
const topic = getBluetoothDisconnectTopic(this.deviceId)
|
|
533
|
+
return this.#publish(topic, '')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get Bluetooth state
|
|
538
|
+
* @returns {Promise<void>}
|
|
539
|
+
*/
|
|
540
|
+
async bluetoothGetState () {
|
|
541
|
+
const topic = getBluetoothStateTopic(this.deviceId)
|
|
542
|
+
return this.#publish(topic, '')
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Preview display icon
|
|
547
|
+
* @param {Object} options - Display preview options
|
|
548
|
+
* @param {string} options.uri - Icon URI
|
|
549
|
+
* @param {number} options.timeout - Display duration in seconds
|
|
550
|
+
* @param {boolean} options.animated - Whether icon is animated
|
|
551
|
+
* @returns {Promise<void>}
|
|
552
|
+
*/
|
|
553
|
+
async displayPreview (options) {
|
|
554
|
+
const command = commands.displayPreview(options)
|
|
555
|
+
const topic = getDisplayPreviewTopic(this.deviceId)
|
|
556
|
+
return this.#publish(topic, command)
|
|
557
|
+
}
|
|
558
|
+
}
|