UNPKG

@pst-on-npm/homebridge-enocean

Version:
418 lines • 22.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EnOceanHomebridgePlatform = void 0; const fs = __importStar(require("fs")); const path_1 = __importDefault(require("path")); const settings_1 = require("./settings"); const DeviceConfig_1 = require("./homebridge/DeviceConfig"); const AccessoryFactory_1 = require("./accessories/AccessoryFactory"); const EnoGateway_1 = require("./enocean/EnoGateway"); const LearnSwitchAccessory_1 = require("./LearnSwitchAccessory"); const EnoAccessoryContext_1 = require("./homebridge/EnoAccessoryContext"); const HbConfigUpdater_1 = require("./homebridge/HbConfigUpdater"); const EnOCore = __importStar(require("enocean-core")); const EnoMessageFactory_1 = require("./enocean/EnoMessageFactory"); const util_1 = require("./util"); /** * HomebridgePlatform * This class is the main constructor for your plugin, this is where you should * parse the user config and discover/register accessories with Homebridge. */ class EnOceanHomebridgePlatform { log; config; api; Service; Characteristic; /** * History support for the Elgato Eve App */ FakeGatoHistoryService; /** * The version string of this platform accessory. * Keep it semver compliant (http://semver.org) */ version; // This is used to track restored cached accessories cachedAccessoriesByUUID = new Map(); // All enocean accessories we are aware of discoveredAccessoriesByUUID = new Map(); // All enocean accessories we validated from the configuration configuredDevicesByUUID = new Map(); // Valid after DiscoverDevices, requires async/await _enoGateway; enoAccessoryFactory; /** * Constructs a new instance of the platform. * * @param log - The logging utility. * @param config - The platform configuration. * @param api - The Homebridge API. * * Initializes the platform with the provided configuration and sets up the necessary services and event listeners. * * - Initializes the HAP Service and Characteristic. * - Retrieves the platform version. * - Optionally sets up the FakeGato history service if enabled in the configuration. * - Creates a new EnOcean gateway with the specified communication device. * - Initializes the accessory factory. * - Registers event listeners for Homebridge lifecycle events: * - `didFinishLaunching`: Discovers and registers devices as accessories. * - `shutdown`: Handles platform shutdown. * * Logs the initialization process and important events for debugging purposes. */ 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('New EnOcean gateway with device ', this.config.comDevice); this.version = this.getVersion(); if (config.isHistoryServiceEnabled) { // See: https://github.com/simont77/fakegato-history this.log.debug('New fakegato history service'); // eslint-disable-next-line @typescript-eslint/no-require-imports this.FakeGatoHistoryService = require('fakegato-history')(this.api); } this._enoGateway = new EnoGateway_1.EnoGateway(this.config.comDevice, this.config.isAutoCreateEnabled, log); this.enoAccessoryFactory = new AccessoryFactory_1.AccessoryFactory(); // 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', async () => { log.debug('Executed didFinishLaunching callback'); // Run the method to discover / register your devices as accessories await this.discoverDevices(); // Print a table with all configured devices if (this.discoveredAccessoriesByUUID.size > 0) { const table3 = Array.from(this.discoveredAccessoriesByUUID.values()).map(d => d.config); console.table(table3, ['name', 'id', 'eep', 'model', 'manufacturer', 'time']); } else { log.info('No EnOcean devices configured'); } }); this.api.on('shutdown', () => { this.log.debug('shutting down'); }); this.log.debug('Finished initializing platform:', this.config.name); } /** * Configures an accessory that has been restored from the cache. * Invoked when homebridge restores cached accessories from disk at startup. * * @param accessory - The accessory to configure. * * This method logs the loading of the accessory and adds it to the accessories cache. * The cache helps in tracking if the accessory has already been registered. */ 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.cachedAccessoriesByUUID.set(accessory.UUID, accessory); } /** * Discovers and initializes devices based on the current configuration and cached accessories. * * This method performs the following steps: * 1. Starts the EnOcean gateway. * 2. Initializes a map of cached accessories. * 3. Manages the Learn Switch accessory if it is enabled in the configuration. * 4. Validates and configures accessories from the configuration. * 5. Sets the teach-in listener for new devices. * 6. Initializes or removes cached accessories based on the current configuration. * 7. Teaches devices that are configured but not cached. * * @returns {Promise<void>} A promise that resolves when the discovery process is complete. */ async discoverDevices() { try { await this._enoGateway.start(); } catch (error) { this.log.error('Failed to start EnOcean gateway:', error); return; } const cached = new Map(this.cachedAccessoriesByUUID); // Assume all orphans // New Learn Switch Accessory, if configured if (this.config.isLearnSwitchEnabled) { const learnSwitchAccessory = this.manageLearnSwitchAccessory(); cached.delete(learnSwitchAccessory.UUID); } // Validate configured devices if (Array.isArray(this.config.devices)) { for (const a of this.config.devices) { let devConfig; try { devConfig = new DeviceConfig_1.DeviceConfig(a.id, a.eep, a.name, a.manufacturer, a.model); devConfig.time = a.time; devConfig.accessoryKind = a.accessoryKind; devConfig.localSenderIndex = a.localSenderIndex; } catch (error) { this.log.warn(`${a.name}: Skipping device. Wrong configuration. Check ID and EEP. ${error}`); continue; } const uuid = this.api.hap.uuid.generate(devConfig.devId.toString()); if (this.configuredDevicesByUUID.has(uuid)) { this.log.warn(`${devConfig.name}: Skipping device. Duplicate EnOcean ID: ${devConfig.devId}`); continue; } this.configuredDevicesByUUID.set(uuid, devConfig); } } // Set the teach-in listener this._enoGateway.addDeviceTeachInEventListener(this.enoGateway_teachInNewDevice.bind(this)); // It is important that the existing accessories are initialized first, // so they can retrieve their already allocated sender id (from the pool of // ids given by BaseID + 0..127) // Loop over the cached ones and initialize or remove if they are not configured any longer for (const [key, value] of cached.entries()) { // Check if this cached accessory is still in the configuration const devConfig = this.configuredDevicesByUUID.get(key); if (devConfig) { // teach the device and rely on the teach-in callback await this._enoGateway.teachDevice(devConfig); } else { this.log.info(`${value.displayName}: removing abandoned cached accessory`); this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [value]); this.cachedAccessoriesByUUID.delete(key); } } // Loop over the configured ones that were not cached. // They might retrieve new sender IDs. for (const [key, value] of this.configuredDevicesByUUID.entries()) { if (!cached.has(key)) { await this._enoGateway.teachDevice(value); } } } /** * Handles the teach-in process for a new EnOcean device. * * This method performs different actions based on the teach-in method and the state of the device * (whether it is cached, configured, or new). It updates existing accessories, creates new ones, * and registers them with the Homebridge platform. * * @param info - The device information containing details such as deviceId, eep, manufacturer, * teachInMethod, and label. * * @returns A promise that resolves when the teach-in process is complete. * * The method performs the following actions based on the teach-in method and device state: * - If the device is manually taught-in, cached, and configured, it updates the existing accessory. * - If the device is manually taught-in, not cached, but configured, it creates a new accessory. * - If the device is not manually taught-in, not cached, and not configured, it creates a new accessory * received via teach-in telegram. * - If the device is manually taught-in, not cached, and not configured, it creates a new accessory * received via MSC teach-in telegram (ELTAKO). * - If none of the above conditions are met, it logs a warning message and ignores the device. * * The method also handles errors during the accessory creation and update processes, logging warnings * if any errors occur. */ async enoGateway_teachInNewDevice(info) { const uuid = this.api.hap.uuid.generate(info.deviceId.toString()); const cachedAccessory = this.cachedAccessoriesByUUID.get(uuid); const isManualTeachIn = (info.teachInMethod === EnOCore.TeachInMethods.Manual); const teachInMethod = EnOCore.TeachInMethods[info.teachInMethod]; let deviceConfig = this.configuredDevicesByUUID.get(uuid); if (isManualTeachIn && cachedAccessory && deviceConfig) { // --------------------------------------------------------------------- // Update existing accessory // --------------------------------------------------------------------- cachedAccessory.updateDisplayName(deviceConfig.name); try { const device = this.enoAccessoryFactory .newAccessory(deviceConfig.eep, this, cachedAccessory, deviceConfig); cachedAccessory.context = cachedAccessory.context ?? new EnoAccessoryContext_1.EnoAccessoryContext(); if (deviceConfig.localSenderIndex) { cachedAccessory.context.localSenderIndex = deviceConfig.localSenderIndex; } await device.setGateway(this._enoGateway); this.api.updatePlatformAccessories([cachedAccessory]); this.log.success(`${cachedAccessory.displayName} (${device.constructor.name}): updated accessory from cache`); this.discoveredAccessoriesByUUID.set(uuid, device); } catch (error) { this.log.warn(`${cachedAccessory.displayName}: failed to update cached accessory. ${error}`); } } else if (isManualTeachIn && !cachedAccessory && deviceConfig) { // --------------------------------------------------------------------- // Create new accessory // --------------------------------------------------------------------- const accessory = new this.api.platformAccessory(deviceConfig.name, uuid); accessory.context = accessory.context ?? new EnoAccessoryContext_1.EnoAccessoryContext(); if (deviceConfig.localSenderIndex) { accessory.context.localSenderIndex = deviceConfig.localSenderIndex; } try { const device = this.enoAccessoryFactory .newAccessory(deviceConfig.eep, this, accessory, deviceConfig); await device.setGateway(this._enoGateway); this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); this.log.success(`${accessory.displayName} (${device.constructor.name}): registered new accessory`); this.discoveredAccessoriesByUUID.set(deviceConfig.devId.toString(), device); } catch (error) { this.log.warn(`${deviceConfig.name}: failed to create accessory. ${error}`); } } else if (!isManualTeachIn && !cachedAccessory && !deviceConfig) { // --------------------------------------------------------------------- // Create new accessory received via teach-in telegram // --------------------------------------------------------------------- const defaultName = `${EnOCore.Manufacturers[info.manufacturer].replace('_', ' ')}-${util_1.Util.getTimeAsFourDigitString()}`; deviceConfig = new DeviceConfig_1.DeviceConfig(info.deviceId.toString(), info.eep.toString(), defaultName.replace(/[_]/g, ' '), EnOCore.Manufacturers[info.manufacturer]); const accessory = new this.api.platformAccessory(deviceConfig.name, uuid); accessory.context = accessory.context ?? new EnoAccessoryContext_1.EnoAccessoryContext(); if (info.localId) { accessory.context.localSenderIndex = this._enoGateway.getSenderIndex(info.localId); this.log(`${accessory.displayName}: ${teachInMethod} teach-in with local sender ID ${info.localId.toString()} ` + `-> sender Index: ${accessory.context.localSenderIndex}`); } try { const device = this.enoAccessoryFactory.newAccessory(deviceConfig.eep, this, accessory, deviceConfig); await device.setGateway(this._enoGateway); this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); this.log.success(`${accessory.displayName} (${device.constructor.name}): registered new accessory by ${teachInMethod}`); this.discoveredAccessoriesByUUID.set(uuid, device); const configUpdater = new HbConfigUpdater_1.HbConfigUpdater(this.api.user.configPath(), settings_1.PLATFORM_NAME); await configUpdater.addDeviceToHomebridgeConfig(deviceConfig); this.configuredDevicesByUUID.set(uuid, deviceConfig); } catch (error) { this.log.warn(`${deviceConfig.name}: failed to create new accessory by ${teachInMethod}. ${error}`); } } else if (isManualTeachIn && !cachedAccessory && !deviceConfig) { // --------------------------------------------------------------------- // Create new accessory received via MSC teach-in telegram (ELTAKO) // --------------------------------------------------------------------- deviceConfig = this._enoGateway.armedConfig; if (deviceConfig === undefined) { throw new Error('Attempt to teach-in a new Eltako device but deviceConfig is undefined'); } const tag = util_1.Util.getTimeAsFourDigitString(); const defaultName = `${EnOCore.Manufacturers[deviceConfig.manufacturerId]}-${deviceConfig.model}-${tag}`; deviceConfig.name = defaultName.replace(/[_]/g, ' '); const accessory = new this.api.platformAccessory(deviceConfig.name, uuid); accessory.context = new EnoAccessoryContext_1.EnoAccessoryContext(); try { const device = this.enoAccessoryFactory.newAccessory(deviceConfig.eep, this, accessory, deviceConfig); this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); await device.setGateway(this._enoGateway); this.log.success(`${accessory.displayName} (${device.constructor.name}): registered new accessory by Eltako MSC`); this.discoveredAccessoriesByUUID.set(uuid, device); const configUpdater = new HbConfigUpdater_1.HbConfigUpdater(this.api.user.configPath(), settings_1.PLATFORM_NAME); await configUpdater.addDeviceToHomebridgeConfig(deviceConfig); this.configuredDevicesByUUID.set(uuid, deviceConfig); // Send the teach message to the device if (accessory.context.localSenderIndex !== undefined) { const senderIndex = accessory.context.localSenderIndex; const eepId = deviceConfig.eepId; const manufacturerId = deviceConfig.manufacturerId; setTimeout(() => { const localId = this._enoGateway.getSenderId(senderIndex); this.log.info(`Sending teach-in to '${accessory.displayName}' with local ID '${localId.toString()}'`); const erp1TeachIn = EnoMessageFactory_1.EnoMessageFactory .newFourBSTeachInMessage(localId, eepId, manufacturerId); this._enoGateway.enqueueErp1Telegram(erp1TeachIn); }, 3000); } else { this.log.warn(`Cannot send teach-in to '${accessory.displayName}': No local ID was provided`); } } catch (error) { this.log.warn(`${deviceConfig.name}: failed to create new accessory by Eltako MSC. ${error}`); } } else { // --------------------------------------------------------------------- let message = `${info.deviceId.toString()}: ignoring device while teach-in (${teachInMethod}).`; if (deviceConfig?.name) { message += ` It is already known as '${deviceConfig.name}'.`; } this.log.warn(message); } } /** * Manages the Learn Switch Accessory by either updating an existing accessory from the cache * or registering a new accessory if it does not exist. * * @returns {PlatformAccessory<LearnSwitchDevice>} The managed Learn Switch Accessory. */ manageLearnSwitchAccessory() { // Learn Switch Accessory const uuid = this.api.hap.uuid.generate(LearnSwitchAccessory_1.LearnSwitchAccessory.Device.id); const existingAccessory = this.cachedAccessoriesByUUID.get(uuid); if (existingAccessory) { existingAccessory.context = LearnSwitchAccessory_1.LearnSwitchAccessory.Device; this.api.updatePlatformAccessories([existingAccessory]); new LearnSwitchAccessory_1.LearnSwitchAccessory(this, existingAccessory) .setGateway(this._enoGateway); this.log.success(`${existingAccessory.displayName}: updated accessory from cache`); return existingAccessory; } const accessory = new this.api.platformAccessory(LearnSwitchAccessory_1.LearnSwitchAccessory.Device.name, uuid); accessory.context = LearnSwitchAccessory_1.LearnSwitchAccessory.Device; new LearnSwitchAccessory_1.LearnSwitchAccessory(this, accessory) .setGateway(this._enoGateway); this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); this.log.success(`${accessory.displayName}: registered new accessory`); return accessory; } /** * Retrieves the version number from the package.json file. * * @returns {string} The version number specified in the package.json file. */ getVersion() { const packageJsonPath = path_1.default.join(__dirname, '../package.json'); const packageJson = fs.readFileSync(packageJsonPath, 'utf-8'); const parsedPackageJson = JSON.parse(packageJson); return parsedPackageJson.version; } } exports.EnOceanHomebridgePlatform = EnOceanHomebridgePlatform; //# sourceMappingURL=platform.js.map