UNPKG

@pst-on-npm/homebridge-enocean

Version:
429 lines • 19.4 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.EnoGateway = void 0; const fs = __importStar(require("fs")); const EnOCore = __importStar(require("enocean-core")); const ParserFactory_1 = require("../eepParser/ParserFactory"); const SemaphoreManager_1 = require("./SemaphoreManager"); const InMemoryDeviceInfoMap_1 = require("./InMemoryDeviceInfoMap"); const EepParser_D1_Eltako_1 = require("../eepParser/EepParser_D1_Eltako"); const util_1 = require("../util"); const ParsedMessage_1 = require("../eepParser/ParsedMessage"); const EnoMessageFactory_1 = require("./EnoMessageFactory"); const TelegramQueue_1 = require("./TelegramQueue"); class AccessoryLink { config; eepMessageReceived; constructor(config, eepMessageReceived) { this.config = config; this.eepMessageReceived = eepMessageReceived; } } /** * EnoGateway * This class is supposed to extent the EnoCore Gateway class * mainly to dispatch received messages. */ class EnoGateway { serialPortPath; isAutoCreateEnabled; log; _coreGateway; _serialPortPath; _log; _eepParserFactory = new ParserFactory_1.EepParserFactory(); _registeredEnoAccessories = new Map(); _baseId; _senderIdManager = new SemaphoreManager_1.SemaphoreManager(); _sendQueue; /** * Creates an instance of EnoGateway. * * @param serialPortPath - The path to the serial port. * @param log - The logging instance. */ constructor(serialPortPath, isAutoCreateEnabled, log) { this.serialPortPath = serialPortPath; this.isAutoCreateEnabled = isAutoCreateEnabled; this.log = log; this._serialPortPath = serialPortPath; this._log = log; this._sendQueue = new TelegramQueue_1.TelegramQueue(this.sendErp1TelegramIntern.bind(this), this.log); // Reserve offset 0 for this gateway this._senderIdManager.acquire(0); } /** * Subscribe to the DeviceTeachIn Event * @param listener the listener to be called when a teach-in event occurred */ addDeviceTeachInEventListener(listener) { this.coreGateway.onDeviceTeachIn(listener); } /** * Adds an event listener for the "stopped learning" event. * * @param listener - A callback function that is invoked when the learning process is stopped. * The callback receives a boolean parameter indicating whether the process * was stopped manually (`true`) or automatically (`false`). */ addStoppedLearningEventListener(listener) { this.coreGateway.onStopLearning(listener); } /** * Claims a sender index for the EnOcean gateway. * * If the provided index is undefined, it will acquire the next free semaphore * from the sender ID manager. If the index is provided, it will attempt to * acquire that specific index. * * @param index - The index to claim, or undefined to acquire the next free index. * @returns The claimed index, or undefined if the index could not be acquired. */ claimSenderIndex(index) { if (index === undefined) { index = this._senderIdManager.acquireNextFreeSemaphore(); } else { const acquired = this._senderIdManager.acquire(index); if (!acquired) { this.log.error(`Local EnOcean ID already acquired. Index ${index}.`); } } return index; } /** * Retrieves the EnOcean Sender ID for the given sender index. * * @param senderIndex - The index of the sender for which to retrieve the ID. * @returns The EnOcean Sender ID corresponding to the given sender index. * @throws Will throw an error if the coreGateway is not initialized. * @throws Will throw an error if the sender index is not acquired. */ getSenderId(senderIndex) { if (this._baseId === undefined) { throw 'coreGateway not initialized. Call start() before calling \'getSenderId\'.'; } if (!this._senderIdManager.isAcquired(senderIndex)) { throw `Sender index '${senderIndex}' is not acquired. Can't provide an EnOcean Sender ID.`; } return EnOCore.DeviceId.fromNumber(this._baseId.toNumber() + senderIndex); } /** * Gets the index of the sender ID. * @param senderId the sender id to get the index for * @returns The index of the sender ID, or undefined if the sender ID is less than or equal to the base ID. */ getSenderIndex(senderId) { if (this._baseId === undefined) { throw 'coreGateway not initialized. Call start() before calling \'getSenderIndex\'.'; } const senderIndex = senderId.toNumber() - this._baseId.toNumber(); if (senderIndex <= 0) { this._log.warn(`'Sender ID '${senderId.toString()}' is less than, or equal to, the base ID. Can't provide an index for it.`); return undefined; } return senderIndex; } isLocalSenderId(senderId) { if (this._baseId === undefined) { throw 'coreGateway not initialized. Call start() before calling \'isLocalSenderId\'.'; } const senderIndex = senderId.toNumber() - this._baseId.toNumber(); return senderIndex >= 0 && senderIndex < 128; } isTeachInMode() { return this.coreGateway.isLearning; } /** * Registers an EnOcean accessory. * - teach the device * - ensure the right parser is available * - stores the accessory to allow message dispatching * * @param accessory The EnOcean accessory to registered */ async registerEnoAccessory(config, eepMessageReceived, localSenderId) { const info = this.coreGateway.getDeviceInfo(config.devId); if (info === undefined) { throw `'${config.id}' cannot register, call teach-in first.`; } const link = this._registeredEnoAccessories.get(config.devId.toString()) || new AccessoryLink(config, eepMessageReceived); this.ensureEepParser(config.eepId); this._registeredEnoAccessories.set(config.devId.toString(), link); let message = `${config.name}: registered accessory ID ${config.devId} with EEP ${config.eepId}`; if (localSenderId !== undefined) { info.localId = localSenderId; message += ` and local ID ${localSenderId}`; } this._log.info(message); } enqueueErp1Telegram(telegram) { this._sendQueue.enqueue(telegram); } /** * This will create the internal EnOcean Core Gateway * and connect it to the serial port. */ async start() { if (this._coreGateway !== undefined) { throw this.constructor.name + 'start() can only be called once'; } if (!fs.existsSync(this.serialPortPath)) { throw `${this.serialPortPath}: no such device`; } this._coreGateway = EnOCore.Gateway.connectToSerialPort(this._serialPortPath, new InMemoryDeviceInfoMap_1.InMemoryDeviceInfoMap()); const maxAttempt = 50; let attempt = 0; let chipId; await new Promise(resolve => setTimeout(resolve, 4000)); while (attempt <= maxAttempt) { try { attempt++; await new Promise(resolve => setTimeout(resolve, 500)); chipId = await this._coreGateway.getChipId(); this._baseId = await this._coreGateway.getBaseId(); break; } catch (error) { this.log.debug(`Attempt ${attempt}: ${error}`); await new Promise(resolve => setTimeout(resolve, 500 + (100 * attempt * attempt))); if (attempt === maxAttempt) { throw error; } } } this.log.success(`Connected to EnOcean transceiver on ${attempt}. attempt.`); this.log.success(`Got Chip ID ${chipId?.toString()} and Base ID ${this._baseId?.toString()}`); this._coreGateway.onStopLearning((manually) => { this.log.info(`Stop learning (teach-in) ${manually ? 'manually' : 'timeout'}`); }); this._coreGateway.onReceivedEEPMessage(this.coreGateway_receivedEepMessage.bind(this)); this._coreGateway.onReceivedERP1Telegram(this.coreGateway_receivedErp1Telegram.bind(this)); this._coreGateway.onReceivedERP1TelegramUnfamiliar(this.coreGateway_receivedErp1TelegramUnfamiliar.bind(this)); } async startLearning(learnTimeSec) { const senderIndexForLearning = this._senderIdManager.acquireNextFreeSemaphore(); if (senderIndexForLearning !== undefined) { const senderIdForLearning = this.getSenderId(senderIndexForLearning); // Release it right away, so it can be claimed by the new device this._senderIdManager.release(senderIndexForLearning); await this.coreGateway.startLearning(learnTimeSec, senderIdForLearning); this.log.info(`Start learning (teach-in) for ${learnTimeSec} s with sender ID ${senderIdForLearning.toString()}`); } } async stopLearning() { this.coreGateway.stopLearning(); } /** * This will make tha gateway aware of the new device. * * @param config The configuration of the new device. Mainly the the EnOcean ID. * * The DeviceTeachInEvent will be called. */ async teachDevice(config) { // This will raise a manual teach-in event await this.coreGateway.teachDevice(config.devId, config.eepId, config.manufacturerId); if (config.name !== undefined) { const info = this.coreGateway.getDeviceInfo(config.devId); if (info !== undefined) { info.label = config.name; } } } get coreGateway() { if (this._coreGateway === undefined) { throw 'coreGateway not initialized. Call start() before accessing this property.'; } return this._coreGateway; } coreGateway_receivedEepMessage(message, telegram) { const senderId = telegram.sender; const link = this._registeredEnoAccessories.get(senderId.toString()); if (link === undefined) { if (!this.isLocalSenderId(senderId)) { // Detect own send and repeated telegrams this._log.warn(`${senderId} no such EnOceanID accessory`); this._registeredEnoAccessories.forEach((value, key) => { this._log.warn(`${key} ${value.config.name}`); }); } return; } if (message instanceof ParsedMessage_1.ParsedMessage) { link.eepMessageReceived(message); } else { this._log.warn(`Received unexpected EEPMessage. Expected ParsedDataMessage but got ${typeof message}`); } } /** * Return the last configuration found with and MCS telegram. * Once this has been read, it will be reset to undefined. */ get armedConfig() { const config = this._armedConfig; this._armedConfig = undefined; return config; } coreDeviceInfo() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const coreKnownDevices = this.coreGateway.knownDevices; // eslint-disable-next-line @typescript-eslint/no-explicit-any const table3 = []; // eslint-disable-next-line @typescript-eslint/no-unused-vars coreKnownDevices.forEach((info, device) => { table3.push({ deviceId: info.deviceId.toString(), localId: info.localId.toString(), manufacturer: EnOCore.Manufacturers[info.manufacturer], teachInMethod: EnOCore.TeachInMethods[info.teachInMethod], label: info.label, eep: info.eep.toString(), }); }); return table3; } _armedConfig; mscParser = new EepParser_D1_Eltako_1.EepParser_D1_Eltako(); async coreGateway_receivedErp1Telegram(telegram) { // This method is intercepting all received telegrams. // So we can use it to trap the manufacturer specific MSC messages. if (telegram.rorg === EnOCore.RORGs.MSC) { const mscMessage = this.mscParser.parse(telegram); if (!this.isAutoCreateEnabled) { this.log.debug(`RX: ${telegram.sender.toString()} ${this.mscParser.toString(mscMessage.values)} (Auto-create disabled)`); return; } this.log.info(`RX: ${telegram.sender.toString()} ${this.mscParser.toString(mscMessage.values)}`); const message = `Received Eltako teach-in request from '${telegram.sender}'`; if (mscMessage.values.msc?.config !== undefined) { // Check if this device is already known const info = this.coreGateway.getDeviceInfo(telegram.sender); if (info === undefined) { // Teach-in this new device. Provide the full config for platform this.log.info(`${message} -> adding device...`); this._armedConfig = mscMessage.values.msc.config; await this.teachDevice(mscMessage.values.msc.config); } else { // Device already known this.log.info(message); this.log.info(`${telegram.sender}: is already known as: '${info.label}'`); setTimeout(() => { this.log.info(`Sending learn telegram to '${info.label}' with local ID '${info.localId.toString()}'`); const erp1TeachIn = EnoMessageFactory_1.EnoMessageFactory .newFourBSTeachInMessage(info.localId, info.eep, info.manufacturer); this.enqueueErp1Telegram(erp1TeachIn); }, 3000); } } } } coreGateway_receivedErp1TelegramUnfamiliar(telegram, reason, error) { const reasonString = EnOCore.ERP1TelegramUnfamiliarityReasons[reason]; switch (reason) { case EnOCore.ERP1TelegramUnfamiliarityReasons.EEPParsingError: this.log.error('ParsingError: ', error?.message, '\n', error?.stack); break; default: if (this.coreGateway.isLearning) { this.log.info(`coreGateway_ReceivedERP1TelegramUnfamiliar (while learning):\n${telegram}\nReason: ${reasonString}\nError: ${error}`); } break; } } rssiQuality(rssi) { let q = 'bad'; // < -87 dBm (repeater required) if (rssi >= -76) { q = 'excellent'; // internal standard antenna sufficiently } else if (rssi >= -87) { // good antenna necessary q = 'good'; } return q; } ensureEepParser(eepId) { if (!this.coreGateway.hasEEPParser(eepId)) { const eepParser = this._eepParserFactory.newParser(eepId.toString()); this.log.debug(`Registering parser ${eepParser.constructor.name} for EEP ${eepId.toString()}`); this.coreGateway.setEEPParser(eepId, (telegram) => { // This is the anonymous parser of type const srce = `RX: SRCE:${telegram.sender.toString()}`; const dest = `DEST:${telegram.destination.toString()}`; const data = ` ${EnOCore.RORGs[telegram.rorg]}` + ` '${telegram.userData.toString('hex').toUpperCase()}'-'${util_1.Util.toHexString(telegram.status)}'`; // The easiest way to inject the manufacturer information const devInfo = this.coreGateway.getDeviceInfo(telegram.sender); if (devInfo !== undefined) { eepParser.manufacturerId = devInfo.manufacturer; } else if (this.isLocalSenderId(telegram.sender)) { // This is a local sender ID, so we can ignore it const repeatCount = telegram.status & 0x0f; this.log.debug(`${srce} ${dest} ${data} -- sender id is a local ID, destination is known (own repeated message, count=${repeatCount}). IGNORED!`); return new ParsedMessage_1.ParsedMessage(telegram, eepParser); } else { // Got this message because the destination is known to us // Do not parse in any way this.log.info(`${srce} ${dest} ${data} -- sender unknown, but destination is known (gateway case in enocean-core). IGNORED!`); return new ParsedMessage_1.ParsedMessage(telegram, eepParser); } const type = util_1.Util.toHexString(eepParser.eepType); const info = ` ${eepParser.constructor.name}-${type} ${EnOCore.Manufacturers[eepParser.manufacturerId]}`; this.log.debug(`${srce} ${data} ${info}`); if (this.rssiQuality(-telegram.signalStrength) === 'bad') { const rssi = ` RSSI -${telegram.signalStrength} dBm (bad), consider a repeater`; this.log.warn(`${srce} (${devInfo.label}) ${rssi}`); } return eepParser.parse(telegram); }); } } async sendErp1TelegramIntern(telegram) { const result = await this.coreGateway.sendERP1Telegram(telegram); const message = telegram.toESP3Packet().toString(); if (result === EnOCore.SendingResults.Success) { this.log.debug(`TX: ${message}`); } else { throw `${message}: ${EnOCore.SendingResults[result]}`; } } } exports.EnoGateway = EnoGateway; //# sourceMappingURL=EnoGateway.js.map