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 { 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 { void contextArg; return new NanoleafRuntime(new NanoleafClient(configArg)); } public async destroy(): Promise {} } export class HomeAssistantNanoleafIntegration extends NanoleafIntegration {} class NanoleafRuntime implements IIntegrationRuntime { public domain = 'nanoleaf'; constructor(private readonly client: NanoleafClient) {} public async devices(): Promise { return NanoleafMapper.toDevices(await this.client.getSnapshot()); } public async entities(): Promise { return NanoleafMapper.toEntities(await this.client.getSnapshot()); } public async callService(requestArg: IServiceCallRequest): Promise { 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 { await this.client.destroy(); } private async handleTurnOn(requestArg: IServiceCallRequest): Promise { 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 { 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 { 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 { 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 { 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 | 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 | 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 | 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)); } }