homebridge-aplvibe
Version:
Homebridge plugin for SleepMe devices using APL as core logic engine
282 lines • 12.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.APLVibePlatform = void 0;
const types_1 = require("./types");
const apl_engine_1 = require("./apl-engine");
class APLVibePlatform {
log;
config;
api;
Service;
Characteristic;
accessories = [];
aplEngine;
pollingInterval;
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.log.debug('Initializing APLVibe Platform');
// Initialize Homebridge services
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
// Initialize APL engine - this is where all business logic resides
this.aplEngine = new apl_engine_1.APLEngineImpl(this.log);
// Set up APL event handlers for state changes
this.aplEngine.onEvent('stateChange', this.handleStateChange.bind(this));
this.api.on('didFinishLaunching', () => {
this.log.debug('Executed didFinishLaunching callback');
this.initializePlatform();
});
}
/**
* Initialize platform by delegating to APL engine
* This demonstrates the delegation pattern - TypeScript handles Homebridge integration,
* APL handles all business logic
*/
async initializePlatform() {
try {
// Initialize APL engine with configuration
const configJson = JSON.stringify(this.config);
const initResult = await this.aplEngine.init(configJson);
if (!initResult.success) {
this.log.error('Failed to initialize APL engine:', initResult.error);
return;
}
// Discover devices via APL
await this.discoverDevices();
// Start polling for device updates
this.startPolling();
this.log.info('APLVibe platform initialized successfully');
}
catch (error) {
this.log.error('Platform initialization failed:', error);
}
}
/**
* Device discovery delegates to APL engine
* APL handles device management logic, TypeScript handles Homebridge accessory creation
*/
async discoverDevices() {
try {
const devicesResult = await this.aplEngine.getDevices();
if (!devicesResult.success) {
this.log.error('Failed to get devices from APL:', devicesResult.error);
return;
}
const devices = JSON.parse(devicesResult.data);
for (const device of devices) {
await this.addAccessory(device);
}
}
catch (error) {
this.log.error('Device discovery failed:', error);
}
}
/**
* Add accessory - minimal Homebridge setup, delegates state management to APL
*/
async addAccessory(device) {
const uuid = this.api.hap.uuid.generate(device.id);
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
this.log.info('Restoring existing accessory from cache:', device.name);
// Restore accessory via APL
await this.aplEngine.restoreAccessory(uuid);
this.setupAccessoryServices(existingAccessory, device);
}
else {
this.log.info('Adding new accessory:', device.name);
const accessory = new this.api.platformAccessory(device.name, uuid);
accessory.context.device = device;
this.setupAccessoryServices(accessory, device);
this.api.registerPlatformAccessories('homebridge-aplvibe', 'APLVibe', [accessory]);
this.accessories.push(accessory);
}
}
/**
* Setup accessory services - creates Homebridge services but delegates all logic to APL
*/
setupAccessoryServices(accessory, device) {
// Device Information Service
const informationService = accessory.getService(this.Service.AccessoryInformation) ||
accessory.addService(this.Service.AccessoryInformation);
informationService
.setCharacteristic(this.Characteristic.Manufacturer, 'SleepMe')
.setCharacteristic(this.Characteristic.Model, device.model)
.setCharacteristic(this.Characteristic.SerialNumber, device.id);
// Thermostat Service - all state management delegated to APL
const thermostatService = accessory.getService(this.Service.Thermostat) ||
accessory.addService(this.Service.Thermostat);
// Current Temperature - read-only, managed by APL
thermostatService.getCharacteristic(this.Characteristic.CurrentTemperature)
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.CURRENT_TEMPERATURE);
return result.success ? result.data : 20;
});
// Target Temperature - read/write, validation and conversion handled by APL
thermostatService.getCharacteristic(this.Characteristic.TargetTemperature)
.setProps({
minValue: this.config.unit === 'C' ? 13 : 55,
maxValue: this.config.unit === 'C' ? 46 : 115,
minStep: 0.1,
})
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.TARGET_TEMPERATURE);
return result.success ? result.data : 20;
})
.onSet(async (value) => {
// Validate temperature via APL
const validationResult = await this.aplEngine.validateTemperature(value, this.config.unit);
if (!validationResult.success || !validationResult.data) {
throw new this.api.hap.HapStatusError(-70410 /* this.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST */);
}
// Queue temperature change request via APL
const request = {
id: `temp_${Date.now()}`,
priority: 'HIGH',
timestamp: Date.now(),
action: 'setTemperature',
device_id: device.id,
data: { temperature: value }
};
await this.aplEngine.queueRequest(request);
await this.aplEngine.setState(device.id, types_1.Characteristics.TARGET_TEMPERATURE, value);
});
// Heating/Cooling State - managed by APL
thermostatService.getCharacteristic(this.Characteristic.CurrentHeatingCoolingState)
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.HEATING_COOLING_STATE);
return result.success ? result.data : this.Characteristic.CurrentHeatingCoolingState.OFF;
});
thermostatService.getCharacteristic(this.Characteristic.TargetHeatingCoolingState)
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.TARGET_HEATING_COOLING_STATE);
return result.success ? result.data : this.Characteristic.TargetHeatingCoolingState.AUTO;
})
.onSet(async (value) => {
await this.aplEngine.setState(device.id, types_1.Characteristics.TARGET_HEATING_COOLING_STATE, value);
});
// Temperature Display Units
thermostatService.getCharacteristic(this.Characteristic.TemperatureDisplayUnits)
.onGet(() => {
return this.config.unit === 'C' ?
this.Characteristic.TemperatureDisplayUnits.CELSIUS :
this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
})
.onSet((value) => {
// Unit changes handled by APL for temperature conversions
this.log.debug('Temperature unit changed to:', value);
});
// Switch Service for power control - delegates to APL
const switchService = accessory.getService(this.Service.Switch) ||
accessory.addService(this.Service.Switch, 'Power');
switchService.getCharacteristic(this.Characteristic.On)
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.POWER_STATE);
return result.success ? result.data : false;
})
.onSet(async (value) => {
const request = {
id: `power_${Date.now()}`,
priority: 'CRITICAL',
timestamp: Date.now(),
action: 'setPower',
device_id: device.id,
data: { power: value }
};
await this.aplEngine.queueRequest(request);
await this.aplEngine.setState(device.id, types_1.Characteristics.POWER_STATE, value);
});
// Battery Service for water level monitoring
const batteryService = accessory.getService(this.Service.Battery) ||
accessory.addService(this.Service.Battery, 'Water Level');
batteryService.getCharacteristic(this.Characteristic.BatteryLevel)
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.WATER_LEVEL);
return result.success ? result.data : 100;
});
batteryService.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(async () => {
const result = await this.aplEngine.getState(device.id, types_1.Characteristics.WATER_LEVEL);
const waterLevel = result.success ? result.data : 100;
return waterLevel < 20 ?
this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW :
this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
});
}
/**
* Handle state changes from APL engine
* APL emits events when state changes, TypeScript translates to Homebridge
*/
handleStateChange(data) {
const accessory = this.accessories.find(acc => acc.context.device.id === data.deviceId);
if (!accessory) {
return;
}
// Map APL characteristics to Homebridge characteristics
const characteristicMap = {
[types_1.Characteristics.CURRENT_TEMPERATURE]: this.Characteristic.CurrentTemperature,
[types_1.Characteristics.TARGET_TEMPERATURE]: this.Characteristic.TargetTemperature,
[types_1.Characteristics.POWER_STATE]: this.Characteristic.On,
[types_1.Characteristics.WATER_LEVEL]: this.Characteristic.BatteryLevel,
};
const homebridgeChar = characteristicMap[data.characteristic];
if (!homebridgeChar) {
return;
}
// Update appropriate service
let service;
if (data.characteristic === types_1.Characteristics.POWER_STATE) {
service = accessory.getService(this.Service.Switch);
}
else if (data.characteristic === types_1.Characteristics.WATER_LEVEL) {
service = accessory.getService(this.Service.Battery);
}
else {
service = accessory.getService(this.Service.Thermostat);
}
if (service) {
service.updateCharacteristic(homebridgeChar, data.value);
}
}
/**
* Start polling for device updates - delegates to APL for scheduling and rate limiting
*/
startPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.pollingInterval = setInterval(async () => {
try {
// Process any queued requests via APL
await this.aplEngine.processQueue();
// Apply any scheduled temperature changes via APL
for (const accessory of this.accessories) {
const deviceId = accessory.context.device.id;
await this.aplEngine.applySchedule(deviceId);
}
}
catch (error) {
this.log.error('Polling error:', error);
}
}, (this.config.pollingInterval || 90) * 1000);
}
configureAccessory(accessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
this.accessories.push(accessory);
}
/**
* Cleanup - destroy APL engine and clear intervals
*/
destroy() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
if (this.aplEngine) {
this.aplEngine.destroy();
}
}
}
exports.APLVibePlatform = APLVibePlatform;
//# sourceMappingURL=platform.js.map