Add IR remote control (LIRC) with a number-key content mapping
Adds a TCP client for lircd's classic protocol: play/pause/next/prev/ volume/mute map to the same intents every other front-end already emits, and number keys 0-9 play an assigned album/audiobook from the start or a podcast show's newest episode, resolved fresh on every press. The mapping is configured in config.yml and editable from the frontend: a small "Taste zuweisen" button on the play screen (or the A+digit keyboard shortcut) opens a 10-key picker to assign whatever is currently playing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
/** Every call the UI makes. Commands are fire-and-forget: the websocket reports back. */
|
||||
|
||||
import type { Album, HaConfig, HaEntityState, PlayerState, Settings, TrackDetail } from "./types";
|
||||
import type {
|
||||
Album,
|
||||
HaConfig,
|
||||
HaEntityState,
|
||||
LircConfig,
|
||||
PlayerState,
|
||||
RemoteMapping,
|
||||
RemoteSlotInput,
|
||||
Settings,
|
||||
TrackDetail,
|
||||
} from "./types";
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`/api${path}`, {
|
||||
@@ -29,6 +39,15 @@ async function fetchHaConfig(): Promise<HaConfig | null> {
|
||||
return (await response.json()) as HaConfig;
|
||||
}
|
||||
|
||||
/** `null` means the IR remote isn't configured, not an error - same convention as
|
||||
* `fetchHaConfig`. */
|
||||
async function fetchLircConfig(): Promise<LircConfig | null> {
|
||||
const response = await fetch("/api/lirc");
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) throw new Error(`GET /lirc failed: ${response.status}`);
|
||||
return (await response.json()) as LircConfig;
|
||||
}
|
||||
|
||||
/** `null` covers both "unknown to Home Assistant" and "Home Assistant unreachable
|
||||
* right now" (the backend answers the latter with a 502) - the room page treats a
|
||||
* device with no state the same way either way, rather than crashing on a poll. */
|
||||
@@ -80,6 +99,11 @@ export const api = {
|
||||
post(`/ha/services/${domain}/${service}`, body),
|
||||
|
||||
trackDetail: fetchTrackDetail,
|
||||
|
||||
lircConfig: fetchLircConfig,
|
||||
remoteMapping: () => request<RemoteMapping>("/remote/mapping"),
|
||||
saveRemoteMapping: (slots: Record<string, RemoteSlotInput>) =>
|
||||
request<RemoteMapping>("/remote/mapping", { method: "PUT", body: JSON.stringify({ slots }) }),
|
||||
};
|
||||
|
||||
export const coverUrl = (albumId: string) => `/api/albums/${albumId}/cover`;
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface PlayerState {
|
||||
/** Percent, 0..100. The device's configured range never leaves the backend. */
|
||||
volume: number;
|
||||
active_figure: string | null;
|
||||
connected: { firmware: boolean; mqtt: boolean };
|
||||
connected: { firmware: boolean; mqtt: boolean; lirc: boolean };
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
@@ -107,3 +107,31 @@ export interface HaEntityState {
|
||||
state: string;
|
||||
attributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type RemoteTargetKind = "album" | "series";
|
||||
|
||||
/** One number key (0-9) on the IR remote. "album": `target` is an `Album.id`, always
|
||||
* started from track 0. "series": `target` is a podcast show name (`Album.series`),
|
||||
* resolved to that show's newest episode fresh on every press. `resolved_album_id` is
|
||||
* what it plays *right now* - `null` when the target no longer resolves (a moved
|
||||
* album, an unknown show). Mirrors `RemoteSlotOut`. */
|
||||
export interface RemoteSlot {
|
||||
digit: string;
|
||||
target_kind: RemoteTargetKind;
|
||||
target: string;
|
||||
resolved_album_id: string | null;
|
||||
}
|
||||
|
||||
export interface RemoteMapping {
|
||||
slots: RemoteSlot[];
|
||||
}
|
||||
|
||||
export interface RemoteSlotInput {
|
||||
target_kind: RemoteTargetKind;
|
||||
target: string;
|
||||
}
|
||||
|
||||
/** Presence-only, like `HaConfig` - there is nothing secret in a host/port. */
|
||||
export interface LircConfig {
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user