UNPKG

homebridge-virtual-accessories

Version:
224 lines 12.8 kB
/* 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