homebridge-virtual-accessories
Version:
Virtual HomeKit accessories for Homebridge.
224 lines • 12.8 kB
JavaScript
/* eslint-disable brace-style */
import { AccessoryFactory } from './accessoryFactory.js';
import { ConfigurationUtils } from './configuration/utils.js';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
import { VirtualLogger } from './utils/virtualLogger.js';
import { WebhookServer } from './webhookServer.js';
import * as path from 'path';
import fs from 'fs';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore <-- TODO remove this line, unless that gives an error
import packageInfo from '../package.json' with { type: 'json' };
/**
* HomebridgePlatform
*/
export class VirtualAccessoriesPlatform {
config;
api;
static platformName = 'Virtual Accessories Platform';
Service;
Characteristic;
log;
sensorUpdateServer;
// this is used to track restored cached accessories
cachedAccessories = [];
version = packageInfo.version;
constructor(log, config, api) {
this.config = config;
this.api = api;
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;
this.log = new VirtualLogger(log);
// Validate platform name
const platformName = this.config.name;
if (platformName !== VirtualAccessoriesPlatform.platformName) {
this.log.error(`Platform Name is invalid: '${platformName}'`);
this.log.error(`Platform Name must be '${VirtualAccessoriesPlatform.platformName}'`);
}
else {
this.log.debug(`Platform Name is valid: '${platformName}'`);
}
// Create webhook server
const sensorServerConfig = new ConfigurationUtils(this.log)
.deserializeWebhookServerConfig(this.config.sensorServer);
if (sensorServerConfig?.enabled) {
const prefix = 'sensorServer';
let isValid = false;
let errorFields = [prefix];
[isValid, errorFields] = sensorServerConfig.isValid(prefix);
if (!isValid) {
this.log.error(`Sensor Server configuration is invalid: ${JSON.stringify(sensorServerConfig)}`);
this.log.error(`Invalid fields: ${errorFields.toString()}`);
}
else {
this.log.debug(`Sensor Server configuration is valid: ${JSON.stringify(sensorServerConfig)}`);
this.sensorUpdateServer = new WebhookServer(this.log, parseInt(sensorServerConfig.port));
}
}
this.log.debug('Finished initializing platform');
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, () => {
log.debug('Executing didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
this.log.info(`Running Virtual Accessories For Homebridge v${this.version}`);
});
this.api.on("shutdown" /* APIEvent.SHUTDOWN */, () => {
log.debug('Executing shutdown callback');
this.sensorUpdateServer?.stop();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to set up 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, so we can track if it has already been registered
this.cachedAccessories.push(accessory);
}
/**
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
let configDevices = this.config.devices;
if (configDevices === undefined) {
this.log.info('No configured accessories');
configDevices = JSON.parse('[]');
}
this.log.debug(`Found ${configDevices.length} configured accessories: ${JSON.stringify(configDevices)}`);
const accessoryConfigurations = this.deserializeAccessoryConfigurations(configDevices);
this.log.debug(`Deserialized accessories: ${JSON.stringify(accessoryConfigurations)}`);
const virtualAccessories = [];
// loop over the discovered devices and register each one if it has not already been registered
for (const accessoryConfiguration of accessoryConfigurations) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(accessoryConfiguration.accessoryID);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const cachedAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid);
if (cachedAccessory) {
// the accessory already exists
this.log.info(`Restoring existing accessory: ${accessoryConfiguration.accessoryName}`);
// update the device firmware version in the `accessory.context`
cachedAccessory.context.firmwareVersion = this.version;
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. e.g.:
// registeredAccessory.context.device = device;
// this.api.updatePlatformAccessories([registeredAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
const virtualAccessory = AccessoryFactory.createVirtualAccessory(this, cachedAccessory, accessoryConfiguration);
if (virtualAccessory !== undefined) {
if (cachedAccessory.displayName !== accessoryConfiguration.accessoryName) {
this.log.info(`Updating accessory name from ${cachedAccessory.displayName} to ${accessoryConfiguration.accessoryName}`);
virtualAccessory.updateConfiguredName();
cachedAccessory.updateDisplayName(accessoryConfiguration.accessoryName);
}
// Just update all the cached accessories
this.api.updatePlatformAccessories([cachedAccessory]);
this.log.debug(`Updating cache: ${accessoryConfiguration.accessoryName}`);
virtualAccessories.push(virtualAccessory);
}
else {
this.log.error(`Error restoring existing accessory: ${accessoryConfiguration.accessoryName}`);
}
}
else {
// the accessory does not yet exist, so we need to create it
this.log.info(`Adding new accessory: ${accessoryConfiguration.accessoryName}`);
// create a new accessory
const accessory = new this.api.platformAccessory(accessoryConfiguration.accessoryName, uuid, accessoryConfiguration.category);
// store a copy of the device configuration in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.firmwareVersion = this.version;
const storagePath = path.join(this.api.user.persistPath(), `VA4HB_${accessoryConfiguration.accessoryID}.json`);
accessory.context.storagePath = storagePath;
this.log.debug(`Storage path if stateful accessory: ${storagePath}`);
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
const virtualAccessory = AccessoryFactory.createVirtualAccessory(this, accessory, accessoryConfiguration);
if (virtualAccessory === undefined) {
this.log.error(`Error adding new accessory: ${accessoryConfiguration.accessoryName}`);
}
else if (virtualAccessory.isExternalAccessory()) {
this.log.info(`Publishing new external accessory: ${accessoryConfiguration.accessoryName}`);
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
}
else {
// link the accessory to your platform
this.log.info(`Publishing new accessory: ${accessoryConfiguration.accessoryName}`);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
virtualAccessories.push(virtualAccessory);
}
}
// Cleanup config
// const configPath = api.user.configPath();
}
// loop over the cached accessories and unregister each one if it is not in the config
for (const cachedAccessory of this.cachedAccessories) {
const configuredDevice = configDevices.find(device => this.api.hap.uuid.generate(device.accessoryID) === cachedAccessory.UUID);
// If there is no configured device for this cached accessory
if (!configuredDevice) {
this.log.info(`Removing deleted accessory: ${cachedAccessory.displayName}`);
// Unregister the accessory from the platform
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedAccessory]);
// Delete any stateful info, if it exists
const storagePath = cachedAccessory.context.storagePath;
if (fs.existsSync(storagePath)) {
fs.unlink(storagePath, (err) => {
if (err) {
this.log.debug(`No stateful storage found for: ${cachedAccessory.displayName}`);
}
else {
this.log.debug(`Deleted stateful storage for: ${cachedAccessory.displayName}`);
}
});
}
}
}
// Start sensor server
this.sensorUpdateServer?.addAccessories(virtualAccessories);
this.sensorUpdateServer?.start();
}
deserializeAccessoryConfigurations(configDevices) {
const accessoryConfigurations = [];
const accessoryUUIDs = [];
for (const configDevice of configDevices) {
// Deserialize accessory configuration
const configurationUtils = new ConfigurationUtils(this.log);
const accessoryConfiguration = configurationUtils.deserializeAccessoryConfig(configDevice);
// Skip accessory if the configuration is invalid
if (accessoryConfiguration === undefined) {
this.log.error(`Error deserializing: ${JSON.stringify(configDevice)}`);
this.log.info('Skipping accessory until configuration is fixed');
}
else if (accessoryUUIDs.includes(accessoryConfiguration.accessoryID)) {
this.log.error(`Found accessory with duplicate ID: ${JSON.stringify(configDevice)}`);
this.log.info('Skipping accessory until configuration is fixed');
}
else {
this.log.debug(`Deserialized accessory: ${JSON.stringify(configDevice)}`);
let isValidAccessoryConfiguration = false;
let errorFields = [];
[isValidAccessoryConfiguration, errorFields] = accessoryConfiguration.isValid();
if (!isValidAccessoryConfiguration) {
this.log.error(`Skipping accessory. Configuration is invalid: ${JSON.stringify(accessoryConfiguration)}`);
this.log.error(`Invalid fields: ${errorFields.toString()}`);
}
else {
this.log.debug(`Configuration is valid: ${JSON.stringify(accessoryConfiguration)}`);
accessoryConfigurations.push(accessoryConfiguration);
accessoryUUIDs.push(accessoryConfiguration.accessoryID);
}
}
}
return accessoryConfigurations;
}
}
//# sourceMappingURL=platform.js.map