enocean-cli
Version:
EnOcean CLI tool to manage EnOcean dongle and devices
1,118 lines (1,104 loc) • 33.4 kB
JavaScript
;
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();