UNPKG

enocean-cli

Version:

EnOcean CLI tool to manage EnOcean dongle and devices

1,118 lines (1,104 loc) 33.4 kB
#!/usr/bin/env node 'use strict'; var commander = require('commander'); var prompts = require('@inquirer/prompts'); var chalk4 = require('chalk'); var serialport = require('serialport'); var os = require('os'); var path = require('path'); var fs = require('fs/promises'); var events = require('events'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var chalk4__default = /*#__PURE__*/_interopDefault(chalk4); var path__default = /*#__PURE__*/_interopDefault(path); var fs__default = /*#__PURE__*/_interopDefault(fs); // package.json var package_default = { name: "enocean-cli", version: "0.12.2", private: false, description: "EnOcean CLI tool to manage EnOcean dongle and devices", keywords: [ "enocean", "cli" ], homepage: "https://github.com/FrancoisLef/enocean", bugs: "https://github.com/FrancoisLef/enocean/issues", repository: { type: "git", url: "git+https://github.com/FrancoisLef/enocean.git" }, license: "MIT", author: "Fran\xE7ois Lefebvre", type: "module", main: "dist/index.cjs", types: "dist/index.d.ts", bin: { enocean: "dist/bin.cjs" }, files: [ "./dist" ], scripts: { build: "tsup && pkg .", dev: "tsx src/bin.ts", format: "prettier --write .", lint: "eslint src/**/*.ts", release: "semantic-release", "release:version": "semantic-release --dry-run", test: "vitest" }, dependencies: { "@inquirer/prompts": "7.8.0", chalk: "4.1.2", commander: "14.0.0", serialport: "13.0.0" }, devDependencies: { "@eslint/js": "9.32.0", "@semantic-release/changelog": "6.0.3", "@semantic-release/exec": "7.1.0", "@semantic-release/git": "10.0.1", "@semantic-release/github": "11.0.3", "@trivago/prettier-plugin-sort-imports": "5.2.2", "@types/node": "24.2.0", "@vitest/eslint-plugin": "1.3.4", "@yao-pkg/pkg": "6.6.0", eslint: "9.32.0", "eslint-config-prettier": "10.1.8", prettier: "3.6.2", "prettier-plugin-packagejson": "2.5.19", "semantic-release": "24.2.7", shx: "0.4.0", tsup: "8.5.0", tsx: "4.20.3", typescript: "5.9.2", "typescript-eslint": "8.39.0", vitest: "3.2.4" }, engines: { node: ">=18.0.0" }, publishConfig: { access: "public", provenance: true, registry: "https://registry.npmjs.org/" }, pkg: { scripts: "dist/**/*.cjs", assets: [ "node_modules/serialport/**/*", "node_modules/@serialport/**/*" ], outputPath: "binaries", targets: [ "node18-alpine-arm64", "node18-linux-x64", "node18-win-x64", "node18-macos-arm64" ] } }; var FileStorage = class { /** * Ensures that a file exists at the specified path. * If the file does not exist, it creates the file and initializes it with an empty object. * If the parent directory does not exist, it creates the directory recursively. */ async accessFile(...paths) { const file = path__default.default.join(...paths); try { await fs__default.default.access(file); } catch { await fs__default.default.mkdir(path__default.default.dirname(file), { recursive: true }); await fs__default.default.writeFile(file, JSON.stringify({}), "utf8"); } } async readJSON(...paths) { const file = path__default.default.join(...paths); const content = await fs__default.default.readFile(file, "utf8"); return JSON.parse(content); } async writeJSON(data, ...paths) { const file = path__default.default.join(...paths); await fs__default.default.writeFile(file, JSON.stringify(data, null, 2), "utf8"); return data; } }; // src/shared/storage/cache.storage.ts var CacheStorage = class { data; file; storage; constructor(filePath) { this.data = {}; this.file = filePath ?? path.join(os.homedir(), ".cache", "enocean", "cache.json"); this.storage = new FileStorage(); } get(key) { return this.data[key]; } getAll() { return { ...this.data }; } async init() { await this.storage.accessFile(this.file); this.data = await this.storage.readJSON(this.file); return this; } async set(keyOrData, value) { if (typeof keyOrData === "string") { this.data[keyOrData] = value; } else { Object.assign(this.data, keyOrData); } await this.storage.writeJSON(this.data, this.file); return this; } }; // src/command.ts var BaseCommand = class { cache; cli; constructor() { this.cache = new CacheStorage(); this.cli = package_default; } async init() { await this.cache.init(); } handleError(error) { if (error.name === "ExitPromptError") { console.log(chalk4__default.default.dim("\n\u{1F44B} Operation cancelled by user")); process.exit(0); } console.error( `${chalk4__default.default.red("\u2716")} ${chalk4__default.default.red.bold("Error:")} ${error.message}` ); process.exit(1); } async run() { try { await this.init(); await this.execute(); } catch (error) { this.handleError(error); } } }; // src/commands/configure.ts var ConfigureCommand = class extends BaseCommand { async execute() { const ports = await serialport.SerialPort.list(); const paths = ports.map((port2) => port2.path); const port = await prompts.select({ choices: paths.map((path2) => ({ name: path2 === this.cache.get("dongle:port") ? `${chalk4__default.default.bold(path2)} ${chalk4__default.default.italic.dim("(current)")}` : path2, short: path2, value: path2 })), default: this.cache.get("dongle:port"), instructions: { navigation: "<\u2191 \u2193> arrow keys to navigate and <enter> to confirm.", pager: "More options available (use arrow keys \u2191 \u2193)" }, message: "Select your EnOcean device:", theme: { helpMode: "always" } }); const baud = await prompts.number({ default: this.cache.get("dongle:baud") || 57600, message: "Enter the baud rate:" }) ?? 57600; await this.cache.set({ "dongle:baud": baud, "dongle:configured": true, "dongle:port": port }); console.log( `${chalk4__default.default.green("\u2714")} ${chalk4__default.default.bold("Device configured successfully")}` ); } }; // src/libraries/enocean/bit-operations.ts var BitOperations = class { /** * Extracts a single bit from a byte * @param value - The byte value to extract from * @param bitPosition - Bit position (0-7) * @returns 1 if bit is set, 0 otherwise */ static extractBit(value, bitPosition) { return value >> bitPosition & 1; } /** * Extracts bits from a byte using start and end positions * @param value - The byte value to extract from * @param startBit - Starting bit position (0-7) * @param endBit - Ending bit position (0-7), inclusive * @returns Extracted bits as a number */ static extractBits(value, startBit, endBit) { const bitCount = endBit - startBit + 1; const mask = (1 << bitCount) - 1; return value >> startBit & mask; } /** * Extracts the lower nibble (4 bits) from a byte * @param value - The byte value * @returns Lower 4 bits (0-15) */ static getLowerNibble(value) { return this.extractBits(value, 0, 3); } /** * Extracts the upper nibble (4 bits) from a byte * @param value - The byte value * @returns Upper 4 bits (0-15) */ static getUpperNibble(value) { return this.extractBits(value, 4, 7); } /** * Checks if a specific bit is set in a byte * @param value - The byte value to check * @param bitPosition - Bit position (0-7) * @returns true if bit is set, false otherwise */ static isBitSet(value, bitPosition) { return this.extractBit(value, bitPosition) === 1; } /** * Checks if the most significant bit (bit 7) is set * @param value - The byte value to check * @returns true if MSB is set, false otherwise */ static isMostSignificantBitSet(value) { return this.isBitSet(value, 7); } /** * Shifts a value left by specified positions * @param value - Value to shift * @param positions - Number of positions to shift * @returns Shifted value */ static shiftLeft(value, positions) { return value << positions; } /** * Shifts a value right by specified positions * @param value - Value to shift * @param positions - Number of positions to shift * @returns Shifted value */ static shiftRight(value, positions) { return value >> positions; } /** * Masks a value to keep only the lowest 8 bits * @param value - Value to mask * @returns Value with only lower 8 bits */ static toByte(value) { return value & 255; } /** * Applies XOR operation between two values * @param a - First value * @param b - Second value * @returns XOR result */ static xor(a, b) { return a ^ b; } }; var CRC8Calculator = class { static POLYNOMIAL = 7; // EnOcean CRC8 polynomial /** * Calculates CRC8 for the given data buffer * @param data - Data buffer to calculate CRC for * @returns CRC8 value */ static calculate(data) { let crc = 0; for (const byte of data) { crc = BitOperations.xor(crc, byte); for (let bitIndex = 0; bitIndex < 8; bitIndex++) { if (BitOperations.isMostSignificantBitSet(crc)) { crc = BitOperations.shiftLeft(crc, 1); crc = BitOperations.xor(crc, this.POLYNOMIAL); } else { crc = BitOperations.shiftLeft(crc, 1); } crc = BitOperations.toByte(crc); } } return crc; } }; var ByteFieldExtractor = class { /** * Extracts boolean state from a specific bit * @param value - Byte containing the state bit * @param bitPosition - Position of the state bit * @returns Boolean state */ static extractBooleanState(value, bitPosition = 0) { return BitOperations.isBitSet(value, bitPosition); } /** * Extracts channel number from various telegram formats * @param value - Byte containing channel information * @param mask - Bit mask to apply (e.g., 0x0f for 4 bits, 0x1f for 5 bits) * @returns Channel number */ static extractChannel(value, mask = 15) { return value & mask; } /** * Extracts command code from D2-01-12 telegram * @param commandByte - First byte of telegram data * @returns Command type */ static extractD2Command(commandByte) { const commandCode = BitOperations.getUpperNibble(commandByte); switch (commandCode) { case 1: { return "switching"; } case 2: { return "dimming"; } case 3: { return "status_request"; } case 4: { return "status_response"; } default: { return null; } } } /** * Extracts T21 bit from EnOcean status byte (indicates press/release) * @param statusByte - Status byte from telegram * @returns Action type: 'pressed' or 'released' */ static extractRockerAction(statusByte) { const t21Bit = BitOperations.extractBit(statusByte, 5); return t21Bit === 1 ? "pressed" : "released"; } /** * Extracts a 2-bit field representing rocker switch position * @param value - Byte containing the field * @param startBit - Starting bit position * @returns Rocker position: 'down', 'none', 'up' */ static extractRockerPosition(value, startBit) { const bits = BitOperations.extractBits(value, startBit, startBit + 1); switch (bits) { case 0: { return "none"; } case 1: { return "up"; } case 2: { return "down"; } default: { return "none"; } } } }; // src/libraries/enocean/parser.ts var EnOceanParser = class { buffer = Buffer.alloc(0); HEADER_LENGTH = 6; SYNC_BYTE = 85; /** * Ajoute des données au buffer interne * @param data - Nouvelles données à ajouter */ addData(data) { this.buffer = Buffer.concat([this.buffer, data]); } /** * Remet à zéro le buffer interne */ clearBuffer() { this.buffer = Buffer.alloc(0); } /** * Retourne la taille actuelle du buffer */ getBufferSize() { return this.buffer.length; } /** * Tente de parser les paquets disponibles dans le buffer * @returns Array des paquets parsés */ parsePackets() { const packets = []; while (this.buffer.length >= this.HEADER_LENGTH) { const syncIndex = this.buffer.indexOf(this.SYNC_BYTE); if (syncIndex === -1) { this.buffer = Buffer.alloc(0); break; } if (syncIndex > 0) { this.buffer = this.buffer.slice(syncIndex); } if (this.buffer.length < this.HEADER_LENGTH) { break; } const header = this.parseHeader(this.buffer.slice(0, this.HEADER_LENGTH)); if (!header) { this.buffer = this.buffer.slice(1); continue; } const totalPacketSize = this.HEADER_LENGTH + header.dataLength + header.optionalLength + 1; if (this.buffer.length < totalPacketSize) { break; } const dataStart = this.HEADER_LENGTH; const data = this.buffer.slice(dataStart, dataStart + header.dataLength); const optionalData = this.buffer.slice( dataStart + header.dataLength, dataStart + header.dataLength + header.optionalLength ); const checksum = this.buffer[totalPacketSize - 1]; const packetData = this.buffer.slice( this.HEADER_LENGTH, totalPacketSize - 1 ); const calculatedChecksum = CRC8Calculator.calculate(packetData); if (calculatedChecksum !== checksum) { console.warn( `Checksum invalide pour le paquet: calcul\xE9=${calculatedChecksum}, re\xE7u=${checksum}` ); this.buffer = this.buffer.slice(1); continue; } const packet = { checksum, data, header, optionalData }; packets.push(packet); this.buffer = this.buffer.slice(totalPacketSize); } return packets; } /** * Parse un télégrame radio * @param data - Données du télégrame * @param optionalData - Données optionnelles * @returns Télégrame radio parsé */ parseRadioTelegram(data, optionalData) { if (data.length < 6) { console.warn("T\xE9l\xE9grame radio trop court"); return null; } const rorg = data[0]; const userData = data.slice(1, -5); const senderId = data.readUInt32BE(data.length - 5); const status = data[data.length - 1]; let subTelNum = 0; let destinationId = 0; let dbm = -100; let securityLevel = 0; if (optionalData.length >= 7) { subTelNum = optionalData[0]; destinationId = optionalData.readUInt32BE(1); dbm = -Math.abs(optionalData[5]); securityLevel = optionalData[6]; } return { data: userData, dbm, destinationId, rorg, securityLevel, senderId, status, subTelNum }; } /** * Parse l'en-tête d'un paquet ESP3 * @param headerBuffer - Buffer contenant l'en-tête * @returns En-tête parsé ou null si invalide */ parseHeader(headerBuffer) { if (headerBuffer.length < this.HEADER_LENGTH) { return null; } const syncByte = headerBuffer[0]; if (syncByte !== this.SYNC_BYTE) { return null; } const dataLength = headerBuffer.readUInt16BE(1); const optionalLength = headerBuffer[3]; const packetType = headerBuffer[4]; const crc8Header = headerBuffer[5]; const headerForCRC = headerBuffer.slice(1, 5); const calculatedCRC = CRC8Calculator.calculate(headerForCRC); if (calculatedCRC !== crc8Header) { console.warn( `CRC invalide dans l'en-t\xEAte: calcul\xE9=${calculatedCRC}, re\xE7u=${crc8Header}` ); return null; } return { crc8Header, dataLength, optionalLength, packetType, syncByte }; } }; // src/libraries/enocean/profiles.ts var EEPDecoder = class { /** * Décode un télégrame selon son profil EEP * @param telegram - Télégrame radio à décoder * @param profile - Profil EEP à utiliser (optionnel, détecté automatiquement sinon) * @returns Données décodées ou null si impossible */ static decode(telegram, profile) { const detectedProfile = profile || this.detectProfile(telegram); if (!detectedProfile) { return null; } switch (detectedProfile) { case "D2-01-12" /* D2_01_12 */: { return this.decodeD2_01_12(telegram); } case "F6-02-01" /* F6_02_01 */: { return this.decodeF6_02_01(telegram); } default: { return null; } } } /** * Détermine le profil EEP basé sur le RORG et potentiellement d'autres données * @param telegram - Télégrame radio à analyser * @returns Profil EEP détecté ou null */ static detectProfile(telegram) { switch (telegram.rorg) { case 246 /* RPS */: { return "F6-02-01" /* F6_02_01 */; } case 210 /* VLD */: { if (telegram.data.length > 0) { const command = ByteFieldExtractor.extractD2Command(telegram.data[0]); if (command !== null) { return "D2-01-12" /* D2_01_12 */; } } break; } } return null; } /** * Retourne une description textuelle d'un profil EEP * @param profile - Profil EEP * @returns Description du profil */ static getProfileDescription(profile) { switch (profile) { case "D2-01-12" /* D2_01_12 */: { return "Electronic switches and dimmers with Energy Measurement and Local Control"; } case "F6-02-01" /* F6_02_01 */: { return "Rocker Switch, 2 Rocker"; } default: { return "Profil inconnu"; } } } /** * Décode un télégrame D2-01-12 (Nodon Relay Switch) * @param telegram - Télégrame radio * @returns Données décodées D2-01-12 */ static decodeD2_01_12(telegram) { if (telegram.data.length === 0) { return null; } const cmdByte = telegram.data[0]; const command = ByteFieldExtractor.extractD2Command(cmdByte); if (command === null) { console.warn( `Commande D2-01-12 inconnue: ${ByteFieldExtractor.extractChannel(cmdByte, 240)}` ); return null; } let channel = ByteFieldExtractor.extractChannel(cmdByte); let outputValue = 0; let outputState = false; let dimValue; let dimTime; switch (command) { case "dimming": { if (telegram.data.length >= 3) { channel = ByteFieldExtractor.extractChannel(telegram.data[1], 31); dimValue = telegram.data[2]; outputValue = dimValue; outputState = dimValue > 0; if (telegram.data.length >= 4) { const dimTimeRaw = telegram.data[3]; if (dimTimeRaw === 0) { dimTime = 0; } else if (dimTimeRaw <= 127) { dimTime = dimTimeRaw; } else { dimTime = (dimTimeRaw - 127) * 60; } } } break; } case "status_request": { if (telegram.data.length >= 2) { channel = ByteFieldExtractor.extractChannel(telegram.data[1], 31); } break; } case "status_response": { if (telegram.data.length >= 3) { channel = ByteFieldExtractor.extractChannel(telegram.data[1], 31); outputValue = telegram.data[2]; outputState = outputValue > 0; } break; } case "switching": { if (telegram.data.length >= 2) { if (telegram.data.length >= 3) { channel = ByteFieldExtractor.extractChannel(telegram.data[1], 31); outputState = ByteFieldExtractor.extractBooleanState( telegram.data[2] ); outputValue = outputState ? 100 : 0; } else { outputState = ByteFieldExtractor.extractBooleanState( telegram.data[1] ); outputValue = outputState ? 100 : 0; } } break; } } const result = { channel, command, outputState, outputValue, profile: "D2-01-12" /* D2_01_12 */, rssi: telegram.dbm, senderId: telegram.senderId }; if (dimValue !== void 0) { result.dimValue = dimValue; } if (dimTime !== void 0) { result.dimTime = dimTime; } return result; } /** * Décode un télégrame F6-02-01 (Rocker Switch) * @param telegram - Télégrame radio * @returns Données décodées F6-02-01 */ static decodeF6_02_01(telegram) { if (telegram.data.length === 0) { return null; } const dataByte = telegram.data[0]; const rockerA = ByteFieldExtractor.extractRockerPosition(dataByte, 5); const energyBow = ByteFieldExtractor.extractBooleanState(dataByte, 4); const rockerB = ByteFieldExtractor.extractRockerPosition(dataByte, 1); const secondAction = ByteFieldExtractor.extractBooleanState(dataByte, 0); const rockerAction = ByteFieldExtractor.extractRockerAction( telegram.status ); return { energyBow, profile: "F6-02-01" /* F6_02_01 */, rockerA, rockerAction, rockerB, rssi: telegram.dbm, secondAction, senderId: telegram.senderId }; } }; // src/libraries/enocean/manager.ts var EnOceanManager = class extends events.EventEmitter { /** * Configuration par défaut du port série pour TCM 310 */ defaultSerialConfig = { baudRate: 57600, // Vitesse standard pour TCM 310 dataBits: 8, parity: "none", stopBits: 1 }; isConnected = false; maxReconnectAttempts = 5; parser; reconnectAttempts = 0; reconnectTimer = null; serialPort = null; constructor() { super(); this.parser = new EnOceanParser(); } /** * Se connecte au périphérique EnOcean * @param portPath - Chemin du port série (ex: '/dev/ttyUSB0' ou 'COM3') * @param config - Configuration optionnelle du port série */ async connect(portPath, config) { const serialConfig = { ...this.defaultSerialConfig, ...config }; try { this.serialPort = new serialport.SerialPort({ autoOpen: false, baudRate: serialConfig.baudRate, dataBits: serialConfig.dataBits, parity: serialConfig.parity, path: portPath, stopBits: serialConfig.stopBits }); this.setupSerialPortEvents(); await new Promise((resolve, reject) => { if (!this.serialPort) { reject(new Error("Port s\xE9rie non initialis\xE9")); return; } this.serialPort.open((error) => { if (error) { reject( new Error( `Impossible d'ouvrir le port ${portPath}: ${error.message}` ) ); } else { resolve(); } }); }); this.isConnected = true; this.reconnectAttempts = 0; console.log(`Connect\xE9 au stick EnOcean sur ${portPath}`); this.emit("connected", portPath); } catch (error) { console.error("Erreur de connexion:", error); this.emit("error", error); throw error; } } /** * Déconnecte du stick EnOcean */ async disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.serialPort && this.serialPort.isOpen) { await new Promise((resolve) => { this.serialPort.close(() => { resolve(); }); }); } this.isConnected = false; this.serialPort = null; this.parser.clearBuffer(); console.log("D\xE9connect\xE9 du stick EnOcean"); } /** * Retourne des statistiques sur le parser */ getParserStats() { return { bufferSize: this.parser.getBufferSize() }; } /** * Retourne l'état de connexion */ isPortConnected() { return this.isConnected && this.serialPort !== null && this.serialPort.isOpen; } /** * Envoie une commande au module EnOcean * @param data - Données à envoyer */ async sendCommand(data) { if (!this.isConnected || !this.serialPort) { throw new Error("Non connect\xE9 au stick EnOcean"); } return new Promise((resolve, reject) => { this.serialPort.write(data, (error) => { if (error) { reject(new Error(`Erreur d'envoi: ${error.message}`)); } else { resolve(); } }); }); } /** * Tente une reconnexion automatique */ attemptReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts && !this.reconnectTimer) { this.reconnectAttempts++; const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4); console.log( `Tentative de reconnexion ${this.reconnectAttempts}/${this.maxReconnectAttempts} dans ${delay}ms` ); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.emit("reconnectAttempt", this.reconnectAttempts); }, delay); } } /** * Traite les données reçues du port série * @param data - Données brutes reçues */ handleIncomingData(data) { try { this.parser.addData(data); const packets = this.parser.parsePackets(); for (const packet of packets) { this.processPacket(packet); } } catch (error) { console.error("Erreur lors du traitement des donn\xE9es:", error); this.emit("error", error); } } /** * Parse basique d'un télégrame radio (version simplifiée pour compatibilité) * @param data - Données du télégrame * @param optionalData - Données optionnelles * @returns Télégrame radio parsé */ parseRadioTelegramBasic(data, optionalData) { if (data.length < 6) { console.warn("T\xE9l\xE9grame radio trop court"); return null; } const rorg = data[0]; const userData = data.slice(1, -5); const senderId = data.readUInt32BE(data.length - 5); const status = data[data.length - 1]; let subTelNum = 0; let destinationId = 0; let dbm = -100; let securityLevel = 0; if (optionalData.length >= 7) { subTelNum = optionalData[0]; destinationId = optionalData.readUInt32BE(1); dbm = -Math.abs(optionalData[5]); securityLevel = optionalData[6]; } return { data: userData, dbm, destinationId, rorg, securityLevel, senderId, status, subTelNum }; } /** * Traite un paquet d'événement * @param packet - Paquet d'événement */ processEventPacket(packet) { console.log("\xC9v\xE9nement re\xE7u:", packet.data.toString("hex")); this.emit("event", packet.data); } /** * Traite un paquet ESP3 parsé * @param packet - Paquet à traiter */ processPacket(packet) { this.emit("packet", packet); switch (packet.header.packetType) { case 4 /* EVENT */: { this.processEventPacket(packet); break; } case 1 /* RADIO */: { this.processRadioPacket(packet); break; } case 2 /* RESPONSE */: { this.processResponsePacket(packet); break; } default: { console.log(`Type de paquet non g\xE9r\xE9: ${packet.header.packetType}`); break; } } } /** * Traite un paquet radio * @param packet - Paquet radio à traiter */ processRadioPacket(packet) { try { const radioTelegram = this.parser.parseRadioTelegram ? this.parser.parseRadioTelegram(packet.data, packet.optionalData) : this.parseRadioTelegramBasic(packet.data, packet.optionalData); if (radioTelegram) { console.log( `Paquet radio re\xE7u: RORG=0x${radioTelegram.rorg.toString(16).padStart(2, "0")} de ${radioTelegram.senderId.toString(16).padStart(8, "0")}` ); console.log(` Signal: ${radioTelegram.dbm} dBm`); this.emit("radioTelegram", radioTelegram); try { const decodedData = EEPDecoder.decode(radioTelegram); if (decodedData) { console.log(` Profil EEP d\xE9tect\xE9: ${decodedData.profile}`); this.emit("eepData", decodedData); } } catch (eepError) { console.warn("Erreur lors du d\xE9codage EEP:", eepError); } } } catch (error) { console.error("Erreur lors du traitement du paquet radio:", error); this.emit("error", error); } } /** * Traite un paquet de réponse * @param packet - Paquet de réponse */ processResponsePacket(packet) { console.log("R\xE9ponse re\xE7ue du module:", packet.data.toString("hex")); this.emit("response", packet.data); } /** * Configure les événements du port série */ setupSerialPortEvents() { if (!this.serialPort) return; this.serialPort.on("data", (data) => { this.handleIncomingData(data); }); this.serialPort.on("error", (error) => { console.error("Erreur du port s\xE9rie:", error); this.isConnected = false; this.emit("error", error); this.attemptReconnect(); }); this.serialPort.on("close", () => { console.log("Port s\xE9rie ferm\xE9"); this.isConnected = false; this.emit("disconnected"); this.attemptReconnect(); }); } }; // src/commands/listen.ts var ListenCommand = class extends BaseCommand { async execute() { const { "dongle:baud": baud, "dongle:configured": isConfigured, "dongle:port": port } = this.cache.getAll(); if (!isConfigured || !port || !baud) { throw new Error( "The dongle is not configured. Run `enocean configure` to set up the dongle." ); } const manager = new EnOceanManager(); manager.on("eepData", (data) => { if (data.profile === "F6-02-01") { console.log( `${chalk4__default.default.blue("Switch")} ${chalk4__default.default.dim(data.senderId.toString(16))}: Rocker A=${data.rockerA}, Action=${data.rockerAction}` ); } else if (data.profile === "D2-01-12") { console.log( `${chalk4__default.default.green("Relay")} ${chalk4__default.default.dim(data.senderId.toString(16))}: Channel=${data.channel}, State=${data.outputState}, Value=${data.outputValue}%` ); } }); console.log( `${chalk4__default.default.blue("\u{1F3A7}")} ${chalk4__default.default.bold("Listening for EnOcean telegrams on")} ${chalk4__default.default.yellow(port)} ${chalk4__default.default.dim(`(${baud} baud)`)}` ); console.log(chalk4__default.default.italic.dim("Press Ctrl+C to stop")); manager.connect(port, { baudRate: baud }); } }; var UpdateCommand = class extends BaseCommand { async execute() { const { version, name, homepage } = this.cli; console.log(`\u{1F50D} Checking for updates for ${chalk4__default.default.bold(name)}\u2026`); const response = await fetch(`https://registry.npmjs.org/${name}/latest`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const { version: latest } = await response.json() || { version: "" }; if (version === latest) { console.log(`\u{1F4E6} ${chalk4__default.default.dim("Current:")} ${chalk4__default.default.bold.blue(version)}`); console.log(`\u{1F4E6} ${chalk4__default.default.dim("Latest:")} ${chalk4__default.default.dim(latest)}`); console.log( `${chalk4__default.default.green("\u2714")} ${chalk4__default.default.bold("You are already using the latest version!")}` ); } else { console.log(`\u{1F4E6} ${chalk4__default.default.dim("Current:")} ${chalk4__default.default.blue(version)}`); console.log(`\u{1F4E6} ${chalk4__default.default.dim("Latest:")} ${chalk4__default.default.bold.green(latest)}`); console.log( `${chalk4__default.default.cyan("\u{1F195}")} ${chalk4__default.default.bold("A new version is available!")}` ); console.log(""); console.log(chalk4__default.default.dim("To update:")); console.log(""); console.log("\u{1F4BE} Binary download:"); console.log(` ${chalk4__default.default.blue(`${homepage}/releases`)}`); console.log(""); console.log("\u{1F4E6} Via npm:"); console.log(` ${chalk4__default.default.green(`npm install -g ${name}@latest`)}`); console.log(""); console.log("\u{1F9F6} Via yarn:"); console.log(` ${chalk4__default.default.green(`yarn global add ${name}@latest`)}`); } } }; // src/bin.ts var program = new commander.Command(); program.name(package_default.name).description(package_default.description).version(package_default.version); program.command("configure").description("Configure dongle").action(() => new ConfigureCommand().run()); program.command("listen").description("Listen for telegrams").action(() => new ListenCommand().run()); program.command("update").description("Check for CLI updates").action(() => new UpdateCommand().run()); program.parse();