homebridge-yoto 0.0.27 → 0.0.31
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/AGENTS.md +42 -157
- package/CHANGELOG.md +13 -5
- package/NOTES.md +87 -0
- package/PLAN.md +320 -504
- package/README.md +18 -314
- package/config.schema.cjs +3 -0
- package/config.schema.json +19 -155
- package/homebridge-ui/server.js +264 -0
- package/index.js +1 -1
- package/index.test.js +1 -1
- package/lib/accessory.js +1870 -0
- package/lib/constants.js +8 -149
- package/lib/platform.js +303 -364
- package/lib/sanitize-name.js +49 -0
- package/lib/settings.js +16 -0
- package/lib/sync-service-names.js +34 -0
- package/logo.png +0 -0
- package/package.json +17 -22
- package/pnpm-workspace.yaml +4 -0
- package/declaration.tsconfig.json +0 -15
- package/lib/auth.js +0 -237
- package/lib/playerAccessory.js +0 -1724
- package/lib/types.js +0 -253
- package/lib/yotoApi.js +0 -270
- package/lib/yotoMqtt.js +0 -570
package/PLAN.md
CHANGED
|
@@ -1,609 +1,425 @@
|
|
|
1
|
-
# Homebridge Yoto Plugin - Implementation
|
|
1
|
+
# Homebridge Yoto Plugin - Implementation Status
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## ✅ What's Implemented
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Architecture
|
|
6
|
+
- ✅ Platform plugin using `yoto-nodejs-client` for device management
|
|
7
|
+
- ✅ One accessory per Yoto device (published as external accessory for SmartSpeaker support)
|
|
8
|
+
- ✅ Real-time updates via MQTT + periodic HTTP polling fallback
|
|
9
|
+
- ✅ Offline detection and "No Response" status handling
|
|
10
|
+
- ✅ Capability-based service registration (v2/v3/mini device support)
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
### Core Services (Always Present)
|
|
13
|
+
- ✅ **AccessoryInformation** - Device metadata (manufacturer, model, serial, firmware)
|
|
14
|
+
- ✅ **SmartSpeaker** (PRIMARY) - Playback control and volume
|
|
15
|
+
- ✅ **Battery** - Battery level, charging state, low battery indicator
|
|
16
|
+
- ✅ **ContactSensor (CardSlot)** - Card insertion detection
|
|
17
|
+
- ✅ **OccupancySensor (NightModeStatus)** - Day/night mode indicator
|
|
18
|
+
- ✅ **Switch (SleepTimer)** - Toggle sleep timer (30 min default)
|
|
19
|
+
- ✅ **Switch (Bluetooth)** - Toggle Bluetooth on/off
|
|
20
|
+
- ✅ **Fanv2 (DayMaxVolume)** - Day mode max volume limit control
|
|
21
|
+
- ✅ **Fanv2 (NightMaxVolume)** - Night mode max volume limit control
|
|
8
22
|
|
|
9
|
-
|
|
23
|
+
### Optional Services (Capability-Based)
|
|
24
|
+
- ✅ **TemperatureSensor** - Temperature reading (v3 only)
|
|
25
|
+
- ✅ **Lightbulb (DayNightlight)** - Day nightlight color/brightness control (v3 only)
|
|
26
|
+
- ✅ **Lightbulb (NightNightlight)** - Night nightlight color/brightness control (v3 only)
|
|
27
|
+
- ✅ **ContactSensor (NightlightActive)** - Live nightlight status (v3 only)
|
|
28
|
+
- ✅ **ContactSensor (DayNightlightActive)** - Day nightlight status (v3 only)
|
|
29
|
+
- ✅ **ContactSensor (NightNightlightActive)** - Night nightlight status (v3 only)
|
|
10
30
|
|
|
11
|
-
##
|
|
31
|
+
## Service & Characteristic Reference
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
homebridge-yoto/
|
|
15
|
-
├── lib/
|
|
16
|
-
│ ├── platform.js # Main YotoPlatform class
|
|
17
|
-
│ ├── playerAccessory.js # YotoPlayerAccessory handler
|
|
18
|
-
│ ├── yotoApi.js # Yoto REST API client wrapper
|
|
19
|
-
│ ├── yotoMqtt.js # MQTT client for real-time updates
|
|
20
|
-
│ ├── auth.js # OAuth2 authentication handler
|
|
21
|
-
│ ├── types.js # JSDoc type definitions
|
|
22
|
-
│ └── constants.js # Plugin constants and defaults
|
|
23
|
-
├── index.js # Plugin entry point
|
|
24
|
-
├── config.schema.json # Homebridge config UI schema
|
|
25
|
-
└── package.json
|
|
26
|
-
```
|
|
33
|
+
All services are named consistently using `generateServiceName()` helper: `"[Device Name] [Service Name]"`
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
### Yoto Player Accessory
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
Each Yoto device is represented as a single HomeKit accessory with multiple services.
|
|
31
38
|
|
|
32
|
-
**
|
|
39
|
+
**Category**: `SPEAKER` (required for SmartSpeaker service)
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
---
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
- `lib/auth.js`
|
|
43
|
+
#### Service: AccessoryInformation (Required)
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
- Use `POST /oauth/device/code` to initiate device flow
|
|
41
|
-
- Display user_code and verification_uri in Homebridge logs
|
|
42
|
-
- Poll `POST /oauth/token` until user completes authorization
|
|
43
|
-
- Store access_token and refresh_token in platform config
|
|
44
|
-
- Implement automatic token refresh logic
|
|
45
|
-
- Handle token expiration gracefully
|
|
45
|
+
Standard HomeKit service providing device identification.
|
|
46
46
|
|
|
47
|
-
**
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
47
|
+
**Characteristics:**
|
|
48
|
+
- `Manufacturer` (GET) - "Yoto Inc."
|
|
49
|
+
- `Model` (GET) - Device family (e.g., "v3", "v2", "mini")
|
|
50
|
+
- `SerialNumber` (GET) - Device ID
|
|
51
|
+
- `HardwareRevision` (GET) - Generation and form factor
|
|
52
|
+
- `FirmwareRevision` (GET) - Firmware version from device status
|
|
50
53
|
|
|
51
|
-
**
|
|
52
|
-
```javascript
|
|
53
|
-
{
|
|
54
|
-
"platform": "Yoto",
|
|
55
|
-
"name": "Yoto",
|
|
56
|
-
"clientId": "YOUR_CLIENT_ID",
|
|
57
|
-
"accessToken": "(stored after auth)",
|
|
58
|
-
"refreshToken": "(stored after auth)",
|
|
59
|
-
"tokenExpiresAt": 0
|
|
60
|
-
}
|
|
61
|
-
```
|
|
54
|
+
**Source:** `device` metadata + `status.firmwareVersion`
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
---
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
#### Service: SmartSpeaker (PRIMARY)
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
Controls playback and volume. Marked as primary service for the accessory.
|
|
68
61
|
|
|
69
|
-
**
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
62
|
+
**Characteristics:**
|
|
63
|
+
- `CurrentMediaState` (GET) - PLAY/PAUSE/STOP based on playback status
|
|
64
|
+
- `TargetMediaState` (GET/SET) - Control playback (play/pause/stop)
|
|
65
|
+
- `Volume` (GET/SET) - Volume level (0-16 native scale, dynamic max based on volume limit)
|
|
66
|
+
- `Mute` (GET/SET) - Mute state (derived from volume === 0)
|
|
67
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
72
68
|
|
|
73
|
-
**
|
|
74
|
-
|
|
75
|
-
- Automatic token refresh on 401 responses
|
|
76
|
-
- Error handling and retry logic
|
|
77
|
-
- Rate limiting protection
|
|
78
|
-
- Request/response logging for debugging
|
|
69
|
+
**Source:** `status.volume`, `playback.playbackStatus`
|
|
70
|
+
**Control:** `sendCommand({ action: 'play' | 'pause' | 'stop' | 'volume', volume: N })`
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
class YotoApi {
|
|
83
|
-
async getDevices() // GET /device-v2/devices/mine
|
|
84
|
-
async getDeviceConfig(deviceId) // GET /device-v2/{deviceId}/config
|
|
85
|
-
async updateDeviceConfig(deviceId, config) // PUT /device-v2/{deviceId}/config
|
|
86
|
-
async getContent(cardId) // GET /content/{cardId}
|
|
87
|
-
async getLibraryGroups() // GET /card/family/library/groups
|
|
88
|
-
}
|
|
89
|
-
```
|
|
72
|
+
---
|
|
90
73
|
|
|
91
|
-
|
|
92
|
-
- Real-time device status updates via `/device/{id}/data/status`
|
|
93
|
-
- Real-time playback events via `/device/{id}/data/events`
|
|
94
|
-
- Command publishing to control devices
|
|
95
|
-
- Automatic reconnection on disconnect
|
|
96
|
-
- Per-device topic subscriptions
|
|
74
|
+
#### Service: Battery
|
|
97
75
|
|
|
98
|
-
|
|
99
|
-
```javascript
|
|
100
|
-
class YotoMqtt {
|
|
101
|
-
async connect(accessToken)
|
|
102
|
-
async disconnect()
|
|
103
|
-
subscribeToDevice(deviceId, callback)
|
|
104
|
-
unsubscribeFromDevice(deviceId)
|
|
105
|
-
|
|
106
|
-
// Command methods
|
|
107
|
-
async setVolume(deviceId, volume)
|
|
108
|
-
async startCard(deviceId, uri, options)
|
|
109
|
-
async pauseCard(deviceId)
|
|
110
|
-
async resumeCard(deviceId)
|
|
111
|
-
async stopCard(deviceId)
|
|
112
|
-
async setSleepTimer(deviceId, seconds)
|
|
113
|
-
async setAmbientLight(deviceId, r, g, b)
|
|
114
|
-
async requestStatus(deviceId)
|
|
115
|
-
async requestEvents(deviceId)
|
|
116
|
-
}
|
|
117
|
-
```
|
|
76
|
+
Battery status information.
|
|
118
77
|
|
|
119
|
-
|
|
78
|
+
**Characteristics:**
|
|
79
|
+
- `BatteryLevel` (GET) - Battery percentage (0-100)
|
|
80
|
+
- `ChargingState` (GET) - CHARGING or NOT_CHARGING
|
|
81
|
+
- `StatusLowBattery` (GET) - LOW when ≤20%, NORMAL otherwise
|
|
82
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
120
83
|
|
|
121
|
-
**
|
|
84
|
+
**Source:** `status.batteryLevelPercentage`, `status.isCharging`
|
|
122
85
|
|
|
123
|
-
|
|
86
|
+
---
|
|
124
87
|
|
|
125
|
-
|
|
126
|
-
- `lib/platform.js`
|
|
127
|
-
- `index.js`
|
|
88
|
+
#### Service: TemperatureSensor (OPTIONAL - v3 only)
|
|
128
89
|
|
|
129
|
-
|
|
130
|
-
- Register platform in `index.js`
|
|
131
|
-
- Implement `configureAccessory()` to restore cached accessories
|
|
132
|
-
- Implement `discoverDevices()` called on `didFinishLaunching`
|
|
133
|
-
- Create Map to track accessories by deviceId
|
|
134
|
-
- Handle device addition/removal/updates
|
|
135
|
-
- Store device metadata in `accessory.context`
|
|
90
|
+
Temperature reading from device sensor.
|
|
136
91
|
|
|
137
|
-
**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
4. Device sync: Update existing, add new, remove stale accessories
|
|
92
|
+
**Characteristics:**
|
|
93
|
+
- `CurrentTemperature` (GET) - Temperature in Celsius
|
|
94
|
+
- `StatusFault` (GET) - NO_FAULT (only shown when temperature available)
|
|
95
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
142
96
|
|
|
143
|
-
|
|
97
|
+
**Source:** `status.temperatureCelsius`
|
|
98
|
+
**Availability:** `deviceModel.capabilities.hasTemperatureSensor`
|
|
144
99
|
|
|
145
|
-
|
|
100
|
+
---
|
|
146
101
|
|
|
147
|
-
|
|
102
|
+
#### Service: Lightbulb (subtype: "DayNightlight") - OPTIONAL - v3 only
|
|
148
103
|
|
|
149
|
-
|
|
150
|
-
- `lib/playerAccessory.js`
|
|
104
|
+
Day mode nightlight color and brightness control (config-based).
|
|
151
105
|
|
|
152
|
-
**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- FirmwareRevision: device.releaseChannel
|
|
106
|
+
**Characteristics:**
|
|
107
|
+
- `On` (GET/SET) - Turn nightlight on/off (off states: '0x000000', 'off')
|
|
108
|
+
- `Brightness` (GET/SET) - Screen brightness 0-100% (or 'auto')
|
|
109
|
+
- `Hue` (GET/SET) - Color hue 0-360° (derived from hex color)
|
|
110
|
+
- `Saturation` (GET/SET) - Color saturation 0-100% (derived from hex color)
|
|
158
111
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- Volume: 0-100 (read/write)
|
|
163
|
-
- Mute: boolean (read/write)
|
|
112
|
+
**Source:** `config.ambientColour`, `config.dayDisplayBrightness`
|
|
113
|
+
**Control:** `updateConfig({ ambientColour: '0xRRGGBB', dayDisplayBrightness: 'N' })`
|
|
114
|
+
**Availability:** `deviceModel.capabilities.hasColoredNightlight`
|
|
164
115
|
|
|
165
|
-
|
|
166
|
-
- BatteryLevel: 0-100
|
|
167
|
-
- ChargingState: NOT_CHARGING/CHARGING
|
|
168
|
-
- StatusLowBattery: NORMAL/LOW (< 20%)
|
|
116
|
+
**Color Conversion:** Uses `color-convert` library for hex ↔ HSV conversion
|
|
169
117
|
|
|
170
|
-
|
|
171
|
-
- Constructor: Set up services and characteristics
|
|
172
|
-
- Implement onGet/onSet handlers for each characteristic
|
|
173
|
-
- Subscribe to MQTT topics for real-time updates
|
|
174
|
-
- Use `updateCharacteristic()` when MQTT messages arrive
|
|
175
|
-
- Cache last known state for onGet handlers
|
|
118
|
+
---
|
|
176
119
|
|
|
177
|
-
|
|
120
|
+
#### Service: Lightbulb (subtype: "NightNightlight") - OPTIONAL - v3 only
|
|
178
121
|
|
|
179
|
-
|
|
122
|
+
Night mode nightlight color and brightness control (config-based).
|
|
180
123
|
|
|
181
|
-
**
|
|
124
|
+
**Characteristics:**
|
|
125
|
+
- `On` (GET/SET) - Turn nightlight on/off (off states: '0x000000', 'off')
|
|
126
|
+
- `Brightness` (GET/SET) - Screen brightness 0-100% (or 'auto')
|
|
127
|
+
- `Hue` (GET/SET) - Color hue 0-360° (derived from hex color)
|
|
128
|
+
- `Saturation` (GET/SET) - Color saturation 0-100% (derived from hex color)
|
|
182
129
|
|
|
183
|
-
**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- Subscribe to `/device/{id}/data/events` for playback events
|
|
187
|
-
- Update characteristics immediately when MQTT messages arrive
|
|
188
|
-
- Request initial status via `/device/{id}/command/status/request` on startup
|
|
189
|
-
- Implement reconnection logic with exponential backoff
|
|
190
|
-
- Handle offline devices (no recent MQTT messages)
|
|
130
|
+
**Source:** `config.nightAmbientColour`, `config.nightDisplayBrightness`
|
|
131
|
+
**Control:** `updateConfig({ nightAmbientColour: '0xRRGGBB', nightDisplayBrightness: 'N' })`
|
|
132
|
+
**Availability:** `deviceModel.capabilities.hasColoredNightlight`
|
|
191
133
|
|
|
192
|
-
|
|
193
|
-
```javascript
|
|
194
|
-
// Per device subscriptions
|
|
195
|
-
/device/{deviceId}/data/status → Battery, volume, config state
|
|
196
|
-
/device/{deviceId}/data/events → Playback state, current track
|
|
197
|
-
/device/{deviceId}/response → Command confirmations
|
|
198
|
-
```
|
|
134
|
+
---
|
|
199
135
|
|
|
200
|
-
|
|
201
|
-
```javascript
|
|
202
|
-
// MQTT data/status → HomeKit
|
|
203
|
-
batteryLevel → BatteryLevel
|
|
204
|
-
charging (0/1) → ChargingState (NOT_CHARGING/CHARGING)
|
|
205
|
-
userVolume → Volume
|
|
206
|
-
activeCard → CurrentMediaState context
|
|
207
|
-
|
|
208
|
-
// MQTT data/events → HomeKit
|
|
209
|
-
playbackStatus ("playing"/"paused"/"stopped") → CurrentMediaState
|
|
210
|
-
volume → Volume (real-time)
|
|
211
|
-
cardId → Active content tracking
|
|
212
|
-
```
|
|
136
|
+
#### Service: ContactSensor (subtype: "NightlightActive") - OPTIONAL - v3 only
|
|
213
137
|
|
|
214
|
-
|
|
215
|
-
```javascript
|
|
216
|
-
// HomeKit actions → MQTT commands
|
|
217
|
-
Set Volume → /device/{id}/command/volume/set
|
|
218
|
-
Play/Pause → /device/{id}/command/card/pause or /resume
|
|
219
|
-
Stop → /device/{id}/command/card/stop
|
|
220
|
-
```
|
|
138
|
+
Shows if nightlight is currently active (live device state).
|
|
221
139
|
|
|
222
|
-
|
|
140
|
+
**Characteristics:**
|
|
141
|
+
- `ContactSensorState` (GET) - CONTACT_DETECTED when nightlight on
|
|
142
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
143
|
+
|
|
144
|
+
**Source:** `status.nightlightMode !== 'off'`
|
|
145
|
+
**Availability:** `deviceModel.capabilities.hasColoredNightlight`
|
|
223
146
|
|
|
224
|
-
|
|
147
|
+
---
|
|
225
148
|
|
|
226
|
-
|
|
149
|
+
#### Service: ContactSensor (subtype: "DayNightlightActive") - OPTIONAL - v3 only
|
|
227
150
|
|
|
228
|
-
|
|
151
|
+
Shows if day nightlight is currently active and showing.
|
|
229
152
|
|
|
230
153
|
**Characteristics:**
|
|
231
|
-
-
|
|
232
|
-
-
|
|
154
|
+
- `ContactSensorState` (GET) - CONTACT_DETECTED when day mode + nightlight on
|
|
155
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
233
156
|
|
|
234
|
-
**
|
|
235
|
-
|
|
236
|
-
- Handle "auto" mode as max brightness (100)
|
|
237
|
-
- Numeric values map directly to percentage
|
|
238
|
-
- Create separate services for day/night if needed
|
|
157
|
+
**Source:** `status.dayMode === 'day' && status.nightlightMode !== 'off'`
|
|
158
|
+
**Availability:** `deviceModel.capabilities.hasColoredNightlight`
|
|
239
159
|
|
|
240
|
-
|
|
160
|
+
---
|
|
241
161
|
|
|
242
|
-
|
|
162
|
+
#### Service: ContactSensor (subtype: "NightNightlightActive") - OPTIONAL - v3 only
|
|
243
163
|
|
|
244
|
-
|
|
164
|
+
Shows if night nightlight is currently active and showing.
|
|
245
165
|
|
|
246
166
|
**Characteristics:**
|
|
247
|
-
-
|
|
167
|
+
- `ContactSensorState` (GET) - CONTACT_DETECTED when night mode + nightlight on
|
|
168
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
248
169
|
|
|
249
|
-
**
|
|
250
|
-
|
|
251
|
-
{
|
|
252
|
-
"exposeTemperature": true
|
|
253
|
-
}
|
|
254
|
-
```
|
|
170
|
+
**Source:** `status.dayMode === 'night' && status.nightlightMode !== 'off'`
|
|
171
|
+
**Availability:** `deviceModel.capabilities.hasColoredNightlight`
|
|
255
172
|
|
|
256
|
-
|
|
173
|
+
---
|
|
257
174
|
|
|
258
|
-
|
|
175
|
+
#### Service: ContactSensor (subtype: "CardSlot")
|
|
259
176
|
|
|
260
|
-
|
|
177
|
+
Shows if a card is currently inserted.
|
|
261
178
|
|
|
262
179
|
**Characteristics:**
|
|
263
|
-
- ContactSensorState
|
|
264
|
-
- StatusActive
|
|
180
|
+
- `ContactSensorState` (GET) - CONTACT_DETECTED when card present
|
|
181
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
265
182
|
|
|
266
|
-
**
|
|
267
|
-
- Automation triggers when player comes online
|
|
268
|
-
- Notifications when player goes offline
|
|
269
|
-
- Parent monitoring of device usage
|
|
183
|
+
**Source:** `status.cardInsertionState !== 'none'`
|
|
270
184
|
|
|
271
|
-
|
|
185
|
+
---
|
|
272
186
|
|
|
273
|
-
|
|
187
|
+
#### Service: OccupancySensor (subtype: "NightModeStatus")
|
|
274
188
|
|
|
275
|
-
|
|
189
|
+
Shows if device is in night mode (vs day mode).
|
|
276
190
|
|
|
277
|
-
**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
3. Repeat All → `repeatAll`
|
|
191
|
+
**Characteristics:**
|
|
192
|
+
- `OccupancyDetected` (GET) - OCCUPANCY_DETECTED when in night mode
|
|
193
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
281
194
|
|
|
282
|
-
**
|
|
283
|
-
- Create Switch service for each setting
|
|
284
|
-
- onSet: Update via `PUT /device-v2/{deviceId}/config`
|
|
285
|
-
- onGet: Read from cached config or status
|
|
286
|
-
- Refresh config on status poll
|
|
195
|
+
**Source:** `status.dayMode === 'night'`
|
|
287
196
|
|
|
288
|
-
**
|
|
289
|
-
```javascript
|
|
290
|
-
{
|
|
291
|
-
"exposeAdvancedControls": true
|
|
292
|
-
}
|
|
293
|
-
```
|
|
197
|
+
**Use Case:** Trigger automations based on device's day/night schedule
|
|
294
198
|
|
|
295
|
-
|
|
199
|
+
---
|
|
296
200
|
|
|
297
|
-
|
|
201
|
+
#### Service: Switch (subtype: "SleepTimer")
|
|
298
202
|
|
|
299
|
-
|
|
203
|
+
Toggle sleep timer on/off.
|
|
300
204
|
|
|
301
|
-
**
|
|
302
|
-
-
|
|
303
|
-
-
|
|
205
|
+
**Characteristics:**
|
|
206
|
+
- `On` (GET/SET) - Sleep timer active state
|
|
207
|
+
- `StatusActive` (GET) - Online/offline indicator
|
|
304
208
|
|
|
305
|
-
**
|
|
306
|
-
|
|
307
|
-
- Name: "Max Volume Day" and "Max Volume Night"
|
|
209
|
+
**Source:** `playback.sleepTimerActive`
|
|
210
|
+
**Control:** `sendCommand({ action: 'sleep-timer', minutes: 30 })` (on) or `minutes: 0` (off)
|
|
308
211
|
|
|
309
|
-
|
|
310
|
-
- Max Volume Limit (Day): `maxVolumeLimit` (0-16)
|
|
311
|
-
- Max Volume Limit (Night): `nightMaxVolumeLimit` (0-16)
|
|
212
|
+
---
|
|
312
213
|
|
|
313
|
-
|
|
214
|
+
#### Service: Switch (subtype: "Bluetooth")
|
|
314
215
|
|
|
315
|
-
|
|
216
|
+
Toggle Bluetooth on/off (config-based, works offline).
|
|
316
217
|
|
|
317
218
|
**Characteristics:**
|
|
318
|
-
- On
|
|
319
|
-
- Brightness: Light intensity
|
|
320
|
-
- Hue/Saturation: Parse `ambientColour` hex to HSV
|
|
219
|
+
- `On` (GET/SET) - Bluetooth enabled state
|
|
321
220
|
|
|
322
|
-
**
|
|
323
|
-
|
|
324
|
-
- Separate services for day and night colors
|
|
325
|
-
- Update via config: `ambientColour`, `nightAmbientColour`
|
|
221
|
+
**Source:** `config.bluetoothEnabled` (string '0' or '1')
|
|
222
|
+
**Control:** `updateConfig({ bluetoothEnabled: '1' | '0' })`
|
|
326
223
|
|
|
327
|
-
|
|
224
|
+
**Note:** No StatusActive - config-based services work offline
|
|
328
225
|
|
|
329
|
-
|
|
226
|
+
---
|
|
330
227
|
|
|
331
|
-
|
|
228
|
+
#### Service: Fanv2 (subtype: "DayMaxVolume")
|
|
229
|
+
|
|
230
|
+
Control day mode maximum volume limit (config-based, works offline).
|
|
332
231
|
|
|
333
232
|
**Characteristics:**
|
|
334
|
-
-
|
|
335
|
-
-
|
|
233
|
+
- `Active` (GET) - Always ACTIVE
|
|
234
|
+
- `RotationSpeed` (GET/SET) - Volume limit 0-100%
|
|
336
235
|
|
|
337
|
-
**
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
cardInsertionState === 1 → CONTACT_DETECTED (physical)
|
|
341
|
-
cardInsertionState === 2 → CONTACT_DETECTED (remote)
|
|
342
|
-
```
|
|
236
|
+
**Source:** `config.maxVolumeLimit` (device: 0-16, HomeKit: 0-100%)
|
|
237
|
+
**Control:** `updateConfig({ maxVolumeLimit: 'N' })`
|
|
238
|
+
**Conversion:** `percentage = (limit / 16) * 100`
|
|
343
239
|
|
|
344
|
-
|
|
345
|
-
- Trigger automations when card inserted
|
|
346
|
-
- Parent monitoring of what's playing
|
|
347
|
-
- Bedtime routine detection
|
|
240
|
+
---
|
|
348
241
|
|
|
349
|
-
|
|
242
|
+
#### Service: Fanv2 (subtype: "NightMaxVolume")
|
|
350
243
|
|
|
351
|
-
|
|
244
|
+
Control night mode maximum volume limit (config-based, works offline).
|
|
352
245
|
|
|
353
|
-
**
|
|
354
|
-
-
|
|
355
|
-
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
246
|
+
**Characteristics:**
|
|
247
|
+
- `Active` (GET) - Always ACTIVE
|
|
248
|
+
- `RotationSpeed` (GET/SET) - Volume limit 0-100%
|
|
249
|
+
|
|
250
|
+
**Source:** `config.nightMaxVolumeLimit` (device: 0-16, HomeKit: 0-100%)
|
|
251
|
+
**Control:** `updateConfig({ nightMaxVolumeLimit: 'N' })`
|
|
252
|
+
**Conversion:** `percentage = (limit / 16) * 100`
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
#### Service: StatelessProgrammableSwitch (DYNAMIC) - NOT YET IMPLEMENTED
|
|
257
|
+
|
|
258
|
+
One service per shortcut configured on device. Trigger shortcuts from HomeKit.
|
|
259
|
+
|
|
260
|
+
**Characteristics:**
|
|
261
|
+
- `ProgrammableSwitchEvent` (READ/NOTIFY) - SINGLE_PRESS event
|
|
262
|
+
- `ServiceLabelIndex` (GET) - Index in shortcuts array
|
|
263
|
+
|
|
264
|
+
**Source:** `config.shortcuts.modes.day.content[]` and `config.shortcuts.modes.night.content[]`
|
|
265
|
+
**Control:** `sendCommand({ action: 'play', shortcut: X })`
|
|
266
|
+
|
|
267
|
+
**Note:** Dynamic services - created based on device configuration
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### Service Availability Matrix
|
|
272
|
+
|
|
273
|
+
| Service | v2 | v3 | mini |
|
|
274
|
+
|---------|----|----|------|
|
|
275
|
+
| AccessoryInformation | ✅ | ✅ | ✅ |
|
|
276
|
+
| SmartSpeaker | ✅ | ✅ | ✅ |
|
|
277
|
+
| Battery | ✅ | ✅ | ✅ |
|
|
278
|
+
| ContactSensor (CardSlot) | ✅ | ✅ | ✅ |
|
|
279
|
+
| OccupancySensor (NightMode) | ✅ | ✅ | ✅ |
|
|
280
|
+
| Switch (SleepTimer) | ✅ | ✅ | ✅ |
|
|
281
|
+
| Switch (Bluetooth) | ✅ | ✅ | ✅ |
|
|
282
|
+
| Fanv2 (Volume Limits) | ✅ | ✅ | ✅ |
|
|
283
|
+
| TemperatureSensor | ❌ | ✅ | ❌ |
|
|
284
|
+
| Lightbulb (Nightlights) | ❌ | ✅ | ❌ |
|
|
285
|
+
| ContactSensor (Nightlight Status) | ❌ | ✅ | ❌ |
|
|
286
|
+
| StatelessProgrammableSwitch | 🚧 | 🚧 | 🚧 |
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Offline Behavior
|
|
291
|
+
|
|
292
|
+
### What Gets Marked as "No Response"
|
|
293
|
+
|
|
294
|
+
Services are categorized by data source:
|
|
360
295
|
|
|
361
|
-
|
|
296
|
+
**Device State Services** (require online connection):
|
|
297
|
+
- SmartSpeaker (playback, volume)
|
|
298
|
+
- Battery (level, charging)
|
|
299
|
+
- TemperatureSensor (temperature)
|
|
300
|
+
- ContactSensor - all variants (card, nightlight status)
|
|
301
|
+
- OccupancySensor (night mode)
|
|
302
|
+
- Switch (SleepTimer) - reads playback state
|
|
362
303
|
|
|
363
|
-
|
|
304
|
+
**Config-Based Services** (work offline):
|
|
305
|
+
- Lightbulb (nightlight color/brightness)
|
|
306
|
+
- Fanv2 (volume limits)
|
|
307
|
+
- Switch (Bluetooth)
|
|
308
|
+
- StatelessProgrammableSwitch (shortcuts)
|
|
364
309
|
|
|
365
|
-
|
|
310
|
+
### Implementation
|
|
366
311
|
|
|
367
|
-
|
|
368
|
-
- When `activeCard` changes, fetch via `GET /content/{cardId}`
|
|
369
|
-
- Store card metadata in accessory context
|
|
370
|
-
- Display title/author in logs or as custom characteristics
|
|
371
|
-
- Update accessory display name with current content (optional)
|
|
312
|
+
Device state services use `StatusActive` characteristic to indicate offline status. When `StatusActive` is false, HomeKit shows "No Response" for the service.
|
|
372
313
|
|
|
373
|
-
|
|
314
|
+
Config-based services do NOT have `StatusActive` and remain accessible when offline since they only read/write device configuration (cached in `deviceModel.config`).
|
|
374
315
|
|
|
375
|
-
|
|
316
|
+
---
|
|
376
317
|
|
|
377
|
-
|
|
378
|
-
- `PUT /device-v2/{deviceId}/shortcuts`
|
|
318
|
+
## ❌ What's Left to Implement
|
|
379
319
|
|
|
380
|
-
|
|
320
|
+
### StatelessProgrammableSwitch Services (Shortcuts)
|
|
381
321
|
|
|
382
|
-
|
|
383
|
-
- Create button for each configured shortcut
|
|
384
|
-
- Press triggers associated content/command
|
|
385
|
-
- Separate buttons for day/night mode shortcuts
|
|
386
|
-
- Read from device config: `dayYotoDaily`, `nightYotoRadio`, etc.
|
|
322
|
+
Dynamic services created based on `config.shortcuts` configuration.
|
|
387
323
|
|
|
388
|
-
**
|
|
324
|
+
**Implementation Notes:**
|
|
325
|
+
- Parse `config.shortcuts.modes.day.content[]` and `config.shortcuts.modes.night.content[]`
|
|
326
|
+
- Create one service per unique shortcut
|
|
327
|
+
- Handle shortcuts refresh when config changes
|
|
328
|
+
- Trigger with `sendCommand({ action: 'play', shortcut: X })`
|
|
389
329
|
|
|
390
|
-
|
|
330
|
+
**Complexity:** Dynamic service lifecycle management, shortcut identification
|
|
391
331
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## API Reference Documentation
|
|
335
|
+
|
|
336
|
+
### YotoDeviceModel - State & Events
|
|
337
|
+
|
|
338
|
+
**State Properties:**
|
|
339
|
+
- `device` - Device metadata (deviceId, name, deviceType, etc.)
|
|
340
|
+
- `status` - Live device status (volume, battery, temperature, etc.)
|
|
341
|
+
- `config` - Device configuration (nightlight colors, volume limits, etc.)
|
|
342
|
+
- `shortcuts` - Configured shortcuts
|
|
343
|
+
- `playback` - Playback state (status, track, sleep timer, etc.)
|
|
344
|
+
|
|
345
|
+
**Events:**
|
|
346
|
+
- `statusUpdate(status, source, changedFields)` - Device status changed
|
|
347
|
+
- `configUpdate(config, changedFields)` - Configuration changed
|
|
348
|
+
- `playbackUpdate(playback, changedFields)` - Playback state changed
|
|
349
|
+
- `online({ reason })` - Device came online
|
|
350
|
+
- `offline({ reason })` - Device went offline
|
|
351
|
+
|
|
352
|
+
### Device API Endpoints
|
|
353
|
+
|
|
354
|
+
- `GET /devices` - List all devices
|
|
355
|
+
- `GET /devices/:deviceId/config` - Get device config
|
|
356
|
+
- `GET /devices/:deviceId/status` - Get device status
|
|
357
|
+
- `POST /devices/:deviceId/config` - Update device config
|
|
358
|
+
- `POST /devices/:deviceId/mq` - Send device command
|
|
359
|
+
|
|
360
|
+
### Command Examples
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
// Playback control
|
|
364
|
+
await deviceModel.sendCommand({ action: 'play' })
|
|
365
|
+
await deviceModel.sendCommand({ action: 'pause' })
|
|
366
|
+
await deviceModel.sendCommand({ action: 'stop' })
|
|
367
|
+
|
|
368
|
+
// Volume control
|
|
369
|
+
await deviceModel.sendCommand({ action: 'volume', volume: 10 })
|
|
370
|
+
|
|
371
|
+
// Sleep timer
|
|
372
|
+
await deviceModel.sendCommand({ action: 'sleep-timer', minutes: 30 })
|
|
402
373
|
```
|
|
403
374
|
|
|
404
|
-
###
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
"statusTimeoutSeconds": 120,
|
|
418
|
-
"debug": false
|
|
419
|
-
}
|
|
375
|
+
### Config Update Examples
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
// Nightlight colors
|
|
379
|
+
await deviceModel.updateConfig({ ambientColour: '0xff5733' })
|
|
380
|
+
await deviceModel.updateConfig({ nightAmbientColour: '0x3366ff' })
|
|
381
|
+
|
|
382
|
+
// Volume limits
|
|
383
|
+
await deviceModel.updateConfig({ maxVolumeLimit: '12' })
|
|
384
|
+
await deviceModel.updateConfig({ nightMaxVolumeLimit: '8' })
|
|
385
|
+
|
|
386
|
+
// Bluetooth
|
|
387
|
+
await deviceModel.updateConfig({ bluetoothEnabled: '1' })
|
|
420
388
|
```
|
|
421
389
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
-
|
|
428
|
-
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
-
|
|
436
|
-
-
|
|
437
|
-
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
-
|
|
441
|
-
-
|
|
442
|
-
-
|
|
443
|
-
-
|
|
444
|
-
|
|
445
|
-
###
|
|
446
|
-
-
|
|
447
|
-
-
|
|
448
|
-
-
|
|
449
|
-
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
-
|
|
454
|
-
-
|
|
455
|
-
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
- Persist all state in accessory.context
|
|
459
|
-
- Gracefully restore on restart
|
|
460
|
-
- Re-establish API connection
|
|
461
|
-
- Resume polling from last known state
|
|
462
|
-
|
|
463
|
-
## Testing Strategy
|
|
464
|
-
|
|
465
|
-
### Unit Tests
|
|
466
|
-
- API client methods (mock fetch)
|
|
467
|
-
- Token refresh logic
|
|
468
|
-
- Status mapping functions
|
|
469
|
-
- Config validation
|
|
470
|
-
|
|
471
|
-
### Integration Tests
|
|
472
|
-
- OAuth flow (with mock server)
|
|
473
|
-
- Device discovery
|
|
474
|
-
- Characteristic updates
|
|
475
|
-
- Error recovery
|
|
476
|
-
|
|
477
|
-
### Manual Testing Checklist
|
|
478
|
-
- [ ] Initial OAuth setup flow
|
|
479
|
-
- [ ] Device discovery and registration
|
|
480
|
-
- [ ] Play/pause control
|
|
481
|
-
- [ ] Volume adjustment
|
|
482
|
-
- [ ] Battery status display
|
|
483
|
-
- [ ] MQTT connection establishment
|
|
484
|
-
- [ ] Real-time status updates via MQTT
|
|
485
|
-
- [ ] Device offline handling
|
|
486
|
-
- [ ] Token refresh
|
|
487
|
-
- [ ] Homebridge restart recovery
|
|
488
|
-
- [ ] Multiple device support
|
|
489
|
-
- [ ] Device removal from account
|
|
490
|
-
|
|
491
|
-
## Documentation
|
|
492
|
-
|
|
493
|
-
### README.md
|
|
494
|
-
- Overview and features
|
|
495
|
-
- Prerequisites (Yoto account, Homebridge)
|
|
496
|
-
- Installation instructions
|
|
497
|
-
- OAuth setup guide with screenshots
|
|
498
|
-
- Configuration options
|
|
499
|
-
- Troubleshooting common issues
|
|
500
|
-
- Supported devices
|
|
501
|
-
|
|
502
|
-
### CHANGELOG.md
|
|
503
|
-
- Version history
|
|
504
|
-
- Feature additions
|
|
505
|
-
- Bug fixes
|
|
506
|
-
- Breaking changes
|
|
507
|
-
|
|
508
|
-
### Contributing Guide
|
|
509
|
-
- Development setup
|
|
510
|
-
- Code style (JSDoc, ESLint)
|
|
511
|
-
- Testing requirements
|
|
512
|
-
- Pull request process
|
|
513
|
-
|
|
514
|
-
## Future Enhancements (Post-MVP)
|
|
515
|
-
|
|
516
|
-
### Content Management
|
|
517
|
-
- [ ] Recently played content tracking
|
|
518
|
-
- [ ] MYO card management
|
|
519
|
-
- [ ] Active content information display
|
|
520
|
-
|
|
521
|
-
### Advanced Device Control
|
|
522
|
-
- [ ] Alarm management
|
|
523
|
-
- [ ] Clock face selection
|
|
524
|
-
- [ ] Scheduled ambient light changes
|
|
525
|
-
- [ ] Audio output routing
|
|
526
|
-
|
|
527
|
-
### Multi-Device Features
|
|
528
|
-
- [ ] Synchronized playback
|
|
529
|
-
- [ ] Family dashboard accessory
|
|
530
|
-
- [ ] Group controls
|
|
531
|
-
|
|
532
|
-
### Parental Controls
|
|
533
|
-
- [ ] Time-based restrictions
|
|
534
|
-
- [ ] Content filtering
|
|
535
|
-
- [ ] Usage monitoring
|
|
536
|
-
|
|
537
|
-
### Automation Integration
|
|
538
|
-
- [ ] Bedtime scene triggers
|
|
539
|
-
- [ ] Presence detection
|
|
540
|
-
- [ ] Weather-based content
|
|
541
|
-
- [ ] HomeKit Secure Video integration (if camera added)
|
|
542
|
-
|
|
543
|
-
## Success Metrics
|
|
544
|
-
|
|
545
|
-
### MVP Success Criteria
|
|
546
|
-
- [ ] Successfully authenticate with Yoto API
|
|
547
|
-
- [ ] Discover and register all user devices
|
|
548
|
-
- [ ] Control play/pause from Home app
|
|
549
|
-
- [ ] Adjust volume from Home app
|
|
550
|
-
- [ ] Display accurate battery status
|
|
551
|
-
- [ ] Status updates within 60 seconds
|
|
552
|
-
- [ ] Survive Homebridge restart
|
|
553
|
-
- [ ] Handle device offline gracefully
|
|
554
|
-
|
|
555
|
-
### Phase 2 Success Criteria
|
|
556
|
-
- [ ] Brightness control functional
|
|
557
|
-
- [ ] Temperature sensor (if enabled)
|
|
558
|
-
- [ ] Connection status monitoring
|
|
559
|
-
- [ ] Configuration switches work
|
|
560
|
-
|
|
561
|
-
### User Satisfaction Goals
|
|
562
|
-
- Setup time under 5 minutes
|
|
563
|
-
- No more than 2 second latency for controls
|
|
564
|
-
- Clear error messages and recovery steps
|
|
565
|
-
- Stable over 7 days continuous operation
|
|
566
|
-
|
|
567
|
-
## Development Phases Timeline
|
|
568
|
-
|
|
569
|
-
### Week 1: Foundation
|
|
570
|
-
- Day 1-2: Project setup, REST API client, auth
|
|
571
|
-
- Day 3-4: MQTT client implementation
|
|
572
|
-
- Day 5-7: Platform implementation with MQTT integration
|
|
573
|
-
|
|
574
|
-
### Week 2: Core Features
|
|
575
|
-
- Day 1-2: Basic accessory with playback/volume controls via MQTT
|
|
576
|
-
- Day 3-4: Battery service, real-time status updates
|
|
577
|
-
- Day 5-7: Display control, temperature sensor
|
|
578
|
-
|
|
579
|
-
### Week 3: Advanced Features
|
|
580
|
-
- Day 1-3: Advanced controls, volume limits
|
|
581
|
-
- Day 4-5: Card detection, connection status
|
|
582
|
-
- Day 6-7: Integration testing, polish
|
|
583
|
-
|
|
584
|
-
### Week 4: Release Prep
|
|
585
|
-
- Day 1-3: Documentation, examples
|
|
586
|
-
- Day 4-5: Beta testing with users
|
|
587
|
-
- Day 6-7: Bug fixes, npm publish
|
|
588
|
-
|
|
589
|
-
## Next Steps
|
|
590
|
-
|
|
591
|
-
1. ✅ Review and approve plan
|
|
592
|
-
2. ✅ Discover MQTT integration capability
|
|
593
|
-
3. ✅ Set up project structure and dependencies (including MQTT library)
|
|
594
|
-
4. ✅ Implement auth.js with OAuth flow
|
|
595
|
-
5. ✅ Create REST API client wrapper
|
|
596
|
-
6. ✅ Create MQTT client wrapper with subscriptions
|
|
597
|
-
7. ✅ Build platform foundation with MQTT connection
|
|
598
|
-
8. ✅ Implement basic player accessory with real-time updates
|
|
599
|
-
9. ✅ Add display brightness control
|
|
600
|
-
10. ✅ Add sleep timer control
|
|
601
|
-
11. ✅ Add advanced control switches
|
|
602
|
-
12. ✅ Improve MQTT reconnection logic
|
|
603
|
-
13. 🔄 Test with real Yoto devices (PRIORITY)
|
|
604
|
-
14. 🔄 Add volume limit controls (IN PROGRESS)
|
|
605
|
-
15. 🔄 Add ambient light color control (IN PROGRESS)
|
|
606
|
-
16. ⏳ Add active content information display
|
|
607
|
-
17. ⏳ Add shortcuts support (future)
|
|
608
|
-
18. ⏳ Publish to npm
|
|
609
|
-
19. ⏳ Iterate based on user feedback
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Testing Checklist
|
|
393
|
+
|
|
394
|
+
### Basic Functionality
|
|
395
|
+
- [ ] Device discovery and accessory creation
|
|
396
|
+
- [ ] Playback control (play/pause/stop)
|
|
397
|
+
- [ ] Volume control (0-16 range)
|
|
398
|
+
- [ ] Battery status updates
|
|
399
|
+
- [ ] Online/offline detection
|
|
400
|
+
- [ ] Firmware version display
|
|
401
|
+
|
|
402
|
+
### Optional Services
|
|
403
|
+
- [ ] Temperature sensor (v3 only)
|
|
404
|
+
- [ ] Nightlight color control (v3 only)
|
|
405
|
+
- [ ] Nightlight brightness control (v3 only)
|
|
406
|
+
- [ ] Nightlight status sensors (v3 only)
|
|
407
|
+
- [ ] Card slot detection (all devices)
|
|
408
|
+
- [ ] Night mode detection (all devices)
|
|
409
|
+
- [ ] Sleep timer control (all devices)
|
|
410
|
+
- [ ] Bluetooth toggle (all devices)
|
|
411
|
+
- [ ] Volume limit controls (all devices)
|
|
412
|
+
|
|
413
|
+
### Edge Cases
|
|
414
|
+
- [ ] Device goes offline during operation
|
|
415
|
+
- [ ] MQTT disconnection with HTTP fallback
|
|
416
|
+
- [ ] Multiple devices in one account
|
|
417
|
+
- [ ] Device rename handling
|
|
418
|
+
- [ ] Config changes from Yoto app
|
|
419
|
+
|
|
420
|
+
### Automations
|
|
421
|
+
- [ ] Trigger on card insertion
|
|
422
|
+
- [ ] Trigger on night mode change
|
|
423
|
+
- [ ] Trigger on nightlight activation
|
|
424
|
+
- [ ] Volume limit changes based on time
|
|
425
|
+
- [ ] Sleep timer activation
|