@pst-on-npm/homebridge-enocean
Version:
Integrate EnOcean® devices into Homebridge.
418 lines • 22.8 kB
JavaScript
"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