node-red-contrib-victron-ble
Version:
node-red node to parse Instant Readout advertisement data from Victron BLE devices
354 lines (348 loc) • 17.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BitReader = exports.kelvinToCelsius = exports.Device = exports.ACInState = exports.AlarmNotification = exports.AlarmReason = exports.OffReason = exports.ChargerError = exports.OperationMode = exports.EnumField = void 0;
const crypto_1 = require("crypto");
require("reflect-metadata");
// Decorator to mark a property as an enum field
function EnumField(enumType) {
return function (target, propertyKey) {
Reflect.defineMetadata('enumType', enumType, target, propertyKey);
};
}
exports.EnumField = EnumField;
// Sourced from VE.Direct docs
var OperationMode;
(function (OperationMode) {
OperationMode[OperationMode["OFF"] = 0] = "OFF";
OperationMode[OperationMode["LOW_POWER"] = 1] = "LOW_POWER";
OperationMode[OperationMode["FAULT"] = 2] = "FAULT";
OperationMode[OperationMode["BULK"] = 3] = "BULK";
OperationMode[OperationMode["ABSORPTION"] = 4] = "ABSORPTION";
OperationMode[OperationMode["FLOAT"] = 5] = "FLOAT";
OperationMode[OperationMode["STORAGE"] = 6] = "STORAGE";
OperationMode[OperationMode["EQUALIZE_MANUAL"] = 7] = "EQUALIZE_MANUAL";
OperationMode[OperationMode["INVERTING"] = 9] = "INVERTING";
OperationMode[OperationMode["POWER_SUPPLY"] = 11] = "POWER_SUPPLY";
OperationMode[OperationMode["STARTING_UP"] = 245] = "STARTING_UP";
OperationMode[OperationMode["REPEATED_ABSORPTION"] = 246] = "REPEATED_ABSORPTION";
OperationMode[OperationMode["RECONDITION"] = 247] = "RECONDITION";
OperationMode[OperationMode["BATTERY_SAFE"] = 248] = "BATTERY_SAFE";
OperationMode[OperationMode["ACTIVE"] = 249] = "ACTIVE";
OperationMode[OperationMode["EXTERNAL_CONTROL"] = 252] = "EXTERNAL_CONTROL";
OperationMode[OperationMode["NOT_AVAILABLE"] = 255] = "NOT_AVAILABLE";
})(OperationMode || (exports.OperationMode = OperationMode = {}));
// Source: VE.Direct-Protocol-3.32.pdf & https://www.victronenergy.com/live/mppt-error-codes
var ChargerError;
(function (ChargerError) {
// No error
ChargerError[ChargerError["NO_ERROR"] = 0] = "NO_ERROR";
// Err 1 - Battery temperature too high
ChargerError[ChargerError["TEMPERATURE_BATTERY_HIGH"] = 1] = "TEMPERATURE_BATTERY_HIGH";
// Err 2 - Battery voltage too high
ChargerError[ChargerError["VOLTAGE_HIGH"] = 2] = "VOLTAGE_HIGH";
// Err 3 - Remote temperature sensor failure (auto-reset)
ChargerError[ChargerError["REMOTE_TEMPERATURE_A"] = 3] = "REMOTE_TEMPERATURE_A";
// Err 4 - Remote temperature sensor failure (auto-reset)
ChargerError[ChargerError["REMOTE_TEMPERATURE_B"] = 4] = "REMOTE_TEMPERATURE_B";
// Err 5 - Remote temperature sensor failure (not auto-reset)
ChargerError[ChargerError["REMOTE_TEMPERATURE_C"] = 5] = "REMOTE_TEMPERATURE_C";
// Err 6 - Remote battery voltage sense failure
ChargerError[ChargerError["REMOTE_BATTERY_A"] = 6] = "REMOTE_BATTERY_A";
// Err 7 - Remote battery voltage sense failure
ChargerError[ChargerError["REMOTE_BATTERY_B"] = 7] = "REMOTE_BATTERY_B";
// Err 8 - Remote battery voltage sense failure
ChargerError[ChargerError["REMOTE_BATTERY_C"] = 8] = "REMOTE_BATTERY_C";
// Err 11 - Battery high ripple voltage
ChargerError[ChargerError["HIGH_RIPPLE"] = 11] = "HIGH_RIPPLE";
// Err 14 - Battery temperature too low
ChargerError[ChargerError["TEMPERATURE_BATTERY_LOW"] = 14] = "TEMPERATURE_BATTERY_LOW";
// Err 17 - Charger temperature too high
ChargerError[ChargerError["TEMPERATURE_CHARGER"] = 17] = "TEMPERATURE_CHARGER";
// Err 18 - Charger over current
ChargerError[ChargerError["OVER_CURRENT"] = 18] = "OVER_CURRENT";
// Err 20 - Bulk time limit exceeded
ChargerError[ChargerError["BULK_TIME"] = 20] = "BULK_TIME";
// Err 21 - Current sensor issue (sensor bias/sensor broken)
ChargerError[ChargerError["CURRENT_SENSOR"] = 21] = "CURRENT_SENSOR";
// Err 22 - Internal temperature sensor failure
ChargerError[ChargerError["INTERNAL_TEMPERATURE_A"] = 22] = "INTERNAL_TEMPERATURE_A";
// Err 23 - Internal temperature sensor failure
ChargerError[ChargerError["INTERNAL_TEMPERATURE_B"] = 23] = "INTERNAL_TEMPERATURE_B";
// Err 24 - Fan failure
ChargerError[ChargerError["FAN"] = 24] = "FAN";
// Err 26 - Terminals overheated
ChargerError[ChargerError["OVERHEATED"] = 26] = "OVERHEATED";
// Err 27 - Charger short circuit
ChargerError[ChargerError["SHORT_CIRCUIT"] = 27] = "SHORT_CIRCUIT";
// Err 28 - Power stage issue Converter issue (dual converter models only)
ChargerError[ChargerError["CONVERTER_ISSUE"] = 28] = "CONVERTER_ISSUE";
// Err 29 - Over-Charge protection
ChargerError[ChargerError["OVER_CHARGE"] = 29] = "OVER_CHARGE";
// Err 33 - Input voltage too high (solar panel) PV over-voltage
ChargerError[ChargerError["INPUT_VOLTAGE"] = 33] = "INPUT_VOLTAGE";
// Err 34 - Input current too high (solar panel) PV over-current
ChargerError[ChargerError["INPUT_CURRENT"] = 34] = "INPUT_CURRENT";
// Err 35 - PV over-power
ChargerError[ChargerError["INPUT_POWER"] = 35] = "INPUT_POWER";
// Err 38 - Input shutdown (due to excessive battery voltage)
ChargerError[ChargerError["INPUT_SHUTDOWN_VOLTAGE"] = 38] = "INPUT_SHUTDOWN_VOLTAGE";
// Err 39 - Input shutdown (due to current flow during off mode)
ChargerError[ChargerError["INPUT_SHUTDOWN_CURRENT"] = 39] = "INPUT_SHUTDOWN_CURRENT";
// Err 40 - PV Input failed to shutdown
ChargerError[ChargerError["INPUT_SHUTDOWN_FAILURE"] = 40] = "INPUT_SHUTDOWN_FAILURE";
// Err 41 - Inverter shutdown (PV isolation)
ChargerError[ChargerError["INVERTER_SHUTDOWN_41"] = 41] = "INVERTER_SHUTDOWN_41";
// Err 42 - Inverter shutdown (PV isolation)
ChargerError[ChargerError["INVERTER_SHUTDOWN_42"] = 42] = "INVERTER_SHUTDOWN_42";
// Err 43 - Inverter shutdown (Ground Fault)
ChargerError[ChargerError["INVERTER_SHUTDOWN_43"] = 43] = "INVERTER_SHUTDOWN_43";
// Err 50 - Inverter overload
ChargerError[ChargerError["INVERTER_OVERLOAD"] = 50] = "INVERTER_OVERLOAD";
// Err 51 - Inverter temperature too high
ChargerError[ChargerError["INVERTER_TEMPERATURE"] = 51] = "INVERTER_TEMPERATURE";
// Err 52 - Inverter peak current
ChargerError[ChargerError["INVERTER_PEAK_CURRENT"] = 52] = "INVERTER_PEAK_CURRENT";
// Err 53 - Inverter output voltage
ChargerError[ChargerError["INVERTER_OUPUT_VOLTAGE_A"] = 53] = "INVERTER_OUPUT_VOLTAGE_A";
// Err 54 - Inverter output voltage
ChargerError[ChargerError["INVERTER_OUPUT_VOLTAGE_B"] = 54] = "INVERTER_OUPUT_VOLTAGE_B";
// Err 55 - Inverter self test failed
ChargerError[ChargerError["INVERTER_SELF_TEST_A"] = 55] = "INVERTER_SELF_TEST_A";
// Err 56 - Inverter self test failed
ChargerError[ChargerError["INVERTER_SELF_TEST_B"] = 56] = "INVERTER_SELF_TEST_B";
// Err 57 - Inverter ac voltage on output
ChargerError[ChargerError["INVERTER_AC"] = 57] = "INVERTER_AC";
// Err 58 - Inverter self test failed
ChargerError[ChargerError["INVERTER_SELF_TEST_C"] = 58] = "INVERTER_SELF_TEST_C";
// Information 65 - Communication warning Lost communication with one of devices
ChargerError[ChargerError["COMMUNICATION"] = 65] = "COMMUNICATION";
// Information 66 - Incompatible device Synchronised charging device configuration issue
ChargerError[ChargerError["SYNCHRONISATION"] = 66] = "SYNCHRONISATION";
// Err 67 - BMS Connection lost
ChargerError[ChargerError["BMS"] = 67] = "BMS";
// Err 68 - Network misconfigured
ChargerError[ChargerError["NETWORK_A"] = 68] = "NETWORK_A";
// Err 69 - Network misconfigured
ChargerError[ChargerError["NETWORK_B"] = 69] = "NETWORK_B";
// Err 70 - Network misconfigured
ChargerError[ChargerError["NETWORK_C"] = 70] = "NETWORK_C";
// Err 71 - Network misconfigured
ChargerError[ChargerError["NETWORK_D"] = 71] = "NETWORK_D";
// Err 80 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_80"] = 80] = "PV_INPUT_SHUTDOWN_80";
// Err 81 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_81"] = 81] = "PV_INPUT_SHUTDOWN_81";
// Err 82 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_82"] = 82] = "PV_INPUT_SHUTDOWN_82";
// Err 83 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_83"] = 83] = "PV_INPUT_SHUTDOWN_83";
// Err 84 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_84"] = 84] = "PV_INPUT_SHUTDOWN_84";
// Err 85 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_85"] = 85] = "PV_INPUT_SHUTDOWN_85";
// Err 86 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_86"] = 86] = "PV_INPUT_SHUTDOWN_86";
// Err 87 - PV Input shutdown
ChargerError[ChargerError["PV_INPUT_SHUTDOWN_87"] = 87] = "PV_INPUT_SHUTDOWN_87";
// Err 114 - CPU temperature too high
ChargerError[ChargerError["CPU_TEMPERATURE"] = 114] = "CPU_TEMPERATURE";
// Err 116 - Factory calibration data lost
ChargerError[ChargerError["CALIBRATION_LOST"] = 116] = "CALIBRATION_LOST";
// Err 117 - Invalid/incompatible firmware
ChargerError[ChargerError["FIRMWARE"] = 117] = "FIRMWARE";
// Err 119 - Settings data lost
ChargerError[ChargerError["SETTINGS"] = 119] = "SETTINGS";
// Err 121 - Tester fail
ChargerError[ChargerError["TESTER_FAIL"] = 121] = "TESTER_FAIL";
// Err 200 - Internal DC voltage error
ChargerError[ChargerError["INTERNAL_DC_VOLTAGE_A"] = 200] = "INTERNAL_DC_VOLTAGE_A";
// Err 201 - Internal DC voltage error
ChargerError[ChargerError["INTERNAL_DC_VOLTAGE_B"] = 201] = "INTERNAL_DC_VOLTAGE_B";
// Err 202 - PV residual current sensor self-test failure Internal GFCI sensor error
ChargerError[ChargerError["SELF_TEST"] = 202] = "SELF_TEST";
// Err 203 - Internal supply voltage error
ChargerError[ChargerError["INTERNAL_SUPPLY_A"] = 203] = "INTERNAL_SUPPLY_A";
// Err 205 - Internal supply voltage error
ChargerError[ChargerError["INTERNAL_SUPPLY_B"] = 205] = "INTERNAL_SUPPLY_B";
// Err 212 - Internal supply voltage error
ChargerError[ChargerError["INTERNAL_SUPPLY_C"] = 212] = "INTERNAL_SUPPLY_C";
// Err 215 - Internal supply voltage error
ChargerError[ChargerError["INTERNAL_SUPPLY_D"] = 215] = "INTERNAL_SUPPLY_D";
})(ChargerError || (exports.ChargerError = ChargerError = {}));
var OffReason;
(function (OffReason) {
OffReason[OffReason["NO_REASON"] = 0] = "NO_REASON";
OffReason[OffReason["NO_INPUT_POWER"] = 1] = "NO_INPUT_POWER";
OffReason[OffReason["SWITCHED_OFF_SWITCH"] = 2] = "SWITCHED_OFF_SWITCH";
OffReason[OffReason["SWITCHED_OFF_REGISTER"] = 4] = "SWITCHED_OFF_REGISTER";
OffReason[OffReason["REMOTE_INPUT"] = 8] = "REMOTE_INPUT";
OffReason[OffReason["PROTECTION_ACTIVE"] = 16] = "PROTECTION_ACTIVE";
OffReason[OffReason["PAY_AS_YOU_GO_OUT_OF_CREDIT"] = 32] = "PAY_AS_YOU_GO_OUT_OF_CREDIT";
OffReason[OffReason["BMS"] = 64] = "BMS";
OffReason[OffReason["ENGINE_SHUTDOWN"] = 128] = "ENGINE_SHUTDOWN";
OffReason[OffReason["ANALYSING_INPUT_VOLTAGE"] = 256] = "ANALYSING_INPUT_VOLTAGE";
})(OffReason || (exports.OffReason = OffReason = {}));
var AlarmReason;
(function (AlarmReason) {
AlarmReason[AlarmReason["NO_ALARM"] = 0] = "NO_ALARM";
AlarmReason[AlarmReason["LOW_VOLTAGE"] = 1] = "LOW_VOLTAGE";
AlarmReason[AlarmReason["HIGH_VOLTAGE"] = 2] = "HIGH_VOLTAGE";
AlarmReason[AlarmReason["LOW_SOC"] = 4] = "LOW_SOC";
AlarmReason[AlarmReason["LOW_STARTER_VOLTAGE"] = 8] = "LOW_STARTER_VOLTAGE";
AlarmReason[AlarmReason["HIGH_STARTER_VOLTAGE"] = 16] = "HIGH_STARTER_VOLTAGE";
AlarmReason[AlarmReason["LOW_TEMPERATURE"] = 32] = "LOW_TEMPERATURE";
AlarmReason[AlarmReason["HIGH_TEMPERATURE"] = 64] = "HIGH_TEMPERATURE";
AlarmReason[AlarmReason["MID_VOLTAGE"] = 128] = "MID_VOLTAGE";
AlarmReason[AlarmReason["OVERLOAD"] = 256] = "OVERLOAD";
AlarmReason[AlarmReason["DC_RIPPLE"] = 512] = "DC_RIPPLE";
AlarmReason[AlarmReason["LOW_V_AC_OUT"] = 1024] = "LOW_V_AC_OUT";
AlarmReason[AlarmReason["HIGH_V_AC_OUT"] = 2048] = "HIGH_V_AC_OUT";
AlarmReason[AlarmReason["SHORT_CIRCUIT"] = 4096] = "SHORT_CIRCUIT";
})(AlarmReason || (exports.AlarmReason = AlarmReason = {}));
var AlarmNotification;
(function (AlarmNotification) {
AlarmNotification[AlarmNotification["NO_ALARM"] = 0] = "NO_ALARM";
AlarmNotification[AlarmNotification["WARNING"] = 1] = "WARNING";
AlarmNotification[AlarmNotification["ALARM"] = 2] = "ALARM";
})(AlarmNotification || (exports.AlarmNotification = AlarmNotification = {}));
var ACInState;
(function (ACInState) {
ACInState[ACInState["AC_IN_1"] = 0] = "AC_IN_1";
ACInState[ACInState["AC_IN_2"] = 1] = "AC_IN_2";
ACInState[ACInState["NOT_CONNECTED"] = 2] = "NOT_CONNECTED";
ACInState[ACInState["UNKNOWN"] = 3] = "UNKNOWN";
})(ACInState || (exports.ACInState = ACInState = {}));
/*
export abstract class DeviceData {
protected _data: Record<string, any>;
protected _modelId: number;
constructor(modelId: number, data: Record<string, any>) {
this._modelId = modelId;
this._data = data;
}
getModelName(): string {
const productName = getProductName(this._modelId);
return productName || `Model ${this._modelId.toString(16).toUpperCase()}`;
}
}
*/
class Device {
advertisementKey;
constructor(advertisementKey) {
this.advertisementKey = advertisementKey;
}
parseContainer(data) {
const prefix = data.readUInt16LE(0); // 2 bytes, little endian
const modelId = data.readUInt16LE(2); // 2 bytes, little endian
const readoutType = data.readUInt8(4); // 1 byte
const iv = data.readUInt16LE(5); // 2 bytes, little endian
const encryptedData = data.slice(7); // rest of data starting at offset 7
return {
prefix,
modelId,
readoutType,
iv,
encryptedData,
};
}
getModelId(data) {
return data.readUInt16LE(2);
}
decrypt(data) {
const container = this.parseContainer(data);
// Convert hex key to buffer
const key = Buffer.from(this.advertisementKey, 'hex');
// Key check: first byte of encrypted data should match first byte of key
if (container.encryptedData[0] !== key[0]) {
throw new Error("Incorrect advertisement key");
}
// Skip the first byte (key check byte) and get the actual encrypted data
const encryptedData = container.encryptedData.slice(1);
// Create IV from the 32-bit value (little endian)
const iv = Buffer.alloc(16);
iv.writeUInt32LE(container.iv, 0);
// Create decipher with CTR mode
const decipher = (0, crypto_1.createDecipheriv)('aes-128-ctr', key, iv);
decipher.setAutoPadding(false);
// Decrypt the data (without the key check byte)
const decrypted = Buffer.concat([
decipher.update(encryptedData),
decipher.final()
]);
return decrypted;
}
parse(data) {
const decrypted = this.decrypt(data);
this.parseDecrypted(decrypted);
}
toJson() {
const data = {};
for (const key of Object.keys(this)) {
if (key === 'advertisementKey')
continue;
let value = this[key];
if (value === undefined)
continue;
// Check for enum metadata
const enumType = Reflect.getMetadata('enumType', this, key);
if (enumType && typeof value === 'number') {
for (const enumKey in enumType) {
if (enumType[enumKey] === value) {
data[key] = enumKey;
break;
}
}
}
else if (Array.isArray(value)) {
data[key] = value.map(v => typeof v === 'number' ? v : v);
}
else {
data[key] = value;
}
}
return data;
}
}
exports.Device = Device;
function kelvinToCelsius(tempInKelvin) {
return tempInKelvin - 273.15;
}
exports.kelvinToCelsius = kelvinToCelsius;
class BitReader {
data;
bitPosition = 0;
constructor(data) {
this.data = data;
}
readBit() {
const byteIndex = Math.floor(this.bitPosition / 8);
const bitIndex = this.bitPosition % 8;
const byte = this.data[byteIndex];
if (byteIndex > this.data.length) {
throw new Error(`length error ${byte} ${byteIndex} / ${this.data.length}`);
}
const bit = (byte >> bitIndex) & 1;
this.bitPosition++;
return bit;
}
readUnsignedInt(numBits) {
let result = 0;
for (let i = 0; i < numBits; i++) {
result |= this.readBit() << i;
}
return result;
}
readSignedInt(numBits) {
const value = this.readUnsignedInt(numBits);
return BitReader.toSignedInt(value, numBits);
}
static toSignedInt(value, numBits) {
const maxValue = (1 << numBits) - 1;
const halfMax = 1 << (numBits - 1);
if (value > halfMax) {
return value - (maxValue + 1);
}
return value;
}
}
exports.BitReader = BitReader;