Files
integrations/ts/integrations/nanoleaf/nanoleaf.classes.integration.ts
T

228 lines
9.0 KiB
TypeScript

import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { BaseIntegration } from '../../core/classes.baseintegration.js';
import type { IIntegrationEntity, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
import { NanoleafClient } from './nanoleaf.classes.client.js';
import { NanoleafConfigFlow } from './nanoleaf.classes.configflow.js';
import { createNanoleafDiscoveryDescriptor } from './nanoleaf.discovery.js';
import { NanoleafMapper } from './nanoleaf.mapper.js';
import type { INanoleafConfig } from './nanoleaf.types.js';
export class NanoleafIntegration extends BaseIntegration<INanoleafConfig> {
public readonly domain = 'nanoleaf';
public readonly displayName = 'Nanoleaf';
public readonly status = 'control-runtime' as const;
public readonly discoveryDescriptor = createNanoleafDiscoveryDescriptor();
public readonly configFlow = new NanoleafConfigFlow();
public readonly metadata = {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/nanoleaf',
upstreamDomain: 'nanoleaf',
integrationType: 'device',
iotClass: 'local_push',
documentation: 'https://www.home-assistant.io/integrations/nanoleaf',
requirements: ['aionanoleaf2==1.0.2'],
codeowners: ['@milanmeu', '@joostlek', '@loebi-ch', '@JaspervRijbroek', '@jonathanrobichaud4'],
zeroconf: ['_nanoleafms._tcp.local.', '_nanoleafapi._tcp.local.'],
ssdp: ['Nanoleaf_aurora:light', 'nanoleaf:nl29', 'nanoleaf:nl42', 'nanoleaf:nl52', 'nanoleaf:nl69', 'inanoleaf:nl81'],
homekitModels: ['NL29', 'NL42', 'NL47', 'NL48', 'NL52', 'NL59', 'NL69', 'NL81'],
};
public async setup(configArg: INanoleafConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
void contextArg;
return new NanoleafRuntime(new NanoleafClient(configArg));
}
public async destroy(): Promise<void> {}
}
export class HomeAssistantNanoleafIntegration extends NanoleafIntegration {}
class NanoleafRuntime implements IIntegrationRuntime {
public domain = 'nanoleaf';
constructor(private readonly client: NanoleafClient) {}
public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
return NanoleafMapper.toDevices(await this.client.getSnapshot());
}
public async entities(): Promise<IIntegrationEntity[]> {
return NanoleafMapper.toEntities(await this.client.getSnapshot());
}
public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
try {
if (requestArg.domain === 'nanoleaf' && requestArg.service === 'create_auth_token') {
const result = await this.client.createAuthToken();
return { success: result.success, error: result.error, data: result };
}
if (requestArg.domain === 'button') {
if (requestArg.service === 'press') {
await this.client.identify();
return { success: true };
}
return { success: false, error: `Unsupported Nanoleaf button service: ${requestArg.service}` };
}
if (requestArg.domain === 'select') {
return this.handleSelectEffect(requestArg);
}
if (requestArg.domain === 'number') {
return this.handleNumberService(requestArg);
}
if (requestArg.domain !== 'light') {
return { success: false, error: `Unsupported Nanoleaf service domain: ${requestArg.domain}` };
}
if (requestArg.service === 'turn_on') {
return this.handleTurnOn(requestArg);
}
if (requestArg.service === 'turn_off') {
await this.client.turnOff(this.numberValue(requestArg.data, 'transition'));
return { success: true };
}
if (requestArg.service === 'set_value') {
return this.handleSetValue(requestArg);
}
if (requestArg.service === 'set_percentage' || requestArg.service === 'set_brightness') {
return this.handleSetBrightness(requestArg);
}
if (requestArg.service === 'select_effect') {
return this.handleSelectEffect(requestArg);
}
return { success: false, error: `Unsupported Nanoleaf light service: ${requestArg.service}` };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
public async destroy(): Promise<void> {
await this.client.destroy();
}
private async handleTurnOn(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
const effect = this.stringValue(requestArg.data, 'effect', 'option');
if (effect) {
await this.client.setEffect(effect);
}
const hsColor = requestArg.data?.hs_color;
if (Array.isArray(hsColor) && typeof hsColor[0] === 'number' && typeof hsColor[1] === 'number') {
await this.client.setHue(hsColor[0]);
await this.client.setSaturation(hsColor[1]);
}
const colorTemperature = this.numberValue(requestArg.data, 'color_temp_kelvin', 'kelvin', 'ct');
if (colorTemperature !== undefined) {
await this.client.setColorTemperature(colorTemperature);
}
await this.client.turnOn();
const brightness = this.brightnessPercent(requestArg.data);
if (brightness !== undefined) {
await this.client.setBrightness(brightness, this.numberValue(requestArg.data, 'transition'));
}
return { success: true };
}
private async handleNumberService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service !== 'set_value' && requestArg.service !== 'set_percentage' && requestArg.service !== 'set_brightness') {
return { success: false, error: `Unsupported Nanoleaf number service: ${requestArg.service}` };
}
return requestArg.service === 'set_value' ? this.handleSetValue(requestArg) : this.handleSetBrightness(requestArg);
}
private async handleSetValue(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
const value = this.numberValue(requestArg.data, 'value', 'brightness', 'percentage');
if (value === undefined) {
return { success: false, error: 'Nanoleaf set_value requires data.value.' };
}
const target = `${requestArg.target.entityId ?? ''} ${this.stringValue(requestArg.data, 'attribute', 'feature') ?? ''}`.toLowerCase();
if (target.includes('hue')) {
await this.client.setHue(value);
return { success: true };
}
if (target.includes('saturation') || target.includes('sat')) {
await this.client.setSaturation(value);
return { success: true };
}
if (target.includes('temperature') || target.includes('color_temp') || target.includes('ct')) {
await this.client.setColorTemperature(value);
return { success: true };
}
await this.client.setBrightness(this.brightnessPercent(requestArg.data) ?? value, this.numberValue(requestArg.data, 'transition'));
return { success: true };
}
private async handleSetBrightness(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
const brightness = this.brightnessPercent(requestArg.data);
if (brightness === undefined) {
return { success: false, error: 'Nanoleaf brightness service requires data.percentage, data.brightness, or data.value.' };
}
await this.client.setBrightness(brightness, this.numberValue(requestArg.data, 'transition'));
return { success: true };
}
private async handleSelectEffect(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
const effect = this.stringValue(requestArg.data, 'effect', 'option', 'value');
if (!effect) {
return { success: false, error: 'Nanoleaf select_effect requires data.effect or data.option.' };
}
await this.client.setEffect(effect);
return { success: true };
}
private brightnessPercent(dataArg: Record<string, unknown> | undefined): number | undefined {
const percentage = this.numberValue(dataArg, 'brightness_pct', 'percentage', 'percent', 'value');
if (percentage !== undefined) {
return this.clamp(Math.round(percentage), 0, 100);
}
const brightness = this.numberValue(dataArg, 'brightness');
if (brightness === undefined) {
return undefined;
}
return this.clamp(Math.round(brightness > 100 ? brightness / 2.55 : brightness), 0, 100);
}
private numberValue(dataArg: Record<string, unknown> | undefined, ...keysArg: string[]): number | undefined {
if (!dataArg) {
return undefined;
}
for (const key of keysArg) {
const value = dataArg[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
return Number(value);
}
}
return undefined;
}
private stringValue(dataArg: Record<string, unknown> | undefined, ...keysArg: string[]): string | undefined {
if (!dataArg) {
return undefined;
}
for (const key of keysArg) {
const value = dataArg[key];
if (typeof value === 'string' && value) {
return value;
}
}
return undefined;
}
private clamp(valueArg: number, minArg: number, maxArg: number): number {
return Math.max(minArg, Math.min(maxArg, valueArg));
}
}