@mdjhnson/homebridge-yoto 0.1.0

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.
@@ -0,0 +1,47 @@
1
+ /** @import { PlatformConfig } from 'homebridge' */
2
+
3
+ import { getBooleanSetting } from './utils/get-boolean-setting.js'
4
+ import { serviceSchema } from '../config.schema.cjs'
5
+
6
+ /**
7
+ * @typedef {Object} PlaybackAccessoryConfig
8
+ * @property {boolean} playbackEnabled
9
+ * @property {boolean} volumeEnabled
10
+ * @property {boolean} smartSpeakerEnabled
11
+ * @property {boolean} televisionEnabled
12
+ */
13
+
14
+ /**
15
+ * @param {PlatformConfig} config
16
+ * @returns {PlaybackAccessoryConfig}
17
+ */
18
+ export function getPlaybackAccessoryConfig (config) {
19
+ const services = config && typeof config === 'object' ? config['services'] : undefined
20
+ const serviceConfig = typeof services === 'object' && services !== null
21
+ ? /** @type {Record<string, unknown>} */ (services)
22
+ : {}
23
+
24
+ const playbackEnabled = getBooleanSetting(serviceConfig['playbackControls'], false)
25
+ const smartSpeakerEnabled = getBooleanSetting(serviceConfig['smartSpeaker'], false)
26
+ const televisionEnabled = getBooleanSetting(serviceConfig['television'], false)
27
+
28
+ return {
29
+ playbackEnabled,
30
+ volumeEnabled: playbackEnabled,
31
+ smartSpeakerEnabled,
32
+ televisionEnabled,
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Whether shortcut controls are enabled (switches and TV inputs).
38
+ * @param {PlatformConfig} config
39
+ * @returns {boolean}
40
+ */
41
+ export function getShortcutsEnabled (config) {
42
+ const services = config && typeof config === 'object' ? config['services'] : undefined
43
+ const serviceConfig = typeof services === 'object' && services !== null
44
+ ? /** @type {Record<string, unknown>} */ (services)
45
+ : {}
46
+ return getBooleanSetting(serviceConfig['shortcuts'], serviceSchema.shortcuts.default)
47
+ }
@@ -0,0 +1,56 @@
1
+ import { configSchema } from '../config.schema.cjs'
2
+
3
+ /**
4
+ * This is the name of the platform that users will use to register the plugin in the Homebridge config.json
5
+ */
6
+ export const PLATFORM_NAME = 'Yoto'
7
+
8
+ /**
9
+ * This must match the name of your plugin as defined the package.json `name` property
10
+ */
11
+ export const PLUGIN_NAME = '@mdjhnson/homebridge-yoto'
12
+
13
+ /**
14
+ * Default OAuth Client ID from config schema
15
+ */
16
+ export const DEFAULT_CLIENT_ID = configSchema.schema.properties.clientId.default
17
+
18
+ /**
19
+ * OAuth redirect URI registered on the Yoto developer app. Nothing listens here:
20
+ * the user copies the resulting address back into the plugin settings.
21
+ */
22
+ export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:8787/callback'
23
+
24
+ /**
25
+ * Client IDs from earlier versions that only support the retired device-code
26
+ * sign-in. Configs still using them are switched to DEFAULT_CLIENT_ID on sign-in.
27
+ */
28
+ export const LEGACY_CLIENT_IDS = ['Y4HJ8BFqRQ24GQoLzgOzZ2KSqWmFG8LI']
29
+
30
+ /**
31
+ * OAuth scopes requested during sign-in. The Yoto developer app must have the
32
+ * same scopes enabled in the dashboard.
33
+ * @see https://yoto.dev/authentication/scopes/
34
+ */
35
+ export const OAUTH_SCOPES = [
36
+ 'offline_access', // refresh tokens
37
+ 'family:devices:view', // list players and read status
38
+ 'family:devices:manage', // nightlight, volume limits, Bluetooth settings
39
+ 'family:devices:control', // MQTT playback and volume control
40
+ 'family:library:view', // card titles for shortcut names
41
+ 'user:content:view', // titles of Make Your Own cards
42
+ ].join(' ')
43
+
44
+ /** Device defaults */
45
+ export const DEFAULT_MANUFACTURER = 'Yoto Inc.'
46
+ export const DEFAULT_MODEL = 'Yoto Player'
47
+
48
+ /** Battery threshold */
49
+ export const LOW_BATTERY_THRESHOLD = 20
50
+
51
+ /** Logging prefixes for debugging */
52
+ export const LOG_PREFIX = {
53
+ PLATFORM: '[Platform]',
54
+ ACCESSORY: '[Accessory]',
55
+ MQTT: '[MQTT]',
56
+ }
@@ -0,0 +1,131 @@
1
+ /** @import { YotoDeviceShortcuts } from 'yoto-nodejs-client/lib/api-endpoints/devices.js' */
2
+ /** @import { PlayableCard } from './card-controls.js' */
3
+
4
+ /**
5
+ * A playable shortcut configured on a Yoto device.
6
+ * @typedef {Object} ShortcutEntry
7
+ * @property {string} id - Stable identifier derived from the content it plays
8
+ * @property {number} number - 1-based position, used for fallback labels
9
+ * @property {Array<'day' | 'night'>} modes - Modes this shortcut is configured in
10
+ * @property {string} cardId - Card to play
11
+ * @property {string} [chapterKey] - Chapter to start at
12
+ * @property {string} [trackKey] - Track to start at (may contain a date placeholder; see resolveShortcutKey)
13
+ * @property {string} [builtInName] - Name for Yoto's built-in shortcuts (Daily, Radio)
14
+ */
15
+
16
+ /** @type {Array<'day' | 'night'>} */
17
+ const MODES = ['day', 'night']
18
+
19
+ /**
20
+ * Names for Yoto's built-in shortcuts, keyed by chapter. These live on a system
21
+ * card whose title can't be looked up.
22
+ * @type {Record<string, string>}
23
+ */
24
+ const BUILT_IN_SHORTCUT_NAMES = {
25
+ daily: 'Yoto Daily',
26
+ 'radio-day': 'Yoto Radio',
27
+ 'radio-night': 'Yoto Night Radio',
28
+ }
29
+
30
+ /**
31
+ * Replace date placeholders in shortcut chapter/track keys (e.g. Yoto Daily's
32
+ * `<yyyymmdd>` track) with the current local date.
33
+ * @param {string | undefined} key
34
+ * @param {Date} [now]
35
+ * @returns {string | undefined}
36
+ */
37
+ export function resolveShortcutKey (key, now = new Date()) {
38
+ if (!key || !key.includes('<')) return key
39
+ const yyyy = String(now.getFullYear())
40
+ const mm = String(now.getMonth() + 1).padStart(2, '0')
41
+ const dd = String(now.getDate()).padStart(2, '0')
42
+ return key.replaceAll('<yyyymmdd>', `${yyyy}${mm}${dd}`)
43
+ }
44
+
45
+ /**
46
+ * Content to play for a shortcut, labelled for logs.
47
+ * @param {ShortcutEntry} entry
48
+ * @param {string} label
49
+ * @returns {PlayableCard}
50
+ */
51
+ export function toPlayableCard (entry, label) {
52
+ return {
53
+ label,
54
+ cardId: entry.cardId,
55
+ ...(entry.chapterKey !== undefined && { chapterKey: entry.chapterKey }),
56
+ ...(entry.trackKey !== undefined && { trackKey: entry.trackKey }),
57
+ }
58
+ }
59
+
60
+ /**
61
+ * startCard() options for a playable card, with date placeholders resolved.
62
+ * @param {PlayableCard} card
63
+ * @param {Date} [now]
64
+ * @returns {{ cardId: string, chapterKey?: string, trackKey?: string }}
65
+ */
66
+ export function getCardStartOptions (card, now = new Date()) {
67
+ const chapterKey = resolveShortcutKey(card.chapterKey, now)
68
+ const trackKey = resolveShortcutKey(card.trackKey, now)
69
+ return {
70
+ cardId: card.cardId,
71
+ ...(chapterKey !== undefined && { chapterKey }),
72
+ ...(trackKey !== undefined && { trackKey }),
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Extract playable shortcuts from device shortcut config.
78
+ *
79
+ * The same content configured in both day and night mode is returned once.
80
+ * Only `track-play` commands with a card ID are supported.
81
+ *
82
+ * @param {YotoDeviceShortcuts | null | undefined} shortcuts
83
+ * @returns {ShortcutEntry[]}
84
+ */
85
+ export function getShortcutEntries (shortcuts) {
86
+ /** @type {Map<string, ShortcutEntry>} */
87
+ const entries = new Map()
88
+
89
+ for (const mode of MODES) {
90
+ const content = shortcuts?.modes?.[mode]?.content
91
+ if (!Array.isArray(content)) continue
92
+
93
+ for (const item of content) {
94
+ if (!item || item.cmd !== 'track-play') continue
95
+ const cardId = typeof item.params?.card === 'string' ? item.params.card.trim() : ''
96
+ if (!cardId) continue
97
+
98
+ const chapterKey = typeof item.params.chapter === 'string' && item.params.chapter ? item.params.chapter : undefined
99
+ const trackKey = typeof item.params.track === 'string' && item.params.track ? item.params.track : undefined
100
+ const id = [cardId, chapterKey ?? '', trackKey ?? ''].join(':')
101
+
102
+ const existing = entries.get(id)
103
+ if (existing) {
104
+ if (!existing.modes.includes(mode)) existing.modes.push(mode)
105
+ continue
106
+ }
107
+
108
+ const builtInName = chapterKey ? BUILT_IN_SHORTCUT_NAMES[chapterKey] : undefined
109
+ entries.set(id, {
110
+ id,
111
+ number: entries.size + 1,
112
+ modes: [mode],
113
+ cardId,
114
+ ...(chapterKey !== undefined && { chapterKey }),
115
+ ...(trackKey !== undefined && { trackKey }),
116
+ ...(builtInName !== undefined && { builtInName }),
117
+ })
118
+ }
119
+ }
120
+
121
+ return Array.from(entries.values())
122
+ }
123
+
124
+ /**
125
+ * Signature used to detect when the set of shortcuts changes.
126
+ * @param {ShortcutEntry[]} entries
127
+ * @returns {string}
128
+ */
129
+ export function getShortcutsSignature (entries) {
130
+ return entries.map(entry => entry.id).join('|')
131
+ }