homebridge-kef-speaker
Version:
Homebridge plugin for KEF wireless speakers (LS50 Wireless II, LSX II, LS60)
166 lines • 7.24 kB
JavaScript
import * as cron from 'node-cron';
import { KefSpeakerAccessory } from './platformAccessory.js';
import { PLATFORM_NAME, PLUGIN_NAME, SPEAKER_MODELS } from './settings.js';
import { KefConnector } from './kefConnector.js';
/**
* KEF LSX II Platform
* This class is the main constructor for the KEF speaker plugin
*/
export class KefLsxIIPlatform {
log;
config;
api;
Service;
Characteristic;
// Track restored cached accessories
accessories = new Map();
discoveredCacheUUIDs = [];
// Track speaker accessories
speakerAccessories = new Map();
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;
this.log.debug('Finished initializing platform:', this.config.name);
// Validate configuration
if (!this.config.speakers || !Array.isArray(this.config.speakers)) {
this.log.error('No speakers configured. Please add speakers in the plugin settings.');
return;
}
// When this event is fired it means Homebridge has restored all cached accessories from disk
this.api.on('didFinishLaunching', () => {
this.log.debug('Executed didFinishLaunching callback');
this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// Add the restored accessory to the accessories cache
this.accessories.set(accessory.UUID, accessory);
this.discoveredCacheUUIDs.push(accessory.UUID);
}
/**
* Discover and register speaker devices
*/
async discoverDevices() {
for (const speakerConfig of this.config.speakers) {
await this.setupSpeaker(speakerConfig);
}
// Remove any accessories that are no longer configured
for (const [uuid, accessory] of this.accessories) {
if (!this.discoveredCacheUUIDs.includes(uuid)) {
this.log.info('Removing existing accessory from cache:', accessory.displayName);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.accessories.delete(uuid);
}
}
}
/**
* Setup individual speaker
*/
async setupSpeaker(speakerConfig) {
// Validate speaker configuration
if (!speakerConfig.name || !speakerConfig.ip || !speakerConfig.model) {
this.log.error('Invalid speaker configuration. Name, IP, and model are required.');
return;
}
// Validate speaker model
if (!SPEAKER_MODELS[speakerConfig.model]) {
this.log.error(`Unsupported speaker model: ${speakerConfig.model}`);
return;
}
// Generate UUID for this speaker
const uuid = this.api.hap.uuid.generate(speakerConfig.ip);
// Check if accessory already exists
let accessory = this.accessories.get(uuid);
if (accessory) {
// Update existing accessory
this.log.info('Restoring existing accessory from cache:', accessory.displayName);
// Update context with new config
accessory.context.speaker = speakerConfig;
// Update accessory information
accessory.getService(this.Service.AccessoryInformation)
.setCharacteristic(this.Characteristic.Manufacturer, 'KEF')
.setCharacteristic(this.Characteristic.Model, SPEAKER_MODELS[speakerConfig.model].name)
.setCharacteristic(this.Characteristic.Name, speakerConfig.name);
}
else {
// Create new accessory
this.log.info('Adding new accessory:', speakerConfig.name);
accessory = new this.api.platformAccessory(speakerConfig.name, uuid);
// Set accessory context
accessory.context.speaker = speakerConfig;
// Set accessory information
accessory.getService(this.Service.AccessoryInformation)
.setCharacteristic(this.Characteristic.Manufacturer, 'KEF')
.setCharacteristic(this.Characteristic.Model, SPEAKER_MODELS[speakerConfig.model].name)
.setCharacteristic(this.Characteristic.SerialNumber, speakerConfig.ip);
// Register the accessory
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.accessories.set(uuid, accessory);
}
// Create speaker accessory handler
const speakerAccessory = new KefSpeakerAccessory(this, accessory);
this.speakerAccessories.set(uuid, speakerAccessory);
// Test connection
try {
const connector = new KefConnector(speakerConfig.ip, this.log);
await connector.getStatus();
this.log.info(`Successfully connected to speaker: ${speakerConfig.name} (${speakerConfig.ip})`);
}
catch (error) {
this.log.error(`Failed to connect to speaker: ${speakerConfig.name} (${speakerConfig.ip})`, error);
}
// Setup night mode cron job if enabled
if (speakerConfig.nightMode?.enabled) {
this.setupNightMode(speakerConfig, speakerAccessory);
}
// Mark as discovered
this.discoveredCacheUUIDs.push(uuid);
}
/**
* Setup night mode scheduling
*/
setupNightMode(speakerConfig, speakerAccessory) {
const nightMode = speakerConfig.nightMode;
// Parse schedule times
const [startHour, startMinute] = nightMode.schedule.start.split(':').map(Number);
const [endHour, endMinute] = nightMode.schedule.end.split(':').map(Number);
// Start night mode cron job
cron.schedule(`${startMinute} ${startHour} * * *`, () => {
this.log.info(`Activating night mode for ${speakerConfig.name}`);
speakerAccessory.activateNightMode();
}, {
scheduled: true,
timezone: 'America/New_York', // You might want to make this configurable
});
// End night mode cron job
cron.schedule(`${endMinute} ${endHour} * * *`, () => {
this.log.info(`Deactivating night mode for ${speakerConfig.name}`);
speakerAccessory.deactivateNightMode();
}, {
scheduled: true,
timezone: 'America/New_York', // You might want to make this configurable
});
this.log.info(`Night mode scheduled for ${speakerConfig.name}: ${nightMode.schedule.start} - ${nightMode.schedule.end}`);
}
/**
* Get speaker accessory by UUID
*/
getSpeakerAccessory(uuid) {
return this.speakerAccessories.get(uuid);
}
/**
* Get all speaker accessories
*/
getAllSpeakerAccessories() {
return Array.from(this.speakerAccessories.values());
}
}
//# sourceMappingURL=platform.js.map