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