Add native local network integrations

This commit is contained in:
2026-05-05 18:45:46 +00:00
parent 282283d344
commit cfab8c593e
70 changed files with 9688 additions and 176 deletions
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,331 @@
import {
androidtvRemoteApiPort,
androidtvRemoteKeyAliases,
androidtvRemoteKnownApps,
androidtvRemotePairPort,
} from './androidtv_remote.constants.js';
import type {
IAndroidtvRemoteApp,
IAndroidtvRemoteCommand,
IAndroidtvRemoteCommandContext,
IAndroidtvRemoteConfig,
IAndroidtvRemoteConfiguredApp,
IAndroidtvRemoteDeviceInfo,
IAndroidtvRemoteDeviceState,
IAndroidtvRemoteKeyPress,
IAndroidtvRemoteSnapshot,
IAndroidtvRemoteVolumeInfo,
TAndroidtvRemoteCommandDirection,
TAndroidtvRemoteCommandExecutor,
TAndroidtvRemoteCommandReason,
TAndroidtvRemoteKeyCode,
} from './androidtv_remote.types.js';
export class AndroidtvRemoteUnsupportedProtocolError extends Error {
constructor(commandArg: IAndroidtvRemoteCommand) {
super(`Android TV Remote protocol action "${commandArg.action}" requires an injected executor. This TypeScript port does not implement pairing or live androidtvremote2 transport.`);
this.name = 'AndroidtvRemoteUnsupportedProtocolError';
}
}
export class AndroidtvRemoteClient {
private readonly snapshot?: IAndroidtvRemoteSnapshot;
constructor(private readonly config: IAndroidtvRemoteConfig) {
this.snapshot = config.snapshot ? this.cloneSnapshot(config.snapshot) : undefined;
}
public async getSnapshot(): Promise<IAndroidtvRemoteSnapshot> {
return this.normalizeSnapshot(this.snapshot || this.snapshotFromManualConfig());
}
public async connect(): Promise<void> {
await this.execute({ action: 'connect', reason: 'connect' });
}
public async startPairing(): Promise<void> {
await this.execute({ action: 'start_pairing', reason: 'start_pairing' });
}
public async finishPairing(pinArg: string): Promise<void> {
await this.execute({ action: 'finish_pairing', reason: 'finish_pairing', pin: pinArg });
}
public async turnOn(): Promise<void> {
await this.sendKeyCommand('POWER', 'SHORT', 'turn_on');
}
public async turnOff(): Promise<void> {
await this.sendKeyCommand('POWER', 'SHORT', 'turn_off');
}
public async volumeUp(): Promise<void> {
await this.sendKeyCommand('VOLUME_UP', 'SHORT', 'volume_up');
}
public async volumeDown(): Promise<void> {
await this.sendKeyCommand('VOLUME_DOWN', 'SHORT', 'volume_down');
}
public async muteVolume(mutedArg: boolean): Promise<void> {
await this.sendKeyCommand('VOLUME_MUTE', 'SHORT', 'volume_mute', { muted: mutedArg });
}
public async setVolumeLevel(volumeLevelArg: number): Promise<void> {
const volumeLevel = Math.max(0, Math.min(1, volumeLevelArg));
await this.execute({ action: 'volume_set', reason: 'volume_set', volumeLevel });
}
public async mediaPlay(): Promise<void> {
await this.sendKeyCommand('MEDIA_PLAY', 'SHORT', 'media_play');
}
public async mediaPause(): Promise<void> {
await this.sendKeyCommand('MEDIA_PAUSE', 'SHORT', 'media_pause');
}
public async mediaPlayPause(): Promise<void> {
await this.sendKeyCommand('MEDIA_PLAY_PAUSE', 'SHORT', 'media_play_pause');
}
public async mediaStop(): Promise<void> {
await this.sendKeyCommand('MEDIA_STOP', 'SHORT', 'media_stop');
}
public async mediaPreviousTrack(): Promise<void> {
await this.sendKeyCommand('MEDIA_PREVIOUS', 'SHORT', 'media_previous_track');
}
public async mediaNextTrack(): Promise<void> {
await this.sendKeyCommand('MEDIA_NEXT', 'SHORT', 'media_next_track');
}
public async playChannel(channelArg: string): Promise<void> {
if (!/^\d+$/.test(channelArg)) {
throw new Error(`Android TV Remote channel media_id must be numeric: ${channelArg}`);
}
await this.sendCommand(channelArg.split(''), { reason: 'play_channel' });
}
public async launchApp(appLinkOrAppIdArg: string, reasonArg: TAndroidtvRemoteCommandReason = 'launch_app'): Promise<void> {
const app = await this.appForActivity(appLinkOrAppIdArg);
const appId = app?.id || (this.hasUrlScheme(appLinkOrAppIdArg) ? undefined : appLinkOrAppIdArg);
const appLink = app?.link || (this.hasUrlScheme(appLinkOrAppIdArg) ? appLinkOrAppIdArg : `market://launch?id=${appLinkOrAppIdArg}`);
await this.execute({
action: 'launch_app',
reason: reasonArg,
appId,
appLink,
appName: app?.name || (appId ? androidtvRemoteKnownApps[appId] : undefined),
});
}
public async selectActivity(activityArg: string): Promise<void> {
const app = await this.appForActivity(activityArg);
await this.launchApp(app?.id || activityArg, 'select_activity');
}
public async sendText(textArg: string): Promise<void> {
await this.execute({ action: 'send_text', reason: 'send_text', text: textArg });
}
public async sendKeyCommand(
keyCodeArg: TAndroidtvRemoteKeyCode | string,
directionArg: TAndroidtvRemoteCommandDirection = 'SHORT',
reasonArg: TAndroidtvRemoteCommandReason = 'remote_send_command',
extraArg: Partial<IAndroidtvRemoteCommand> = {}
): Promise<void> {
await this.execute({
action: 'key_command',
reason: reasonArg,
keyCode: this.normalizeKeyCode(keyCodeArg),
direction: directionArg,
...extraArg,
});
}
public async sendCommand(
commandsArg: Array<TAndroidtvRemoteKeyCode | string>,
optionsArg: {
repeats?: number;
delaySecs?: number;
holdSecs?: number;
reason?: TAndroidtvRemoteCommandReason;
} = {}
): Promise<void> {
const keys: IAndroidtvRemoteKeyPress[] = commandsArg.flatMap((keyArg): IAndroidtvRemoteKeyPress[] => {
const keyCode = this.normalizeKeyCode(keyArg);
if (optionsArg.holdSecs) {
return [
{ keyCode, direction: 'START_LONG' },
{ keyCode, direction: 'END_LONG' },
];
}
return [{ keyCode, direction: 'SHORT' }];
});
await this.execute({
action: 'remote_send_command',
reason: optionsArg.reason || 'remote_send_command',
keys,
repeats: this.repeats(optionsArg.repeats),
delaySecs: optionsArg.delaySecs,
holdSecs: optionsArg.holdSecs,
});
}
public async destroy(): Promise<void> {}
private async execute(commandArg: IAndroidtvRemoteCommand): Promise<void> {
const executor = this.config.executor;
if (!executor) {
throw new AndroidtvRemoteUnsupportedProtocolError(commandArg);
}
const context: IAndroidtvRemoteCommandContext = {
config: this.config,
snapshot: await this.getSnapshot(),
};
if (typeof executor === 'function') {
await executor(commandArg, context);
return;
}
await executor.execute(commandArg, context);
}
private snapshotFromManualConfig(): IAndroidtvRemoteSnapshot {
const deviceInfo: IAndroidtvRemoteDeviceInfo = {
...this.config.deviceInfo,
host: this.config.deviceInfo?.host || this.config.host,
apiPort: this.config.deviceInfo?.apiPort || this.config.apiPort || androidtvRemoteApiPort,
pairPort: this.config.deviceInfo?.pairPort || this.config.pairPort || androidtvRemotePairPort,
name: this.config.deviceInfo?.name || this.config.deviceName || this.config.host || 'Android TV Remote',
macAddress: this.config.deviceInfo?.macAddress || this.config.macAddress,
manufacturer: this.config.deviceInfo?.manufacturer || this.config.manufacturer,
model: this.config.deviceInfo?.model || this.config.model,
};
const state: IAndroidtvRemoteDeviceState = {
mediaState: 'unknown',
available: false,
...this.config.state,
volumeInfo: this.config.state?.volumeInfo || this.config.volumeInfo,
};
return {
deviceInfo,
state,
apps: this.normalizeApps(this.config.apps),
};
}
private normalizeSnapshot(snapshotArg: IAndroidtvRemoteSnapshot): IAndroidtvRemoteSnapshot {
const deviceInfo: IAndroidtvRemoteDeviceInfo = {
...snapshotArg.deviceInfo,
host: snapshotArg.deviceInfo.host || this.config.host,
apiPort: snapshotArg.deviceInfo.apiPort || this.config.apiPort || androidtvRemoteApiPort,
pairPort: snapshotArg.deviceInfo.pairPort || this.config.pairPort || androidtvRemotePairPort,
macAddress: snapshotArg.deviceInfo.macAddress || this.config.macAddress,
};
if (!deviceInfo.name) {
deviceInfo.name = this.config.deviceName || deviceInfo.host || 'Android TV Remote';
}
const apps = this.normalizeApps(snapshotArg.apps.length ? snapshotArg.apps : this.config.apps);
const volumeInfo = this.normalizeVolumeInfo(snapshotArg.state.volumeInfo || this.config.volumeInfo);
const state: IAndroidtvRemoteDeviceState = {
...snapshotArg.state,
volumeInfo,
};
if (state.available === undefined) {
state.available = state.isOn !== undefined || Boolean(state.currentApp || volumeInfo);
}
if (!state.currentAppName && state.currentApp) {
state.currentAppName = this.appName(apps, state.currentApp);
}
if (!state.currentActivity) {
state.currentActivity = state.currentAppName || state.currentApp;
}
if (state.isVolumeMuted === undefined && volumeInfo?.muted !== undefined) {
state.isVolumeMuted = volumeInfo.muted;
}
if (state.volumeLevel === undefined) {
state.volumeLevel = this.volumeLevel(volumeInfo);
}
return {
deviceInfo,
state,
apps,
updatedAt: snapshotArg.updatedAt,
};
}
private normalizeApps(appsArg: IAndroidtvRemoteConfig['apps']): IAndroidtvRemoteApp[] {
if (!appsArg) {
return [];
}
if (Array.isArray(appsArg)) {
return appsArg.map((appArg) => ({
...appArg,
name: appArg.name || androidtvRemoteKnownApps[appArg.id],
}));
}
return Object.entries(appsArg).map(([id, appArg]) => this.normalizeConfiguredApp(id, appArg));
}
private normalizeConfiguredApp(idArg: string, appArg: IAndroidtvRemoteConfiguredApp): IAndroidtvRemoteApp {
return {
id: idArg,
name: appArg.name || appArg.appName || appArg.app_name || androidtvRemoteKnownApps[idArg],
icon: appArg.icon || appArg.appIcon || appArg.app_icon,
link: appArg.link,
};
}
private normalizeVolumeInfo(volumeInfoArg?: IAndroidtvRemoteVolumeInfo): IAndroidtvRemoteVolumeInfo | undefined {
return volumeInfoArg ? { ...volumeInfoArg } : undefined;
}
private volumeLevel(volumeInfoArg?: IAndroidtvRemoteVolumeInfo): number | undefined {
if (!volumeInfoArg) {
return undefined;
}
if (typeof volumeInfoArg.max === 'number' && volumeInfoArg.max > 0 && typeof volumeInfoArg.level === 'number') {
return volumeInfoArg.level / volumeInfoArg.max;
}
return undefined;
}
private async appForActivity(activityArg: string): Promise<IAndroidtvRemoteApp | undefined> {
const snapshot = await this.getSnapshot();
return snapshot.apps.find((appArg) => activityArg === appArg.id || activityArg === appArg.name || activityArg === appArg.link);
}
private appName(appsArg: IAndroidtvRemoteApp[], appIdArg: string): string | undefined {
return appsArg.find((appArg) => appArg.id === appIdArg)?.name || androidtvRemoteKnownApps[appIdArg];
}
private normalizeKeyCode(keyCodeArg: TAndroidtvRemoteKeyCode | string): TAndroidtvRemoteKeyCode | string {
const raw = String(keyCodeArg).trim();
if (!raw) {
return raw;
}
const withoutPrefix = raw.toUpperCase().replace(/^KEYCODE_/, '').replace(/[\s-]+/g, '_');
return androidtvRemoteKeyAliases[withoutPrefix] || withoutPrefix;
}
private repeats(repeatsArg?: number): number {
return typeof repeatsArg === 'number' && Number.isFinite(repeatsArg) ? Math.max(1, Math.floor(repeatsArg)) : 1;
}
private hasUrlScheme(valueArg: string): boolean {
return /^[a-z][a-z0-9+.-]*:/i.test(valueArg);
}
private cloneSnapshot(snapshotArg: IAndroidtvRemoteSnapshot): IAndroidtvRemoteSnapshot {
return {
deviceInfo: { ...snapshotArg.deviceInfo },
state: {
...snapshotArg.state,
volumeInfo: snapshotArg.state.volumeInfo ? { ...snapshotArg.state.volumeInfo } : undefined,
},
apps: snapshotArg.apps.map((appArg) => ({ ...appArg })),
updatedAt: snapshotArg.updatedAt,
};
}
}
@@ -0,0 +1,64 @@
import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
import { androidtvRemoteApiPort, androidtvRemotePairPort } from './androidtv_remote.constants.js';
import type { IAndroidtvRemoteConfig } from './androidtv_remote.types.js';
export class AndroidtvRemoteConfigFlow implements IConfigFlow<IAndroidtvRemoteConfig> {
public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IAndroidtvRemoteConfig>> {
void contextArg;
return {
kind: 'form',
title: 'Connect Android TV Remote',
description: 'Configure an Android TV Remote protocol v2 host. Pairing and live protocol transport require an injected executor.',
fields: [
{ name: 'host', label: 'Host', type: 'text', required: true },
{ name: 'apiPort', label: 'API port', type: 'number' },
{ name: 'pairPort', label: 'Pairing port', type: 'number' },
{ name: 'deviceName', label: 'Device name', type: 'text' },
{ name: 'macAddress', label: 'MAC address', type: 'text' },
{ name: 'enableIme', label: 'Enable IME updates', type: 'boolean' },
],
submit: async (valuesArg) => {
const host = this.stringValue(valuesArg.host) || candidateArg.host;
if (!host) {
return { kind: 'error', title: 'Android TV Remote configuration failed', error: 'Host is required.' };
}
const apiPort = this.numberValue(valuesArg.apiPort) || this.numberValue(candidateArg.metadata?.apiPort) || candidateArg.port || androidtvRemoteApiPort;
const pairPort = this.numberValue(valuesArg.pairPort) || this.numberValue(candidateArg.metadata?.pairPort) || androidtvRemotePairPort;
const deviceName = this.stringValue(valuesArg.deviceName) || candidateArg.name;
const macAddress = this.stringValue(valuesArg.macAddress) || candidateArg.macAddress || this.stringValue(candidateArg.metadata?.macAddress);
return {
kind: 'done',
title: 'Android TV Remote configured',
config: {
host,
apiPort,
pairPort,
deviceName,
macAddress,
manufacturer: candidateArg.manufacturer,
model: candidateArg.model,
enableIme: typeof valuesArg.enableIme === 'boolean' ? valuesArg.enableIme : true,
deviceInfo: {
id: candidateArg.id || macAddress,
name: deviceName,
host,
apiPort,
pairPort,
macAddress,
manufacturer: candidateArg.manufacturer,
model: candidateArg.model,
},
},
};
},
};
}
private stringValue(valueArg: unknown): string | undefined {
return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
}
private numberValue(valueArg: unknown): number | undefined {
return typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg > 0 ? valueArg : undefined;
}
}
@@ -1,28 +1,236 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import * as plugins from '../../plugins.js';
import { BaseIntegration } from '../../core/classes.baseintegration.js';
import type { IIntegrationEntity, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
import { AndroidtvRemoteClient } from './androidtv_remote.classes.client.js';
import { AndroidtvRemoteConfigFlow } from './androidtv_remote.classes.configflow.js';
import { createAndroidtvRemoteDiscoveryDescriptor } from './androidtv_remote.discovery.js';
import { AndroidtvRemoteMapper } from './androidtv_remote.mapper.js';
import type { IAndroidtvRemoteConfig } from './androidtv_remote.types.js';
export class HomeAssistantAndroidtvRemoteIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "androidtv_remote",
displayName: "Android TV Remote",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/androidtv_remote",
"upstreamDomain": "androidtv_remote",
"integrationType": "device",
"iotClass": "local_push",
"qualityScale": "platinum",
"requirements": [
"androidtvremote2==0.3.1"
],
"dependencies": [],
"afterDependencies": [],
"codeowners": [
"@tronikos",
"@Drafteed"
]
},
export class AndroidtvRemoteIntegration extends BaseIntegration<IAndroidtvRemoteConfig> {
public readonly domain = 'androidtv_remote';
public readonly displayName = 'Android TV Remote';
public readonly status = 'control-runtime' as const;
public readonly discoveryDescriptor = createAndroidtvRemoteDiscoveryDescriptor();
public readonly configFlow = new AndroidtvRemoteConfigFlow();
public readonly metadata = {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/androidtv_remote',
upstreamDomain: 'androidtv_remote',
integrationType: 'device',
iotClass: 'local_push',
qualityScale: 'platinum',
requirements: ['androidtvremote2==0.3.1'],
dependencies: [],
afterDependencies: [],
codeowners: ['@tronikos', '@Drafteed'],
configFlow: true,
documentation: 'https://www.home-assistant.io/integrations/androidtv_remote',
};
public async setup(configArg: IAndroidtvRemoteConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
void contextArg;
return new AndroidtvRemoteRuntime(new AndroidtvRemoteClient(configArg));
}
public async destroy(): Promise<void> {}
}
export class HomeAssistantAndroidtvRemoteIntegration extends AndroidtvRemoteIntegration {}
class AndroidtvRemoteRuntime implements IIntegrationRuntime {
public domain = 'androidtv_remote';
constructor(private readonly client: AndroidtvRemoteClient) {}
public async devices(): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
return AndroidtvRemoteMapper.toDevices(await this.client.getSnapshot());
}
public async entities(): Promise<IIntegrationEntity[]> {
return AndroidtvRemoteMapper.toEntities(await this.client.getSnapshot());
}
public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
try {
if (requestArg.domain === 'remote') {
return await this.callRemoteService(requestArg);
}
if (requestArg.domain === 'androidtv_remote') {
return await this.callAndroidtvRemoteService(requestArg);
}
if (requestArg.domain !== 'media_player') {
return { success: false, error: `Unsupported Android TV Remote service domain: ${requestArg.domain}` };
}
return await this.callMediaPlayerService(requestArg);
} catch (errorArg) {
return { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg) };
}
}
public async destroy(): Promise<void> {
await this.client.destroy();
}
private async callRemoteService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service === 'turn_on') {
await this.client.turnOn();
const activity = this.stringValue(requestArg.data?.activity);
if (activity) {
await this.client.selectActivity(activity);
}
return { success: true };
}
if (requestArg.service === 'turn_off') {
await this.client.turnOff();
return { success: true };
}
if (requestArg.service !== 'send_command') {
return { success: false, error: `Unsupported Android TV Remote remote service: ${requestArg.service}` };
}
const command = requestArg.data?.command;
const commands = typeof command === 'string' ? [command] : Array.isArray(command) ? command.filter((itemArg): itemArg is string => typeof itemArg === 'string' && Boolean(itemArg)) : [];
if (!commands.length) {
return { success: false, error: 'Android TV Remote remote.send_command requires data.command.' };
}
await this.client.sendCommand(commands, {
repeats: this.numberValue(requestArg.data?.num_repeats ?? requestArg.data?.numRepeats),
delaySecs: this.numberValue(requestArg.data?.delay_secs ?? requestArg.data?.delaySecs),
holdSecs: this.numberValue(requestArg.data?.hold_secs ?? requestArg.data?.holdSecs),
reason: 'remote_send_command',
});
return { success: true };
}
private async callAndroidtvRemoteService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service === 'connect') {
await this.client.connect();
return { success: true };
}
if (requestArg.service === 'start_pairing') {
await this.client.startPairing();
return { success: true };
}
if (requestArg.service === 'finish_pairing') {
const pin = this.stringValue(requestArg.data?.pin);
if (!pin) {
return { success: false, error: 'Android TV Remote finish_pairing requires data.pin.' };
}
await this.client.finishPairing(pin);
return { success: true };
}
if (requestArg.service === 'launch_app') {
const app = this.stringValue(requestArg.data?.app_id ?? requestArg.data?.appId ?? requestArg.data?.app_link ?? requestArg.data?.appLink);
if (!app) {
return { success: false, error: 'Android TV Remote launch_app requires data.app_id or data.app_link.' };
}
await this.client.launchApp(app);
return { success: true };
}
if (requestArg.service === 'send_text') {
const text = this.stringValue(requestArg.data?.text);
if (!text) {
return { success: false, error: 'Android TV Remote send_text requires data.text.' };
}
await this.client.sendText(text);
return { success: true };
}
return { success: false, error: `Unsupported Android TV Remote service: ${requestArg.service}` };
}
private async callMediaPlayerService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service === 'turn_on') {
await this.client.turnOn();
return { success: true };
}
if (requestArg.service === 'turn_off') {
await this.client.turnOff();
return { success: true };
}
if (requestArg.service === 'play' || requestArg.service === 'media_play') {
await this.client.mediaPlay();
return { success: true };
}
if (requestArg.service === 'pause' || requestArg.service === 'media_pause') {
await this.client.mediaPause();
return { success: true };
}
if (requestArg.service === 'play_pause' || requestArg.service === 'media_play_pause') {
await this.client.mediaPlayPause();
return { success: true };
}
if (requestArg.service === 'stop' || requestArg.service === 'media_stop') {
await this.client.mediaStop();
return { success: true };
}
if (requestArg.service === 'next_track' || requestArg.service === 'media_next_track') {
await this.client.mediaNextTrack();
return { success: true };
}
if (requestArg.service === 'previous_track' || requestArg.service === 'media_previous_track') {
await this.client.mediaPreviousTrack();
return { success: true };
}
if (requestArg.service === 'volume_up') {
await this.client.volumeUp();
return { success: true };
}
if (requestArg.service === 'volume_down') {
await this.client.volumeDown();
return { success: true };
}
if (requestArg.service === 'volume_mute' || requestArg.service === 'mute') {
const muted = requestArg.data?.is_volume_muted ?? requestArg.data?.muted;
if (typeof muted !== 'boolean') {
return { success: false, error: 'Android TV Remote volume_mute requires data.is_volume_muted.' };
}
await this.client.muteVolume(muted);
return { success: true };
}
if (requestArg.service === 'volume_set') {
const level = requestArg.data?.volume_level;
if (typeof level !== 'number') {
return { success: false, error: 'Android TV Remote volume_set requires data.volume_level.' };
}
await this.client.setVolumeLevel(level);
return { success: true };
}
if (requestArg.service === 'select_source') {
const source = this.stringValue(requestArg.data?.source);
if (!source) {
return { success: false, error: 'Android TV Remote select_source requires data.source.' };
}
await this.client.selectActivity(source);
return { success: true };
}
if (requestArg.service === 'play_media') {
return await this.callPlayMediaService(requestArg);
}
return { success: false, error: `Unsupported Android TV Remote media_player service: ${requestArg.service}` };
}
private async callPlayMediaService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
const mediaId = this.stringValue(requestArg.data?.media_content_id ?? requestArg.data?.media_id ?? requestArg.data?.uri);
const mediaType = this.stringValue(requestArg.data?.media_content_type ?? requestArg.data?.media_type);
if (!mediaId) {
return { success: false, error: 'Android TV Remote play_media requires data.media_content_id.' };
}
if (mediaType === 'channel') {
await this.client.playChannel(mediaId);
return { success: true };
}
if (mediaType === 'app' || mediaType === 'url') {
await this.client.launchApp(mediaId);
return { success: true };
}
return { success: false, error: `Unsupported Android TV Remote media type: ${mediaType || 'unknown'}` };
}
private stringValue(valueArg: unknown): string | undefined {
return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
}
private numberValue(valueArg: unknown): number | undefined {
return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
}
}
@@ -0,0 +1,56 @@
import type { TAndroidtvRemoteKeyCode } from './androidtv_remote.types.js';
export const androidtvRemoteApiPort = 6466;
export const androidtvRemotePairPort = 6467;
export const androidtvRemoteMdnsService = '_androidtvremote2._tcp.local.';
export const androidtvRemoteKnownApps: Record<string, string> = {
'com.android.tv.settings': 'Settings',
'com.disney.disneyplus': 'Disney+',
'com.google.android.apps.tv.launcherx': 'Google TV Launcher',
'com.google.android.tvlauncher': 'Android TV Launcher',
'com.google.android.youtube.tv': 'YouTube',
'com.google.android.youtube.tvkids': 'YouTube Kids',
'com.google.android.youtube.tvmusic': 'YouTube Music',
'com.hbo.hbonow': 'HBO Max',
'com.hulu.plus': 'Hulu',
'com.netflix.ninja': 'Netflix',
'com.plexapp.android': 'Plex',
'com.spotify.tv.android': 'Spotify',
'org.jellyfin.androidtv': 'Jellyfin',
'org.videolan.vlc': 'VLC',
'org.xbmc.kodi': 'Kodi',
'tv.twitch.android.app': 'Twitch',
};
export const androidtvRemoteKeyAliases: Record<string, TAndroidtvRemoteKeyCode> = {
BLUE: 'PROG_BLUE',
CENTER: 'DPAD_CENTER',
CH_DOWN: 'CHANNEL_DOWN',
CH_UP: 'CHANNEL_UP',
CHANNELDOWN: 'CHANNEL_DOWN',
CHANNELUP: 'CHANNEL_UP',
DOWN: 'DPAD_DOWN',
FAST_FORWARD: 'MEDIA_FAST_FORWARD',
FORWARD: 'MEDIA_FAST_FORWARD',
GREEN: 'PROG_GREEN',
INFO: 'INFO',
LEFT: 'DPAD_LEFT',
NEXT: 'MEDIA_NEXT',
PAUSE: 'MEDIA_PAUSE',
PLAY: 'MEDIA_PLAY',
PLAY_PAUSE: 'MEDIA_PLAY_PAUSE',
PREVIOUS: 'MEDIA_PREVIOUS',
RED: 'PROG_RED',
REWIND: 'MEDIA_REWIND',
RIGHT: 'DPAD_RIGHT',
SELECT: 'DPAD_CENTER',
STOP: 'MEDIA_STOP',
UP: 'DPAD_UP',
VOL_DOWN: 'VOLUME_DOWN',
VOL_UP: 'VOLUME_UP',
VOLUMEDOWN: 'VOLUME_DOWN',
VOLUMEMUTE: 'VOLUME_MUTE',
VOLUMEUP: 'VOLUME_UP',
YELLOW: 'PROG_YELLOW',
};
@@ -0,0 +1,127 @@
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { androidtvRemoteApiPort, androidtvRemoteMdnsService, androidtvRemotePairPort } from './androidtv_remote.constants.js';
import type { IAndroidtvRemoteManualEntry, IAndroidtvRemoteMdnsRecord } from './androidtv_remote.types.js';
export class AndroidtvRemoteMdnsMatcher implements IDiscoveryMatcher<IAndroidtvRemoteMdnsRecord> {
public id = 'androidtv-remote-mdns-match';
public source = 'mdns' as const;
public description = 'Recognize Android TV Remote protocol v2 mDNS advertisements.';
public async matches(recordArg: IAndroidtvRemoteMdnsRecord): Promise<IDiscoveryMatch> {
const type = this.stringValue(recordArg.type || recordArg.metadata?.type).toLowerCase();
const name = this.stringValue(recordArg.name || recordArg.metadata?.name);
const matched = type.includes('androidtvremote2') || name.toLowerCase().includes(androidtvRemoteMdnsService);
if (!matched) {
return { matched: false, confidence: 'low', reason: 'mDNS record is not an Android TV Remote advertisement.' };
}
const properties = { ...(recordArg.txt || {}), ...(recordArg.properties || {}) };
const macAddress = this.stringValue(properties.bt || properties.mac || properties.macAddress || recordArg.metadata?.macAddress);
const id = this.stringValue(properties.id || macAddress);
const displayName = this.displayName(name);
const apiPort = this.numberValue(recordArg.port) || androidtvRemoteApiPort;
return {
matched: true,
confidence: recordArg.host && macAddress ? 'certain' : recordArg.host ? 'high' : 'medium',
reason: 'mDNS record matches Android TV Remote protocol v2.',
normalizedDeviceId: id || recordArg.host,
candidate: {
source: 'mdns',
integrationDomain: 'androidtv_remote',
id,
host: recordArg.host,
port: apiPort,
name: displayName,
macAddress,
metadata: {
type: recordArg.type,
txt: properties,
apiPort,
pairPort: androidtvRemotePairPort,
},
},
};
}
private displayName(nameArg: string): string | undefined {
const name = nameArg.replace(androidtvRemoteMdnsService, '').replace(/\.$/, '').trim();
return name || undefined;
}
private stringValue(valueArg: unknown): string {
return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : '';
}
private numberValue(valueArg: unknown): number | undefined {
return typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg > 0 ? valueArg : undefined;
}
}
export class AndroidtvRemoteManualMatcher implements IDiscoveryMatcher<IAndroidtvRemoteManualEntry> {
public id = 'androidtv-remote-manual-match';
public source = 'manual' as const;
public description = 'Recognize manual Android TV Remote host entries.';
public async matches(inputArg: IAndroidtvRemoteManualEntry): Promise<IDiscoveryMatch> {
const matched = Boolean(inputArg.host || inputArg.metadata?.androidtvRemote || inputArg.metadata?.androidtv_remote);
if (!matched) {
return { matched: false, confidence: 'low', reason: 'Manual entry does not include an Android TV Remote host.' };
}
const apiPort = this.numberValue(inputArg.apiPort || inputArg.port || inputArg.metadata?.apiPort) || androidtvRemoteApiPort;
const pairPort = this.numberValue(inputArg.pairPort || inputArg.metadata?.pairPort) || androidtvRemotePairPort;
return {
matched: true,
confidence: inputArg.host ? 'high' : 'medium',
reason: 'Manual entry can start Android TV Remote setup.',
normalizedDeviceId: inputArg.id || inputArg.macAddress || inputArg.host,
candidate: {
source: 'manual',
integrationDomain: 'androidtv_remote',
id: inputArg.id || inputArg.macAddress,
host: inputArg.host,
port: apiPort,
name: inputArg.deviceName || inputArg.name,
manufacturer: inputArg.manufacturer,
model: inputArg.model,
macAddress: inputArg.macAddress,
metadata: {
...inputArg.metadata,
apiPort,
pairPort,
enableIme: inputArg.enableIme,
},
},
};
}
private numberValue(valueArg: unknown): number | undefined {
return typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg > 0 ? valueArg : undefined;
}
}
export class AndroidtvRemoteCandidateValidator implements IDiscoveryValidator {
public id = 'androidtv-remote-candidate-validator';
public description = 'Validate Android TV Remote candidates.';
public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
const matched = candidateArg.integrationDomain === 'androidtv_remote' || Boolean(candidateArg.metadata?.androidtvRemote || candidateArg.metadata?.androidtv_remote);
return {
matched,
confidence: matched && candidateArg.host && (candidateArg.id || candidateArg.macAddress) ? 'certain' : matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
reason: matched ? 'Candidate has Android TV Remote metadata.' : 'Candidate is not Android TV Remote.',
candidate: matched ? candidateArg : undefined,
normalizedDeviceId: candidateArg.id || candidateArg.macAddress || candidateArg.host,
metadata: matched ? {
apiPort: candidateArg.port || candidateArg.metadata?.apiPort || androidtvRemoteApiPort,
pairPort: candidateArg.metadata?.pairPort || androidtvRemotePairPort,
} : undefined,
};
}
}
export const createAndroidtvRemoteDiscoveryDescriptor = (): DiscoveryDescriptor => {
return new DiscoveryDescriptor({ integrationDomain: 'androidtv_remote', displayName: 'Android TV Remote' })
.addMatcher(new AndroidtvRemoteMdnsMatcher())
.addMatcher(new AndroidtvRemoteManualMatcher())
.addValidator(new AndroidtvRemoteCandidateValidator());
};
@@ -0,0 +1,165 @@
import * as plugins from '../../plugins.js';
import type { IIntegrationEntity } from '../../core/types.js';
import { androidtvRemoteKnownApps } from './androidtv_remote.constants.js';
import type { IAndroidtvRemoteApp, IAndroidtvRemoteSnapshot, IAndroidtvRemoteVolumeInfo } from './androidtv_remote.types.js';
export class AndroidtvRemoteMapper {
public static toDevices(snapshotArg: IAndroidtvRemoteSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
return [{
id: this.deviceId(snapshotArg),
integrationDomain: 'androidtv_remote',
name: this.deviceName(snapshotArg),
protocol: 'unknown',
manufacturer: snapshotArg.deviceInfo.manufacturer || 'Android',
model: snapshotArg.deviceInfo.model || 'Android TV Remote',
online: this.available(snapshotArg),
features: [
{ id: 'power', capability: 'media', name: 'Power', readable: true, writable: true },
{ id: 'media_state', capability: 'media', name: 'Media state', readable: true, writable: false },
{ id: 'activity', capability: 'media', name: 'Activity', readable: true, writable: true },
{ id: 'volume', capability: 'media', name: 'Volume', readable: true, writable: true },
{ id: 'mute', capability: 'media', name: 'Mute', readable: true, writable: true },
{ id: 'remote_key', capability: 'media', name: 'Remote key', readable: false, writable: true },
],
state: [
{ featureId: 'power', value: this.powerState(snapshotArg), updatedAt },
{ featureId: 'media_state', value: this.mediaState(snapshotArg), updatedAt },
{ featureId: 'activity', value: this.activity(snapshotArg) || null, updatedAt },
{ featureId: 'volume', value: this.volumePercent(snapshotArg), updatedAt },
{ featureId: 'mute', value: this.muted(snapshotArg), updatedAt },
],
metadata: {
host: snapshotArg.deviceInfo.host,
protocol: 'androidtvremote2',
apiPort: snapshotArg.deviceInfo.apiPort,
pairPort: snapshotArg.deviceInfo.pairPort,
macAddress: snapshotArg.deviceInfo.macAddress,
softwareVersion: snapshotArg.deviceInfo.softwareVersion,
appVersion: snapshotArg.deviceInfo.appVersion,
currentApp: snapshotArg.state.currentApp,
voiceEnabled: snapshotArg.state.voiceEnabled,
apps: snapshotArg.apps.map((appArg) => ({ id: appArg.id, name: this.appName(appArg), icon: appArg.icon, link: appArg.link })),
},
}];
}
public static toEntities(snapshotArg: IAndroidtvRemoteSnapshot): IIntegrationEntity[] {
return [{
id: `media_player.${this.slug(this.deviceName(snapshotArg))}`,
uniqueId: `androidtv_remote_${this.slug(this.stableDeviceKey(snapshotArg))}`,
integrationDomain: 'androidtv_remote',
deviceId: this.deviceId(snapshotArg),
platform: 'media_player',
name: this.deviceName(snapshotArg),
state: this.mediaState(snapshotArg),
attributes: {
appId: snapshotArg.state.currentApp,
appName: snapshotArg.state.currentAppName || (snapshotArg.state.currentApp ? this.appNameById(snapshotArg, snapshotArg.state.currentApp) : undefined),
currentActivity: this.activity(snapshotArg),
activityList: this.activityList(snapshotArg),
source: this.activity(snapshotArg),
sourceList: this.activityList(snapshotArg),
volumeLevel: this.normalizedVolumeLevel(snapshotArg),
isVolumeMuted: this.muted(snapshotArg),
assumedState: true,
voiceEnabled: snapshotArg.state.voiceEnabled,
rawState: snapshotArg.state.rawState,
},
available: this.available(snapshotArg),
}];
}
public static mediaState(snapshotArg: IAndroidtvRemoteSnapshot): string {
if (!this.available(snapshotArg)) {
return 'unavailable';
}
const rawState = String(snapshotArg.state.mediaState || snapshotArg.state.rawState || '').toLowerCase();
if (snapshotArg.state.isOn === false || snapshotArg.state.powerState === 'off' || rawState === 'off') {
return 'off';
}
if (['playing', 'paused', 'stopped', 'idle', 'buffering', 'on'].includes(rawState)) {
return rawState;
}
if (snapshotArg.state.isOn === true || snapshotArg.state.currentApp) {
return 'on';
}
return 'unknown';
}
public static powerState(snapshotArg: IAndroidtvRemoteSnapshot): string {
if (snapshotArg.state.isOn === true || snapshotArg.state.powerState === 'on') {
return 'on';
}
if (snapshotArg.state.isOn === false || snapshotArg.state.powerState === 'off' || String(snapshotArg.state.mediaState || '').toLowerCase() === 'off') {
return 'off';
}
return this.available(snapshotArg) && snapshotArg.state.currentApp ? 'on' : 'unknown';
}
private static available(snapshotArg: IAndroidtvRemoteSnapshot): boolean {
return snapshotArg.state.available !== false;
}
private static activity(snapshotArg: IAndroidtvRemoteSnapshot): string | undefined {
return snapshotArg.state.currentActivity || snapshotArg.state.currentAppName || (snapshotArg.state.currentApp ? this.appNameById(snapshotArg, snapshotArg.state.currentApp) || snapshotArg.state.currentApp : undefined);
}
private static activityList(snapshotArg: IAndroidtvRemoteSnapshot): string[] {
const activities = new Set<string>();
for (const appArg of snapshotArg.apps) {
const name = this.appName(appArg);
if (name) {
activities.add(name);
}
}
return [...activities];
}
private static appNameById(snapshotArg: IAndroidtvRemoteSnapshot, appIdArg: string): string | undefined {
return snapshotArg.apps.find((appArg) => appArg.id === appIdArg)?.name || androidtvRemoteKnownApps[appIdArg];
}
private static appName(appArg: IAndroidtvRemoteApp): string {
return appArg.name || androidtvRemoteKnownApps[appArg.id] || appArg.id;
}
private static normalizedVolumeLevel(snapshotArg: IAndroidtvRemoteSnapshot): number | undefined {
if (typeof snapshotArg.state.volumeLevel === 'number') {
return Math.max(0, Math.min(1, snapshotArg.state.volumeLevel));
}
return this.volumeLevelFromInfo(snapshotArg.state.volumeInfo);
}
private static volumePercent(snapshotArg: IAndroidtvRemoteSnapshot): number | null {
const level = this.normalizedVolumeLevel(snapshotArg);
return typeof level === 'number' ? Math.round(level * 100) : null;
}
private static volumeLevelFromInfo(volumeInfoArg?: IAndroidtvRemoteVolumeInfo): number | undefined {
if (!volumeInfoArg || typeof volumeInfoArg.level !== 'number' || typeof volumeInfoArg.max !== 'number' || volumeInfoArg.max <= 0) {
return undefined;
}
return Math.max(0, Math.min(1, volumeInfoArg.level / volumeInfoArg.max));
}
private static muted(snapshotArg: IAndroidtvRemoteSnapshot): boolean | null {
return snapshotArg.state.isVolumeMuted ?? snapshotArg.state.volumeInfo?.muted ?? null;
}
private static deviceId(snapshotArg: IAndroidtvRemoteSnapshot): string {
return `androidtv_remote.device.${this.slug(this.stableDeviceKey(snapshotArg))}`;
}
private static stableDeviceKey(snapshotArg: IAndroidtvRemoteSnapshot): string {
return snapshotArg.deviceInfo.id || snapshotArg.deviceInfo.macAddress || snapshotArg.deviceInfo.host || this.deviceName(snapshotArg);
}
private static deviceName(snapshotArg: IAndroidtvRemoteSnapshot): string {
return snapshotArg.deviceInfo.name || snapshotArg.deviceInfo.model || snapshotArg.deviceInfo.host || 'Android TV Remote';
}
private static slug(valueArg: string): string {
return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'androidtv_remote';
}
}
@@ -1,4 +1,254 @@
export interface IHomeAssistantAndroidtvRemoteConfig {
// TODO: replace with the TypeScript-native config for androidtv_remote.
[key: string]: unknown;
export type TAndroidtvRemoteMediaState =
| 'off'
| 'on'
| 'idle'
| 'playing'
| 'paused'
| 'stopped'
| 'buffering'
| 'unknown';
export type TAndroidtvRemotePowerState = 'on' | 'off' | 'unknown';
export type TAndroidtvRemoteCommandDirection = 'SHORT' | 'START_LONG' | 'END_LONG';
export type TAndroidtvRemoteKeyCode =
| '0'
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
| 'ASSIST'
| 'BACK'
| 'BUTTON_A'
| 'BUTTON_B'
| 'BUTTON_MODE'
| 'BUTTON_X'
| 'BUTTON_Y'
| 'CAPTIONS'
| 'CHANNEL_DOWN'
| 'CHANNEL_UP'
| 'DEL'
| 'DPAD_CENTER'
| 'DPAD_DOWN'
| 'DPAD_LEFT'
| 'DPAD_RIGHT'
| 'DPAD_UP'
| 'DVR'
| 'ENTER'
| 'EXPLORER'
| 'F1'
| 'F2'
| 'F3'
| 'F4'
| 'F5'
| 'F6'
| 'F7'
| 'F8'
| 'F9'
| 'F10'
| 'F11'
| 'F12'
| 'GUIDE'
| 'HOME'
| 'INFO'
| 'MEDIA_AUDIO_TRACK'
| 'MEDIA_FAST_FORWARD'
| 'MEDIA_NEXT'
| 'MEDIA_PAUSE'
| 'MEDIA_PLAY'
| 'MEDIA_PLAY_PAUSE'
| 'MEDIA_PREVIOUS'
| 'MEDIA_RECORD'
| 'MEDIA_REWIND'
| 'MEDIA_STOP'
| 'MENU'
| 'MUTE'
| 'POWER'
| 'PROG_BLUE'
| 'PROG_GREEN'
| 'PROG_RED'
| 'PROG_YELLOW'
| 'SEARCH'
| 'SETTINGS'
| 'TV'
| 'TV_TELETEXT'
| 'VOLUME_DOWN'
| 'VOLUME_MUTE'
| 'VOLUME_UP';
export type TAndroidtvRemoteCommandReason =
| 'connect'
| 'finish_pairing'
| 'launch_app'
| 'media_next_track'
| 'media_pause'
| 'media_play'
| 'media_play_pause'
| 'media_previous_track'
| 'media_stop'
| 'play_channel'
| 'remote_send_command'
| 'select_activity'
| 'send_text'
| 'start_pairing'
| 'turn_off'
| 'turn_on'
| 'volume_down'
| 'volume_mute'
| 'volume_set'
| 'volume_up';
export type TAndroidtvRemoteCommandAction =
| 'connect'
| 'finish_pairing'
| 'key_command'
| 'launch_app'
| 'remote_send_command'
| 'send_text'
| 'start_pairing'
| 'volume_set';
export interface IAndroidtvRemoteVolumeInfo {
level?: number;
max?: number;
muted?: boolean;
playerModel?: string;
}
export interface IAndroidtvRemoteDeviceInfo {
id?: string;
name?: string;
host?: string;
apiPort?: number;
pairPort?: number;
macAddress?: string;
manufacturer?: string;
model?: string;
softwareVersion?: string;
appVersion?: string;
}
export interface IAndroidtvRemoteDeviceState {
available?: boolean;
isOn?: boolean | null;
powerState?: TAndroidtvRemotePowerState;
mediaState?: TAndroidtvRemoteMediaState | string;
rawState?: string;
currentApp?: string;
currentAppName?: string;
currentActivity?: string;
volumeInfo?: IAndroidtvRemoteVolumeInfo;
volumeLevel?: number;
isVolumeMuted?: boolean;
voiceEnabled?: boolean;
}
export interface IAndroidtvRemoteApp {
id: string;
name?: string;
icon?: string;
link?: string;
}
export interface IAndroidtvRemoteConfiguredApp {
appName?: string;
appIcon?: string;
app_name?: string;
app_icon?: string;
name?: string;
icon?: string;
link?: string;
}
export interface IAndroidtvRemoteSnapshot {
deviceInfo: IAndroidtvRemoteDeviceInfo;
state: IAndroidtvRemoteDeviceState;
apps: IAndroidtvRemoteApp[];
updatedAt?: string;
}
export interface IAndroidtvRemoteKeyPress {
keyCode: TAndroidtvRemoteKeyCode | string;
direction?: TAndroidtvRemoteCommandDirection;
}
export interface IAndroidtvRemoteCommand {
action: TAndroidtvRemoteCommandAction;
reason?: TAndroidtvRemoteCommandReason;
keyCode?: TAndroidtvRemoteKeyCode | string;
direction?: TAndroidtvRemoteCommandDirection;
keys?: IAndroidtvRemoteKeyPress[];
appId?: string;
appLink?: string;
appName?: string;
text?: string;
pin?: string;
volumeLevel?: number;
muted?: boolean;
repeats?: number;
delaySecs?: number;
holdSecs?: number;
}
export interface IAndroidtvRemoteCommandContext {
config: IAndroidtvRemoteConfig;
snapshot: IAndroidtvRemoteSnapshot;
}
export type TAndroidtvRemoteCommandExecutor =
| ((commandArg: IAndroidtvRemoteCommand, contextArg: IAndroidtvRemoteCommandContext) => Promise<void> | void)
| {
execute(commandArg: IAndroidtvRemoteCommand, contextArg: IAndroidtvRemoteCommandContext): Promise<void> | void;
};
export interface IAndroidtvRemoteConfig {
host?: string;
apiPort?: number;
pairPort?: number;
deviceName?: string;
macAddress?: string;
manufacturer?: string;
model?: string;
enableIme?: boolean;
deviceInfo?: IAndroidtvRemoteDeviceInfo;
state?: IAndroidtvRemoteDeviceState;
volumeInfo?: IAndroidtvRemoteVolumeInfo;
apps?: IAndroidtvRemoteApp[] | Record<string, IAndroidtvRemoteConfiguredApp>;
snapshot?: IAndroidtvRemoteSnapshot;
executor?: TAndroidtvRemoteCommandExecutor;
}
export interface IAndroidtvRemoteMdnsRecord {
type?: string;
name?: string;
host?: string;
port?: number;
txt?: Record<string, unknown>;
properties?: Record<string, unknown>;
metadata?: Record<string, unknown>;
}
export interface IAndroidtvRemoteManualEntry {
host?: string;
apiPort?: number;
pairPort?: number;
port?: number;
id?: string;
name?: string;
deviceName?: string;
macAddress?: string;
manufacturer?: string;
model?: string;
enableIme?: boolean;
metadata?: Record<string, unknown>;
}
export type TAndroidtvRemoteDiscoveryRecord = IAndroidtvRemoteMdnsRecord | IAndroidtvRemoteManualEntry;
export type IHomeAssistantAndroidtvRemoteConfig = IAndroidtvRemoteConfig;
@@ -1,2 +1,7 @@
export * from './androidtv_remote.classes.client.js';
export * from './androidtv_remote.classes.configflow.js';
export * from './androidtv_remote.classes.integration.js';
export * from './androidtv_remote.constants.js';
export * from './androidtv_remote.discovery.js';
export * from './androidtv_remote.mapper.js';
export * from './androidtv_remote.types.js';