diff --git a/.gitignore b/.gitignore index 999a3ab..5132fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,3 @@ secrets.yaml # OS specific .DS_Store Thumbs.db - -#Build Tools -build-release.sh -VERSION diff --git a/README.md b/README.md index c5902f8..ee7b7a5 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,121 @@ -# RelayTV Home Assistant Integration +

+ RelayTV +

-![RelayTV Screenshots](https://raw.githubusercontent.com/mcgeezy/relaytv-ha/main/relaytv-ha.png) +

Your TV, native in Home Assistant.

-RelayTV for Home Assistant adds your self-hosted RelayTV servers as Home Assistant entities and services, making it easy to control playback, launch media, trigger overlays, and integrate RelayTV into automations and dashboards. +

+ Add your self-hosted RelayTV screens to Home Assistant as real media-player + devices — control playback, launch media, trigger overlays, and drop RelayTV + into automations, dashboards, and voice. +

-RelayTV server: -https://github.com/mcgeezy/relaytv +

+ Install · + See it in action · + Services · + RelayTV server +

-Android companion app: -https://github.com/mcgeezy/relaytv-android +

+ Build status + HACS custom repository + Home Assistant 2026.7 or newer + GPL-3.0-only license +

-Support the project: -https://buymeacoffee.com/relaytv +

+ RelayTV running as a Home Assistant media player, panel, and mobile control +

---- +

+ Local-first · No RelayTV account · Bearer-token auth · + Credential-safe playback state +

-## What This Integration Adds +## One integration. Every way you control the TV. -RelayTV integrates with Home Assistant as a local `media_player` plus RelayTV-specific services. +### 🎛️ Control it -### Core features +Every RelayTV server becomes a Home Assistant `media_player`: play, pause, +stop, next, previous, seek, volume, mute, turn on/off — with live artwork, +title, and progress on your dashboards, the mobile app, and voice assistants. -- Creates a `media_player` entity for each RelayTV config entry -- Supports multiple RelayTV servers -- Uses RelayTV live event updates plus `/status` refresh fallback -- Supports an optional RelayTV API bearer token with reauthentication -- Adds a Home Assistant sidebar panel for the RelayTV web UI -- Exposes RelayTV-specific services for smart play, temporary playback, overlays, snapshots, synchronized playback, upload/play, upload/enqueue, and resume behavior -- Supports automation-friendly control from scripts, dashboards, and mobile workflows +### 🤖 Automate it -### Supported media controls +Call RelayTV-specific actions from scripts and automations — smart play, +temporary interrupt-and-resume, overlays, snapshots, synchronized multi-screen +start, and direct uploads from Home Assistant media to the TV. -- Play -- Pause -- Stop -- Next -- Previous -- Seek -- Set volume -- Mute and unmute -- Turn on -- Turn off +### 🧩 Embed it -### Advanced RelayTV features +Pin the full RelayTV web UI as a Home Assistant sidebar panel, so the remote, +queue, and Jellyfin/Emby library are always one click away — without leaving +Home Assistant. -- Smart queue/play behavior -- Temporary interrupt + resume playback -- Text and image overlays -- Snapshot capture -- Multi-target synchronized playback -- Resume-position support -- Direct media upload from Home Assistant local media/files to RelayTV ingest endpoints -- Optional sensor-to-stream mapping triggers +RelayTV keeps the playback engine beside the TV while Home Assistant stays the +control plane. Live event updates keep entity state fresh, with a `/status` +poll as the reconnect fallback. ---- +## See it in action -## RelayTV Services +

+ RelayTV media-player more-info dialog with artwork, seek, and volume controls +

-| Service | RelayTV endpoint | Notes | -| --- | --- | --- | -| `relaytv.smart_url` | `POST /smart` | Smart play/enqueue behavior | -| `relaytv.play_now` | `POST /play` | Immediate playback; clears queue | -| `relaytv.announce` | `POST /play` | Alias of `play_now` | -| `relaytv.play_temporary` | `POST /play_temporary` | Temporary interrupt + resume flow | -| `relaytv.overlay` | `POST /overlay` | Text/image overlay | -| `relaytv.play_synced` | `POST /play_at` | Multi-entity time-aligned start | -| `relaytv.snapshot` | `POST /snapshot` (fallback `GET /snapshot`) | Captures current frame | -| `relaytv.play_with_resume` | `POST /play` + `POST /seek_abs` | Resume per-URL saved position | -| `relaytv.upload_media` | `POST /ingest/media` | Upload local HA media/file and return RelayTV media URL | -| `relaytv.upload_media_play` | `POST /ingest/media/play` | Upload local HA media/file and start playback | -| `relaytv.upload_media_enqueue` | `POST /ingest/media/enqueue` | Upload local HA media/file and append to queue | +

+ RelayTV device page in Home Assistant with device info, controls, and activity +

---- +

+ The RelayTV web UI embedded as a Home Assistant sidebar panel +

-## Home Assistant Companion App Share Automation +> The images above are captured from a live Home Assistant instance with +> [`scripts/readme-screenshots.mjs`](scripts/readme-screenshots.mjs). -Add this automation to share links to the Home Assistant Phone App and play them on TV +## Install in minutes + +### HACS + +1. Open **HACS** in Home Assistant. +2. Add this repository as a custom repository with category **Integration**. +3. Install **RelayTV** and restart Home Assistant. +4. Add the integration from **Settings → Devices & Services → Add Integration**. + +### Manual + +1. Copy `custom_components/relaytv` into your Home Assistant config: + + ```text + /config/custom_components/relaytv/ + ``` + +2. Restart Home Assistant. +3. Go to **Settings → Devices & Services → Add Integration** and search for **RelayTV**. + +### Configure + +Enter: + +- **RelayTV base URL** — for example `http://relaytv-host:8787` +- **Display name** — how this RelayTV instance appears in Home Assistant +- **RelayTV API token** — only if `RELAYTV_API_TOKEN` is enabled on the server + +Use **Reconfigure** on the integration entry to change the server address, +display name, or API token. The token is stored in the Home Assistant config +entry and sent only as a bearer authorization header to RelayTV. + +From the integration **options** you can also configure: + +- `panel_enabled` — enable or disable the sidebar panel +- `panel_target_entry_id` — which RelayTV server the shared panel points at +- `sensor_stream_mappings` — map sensors to temporary playback URLs + +## Send links from your phone + +Add this automation to relay links shared from the Home Assistant mobile app +straight to the TV: ```yaml alias: RelayTV - Smart play from share @@ -98,69 +138,25 @@ variables: (trigger.event.data.text if trigger.event.data.text is defined else '') }} ``` ---- +## RelayTV Services -## Installation +| Service | RelayTV endpoint | Notes | +| --- | --- | --- | +| `relaytv.smart_url` | `POST /smart` | Smart play/enqueue behavior | +| `relaytv.play_now` | `POST /play` | Immediate playback; clears queue | +| `relaytv.announce` | `POST /play` | Alias of `play_now` | +| `relaytv.play_temporary` | `POST /play_temporary` | Temporary interrupt + resume flow | +| `relaytv.overlay` | `POST /overlay` | Text/image overlay | +| `relaytv.play_synced` | `POST /play_at` | Multi-entity time-aligned start | +| `relaytv.snapshot` | `POST /snapshot` (fallback `GET /snapshot`) | Captures current frame | +| `relaytv.play_with_resume` | `POST /play` + `POST /seek_abs` | Resume per-URL saved position | +| `relaytv.upload_media` | `POST /ingest/media` | Upload local HA media/file and return a RelayTV media URL | +| `relaytv.upload_media_play` | `POST /ingest/media/play` | Upload local HA media/file and start playback | +| `relaytv.upload_media_enqueue` | `POST /ingest/media/enqueue` | Upload local HA media/file and append to queue | -### HACS +### Example service calls -1. Open HACS in Home Assistant -2. Add this repository as a custom repository with category `Integration` -3. Install `RelayTV` -4. Restart Home Assistant -5. Add the `RelayTV` integration from **Settings → Devices & Services** - -### Manual - -1. Copy `custom_components/relaytv` into your Home Assistant config: - - ```text - /config/custom_components/relaytv/ - ``` - -2. Restart Home Assistant -3. Go to **Settings → Devices & Services → Add Integration** -4. Search for **RelayTV** -5. Enter: - - RelayTV base URL, for example `http://relaytv-host:8787` - - Display name for this RelayTV instance - - RelayTV API token, if `RELAYTV_API_TOKEN` is enabled on the server - -Use **Reconfigure** on the integration entry to change the server address, -display name, or API token. The token is stored in the Home Assistant config -entry and is sent only as a bearer authorization header to RelayTV. - ---- - -## Configuration Options - -From the integration options flow, you can configure: - -- `panel_enabled` — enable or disable sidebar panel registration -- `panel_target_entry_id` — choose which RelayTV server is used by the shared sidebar panel -- `sensor_stream_mappings` — map sensors to temporary playback URLs - -## Security and Upgrading to 0.4.0 - -Version 0.4.0 sanitizes credential-bearing playback URLs before they reach -Home Assistant state or resume storage. Existing resume data is migrated and -limited to the 500 most recent media keys automatically. - -Home Assistant recorder history written by an older version is not rewritten. -If a previous media identifier contained an upstream Jellyfin/Emby API key or -signed URL, rotate that credential and consider purging old history for the -RelayTV media-player entity. - -For defense in depth, update the RelayTV server to a version that redacts -private playback fields from public status, queue, history, and SSE payloads. -The matching server update also allows Home Assistant to process RelayTV -thumbnails across origins without browser CORS errors. - ---- - -## Example Service Calls - -### Smart play / enqueue +Smart play / enqueue: ```yaml service: relaytv.smart_url @@ -170,19 +166,7 @@ data: url: https://www.youtube.com/watch?v=dQw4w9WgXcQ ``` -### Immediate playback - -```yaml -service: relaytv.play_now -target: - entity_id: media_player.relaytv_living_room -data: - url: https://www.youtube.com/watch?v=dQw4w9WgXcQ - use_ytdlp: true - cec: false -``` - -### Temporary playback +Temporary playback (interrupt, then resume what was playing): ```yaml service: relaytv.play_temporary @@ -194,7 +178,7 @@ data: volume: 0.6 ``` -### Overlay message +Overlay a message on the TV: ```yaml service: relaytv.overlay @@ -206,7 +190,7 @@ data: position: top-right ``` -### Upload media and play +Upload a local file and play it: ```yaml service: relaytv.upload_media_play @@ -217,42 +201,57 @@ data: title: Shared Clip ``` -`file_path` must be readable by Home Assistant and allowed by `allowlist_external_dirs`. -When using the service UI, the `file` field can also select a local Home Assistant media source item. +`file_path` must be readable by Home Assistant and allowed by +`allowlist_external_dirs`. In the service UI, the `file` field can also select a +local Home Assistant media-source item. ---- - -## Typical Use Cases +## Typical use cases - Send shared links from Home Assistant automations to a RelayTV screen - Upload and play local Home Assistant media files on RelayTV -- Launch temporary doorbell or announcement media, then resume previous playback +- Launch temporary doorbell or announcement media, then resume playback - Display overlay messages on TVs around the home -- Add RelayTV as a dashboard-accessible media target +- Add RelayTV as a dashboard- and voice-accessible media target - Keep multiple RelayTV devices available in one Home Assistant setup - Start synchronized playback across more than one RelayTV screen ---- +## Security and upgrading to 0.4.0 -## Known Limitations +Version 0.4.0 sanitizes credential-bearing playback URLs before they reach Home +Assistant state or resume storage. Existing resume data is migrated and limited +to the 500 most recent media keys automatically. -- `relaytv.play_now` currently maps to RelayTV `POST /play` (queue-clearing behavior) -- RelayTV also exposes `POST /play_now`, but this integration does not currently use its preserve-current behavior -- No dedicated `clear_queue` Home Assistant service is currently registered by this integration -- Upload services require a local media source item or a file path available inside the Home Assistant container +Home Assistant recorder history written by an older version is not rewritten. If +a previous media identifier contained an upstream Jellyfin/Emby API key or +signed URL, rotate that credential and consider purging old history for the +RelayTV media-player entity. + +For defense in depth, update the RelayTV server to a version that redacts +private playback fields from public status, queue, history, and SSE payloads. +The matching server update also lets Home Assistant process RelayTV thumbnails +across origins without browser CORS errors. + +## Known limitations + +- `relaytv.play_now` maps to RelayTV `POST /play` (queue-clearing behavior); + the server's preserve-current `POST /play_now` is not yet used +- No dedicated `clear_queue` Home Assistant service is registered +- Upload services require a local media-source item or a file path available + inside the Home Assistant container - Overlay calls must include at least `text` or `image_url` - Snapshots require active playback on the RelayTV server -- `/ui/events` is treated as a live push stream, not a replay log; `/status` remains the reconnect/bootstrap fallback -- RelayTV releases predating `POST /auth/check` remain compatible, but the - current server release is recommended for public-payload redaction and - cross-origin thumbnails +- `/ui/events` is treated as a live push stream, not a replay log; `/status` + remains the reconnect/bootstrap fallback +- Releases predating `POST /auth/check` remain compatible, but the current + server release is recommended for public-payload redaction and cross-origin + thumbnails ---- +## Companion projects -## Companion Projects - -- RelayTV server: https://github.com/mcgeezy/relaytv -- RelayTV Android app: https://github.com/mcgeezy/relaytv-android +- **[RelayTV server](https://github.com/mcgeezy/relaytv)** — the local playback + engine and web UI. +- **[RelayTV for Android](https://github.com/mcgeezy/relaytv-android)** — share + links directly to the TV and control playback from your phone. ### Planned / work in progress @@ -260,20 +259,9 @@ When using the service UI, the `file` field can also select a local Home Assista - Continued multi-device and automation improvements - Ongoing UX polish across the RelayTV ecosystem ---- - -## Support The Project - -If RelayTV is useful to you, donations help support continued development of the server, Home Assistant integration, Android app, and future companion apps. - -Buy me a coffee: -https://buymeacoffee.com/relaytv - ---- - ## Compatibility -Version 0.4.0 requires Home Assistant 2026.7 or newer. It is automatically +Version 0.4.0 requires **Home Assistant 2026.7 or newer** and is automatically tested against Home Assistant 2026.7.2 and the current RelayTV server API. For local verification: @@ -284,7 +272,25 @@ ruff check custom_components tests python -m pytest -q ``` +To regenerate the README images from a live Home Assistant instance: + +```bash +HA_TOKEN= node scripts/readme-screenshots.mjs \ + --ha=http://homeassistant.local:8123 \ + --device= \ + --entity=media_player.living_room_tv \ + --ws=ws://127.0.0.1:3000/ +``` + +## Support the project + +If RelayTV is useful to you, donations help fund continued development of the +server, this Home Assistant integration, the Android app, and future companion +apps. + +Buy me a coffee: https://buymeacoffee.com/relaytv + ## License -RelayTV Home Assistant is licensed under the GNU General Public License, -version 3 only (`GPL-3.0-only`). See [LICENSE](LICENSE). +RelayTV Home Assistant is licensed under the GNU General Public License, version +3 only (`GPL-3.0-only`). See [LICENSE](LICENSE). diff --git a/docs/images/readme/control-phone.png b/docs/images/readme/control-phone.png new file mode 100644 index 0000000..284ae1d Binary files /dev/null and b/docs/images/readme/control-phone.png differ diff --git a/docs/images/readme/device-desktop.png b/docs/images/readme/device-desktop.png new file mode 100644 index 0000000..a934a75 Binary files /dev/null and b/docs/images/readme/device-desktop.png differ diff --git a/docs/images/readme/hero.png b/docs/images/readme/hero.png new file mode 100644 index 0000000..14fcbc4 Binary files /dev/null and b/docs/images/readme/hero.png differ diff --git a/docs/images/readme/panel-desktop.png b/docs/images/readme/panel-desktop.png new file mode 100644 index 0000000..8aa7106 Binary files /dev/null and b/docs/images/readme/panel-desktop.png differ diff --git a/docs/images/readme/relaytv-banner.png b/docs/images/readme/relaytv-banner.png new file mode 100644 index 0000000..278f465 Binary files /dev/null and b/docs/images/readme/relaytv-banner.png differ diff --git a/relaytv-ha.png b/relaytv-ha.png deleted file mode 100644 index f3f2263..0000000 Binary files a/relaytv-ha.png and /dev/null differ diff --git a/scripts/readme-screenshots.mjs b/scripts/readme-screenshots.mjs new file mode 100644 index 0000000..e8903e8 --- /dev/null +++ b/scripts/readme-screenshots.mjs @@ -0,0 +1,234 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Capture and compose the README product images for the RelayTV Home Assistant + * integration. Screenshots are taken from a live Home Assistant instance that + * already has RelayTV set up and playing something (so the media card is rich), + * then framed in browser/phone chrome for the README and release/ad assets. + * + * Auth: pass a Home Assistant long-lived (or short-lived) access token via the + * HA_TOKEN environment variable. The token is injected into the frontend's + * localStorage so the capture runs as that user; it is never written to disk. + * + * Example: + * HA_TOKEN=xxxxx node scripts/readme-screenshots.mjs \ + * --ws=ws://127.0.0.1:3000/ \ + * --ha=http://homeassistant.local:8123 \ + * --device= \ + * --entity=media_player.living_room_tv \ + * --output=docs/images/readme + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { chromium } from 'playwright'; + +function option(name, fallback) { + const prefix = `--${name}=`; + const found = process.argv.find((arg) => arg.startsWith(prefix)); + return found ? found.slice(prefix.length) : fallback; +} + +const WS = option('ws', 'ws://127.0.0.1:3000/'); +const HA = option('ha', 'http://homeassistant.local:8123').replace(/\/$/, ''); +const DEVICE_ID = option('device', ''); +const ENTITY = option('entity', 'media_player.living_room_tv'); +const PANEL = option('panel', 'relaytv'); +const OUTPUT = path.resolve(option('output', 'docs/images/readme')); +const LOGO = path.resolve(option('logo', 'docs/images/readme/relaytv-banner.png')); +const TOKEN = process.env.HA_TOKEN; +const REFRESH = process.env.HA_REFRESH || ''; + +if (!TOKEN) { + process.stderr.write('HA_TOKEN environment variable is required.\n'); + process.exit(2); +} +fs.mkdirSync(OUTPUT, { recursive: true }); + +const tokens = { + hassUrl: HA, + clientId: `${HA}/`, + access_token: TOKEN, + refresh_token: REFRESH, + token_type: 'Bearer', + expires_in: 1800, + ha_auth_provider: 'homeassistant', + expires: Date.now() + 1800 * 1000, +}; + +const uri = (buffer) => `data:image/png;base64,${buffer.toString('base64')}`; + +async function newContext(browser, viewport) { + const ctx = await browser.newContext({ viewport, colorScheme: 'dark', deviceScaleFactor: 2 }); + await ctx.addInitScript((t) => { + try { localStorage.setItem('hassTokens', JSON.stringify(t)); } catch (_e) {} + try { localStorage.setItem('selectedTheme', JSON.stringify({ dark: true })); } catch (_e) {} + }, tokens); + return ctx; +} + +async function waitApp(page) { + await page.waitForFunction(() => !!document.querySelector('home-assistant'), { timeout: 20000 }); + await page.waitForTimeout(2500); +} + +async function captureMoreInfo(browser) { + const ctx = await newContext(browser, { width: 460, height: 940 }); + const page = await ctx.newPage(); + await page.goto(`${HA}/lovelace/0`, { waitUntil: 'domcontentloaded' }).catch(() => {}); + await waitApp(page); + await page.evaluate((entityId) => { + document.querySelector('home-assistant').dispatchEvent( + new CustomEvent('hass-more-info', { detail: { entityId }, bubbles: true, composed: true })); + }, ENTITY); + await page.waitForTimeout(2500); + const buffer = await page.screenshot({ type: 'png' }); + await ctx.close(); + return buffer; +} + +async function captureDesktop(browser, url, settle = 1500) { + const ctx = await newContext(browser, { width: 1500, height: 950 }); + const page = await ctx.newPage(); + await page.goto(url, { waitUntil: 'domcontentloaded' }).catch(() => {}); + await waitApp(page); + await page.waitForTimeout(settle); + const buffer = await page.screenshot({ type: 'png' }); + await ctx.close(); + return buffer; +} + +const FONT = "Inter,ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif"; +const BG = ` + radial-gradient(circle at 78% 14%,rgba(65,189,245,.26),transparent 30%), + radial-gradient(circle at 16% 88%,rgba(56,189,248,.22),transparent 36%), + linear-gradient(150deg,#050b16,#08182b 55%,#060f1d)`; +const GRID = 'linear-gradient(rgba(255,255,255,.12) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.12) 1px,transparent 1px)'; + +const CSS = ` + *{box-sizing:border-box;margin:0} html,body{width:100%;height:100%;overflow:hidden} + body{position:relative;font-family:${FONT};color:#eef6ff;background:${BG}} + .grid{position:absolute;inset:0;opacity:.11;background-image:${GRID};background-size:54px 54px} + .flare{position:absolute;border-radius:50%;filter:blur(80px)} + .win{position:relative;border-radius:16px;overflow:hidden;background:#0b1420; + border:1px solid rgba(150,200,255,.22);box-shadow:0 44px 96px rgba(0,0,0,.55),0 0 60px rgba(45,150,235,.12)} + .bar{display:flex;align-items:center;gap:9px;height:44px;padding:0 16px;background:#111c2b;border-bottom:1px solid rgba(150,200,255,.14)} + .dot{width:13px;height:13px;border-radius:50%}.dot.r{background:#ff5f57}.dot.y{background:#febc2e}.dot.g{background:#28c840} + .addr{flex:1;margin-left:12px;height:26px;border-radius:8px;background:#0a1420;border:1px solid rgba(150,200,255,.16); + color:#8fb2d6;font-size:13px;font-weight:600;display:flex;align-items:center;padding:0 14px} + .winimg{display:block;width:100%} + .phone{position:relative;padding:11px;border-radius:46px;background:#02060d; + border:1px solid rgba(255,255,255,.26);box-shadow:0 40px 82px rgba(0,0,0,.62),0 0 0 5px rgba(65,189,245,.10)} + .phoneimg{display:block;width:100%;border-radius:36px} + .island{position:absolute;z-index:3;top:22px;left:50%;width:104px;height:26px;transform:translateX(-50%);border-radius:16px;background:#02060d} + .brand{position:absolute;z-index:8;display:flex;align-items:center;gap:16px} + .brand img{height:60px;filter:drop-shadow(0 10px 22px rgba(0,0,0,.4))} + .hapill{display:inline-flex;align-items:center;gap:9px;padding:9px 16px;border-radius:999px; + background:rgba(65,189,245,.12);border:1px solid rgba(65,189,245,.4);color:#bfe8ff;font-size:15px;font-weight:700} + .hadot{width:11px;height:11px;border-radius:50%;background:#41BDF5;box-shadow:0 0 12px #41BDF5} + .tagline{position:absolute;z-index:8;font-weight:760;letter-spacing:-.025em} + .pills{display:flex;gap:11px;margin-top:16px} + .pill{padding:9px 15px;border:1px solid rgba(131,213,255,.26);border-radius:999px;background:rgba(9,24,44,.72); + color:#cdeeff;font-size:13px;font-weight:740;letter-spacing:.09em;text-transform:uppercase} + .eyebrow{color:#65ddff;font-size:15px;font-weight:800;letter-spacing:.16em;text-transform:uppercase} + .label{margin-top:9px;font-size:30px;font-weight:780;letter-spacing:-.03em} + .sub{margin-top:12px;font-size:17px;line-height:1.5;color:#a9c6e0;max-width:88%} + .copy{position:absolute;z-index:8} +`; + +function browserFrame(img, url, w) { + return `
+
+
${url}
+
`; +} + +function phoneFrame(img, w) { + return `
+
`; +} + +function heroHtml(panel, moreinfo, logo) { + return ` +
+
RelayTVWorks with Home Assistant
+
${browserFrame(panel, 'homeassistant.local:8123 / relaytv', 1000)}
+
${phoneFrame(moreinfo, 316)}
+
Your TV. Native in Home Assistant. +
ControlAutomateEmbed
+ `; +} + +function cardHtml({ img, kind, w, eyebrow, label, sub }) { + const frame = kind === 'phone' ? phoneFrame(img, w) : browserFrame(img, 'homeassistant.local:8123', w); + const place = kind === 'phone' + ? '.frame{position:absolute;z-index:4;right:64px;top:50%;transform:translateY(-50%)}' + : '.frame{position:absolute;z-index:4;right:56px;top:50%;transform:translateY(-50%)}'; + return ` +
+
${eyebrow}
${label}
${sub}
+
Home Assistant
+
${frame}
+ `; +} + +async function render(browser, html, out, viewport) { + const ctx = await browser.newContext({ viewport, deviceScaleFactor: 1 }); + const page = await ctx.newPage(); + await page.setContent(html, { waitUntil: 'load' }); + await page.waitForTimeout(400); + await page.screenshot({ path: out, type: 'png' }); + await ctx.close(); +} + +async function main() { + const browser = await chromium.connect(WS); + try { + const moreinfo = uri(await captureMoreInfo(browser)); + const device = uri(await captureDesktop(browser, `${HA}/config/devices/device/${DEVICE_ID}`)); + const panel = uri(await captureDesktop(browser, `${HA}/${PANEL}`, 4000)); + const logo = uri(fs.readFileSync(LOGO)); + + await render(browser, heroHtml(panel, moreinfo, logo), path.join(OUTPUT, 'hero.png'), { width: 1600, height: 1000 }); + await render(browser, cardHtml({ + img: moreinfo, kind: 'phone', w: 360, + eyebrow: 'Control', label: 'Native media controls', + sub: 'Play, pause, seek, set volume, and mute from any Home Assistant dashboard, the mobile app, or a voice assistant — with live artwork and progress.', + }), path.join(OUTPUT, 'control-phone.png'), { width: 1500, height: 760 }); + await render(browser, cardHtml({ + img: device, kind: 'browser', w: 900, + eyebrow: 'Automate', label: 'A first-class Home Assistant device', + sub: 'RelayTV registers as a real device with a media_player entity and RelayTV-specific actions, ready for automations, scripts, and dashboards.', + }), path.join(OUTPUT, 'device-desktop.png'), { width: 1600, height: 820 }); + await render(browser, cardHtml({ + img: panel, kind: 'browser', w: 900, + eyebrow: 'Embed', label: 'Your RelayTV remote in the sidebar', + sub: 'Pin the full RelayTV web UI as a Home Assistant panel so the remote, queue, and Jellyfin/Emby library live one click away.', + }), path.join(OUTPUT, 'panel-desktop.png'), { width: 1600, height: 820 }); + + process.stdout.write(`${JSON.stringify({ + ok: true, ha: HA, output: OUTPUT, + files: ['hero.png', 'control-phone.png', 'device-desktop.png', 'panel-desktop.png'], + }, null, 2)}\n`); + } finally { + await browser.close(); + } +} + +main().catch((error) => { + process.stderr.write(`README screenshot generation failed: ${error.stack || error}\n`); + process.exit(1); +});