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 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
+ }
@@ -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,4 @@
1
+ import type { API } from "homebridge";
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -0,0 +1,6 @@
1
+ import { StreamTriggersPlatform } from "./platform.js";
2
+ import { PLATFORM_NAME } from "./settings.js";
3
+ export default (api) => {
4
+ api.registerPlatform(PLATFORM_NAME, StreamTriggersPlatform);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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,3 @@
1
+ export declare const PLATFORM_NAME = "StreamTriggers";
2
+ export declare const PLUGIN_NAME = "homebridge-stream-triggers";
3
+ //# sourceMappingURL=settings.d.ts.map
@@ -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"}
@@ -0,0 +1,3 @@
1
+ export const PLATFORM_NAME = "StreamTriggers";
2
+ export const PLUGIN_NAME = "homebridge-stream-triggers";
3
+ //# sourceMappingURL=settings.js.map
@@ -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
+ }