homebridge-stream-triggers 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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/config.schema.json +83 -0
- package/lib/config.d.ts +34 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +29 -0
- package/lib/config.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -0
- package/lib/platform.d.ts +19 -0
- package/lib/platform.d.ts.map +1 -0
- package/lib/platform.js +107 -0
- package/lib/platform.js.map +1 -0
- package/lib/settings.d.ts +3 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +3 -0
- package/lib/settings.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Thiesen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# homebridge-stream-triggers
|
|
2
|
+
|
|
3
|
+
A personal Homebridge dynamic platform plugin that exposes one HomeKit switch per
|
|
4
|
+
streaming channel. Flipping a switch launches that channel's live stream on an Apple TV:
|
|
5
|
+
YouTube channels deep-link straight into the live video, Twitch channels open the Twitch
|
|
6
|
+
app. Built to be zero-setup and robust; reusability by others is a non-goal.
|
|
7
|
+
|
|
8
|
+
It piggybacks on [homebridge-appletv-enhanced](https://github.com/maxileith/homebridge-appletv-enhanced)
|
|
9
|
+
for Apple TV pairing credentials and its bundled `atvremote` (pyatv) binary, and manages
|
|
10
|
+
its own `yt-dlp` binary for resolving YouTube live streams.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
When a switch flips on (e.g. via a "Stream Destiny" HomeKit scene):
|
|
15
|
+
|
|
16
|
+
1. **Wake** — `atvremote turn_on` wakes the Apple TV.
|
|
17
|
+
2. **Prime** — `atvremote app_list` (output discarded). This is required: after an Apple TV
|
|
18
|
+
reboot (e.g. a tvOS auto-update), tvOS silently drops app-launch requests with a
|
|
19
|
+
Companion protocol timeout until some client has requested the app list once
|
|
20
|
+
(documented pyatv FAQ behavior). Priming on every launch keeps the system self-healing
|
|
21
|
+
across tvOS updates.
|
|
22
|
+
3. **Resolve** — for YouTube channels, `yt-dlp --print id <url>` resolves the current live
|
|
23
|
+
video id. If the channel isn't live, the launch stops there (TV awake, nothing playing).
|
|
24
|
+
4. **Launch** — `atvremote launch_app=youtube://www.youtube.com/watch?v=<id>` for YouTube,
|
|
25
|
+
or `launch_app=tv.twitch` for Twitch (the tvOS Twitch app has no deep links; opening
|
|
26
|
+
the app is the intended behavior).
|
|
27
|
+
|
|
28
|
+
The switch is momentary: it auto-resets to off after `resetDelay` ms, and the launch runs
|
|
29
|
+
fire-and-forget in the background. Repeat flips while a launch is in flight are ignored.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"platform": "StreamTriggers",
|
|
36
|
+
"channels": [
|
|
37
|
+
{ "key": "destiny", "type": "youtube", "url": "https://www.youtube.com/@destiny/live" },
|
|
38
|
+
{ "key": "hutch", "type": "youtube", "url": "https://www.youtube.com/@Hutch/live" },
|
|
39
|
+
{ "key": "wilburgur", "type": "youtube", "url": "https://www.youtube.com/@Wilburgur/live" },
|
|
40
|
+
{ "key": "vinesauce", "type": "twitch" },
|
|
41
|
+
{ "key": "jerma", "type": "twitch" }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Channels
|
|
47
|
+
|
|
48
|
+
| Field | Required | Description |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `key` | yes | Short unique id (e.g. `destiny`). Seeds the accessory UUID, so it's stable across restarts. |
|
|
51
|
+
| `type` | yes | `youtube` or `twitch`. |
|
|
52
|
+
| `url` | youtube only | The channel's live page, e.g. `https://www.youtube.com/@destiny/live`. |
|
|
53
|
+
| `displayName` | no | Defaults to the capitalized key. The switch is named `<displayName><suffix>` (e.g. `Destiny Trigger`). |
|
|
54
|
+
|
|
55
|
+
### Platform options
|
|
56
|
+
|
|
57
|
+
| Field | Default | Description |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `credentialsDir` | `/var/lib/homebridge/appletv-enhanced` | Where homebridge-appletv-enhanced stores Apple TV pairings. |
|
|
60
|
+
| `appleTvId` | auto | Apple TV identifier. Auto-discovered as the single subdirectory of `credentialsDir` containing a `credentials.txt`; set explicitly if there are multiple. |
|
|
61
|
+
| `atvremotePath` | `<credentialsDir>/.venv/bin/atvremote` path | `atvremote` binary from appletv-enhanced's venv (that plugin keeps pyatv updated). |
|
|
62
|
+
| `ytDlpPath` | self-managed | Override for testing. By default the plugin downloads the latest standalone `yt-dlp` into Homebridge's storage dir and refreshes it when older than 30 days (stale copies break against YouTube within months). |
|
|
63
|
+
| `suffix` | `" Trigger"` | Appended to switch names, avoiding collisions with the "Stream X" scene names. |
|
|
64
|
+
| `resetDelay` | `2000` | How long (ms) a switch reads "on" before auto-resetting. |
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pnpm install
|
|
70
|
+
pnpm run check:write # Biome lint + format (auto-fix)
|
|
71
|
+
pnpm run typecheck
|
|
72
|
+
pnpm run test # lint + typecheck + vitest
|
|
73
|
+
pnpm run build # tsc -> lib/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Pushes to `main` publish to npm via GitHub Actions (skipped if the version is already
|
|
77
|
+
published; bump `version` in `package.json` to release).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "StreamTriggers",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"title": "Name",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"default": "Stream Triggers"
|
|
12
|
+
},
|
|
13
|
+
"channels": {
|
|
14
|
+
"title": "Channels",
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"key": {
|
|
20
|
+
"title": "Key",
|
|
21
|
+
"description": "Short unique id, e.g. \"destiny\". Also seeds the accessory UUID.",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"required": true
|
|
24
|
+
},
|
|
25
|
+
"displayName": {
|
|
26
|
+
"title": "Display Name",
|
|
27
|
+
"description": "Defaults to the capitalized key.",
|
|
28
|
+
"type": "string"
|
|
29
|
+
},
|
|
30
|
+
"type": {
|
|
31
|
+
"title": "Type",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"oneOf": [
|
|
34
|
+
{ "title": "YouTube", "enum": ["youtube"] },
|
|
35
|
+
{ "title": "Twitch", "enum": ["twitch"] }
|
|
36
|
+
],
|
|
37
|
+
"required": true
|
|
38
|
+
},
|
|
39
|
+
"url": {
|
|
40
|
+
"title": "URL",
|
|
41
|
+
"description": "Channel live page (required for YouTube), e.g. https://www.youtube.com/@destiny/live",
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"credentialsDir": {
|
|
48
|
+
"title": "Credentials Directory",
|
|
49
|
+
"description": "Where homebridge-appletv-enhanced stores Apple TV pairings.",
|
|
50
|
+
"type": "string",
|
|
51
|
+
"default": "/var/lib/homebridge/appletv-enhanced"
|
|
52
|
+
},
|
|
53
|
+
"appleTvId": {
|
|
54
|
+
"title": "Apple TV ID",
|
|
55
|
+
"description": "Defaults to auto-discovery: the single subdirectory of the credentials directory containing a credentials.txt.",
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"atvremotePath": {
|
|
59
|
+
"title": "atvremote Path",
|
|
60
|
+
"description": "atvremote binary from appletv-enhanced's venv.",
|
|
61
|
+
"type": "string",
|
|
62
|
+
"default": "/var/lib/homebridge/appletv-enhanced/.venv/bin/atvremote"
|
|
63
|
+
},
|
|
64
|
+
"ytDlpPath": {
|
|
65
|
+
"title": "yt-dlp Path",
|
|
66
|
+
"description": "Override for testing. Defaults to a self-managed binary that is downloaded and refreshed automatically.",
|
|
67
|
+
"type": "string"
|
|
68
|
+
},
|
|
69
|
+
"suffix": {
|
|
70
|
+
"title": "Switch Name Suffix",
|
|
71
|
+
"description": "Appended to switch display names to avoid collisions with HomeKit scene names.",
|
|
72
|
+
"type": "string",
|
|
73
|
+
"default": " Trigger"
|
|
74
|
+
},
|
|
75
|
+
"resetDelay": {
|
|
76
|
+
"title": "Reset Delay (ms)",
|
|
77
|
+
"description": "How long a switch reads \"on\" before auto-resetting to off.",
|
|
78
|
+
"type": "number",
|
|
79
|
+
"default": 2000
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const channelSchema: z.ZodObject<{
|
|
3
|
+
key: z.ZodString;
|
|
4
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
5
|
+
type: z.ZodEnum<{
|
|
6
|
+
youtube: "youtube";
|
|
7
|
+
twitch: "twitch";
|
|
8
|
+
}>;
|
|
9
|
+
url: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type ChannelConfig = z.infer<typeof channelSchema>;
|
|
12
|
+
export declare const configSchema: z.ZodObject<{
|
|
13
|
+
channels: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
14
|
+
key: z.ZodString;
|
|
15
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
16
|
+
type: z.ZodEnum<{
|
|
17
|
+
youtube: "youtube";
|
|
18
|
+
twitch: "twitch";
|
|
19
|
+
}>;
|
|
20
|
+
url: z.ZodOptional<z.ZodString>;
|
|
21
|
+
}, z.core.$strip>>>;
|
|
22
|
+
credentialsDir: z.ZodDefault<z.ZodString>;
|
|
23
|
+
appleTvId: z.ZodOptional<z.ZodString>;
|
|
24
|
+
atvremotePath: z.ZodDefault<z.ZodString>;
|
|
25
|
+
ytDlpPath: z.ZodOptional<z.ZodString>;
|
|
26
|
+
suffix: z.ZodDefault<z.ZodString>;
|
|
27
|
+
resetDelay: z.ZodDefault<z.ZodNumber>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type StreamTriggersConfig = z.infer<typeof configSchema>;
|
|
30
|
+
/** Pure parse of the raw platform config; throws ZodError on invalid input. */
|
|
31
|
+
export declare function parseConfig(raw: unknown): StreamTriggersConfig;
|
|
32
|
+
/** Display name defaults to the capitalized key. */
|
|
33
|
+
export declare function displayNameFor(channel: ChannelConfig): string;
|
|
34
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa;;;;;;;;iBAKxB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;iBAUvB,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEhE,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAE9D;AAED,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAG7D"}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const channelSchema = z.object({
|
|
3
|
+
key: z.string().min(1),
|
|
4
|
+
displayName: z.string().min(1).optional(),
|
|
5
|
+
type: z.enum(["youtube", "twitch"]),
|
|
6
|
+
url: z.string().min(1).optional(),
|
|
7
|
+
});
|
|
8
|
+
export const configSchema = z.object({
|
|
9
|
+
channels: z.array(channelSchema).default([]),
|
|
10
|
+
credentialsDir: z.string().default("/var/lib/homebridge/appletv-enhanced"),
|
|
11
|
+
appleTvId: z.string().optional(),
|
|
12
|
+
atvremotePath: z
|
|
13
|
+
.string()
|
|
14
|
+
.default("/var/lib/homebridge/appletv-enhanced/.venv/bin/atvremote"),
|
|
15
|
+
ytDlpPath: z.string().optional(),
|
|
16
|
+
suffix: z.string().default(" Trigger"),
|
|
17
|
+
resetDelay: z.number().int().positive().default(2000),
|
|
18
|
+
});
|
|
19
|
+
/** Pure parse of the raw platform config; throws ZodError on invalid input. */
|
|
20
|
+
export function parseConfig(raw) {
|
|
21
|
+
return configSchema.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
/** Display name defaults to the capitalized key. */
|
|
24
|
+
export function displayNameFor(channel) {
|
|
25
|
+
if (channel.displayName)
|
|
26
|
+
return channel.displayName;
|
|
27
|
+
return channel.key.charAt(0).toUpperCase() + channel.key.slice(1);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACtB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACnC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,OAAO,CAAC,0DAA0D,CAAC;IACtE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC;IACtC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACtD,CAAC,CAAC;AAIH,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,OAAO,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,OAAsB;IACnD,IAAI,OAAO,CAAC,WAAW;QAAE,OAAO,OAAO,CAAC,WAAW,CAAC;IACpD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;yBAItB,KAAK,GAAG,KAAG,IAAI;AAA/B,wBAEE"}
|
package/lib/index.js
ADDED
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,eAAe,CAAC,GAAQ,EAAQ,EAAE;IAChC,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,sBAAsB,CAAC,CAAC;AAC9D,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { API, Characteristic, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig, Service } from "homebridge";
|
|
2
|
+
export declare class StreamTriggersPlatform implements DynamicPlatformPlugin {
|
|
3
|
+
readonly log: Logging;
|
|
4
|
+
readonly api: API;
|
|
5
|
+
readonly Service: typeof Service;
|
|
6
|
+
readonly Characteristic: typeof Characteristic;
|
|
7
|
+
private readonly config;
|
|
8
|
+
private readonly accessories;
|
|
9
|
+
private readonly launchesInFlight;
|
|
10
|
+
constructor(log: Logging, rawConfig: PlatformConfig, api: API);
|
|
11
|
+
/** Called by Homebridge for each accessory restored from cache at startup. */
|
|
12
|
+
configureAccessory(accessory: PlatformAccessory): void;
|
|
13
|
+
/** Register switches for configured channels; unregister ones no longer configured. */
|
|
14
|
+
private syncAccessories;
|
|
15
|
+
private setUpSwitch;
|
|
16
|
+
/** Fire-and-forget launch; never throws, never awaited by the set handler. */
|
|
17
|
+
private handleTrigger;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,cAAc,EACd,qBAAqB,EACrB,OAAO,EACP,iBAAiB,EACjB,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;AASpB,qBAAa,sBAAuB,YAAW,qBAAqB;aAShD,GAAG,EAAE,OAAO;aAEZ,GAAG,EAAE,GAAG;IAV1B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IAEtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwC;IACpE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;gBAGpC,GAAG,EAAE,OAAO,EAC5B,SAAS,EAAE,cAAc,EACT,GAAG,EAAE,GAAG;IAwB1B,8EAA8E;IAC9E,kBAAkB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAKtD,uFAAuF;IACvF,OAAO,CAAC,eAAe;IAoCvB,OAAO,CAAC,WAAW;IA2BnB,8EAA8E;IAC9E,OAAO,CAAC,aAAa;CAUtB"}
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { configSchema, displayNameFor, } from "./config.js";
|
|
2
|
+
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
|
|
3
|
+
export class StreamTriggersPlatform {
|
|
4
|
+
log;
|
|
5
|
+
api;
|
|
6
|
+
Service;
|
|
7
|
+
Characteristic;
|
|
8
|
+
config;
|
|
9
|
+
accessories = new Map();
|
|
10
|
+
launchesInFlight = new Set();
|
|
11
|
+
constructor(log, rawConfig, api) {
|
|
12
|
+
this.log = log;
|
|
13
|
+
this.api = api;
|
|
14
|
+
this.Service = api.hap.Service;
|
|
15
|
+
this.Characteristic = api.hap.Characteristic;
|
|
16
|
+
const parsed = configSchema.safeParse(rawConfig);
|
|
17
|
+
if (parsed.success) {
|
|
18
|
+
this.config = parsed.data;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
this.log.error(`Invalid config, running with no channels: ${parsed.error.message}`);
|
|
22
|
+
this.config = configSchema.parse({});
|
|
23
|
+
}
|
|
24
|
+
api.on("didFinishLaunching", () => {
|
|
25
|
+
try {
|
|
26
|
+
this.syncAccessories();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
this.log.error(`Failed to sync accessories: ${String(error)}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Called by Homebridge for each accessory restored from cache at startup. */
|
|
34
|
+
configureAccessory(accessory) {
|
|
35
|
+
this.log.info(`Restoring cached accessory: ${accessory.displayName}`);
|
|
36
|
+
this.accessories.set(accessory.UUID, accessory);
|
|
37
|
+
}
|
|
38
|
+
/** Register switches for configured channels; unregister ones no longer configured. */
|
|
39
|
+
syncAccessories() {
|
|
40
|
+
const configuredUuids = new Set();
|
|
41
|
+
for (const channel of this.config.channels) {
|
|
42
|
+
if (channel.type === "youtube" && !channel.url) {
|
|
43
|
+
this.log.error(`[${channel.key}] YouTube channel is missing "url"; skipping`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const uuid = this.api.hap.uuid.generate(`${PLUGIN_NAME}.${channel.key}`);
|
|
47
|
+
configuredUuids.add(uuid);
|
|
48
|
+
const name = `${displayNameFor(channel)}${this.config.suffix}`;
|
|
49
|
+
const existing = this.accessories.get(uuid);
|
|
50
|
+
if (existing) {
|
|
51
|
+
existing.displayName = name;
|
|
52
|
+
this.setUpSwitch(existing, channel, name);
|
|
53
|
+
this.api.updatePlatformAccessories([existing]);
|
|
54
|
+
this.log.info(`[${channel.key}] Updated switch "${name}"`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const accessory = new this.api.platformAccessory(name, uuid);
|
|
58
|
+
this.setUpSwitch(accessory, channel, name);
|
|
59
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
60
|
+
this.accessories.set(uuid, accessory);
|
|
61
|
+
this.log.info(`[${channel.key}] Registered switch "${name}"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const [uuid, accessory] of this.accessories) {
|
|
65
|
+
if (configuredUuids.has(uuid))
|
|
66
|
+
continue;
|
|
67
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
68
|
+
this.accessories.delete(uuid);
|
|
69
|
+
this.log.info(`Unregistered switch "${accessory.displayName}" (channel removed)`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
setUpSwitch(accessory, channel, name) {
|
|
73
|
+
const service = accessory.getService(this.Service.Switch) ??
|
|
74
|
+
accessory.addService(this.Service.Switch);
|
|
75
|
+
service.setCharacteristic(this.Characteristic.Name, name);
|
|
76
|
+
const on = service.getCharacteristic(this.Characteristic.On);
|
|
77
|
+
on.onGet(() => false);
|
|
78
|
+
on.onSet((value) => {
|
|
79
|
+
try {
|
|
80
|
+
if (!value)
|
|
81
|
+
return; // OFF is a no-op; the switch is momentary
|
|
82
|
+
this.handleTrigger(channel);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.log.error(`[${channel.key}] Trigger handler failed: ${String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
// Momentary semantics: flip back to OFF after resetDelay.
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
on.updateValue(false);
|
|
91
|
+
}, this.config.resetDelay);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Fire-and-forget launch; never throws, never awaited by the set handler. */
|
|
96
|
+
handleTrigger(channel) {
|
|
97
|
+
if (this.launchesInFlight.has(channel.key)) {
|
|
98
|
+
this.log.info(`[${channel.key}] Launch already in flight; ignoring`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.launchesInFlight.add(channel.key);
|
|
102
|
+
// TODO(build-out): wake Apple TV, prime app_list, resolve stream, launch_app.
|
|
103
|
+
this.log.info(`[${channel.key}] Triggered (launch flow not implemented yet)`);
|
|
104
|
+
this.launchesInFlight.delete(channel.key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,mBAAmB,CAAC;AAC9C,eAAO,MAAM,WAAW,+BAA+B,CAAC"}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,gBAAgB,CAAC;AAC9C,MAAM,CAAC,MAAM,WAAW,GAAG,4BAA4B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-stream-triggers",
|
|
3
|
+
"displayName": "Homebridge Stream Triggers",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "HomeKit switches that launch live streams (YouTube/Twitch) on an Apple TV via atvremote and yt-dlp.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"homebridge-plugin",
|
|
8
|
+
"homebridge",
|
|
9
|
+
"apple-tv",
|
|
10
|
+
"youtube",
|
|
11
|
+
"twitch",
|
|
12
|
+
"livestream",
|
|
13
|
+
"homeautomation"
|
|
14
|
+
],
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/micthiesen/homebridge-stream-triggers/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git://github.com/micthiesen/homebridge-stream-triggers.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Michael Thiesen",
|
|
25
|
+
"main": "lib/index.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"config.schema.json",
|
|
28
|
+
"lib"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@micthiesen/mitools": "^2",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "2.5.4",
|
|
36
|
+
"@types/node": "^24",
|
|
37
|
+
"hap-nodejs": "~0.14.3",
|
|
38
|
+
"homebridge": "~2.1.1",
|
|
39
|
+
"typescript": "~6.0.2",
|
|
40
|
+
"vitest": "^4.1.10"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"homebridge": "^1.8.0 || ^2.0.0",
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prebuild": "pnpm run clean",
|
|
48
|
+
"build": "tsc --project tsconfig.source.json",
|
|
49
|
+
"clean": "rm -rf lib/",
|
|
50
|
+
"check:write": "biome check --write .",
|
|
51
|
+
"format": "biome format .",
|
|
52
|
+
"lint": "biome check .",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"start": "homebridge -D --plugin-path .",
|
|
55
|
+
"pretest": "pnpm run lint && pnpm run typecheck",
|
|
56
|
+
"test": "pnpm run test:only",
|
|
57
|
+
"test:only": "vitest run"
|
|
58
|
+
}
|
|
59
|
+
}
|