yoto-nodejs-client
v0.0.11
Published
(Unofficial) Node.js client for the Yoto API with automatic token refresh, MQTT device communication, and TypeScript support
Downloads
1,617
Maintainers
Readme
yoto-nodejs-client
A comprehensive Node.js client for the Yoto API with automatic token refresh, MQTT device communication, stateful device management, and full TypeScript support.
Features:
- YotoClient - Low-level HTTP API client with automatic token refresh
- YotoDeviceModel - Stateful device client combining HTTP + MQTT for unified, real time device state
- YotoAccount - Multi-device account manager with automatic discovery and lifecycle management
- Full TypeScript types
- Real-time MQTT device control and monitoring
- Debugging CLI tools for authentication and data inspection
npm install yoto-nodejs-clientUsage
Basic API Client
import { YotoClient } from 'yoto-nodejs-client'
// Authenticate using device flow (CLI/server applications)
const deviceCodeResponse = await YotoClient.requestDeviceCode({
clientId: 'your-client-id'
})
console.log(`Visit ${deviceCodeResponse.verification_uri_complete}`)
console.log(`Enter code: ${deviceCodeResponse.user_code}`)
// Wait for authorization (simplest approach - handles polling automatically)
const tokenResponse = await YotoClient.waitForDeviceAuthorization({
deviceCode: deviceCodeResponse.device_code,
clientId: 'your-client-id',
initialInterval: deviceCodeResponse.interval * 1000,
expiresIn: deviceCodeResponse.expires_in,
onPoll: (result) => {
if (result.status === 'pending') process.stdout.write('.')
if (result.status === 'slow_down') console.log('\nSlowing down...')
}
})
// Create client with automatic token refresh
const client = new YotoClient({
clientId: 'your-client-id',
refreshToken: tokenResponse.refresh_token,
accessToken: tokenResponse.access_token,
onTokenRefresh: async (event) => {
// REQUIRED: Persist tokens when they refresh
await saveTokens({
accessToken: event.updatedAccessToken,
refreshToken: event.updatedRefreshToken,
expiresAt: event.updatedExpiresAt
})
}
})
// Get devices
const { devices } = await client.getDevices()
console.log('Your devices:', devices)
// Get device status
const status = await client.getDeviceStatus({
deviceId: devices[0].deviceId
})
console.log('Battery:', status.batteryLevelPercentage, '%')
// Get user's MYO content
const myoContent = await client.getUserMyoContent()
console.log('Your content:', myoContent)
// Connect to device via MQTT for real-time control
const mqtt = await client.createMqttClient({
deviceId: devices[0].deviceId
})
mqtt.on('events', (message) => {
console.log('Playing:', message.trackTitle)
})
mqtt.on('status', (message) => {
console.log('Volume:', message.volume, '%')
})
await mqtt.connect()
await mqtt.setVolume(50)
await mqtt.setAmbientHex('#FF0000')YotoDeviceModel - Stateful Device Client
import { YotoClient, YotoDeviceModel } from 'yoto-nodejs-client'
// Create API client
const client = new YotoClient({
clientId: 'your-client-id',
refreshToken: 'your-refresh-token',
accessToken: 'your-access-token',
onTokenRefresh: async (event) => {
await saveTokens(event)
}
})
// Get a device
const { devices } = await client.getDevices()
const device = devices[0]
// Create stateful device client (manages HTTP + MQTT state)
const deviceClient = new YotoDeviceModel(client, device, {
httpPollIntervalMs: 600000 // Background polling every 10 minutes
})
// Listen for status updates (from MQTT or HTTP)
deviceClient.on('statusUpdate', (status, source, changedFields) => {
console.log(`Battery: ${status.batteryLevelPercentage}% (via ${source})`)
console.log(`Temperature: ${status.temperatureCelsius}°C`)
console.log(`Online: ${status.isOnline}`)
console.log('Changed fields:', changedFields)
})
// Listen for config changes
deviceClient.on('configUpdate', (config, changedFields) => {
console.log('Config updated:', config.maxVolumeLimit)
console.log('Changed fields:', changedFields)
})
// Listen for playback events
deviceClient.on('playbackUpdate', (playback, changedFields) => {
console.log(`Playing: ${playback.trackTitle}`)
console.log(`Position: ${playback.position}/${playback.trackLength}s`)
console.log('Changed fields:', changedFields)
})
// Listen for online/offline events
deviceClient.on('online', (metadata) => {
if (metadata.reason === 'startup') {
console.log(`Device powered on (uptime: ${metadata.upTime}s)`)
} else {
console.log('Device came online')
}
})
deviceClient.on('offline', (metadata) => {
if (metadata.reason === 'shutdown') {
console.log(`Device shut down: ${metadata.shutDownReason}`)
}
})
// Start the device client (connects MQTT, starts background polling)
await deviceClient.start()
// Access current state
console.log('Current status:', deviceClient.status)
console.log('Current config:', deviceClient.config)
console.log('Current playback:', deviceClient.playback)
console.log('Device capabilities:', deviceClient.capabilities)
// Control the device
await deviceClient.updateConfig({ maxVolumeLimit: 14 })
await deviceClient.updateConfig({
dayDisplayBrightnessAuto: false,
dayDisplayBrightness: 80
})
await deviceClient.updateConfig({
nightYotoRadioEnabled: true,
nightYotoRadio: 'favourites'
})
await deviceClient.sendCommand({ volume: 50 })
// Stop when done
await deviceClient.stop()Account Manager (Multiple Devices)
import { YotoAccount } from 'yoto-nodejs-client'
// Create account manager
const account = new YotoAccount({
clientOptions: {
clientId: 'your-client-id',
refreshToken: 'your-refresh-token',
accessToken: 'your-access-token',
onTokenRefresh: async (event) => {
await saveTokens(event)
}
},
deviceOptions: {
httpPollIntervalMs: 600000 // Applied to all devices
}
})
// Listen for account events
account.on('started', (metadata) => {
console.log(`Managing ${metadata.deviceCount} devices`)
})
// Listen for device events across all devices via the unified bus
account.on('statusUpdate', ({ deviceId, status, source }) => {
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}% (${source})`)
})
account.on('online', ({ deviceId }) => {
console.log(`${deviceId} came online`)
})
account.on('offline', ({ deviceId }) => {
console.log(`${deviceId} went offline`)
})
// Unified error handling
account.on('error', ({ error, context }) => {
console.error(`Error in ${context.source}:`, error.message)
if (context.deviceId) {
console.error(`Device: ${context.deviceId}`)
}
})
// Start managing all devices
await account.start()
// Access individual devices
const device = account.getDevice('abc123')
console.log('Device status:', device.status)
console.log('Device config:', device.config)
// Get all devices
const allDevices = account.devices // Map<deviceId, YotoDeviceModel>
console.log('Total devices:', allDevices.size)
// Refresh device list (add new, remove missing)
await account.refreshDevices()
// Stop all devices
await account.stop()API
Authentication
YotoClient.requestDeviceCode({ clientId, [scope], [audience] })
Start the OAuth2 Device Authorization flow for CLI/server applications. Returns a device code and user verification URL.
- clientId - Your OAuth client ID
- scope - OAuth scopes (default:
'openid profile offline_access') - audience - Token audience (default:
'https://api.yotoplay.com')
const response = await YotoClient.requestDeviceCode({
clientId: 'your-client-id'
})
console.log(`Visit: ${response.verification_uri_complete}`)
console.log(`Or go to ${response.verification_uri} and enter: ${response.user_code}`)YotoClient.exchangeToken({ grantType, ...params })
Exchange authorization code, refresh token, or device code for access tokens.
- grantType -
'authorization_code','refresh_token', or'urn:ietf:params:oauth:grant-type:device_code' - code - Authorization code (for
authorization_codegrant) - refreshToken - Refresh token (for
refresh_tokengrant) - deviceCode - Device code (for device code grant)
- clientId - OAuth client ID
- redirectUri - Redirect URI (for
authorization_codegrant) - codeVerifier - PKCE code verifier (optional)
import { YotoClient, DEVICE_CODE_GRANT_TYPE } from 'yoto-nodejs-client'
// Exchange device code
const tokens = await YotoClient.exchangeToken({
grantType: DEVICE_CODE_GRANT_TYPE,
deviceCode: response.device_code,
clientId: 'your-client-id'
})
// Refresh token
const refreshed = await YotoClient.exchangeToken({
grantType: 'refresh_token',
refreshToken: tokens.refresh_token,
clientId: 'your-client-id'
})YotoClient.waitForDeviceAuthorization({ deviceCode, clientId, [initialInterval], [expiresIn], [onPoll] })
Wait for device authorization to complete with automatic polling. This is the simplest approach - just call it and await the result. It handles all polling logic internally including interval adjustments and timeout detection.
Designed for CLI usage where you want to block until authorization completes. For UI implementations with custom progress feedback, use pollForDeviceToken() directly.
- deviceCode - Device code from
requestDeviceCode() - clientId - OAuth client ID
- initialInterval - Initial polling interval in milliseconds (default: 5000)
- expiresIn - Seconds until device code expires (for timeout detection)
- audience - Audience for the token (default: 'https://api.yotoplay.com')
- onPoll - Optional callback invoked after each poll attempt with the poll result
Returns a promise that resolves to YotoTokenResponse on successful authorization.
Throws YotoAPIError for unrecoverable errors (expired_token, access_denied, invalid_grant) or Error if device code expires.
import { YotoClient } from 'yoto-nodejs-client'
// Simplest approach - just wait for tokens
const deviceAuth = await YotoClient.requestDeviceCode({
clientId: 'your-client-id'
})
console.log(`Visit: ${deviceAuth.verification_uri_complete}`)
console.log(`Code: ${deviceAuth.user_code}`)
// This blocks until authorization completes (or fails)
const tokens = await YotoClient.waitForDeviceAuthorization({
deviceCode: deviceAuth.device_code,
clientId: 'your-client-id',
initialInterval: deviceAuth.interval * 1000,
expiresIn: deviceAuth.expires_in,
onPoll: (result) => {
if (result.status === 'pending') process.stdout.write('.')
if (result.status === 'slow_down') console.log('\nSlowing down...')
}
})
console.log('Got tokens:', tokens)YotoClient.pollForDeviceToken({ deviceCode, clientId, [currentInterval], [audience] })
Poll for device authorization completion with automatic error handling (single poll attempt). This is a lower-level method that gives you control over the polling loop. Use waitForDeviceAuthorization() for a simpler approach.
Non-blocking - returns immediately with poll result. Suitable for:
Manual polling loops in CLI applications
Server-side endpoints that poll on behalf of clients (e.g., Homebridge UI server)
Custom UI implementations with specific polling behavior
deviceCode - Device code from
requestDeviceCode()clientId - OAuth client ID
currentInterval - Current polling interval in milliseconds (default: 5000)
audience - Audience for the token (default: 'https://api.yotoplay.com')
Returns a promise that resolves to one of:
{ status: 'success', tokens: YotoTokenResponse }- Authorization successful{ status: 'pending', interval: number }- Still waiting for user authorization{ status: 'slow_down', interval: number }- Polling too fast, use new interval
Throws YotoAPIError for unrecoverable errors (expired_token, access_denied, invalid_grant, etc).
import { YotoClient } from 'yoto-nodejs-client'
// Manual polling loop with full control
const deviceAuth = await YotoClient.requestDeviceCode({
clientId: 'your-client-id'
})
let interval = deviceAuth.interval * 1000
while (true) {
const result = await YotoClient.pollForDeviceToken({
deviceCode: deviceAuth.device_code,
clientId: 'your-client-id',
currentInterval: interval
})
if (result.status === 'success') {
console.log('Tokens:', result.tokens)
break
} else if (result.status === 'slow_down') {
interval = result.interval
}
// Sleep
await new Promise(resolve => setTimeout(resolve, interval))
}YotoClient.getAuthorizeUrl({ clientId, redirectUri, responseType, state, ...params })
Get authorization URL for browser-based OAuth flow. Returns a URL string to redirect users to.
Client Instance
new YotoClient({ clientId, refreshToken, accessToken, onTokenRefresh, [options] })
Create a new Yoto API client with automatic token refresh.
- clientId - OAuth client ID
- refreshToken - OAuth refresh token
- accessToken - Initial access token (JWT)
- onTokenRefresh - REQUIRED callback for token refresh events. You MUST persist tokens here.
- bufferSeconds - Seconds before expiration to refresh (default: 30)
- userAgent - Optional user agent string to identify your application
- defaultRequestOptions - Optional default undici request options (dispatcher, timeouts, etc.) applied to all requests
- onRefreshStart - Optional callback when refresh starts. Console.logs by default.
- onRefreshError - Optional callback for transient refresh errors. Console.error's by default.
- onInvalid - Optional callback when refresh token is permanently invalid. Console.errors by default.
const client = new YotoClient({
clientId: 'your-client-id',
refreshToken: 'stored-refresh-token',
accessToken: 'stored-access-token',
userAgent: 'MyApp/1.0.0', // Optional - identifies your application
defaultRequestOptions: { // Optional - undici request options for all requests
bodyTimeout: 30000, // 30 second timeout
headersTimeout: 10000, // 10 second header timeout
// dispatcher, signal, etc.
},
onTokenRefresh: async ({ updatedAccessToken, updatedRefreshToken, updatedExpiresAt }) => {
// Save to database, file, etc.
await db.saveTokens({ accessToken: updatedAccessToken, refreshToken: updatedRefreshToken, expiresAt: updatedExpiresAt })
}
})Request Options
All API methods accept an optional undici requestOptions parameter that allows you to override the default undici request options for individual requests. This is useful for setting custom timeouts, using a specific dispatcher, or aborting requests.
// Override timeout for a specific request
const content = await client.getContent({
cardId: '5WsQg',
requestOptions: {
bodyTimeout: 60000, // 60 second timeout for this request only
headersTimeout: 20000
}
})
// Use AbortSignal to cancel requests
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)
try {
const devices = await client.getDevices({
requestOptions: { signal: controller.signal }
})
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request was aborted')
}
}
// Use custom dispatcher for connection pooling
import { Agent } from 'undici'
const agent = new Agent({ connections: 10 })
const status = await client.getDeviceStatus({
deviceId: 'abc123',
requestOptions: { dispatcher: agent }
})Available Request Options:
bodyTimeout- Body timeout in millisecondsheadersTimeout- Headers timeout in millisecondssignal- AbortSignal to cancel the requestdispatcher- Custom undici dispatcher (for connection pooling, proxies, etc.)reset- Reset connection after requestthrowOnError- Throw on HTTP error status codesidempotent- Whether the requests can be safely retriedblocking- Whether the response is expected to take a long time- Other undici RequestOptions (see undici documentation)
Content API
await client.getContent({ cardId, [timezone], [signingType], [playable] })
Get content/card details including metadata, chapters, and optionally playback URLs.
const content = await client.getContent({
cardId: '5WsQg',
playable: true // Include signed playback URLs
})
console.log(content.title)
console.log(content.chapters)await client.getUserMyoContent({ [showDeleted] })
Get user's MYO (Make Your Own) content library.
See Yoto API: Get User's MYO Content
const myoContent = await client.getUserMyoContent({
showDeleted: false
})
myoContent.cards.forEach(card => {
console.log(`${card.metadata.title} - ${card.chapters.length} chapters`)
})await client.createOrUpdateContent({ content })
Create new content or update existing content by cardId.
See Yoto API: Create or Update Content
const newCard = await client.createOrUpdateContent({
content: {
title: 'My Story',
chapters: [
{
title: 'Chapter 1',
tracks: [
{
title: 'Part 1',
key: 'upload-id-from-audio-upload'
}
]
}
]
}
})
console.log('Created card:', newCard.cardId)await client.deleteContent({ cardId })
Delete content/card.
await client.deleteContent({ cardId: '5WsQg' })Devices API
await client.getDevices()
Get all devices for authenticated user.
const { devices } = await client.getDevices()
devices.forEach(device => {
console.log(`${device.name} (${device.deviceId}) - ${device.online ? 'online' : 'offline'}`)
})await client.getDeviceStatus({ deviceId })
Get current status of a specific device including battery, volume, active card, etc.
See Yoto API: Get Device Status
const status = await client.getDeviceStatus({
deviceId: 'abc123'
})
console.log('Battery:', status.batteryLevelPercentage, '%')
console.log('Charging:', status.isCharging)
console.log('Volume:', status.userVolumePercentage, '%')
console.log('Active card:', status.activeCard)await client.getDeviceConfig({ deviceId })
Get device configuration including settings, timezone, shortcuts, etc.
See Yoto API: Get Device Config
const config = await client.getDeviceConfig({
deviceId: 'abc123'
})
console.log('Name:', config.device.name)
console.log('Day time:', config.device.config.dayTime)
console.log('Night time:', config.device.config.nightTime)
console.log('Max volume:', config.device.config.maxVolumeLimit)await client.updateDeviceConfig({ deviceId, configUpdate })
Update device configuration settings.
See Yoto API: Update Device Config
await client.updateDeviceConfig({
deviceId: 'abc123',
configUpdate: {
name: 'Bedroom Player',
config: {
dayTime: '07:00',
nightTime: '19:00',
maxVolumeLimit: '80'
}
}
})await client.updateDeviceShortcuts({ deviceId, shortcutsUpdate })
Update device shortcuts configuration (beta feature).
See Yoto API: Update Shortcuts
await client.sendDeviceCommand({ deviceId, command })
Send MQTT command to device via HTTP API (alternative to MQTT client).
See Yoto API: Send Device Command
await client.sendDeviceCommand({
deviceId: 'abc123',
command: {
volume: 50
}
})Family Library Groups API
await client.getGroups()
Get all family library groups.
const groups = await client.getGroups()
groups.forEach(group => {
console.log(`${group.name}: ${group.items.length} items`)
})await client.createGroup({ group })
Create a new family library group.
const group = await client.createGroup({
group: {
name: 'Bedtime Stories',
imageId: 'fp-cards',
items: [
{ contentId: '5WsQg' },
{ contentId: '7KpLq' }
]
}
})await client.getGroup({ groupId })
Get a specific group by ID.
await client.updateGroup({ groupId, group })
Update an existing group.
await client.deleteGroup({ groupId })
Delete a group permanently.
Family API
await client.getFamilyImages()
Get list of uploaded family images.
See Yoto API: Get Family Images
const { images } = await client.getFamilyImages()
images.forEach(image => {
console.log(`${image.name || 'Unnamed'}: ${image.imageId}`)
})await client.getAFamilyImage({ imageId, size })
Get signed URL for a family image.
See Yoto API: Get a Family Image
const { imageUrl } = await client.getAFamilyImage({
imageId: 'abc123hash',
size: '640x480' // or '320x320'
})
console.log('Image URL:', imageUrl)await client.uploadAFamilyImage({ imageData })
Upload a family image for use across Yoto features.
See Yoto API: Upload Family Image
import { readFile } from 'fs/promises'
const imageData = await readFile('./family-photo.jpg')
const result = await client.uploadAFamilyImage({ imageData })
console.log('Image ID:', result.imageId)Icons API
await client.getPublicIcons()
Get list of public display icons available to all users.
See Yoto API: Get Public Icons
const { displayIcons } = await client.getPublicIcons()
displayIcons.forEach(icon => {
console.log(`${icon.title}: ${icon.displayIconId}`)
})await client.getUserIcons()
Get user's custom uploaded icons.
await client.uploadIcon({ imageData, [autoConvert], [filename] })
Upload a custom 16×16px display icon.
See Yoto API: Upload Custom Icon
import { readFile } from 'fs/promises'
const imageData = await readFile('./my-icon.png')
const result = await client.uploadIcon({
imageData,
autoConvert: true, // Auto-resize and process
filename: 'my-custom-icon'
})
console.log('Icon ID:', result.displayIcon.displayIconId)Media API
await client.getAudioUploadUrl({ sha256, [filename] })
Get signed URL for uploading audio files. Files are deduplicated by SHA256 hash.
See Yoto API: Get Audio Upload URL
import { createHash } from 'crypto'
import { readFile } from 'fs/promises'
const audioData = await readFile('./story.mp3')
const sha256 = createHash('sha256').update(audioData).digest('hex')
const { upload } = await client.getAudioUploadUrl({
sha256,
filename: 'story.mp3'
})
if (upload.uploadUrl) {
// File doesn't exist, upload it
await fetch(upload.uploadUrl, {
method: 'PUT',
body: audioData
})
}
// Use upload.uploadId in content creationawait client.uploadCoverImage({ [imageData], [imageUrl], [coverType], [autoConvert], [filename] })
Upload a cover image for content cards.
See Yoto API: Upload Cover Image
import { readFile } from 'fs/promises'
const imageData = await readFile('./cover.jpg')
const { coverImage } = await client.uploadCoverImage({
imageData,
coverType: 'default', // 638×1011px
autoConvert: true
})
console.log('Cover image ID:', coverImage.mediaId)MQTT Client
await client.createMqttClient({ deviceId, [options] })
Create an MQTT client for real-time device communication and control.
const mqtt = await client.createMqttClient({
deviceId: 'abc123',
autoResubscribe: true,
keepAliveSeconds: 1200
})
// Listen for real-time events
mqtt.on('events', (message) => {
console.log('Track:', message.trackTitle)
console.log('Card:', message.cardTitle)
console.log('Status:', message.playbackStatus)
})
// Listen for status updates
mqtt.on('status', (message) => {
console.log('Volume:', message.volume)
console.log('Battery:', message.batteryLevel)
console.log('Charging:', message.charging)
})
// Listen for command responses
mqtt.on('response', (message) => {
console.log('Command response:', message)
})
// Connect to device
await mqtt.connect()
// Control device
await mqtt.setVolume(50)
await mqtt.setAmbientHex('#FF0000')
await mqtt.setSleepTimer(30) // 30 minutes
await mqtt.startCard({ cardId: '5WsQg' })
await mqtt.pauseCard()
await mqtt.resumeCard()
await mqtt.stopCard()
// Disconnect when done
await mqtt.disconnect()MQTT Events
The MQTT client emits three types of messages:
events- Real-time playback events (track changes, play/pause, volume adjustments)status- Device status updates (battery, configuration, online state)response- Command confirmation responses
MQTT Methods
await mqtt.connect()- Connect to device MQTT brokerawait mqtt.disconnect()- Disconnect from brokerawait mqtt.setVolume(volume)- Set volume (0-100)await mqtt.setAmbientHex(hex)- Set ambient light color (e.g., '#FF0000')await mqtt.setSleepTimer(minutes)- Set sleep timer (0 to disable)await mqtt.startCard({ cardId, chapterKey, trackKey })- Start playing a cardawait mqtt.pauseCard()- Pause current playbackawait mqtt.resumeCard()- Resume playbackawait mqtt.stopCard()- Stop playbackawait mqtt.reboot()- Reboot device
YotoDeviceModel - Stateful Device Client
new YotoDeviceModel(client, device, [options])
Create a stateful device client that manages device state primarily from MQTT with HTTP background sync.
Philosophy:
- MQTT is the primary source for all real-time status updates
- MQTT connection is always maintained and handles its own reconnection
- Device online/offline state is tracked by MQTT activity and explicit shutdown messages
- HTTP background polling runs every 10 minutes to sync config+status regardless of online state
- HTTP status updates emit offline events if device state changes to offline
Parameters:
client- YotoClient instancedevice- Device object fromgetDevices()options.httpPollIntervalMs- Background HTTP polling interval (default: 600000ms / 10 minutes)options.mqttOptions- MQTT.js client options to pass through
Lifecycle:
await deviceClient.start()- Start device client (connects MQTT, starts polling)await deviceClient.stop()- Stop device client (disconnects MQTT, stops polling)await deviceClient.restart()- Restart device client
State Accessors:
deviceClient.device- Device informationdeviceClient.status- Current device status (normalized from HTTP/MQTT)deviceClient.config- Device configuration (normalized numbers/booleans; auto fields split into<field>Auto+ value)deviceClient.shortcuts- Button shortcutsdeviceClient.playback- Current playback statedeviceClient.capabilities- Hardware capabilities (sensors, nightlight support, etc.)deviceClient.nightlight- Nightlight info: { value, name, supported }deviceClient.initialized- Whether device has been initializeddeviceClient.running- Whether device client is currently runningdeviceClient.mqttConnected- MQTT connection statusdeviceClient.deviceOnline- Device online statusdeviceClient.mqttClient- Underlying MQTT client instance (or null)
Device Control:
await deviceClient.refreshConfig()- Refresh config from HTTP APIawait deviceClient.updateConfig(configUpdate)- Update device configurationawait deviceClient.sendCommand(command)- Send device command via HTTPawait deviceClient.startCard({ cardId, [chapterKey], [trackKey] })- Start playing a card
Events:
started(metadata)- Device client started, passes metadata object with device, config, shortcuts, status, playback, initialized, runningstopped()- Device client stoppedstatusUpdate(status, source, changedFields)- Status changed, passes (status, source, changedFields). Source is 'http', 'mqtt', or 'mqtt-event'configUpdate(config, changedFields)- Configuration changed, passes (config, changedFields)playbackUpdate(playback, changedFields)- Playback state changed, passes (playback, changedFields)online(metadata)- Device came online, passes metadata with reason and optional upTimeoffline(metadata)- Device went offline, passes metadata with reason and optional shutDownReason or timeSinceLastSeenmqttConnect(metadata)- MQTT client connected, passes CONNACK metadatamqttDisconnect(metadata)- MQTT disconnect packet received, passes metadata with disconnect packetmqttClose(metadata)- MQTT connection closed, passes metadata with close reasonmqttReconnect()- MQTT client is reconnectingmqttOffline()- MQTT client goes offlinemqttEnd()- MQTT client end is calledmqttStatus(topic, message)- Raw MQTT status messages (documented status topic)mqttEvents(topic, message)- Raw MQTT events messagesmqttStatusLegacy(topic, message)- Raw legacy MQTT status messages (undocumented status topic)mqttResponse(topic, message)- Raw MQTT response messagesmqttUnknown(topic, message)- Raw MQTT messages that do not match known typeserror(error)- Error occurred, passes error
Static Properties & Methods:
YotoDeviceModel.NIGHTLIGHT_COLORS- Map of nightlight color hex codes to official color namesYotoDeviceModel.getNightlightColorName(colorValue)- Get official color name for a nightlight value
import { YotoClient, YotoDeviceModel } from 'yoto-nodejs-client'
const client = new YotoClient({ /* ... */ })
const { devices } = await client.getDevices()
const deviceClient = new YotoDeviceModel(client, devices[0], {
httpPollIntervalMs: 300000 // Poll every 5 minutes
})
deviceClient.on('statusUpdate', (status, source, changedFields) => {
console.log(`Battery: ${status.batteryLevelPercentage}% (${source})`)
console.log('Changed fields:', changedFields)
})
deviceClient.on('online', (metadata) => {
console.log('Device online:', metadata.reason)
})
await deviceClient.start()
// Access state
console.log('Temperature:', deviceClient.status.temperatureCelsius)
console.log('Has temp sensor:', deviceClient.capabilities.hasTemperatureSensor)
console.log('Nightlight:', deviceClient.nightlight) // { value, name, supported }
// Use static nightlight utilities
console.log('Available colors:', YotoDeviceModel.NIGHTLIGHT_COLORS)
console.log('Color name:', YotoDeviceModel.getNightlightColorName('0x643600'))
// Control device
await deviceClient.updateConfig({ maxVolumeLimit: 14 })
await deviceClient.updateConfig({
nightDisplayBrightnessAuto: true,
nightDisplayBrightness: null
})
await deviceClient.updateConfig({
nightYotoRadioEnabled: false,
nightYotoRadio: null
})
await deviceClient.stop()Account Manager
new YotoAccount({ clientOptions, deviceOptions })
Create an account manager that automatically discovers and manages all devices for a Yoto account.
Parameters:
clientOptions- YotoClient constructor options (clientId, refreshToken, accessToken, onTokenRefresh, etc.)deviceOptions- YotoDeviceModel options applied to all devices (httpPollIntervalMs, mqttOptions)
Lifecycle:
await account.start()- Start account (creates client, discovers devices, starts all device clients)await account.stop()- Stop account (stops all device clients gracefully)await account.restart()- Restart accountawait account.refreshDevices()- Refresh device list (add new, remove missing)
State Accessors:
account.client- Underlying YotoClient instanceaccount.devices- Map of all device models (Map<deviceId, YotoDeviceModel>)account.getDevice(deviceId)- Get specific device modelaccount.getDeviceIds()- Get array of all device IDsaccount.running- Whether account is currently runningaccount.initialized- Whether account has been initialized
Events:
started(metadata)- Account started (metadata: { deviceCount, devices })stopped()- Account stoppeddeviceAdded({ deviceId })- Device was addeddeviceRemoved({ deviceId })- Device was removedstatusUpdate({ deviceId, status, source, changedFields })- Re-emitted device status updateconfigUpdate({ deviceId, config, changedFields })- Re-emitted config updateplaybackUpdate({ deviceId, playback, changedFields })- Re-emitted playback updateonline({ deviceId, metadata })- Re-emitted online eventoffline({ deviceId, metadata })- Re-emitted offline eventmqttConnect({ deviceId, metadata })- Re-emitted MQTT connectmqttDisconnect({ deviceId, metadata })- Re-emitted MQTT disconnectmqttClose({ deviceId, metadata })- Re-emitted MQTT closemqttReconnect({ deviceId })- Re-emitted MQTT reconnectmqttOffline({ deviceId })- Re-emitted MQTT offlinemqttEnd({ deviceId })- Re-emitted MQTT endmqttStatus({ deviceId, topic, message })- Re-emitted raw MQTT statusmqttEvents({ deviceId, topic, message })- Re-emitted raw MQTT eventsmqttStatusLegacy({ deviceId, topic, message })- Re-emitted raw MQTT legacy statusmqttResponse({ deviceId, topic, message })- Re-emitted raw MQTT responsemqttUnknown({ deviceId, topic, message })- Re-emitted raw MQTT unknown messageerror({ error, context })- Error occurred (context: { source, deviceId, operation })
Note: You can still listen to individual device events by attaching listeners to each YotoDeviceModel, but the account now re-emits device and MQTT events with device context for unified handling.
import { YotoAccount } from 'yoto-nodejs-client'
const account = new YotoAccount({
clientOptions: {
clientId: 'your-client-id',
refreshToken: 'your-refresh-token',
accessToken: 'your-access-token',
onTokenRefresh: async (event) => {
await saveTokens(event)
}
},
deviceOptions: {
httpPollIntervalMs: 600000 // 10 minutes
}
})
// Account-level error handling
account.on('error', ({ error, context }) => {
console.error(`Error in ${context.source}:`, error.message)
})
// Unified device events across all devices
account.on('statusUpdate', ({ deviceId, status, source }) => {
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}% (${source})`)
})
account.on('online', ({ deviceId, metadata }) => {
console.log(`${deviceId} came online (${metadata.reason})`)
})
account.on('offline', ({ deviceId, metadata }) => {
console.log(`${deviceId} went offline (${metadata.reason})`)
})
await account.start()
// Access individual devices and attach listeners
const device = account.getDevice('abc123')
console.log('Device battery:', device.status.batteryLevelPercentage)
// Listen to specific device events
device.on('playbackUpdate', (playback) => {
console.log('Now playing:', playback.currentCardTitle)
})
// Iterate all devices and attach listeners
for (const [deviceId, deviceModel] of account.devices) {
console.log(`${deviceId}: ${deviceModel.status.batteryLevelPercentage}%`)
deviceModel.on('configUpdate', (config) => {
console.log(`${deviceId} config updated:`, config.name)
})
}
await account.stop()CLI Tools
The library includes CLI tools for authentication and data inspection. After installing the package, these commands are available globally:
Authentication
# Get initial tokens (device flow)
yoto-auth --output .env
# or: node bin/auth.js --output .env
# Refresh existing tokens
yoto-refresh-token
# or: node bin/refresh-token.js
# Show token info and inspect JWT contents
yoto-token-info
# or: node bin/token-info.jsDevices
# List all devices
yoto-devices
# or: node bin/devices.js
# Get device details with config
yoto-devices --device-id abc123
# Get device status only
yoto-devices --device-id abc123 --status
# Connect to MQTT and listen for messages
yoto-devices --device-id abc123 --mqtt
# Sample MQTT messages for 10 seconds
yoto-devices --device-id abc123 --mqtt --mqtt-timeout 10
# Use YotoDeviceModel to monitor device (HTTP + MQTT)
yoto-device-model --device-id abc123
# Interactive TUI for device control (Prototype/WIP/Unpublished)
yoto-device-tui --device-id abc123Content
# List all MYO content
yoto-content
# or: node bin/content.js
# Get specific card details
yoto-content --card-id 5WsQg
# Get card with playable URLs
yoto-content --card-id 5WsQg --playableFamily Library Groups
# List all family library groups
yoto-groups
# or: node bin/groups.js
# Get specific group details
yoto-groups --group-id abc123Icons
# List both public and user icons
yoto-icons
# or: node bin/icons.js
# List only public Yoto icons
yoto-icons --public
# List only user custom icons
yoto-icons --userSee also
License
MIT
