From 1079eb16c1f889bf956276d10074d7f1ad1fc98b Mon Sep 17 00:00:00 2001 From: Carter Call Date: Wed, 20 May 2026 15:15:15 -0600 Subject: [PATCH] Add Vutility vendor and devices --- vendors/vutility/codecs/hotdrop.js | 366 ++++++++++++ vendors/vutility/codecs/pulsedrop.js | 249 ++++++++ .../vutility/codecs/test_decode_hotdrop.json | 22 + .../codecs/test_decode_pulsedrop.json | 19 + .../vutility/codecs/test_decode_voltdrop.json | 30 + .../vutility/codecs/test_encode_hotdrop.json | 16 + .../codecs/test_encode_pulsedrop.json | 16 + .../vutility/codecs/test_encode_voltdrop.json | 16 + vendors/vutility/codecs/voltdrop.js | 551 ++++++++++++++++++ vendors/vutility/devices/hotdrop.toml | 19 + vendors/vutility/devices/pulsedrop.toml | 19 + vendors/vutility/devices/voltdrop.toml | 19 + vendors/vutility/profiles/AS923_1_0_2_B.toml | 25 + vendors/vutility/profiles/AU915_1_0_2_B.toml | 25 + vendors/vutility/profiles/EU868_1_0_2_B.toml | 25 + vendors/vutility/profiles/KR920_1_0_2_B.toml | 25 + vendors/vutility/profiles/US915_1_0_2_B.toml | 25 + vendors/vutility/vendor.toml | 8 + 18 files changed, 1475 insertions(+) create mode 100644 vendors/vutility/codecs/hotdrop.js create mode 100644 vendors/vutility/codecs/pulsedrop.js create mode 100644 vendors/vutility/codecs/test_decode_hotdrop.json create mode 100644 vendors/vutility/codecs/test_decode_pulsedrop.json create mode 100644 vendors/vutility/codecs/test_decode_voltdrop.json create mode 100644 vendors/vutility/codecs/test_encode_hotdrop.json create mode 100644 vendors/vutility/codecs/test_encode_pulsedrop.json create mode 100644 vendors/vutility/codecs/test_encode_voltdrop.json create mode 100644 vendors/vutility/codecs/voltdrop.js create mode 100644 vendors/vutility/devices/hotdrop.toml create mode 100644 vendors/vutility/devices/pulsedrop.toml create mode 100644 vendors/vutility/devices/voltdrop.toml create mode 100644 vendors/vutility/profiles/AS923_1_0_2_B.toml create mode 100644 vendors/vutility/profiles/AU915_1_0_2_B.toml create mode 100644 vendors/vutility/profiles/EU868_1_0_2_B.toml create mode 100644 vendors/vutility/profiles/KR920_1_0_2_B.toml create mode 100644 vendors/vutility/profiles/US915_1_0_2_B.toml create mode 100644 vendors/vutility/vendor.toml diff --git a/vendors/vutility/codecs/hotdrop.js b/vendors/vutility/codecs/hotdrop.js new file mode 100644 index 0000000..b93d129 --- /dev/null +++ b/vendors/vutility/codecs/hotdrop.js @@ -0,0 +1,366 @@ +/** + * @typedef {Object} DecodedUplink + * @property {HotDropDirectData} data - The open JavaScript object representing the decoded uplink payload when no errors occurred + * @property {string[]} errors - A list of error messages while decoding the uplink payload + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from decoding the uplink payload + */ + +/** + * Decode uplink + * @param {Object} input - An object provided by the IoT Flow framework + * @param {number[]} input.bytes - Array of bytes represented as numbers as it has been sent from the device + * @param {number} input.fPort - The Port Field on which the uplink has been sent + * @param {Date} input.recvTime - The uplink message time recorded by the LoRaWAN network server + * @returns {DecodedUplink} The decoded object + */ +function decodeUplink(input) { + let result = { + data: {}, + errors: [], + warnings: [], + }; + const raw = Buffer.from(input.bytes); + + // Uplink payload must be 11 bytes long. + if (raw.byteLength != 11) { + result.errors.push("Payload length must be 11 bytes"); + delete result.data; + return result; + } + + // Packet ID - 1 byte + const packetId = raw[0]; + if (packetId !== 50) { + result.errors.push("Payload packet ID is not equal to 50"); + delete result.data; + return result; + } + + // Constant factors for formulas + const capacitorVoltageFactor = 5.0 / 255.0; + const temperatureCelsiusFactor = 120.0 / 255.0; + const deciToUnitFactor = 0.1; + + // Amp hour accumulation - 4 bytes + // 32-bit unsigned integer in network byte order (MSB/BE) reported in deci-ampere-hour (dAh) + const ampHourAccumulationDeciAmpere = raw.readUInt32BE(1); + + // Average amps - 2 bytes + // 16-bit unsigned integer in network byte order (MSB/BE) reported in deci-ampere (dA), + // this average represents the entire time since the last transmit (one entire transmit period) + const averageAmpsDeciAmpere = raw.readUInt16BE(5); + + // Max Offset - 1 byte + // 8-bit unsigned integer representing the percent offset above the Average amps value. + const maxOffset = raw[7]; + + // Min Offset - 1 byte + // 8-bit unsigned integer representing the percent offset below the Average amps value. + const minOffset = raw[8]; + + // Capacitor Voltage Scalar - 1 byte + // 8-bit unsigned integer representing the capacitor voltage. + // (as if the integer range from 0-255 is scaled to between 0.0V and 5.0V) + const capacitorVoltageScalar = raw[9]; + + // Temperature Scalar + // 8-bit unsigned integer representing the temperature. + // (as if the integer range from 0-255 is scaled to between -40C and 80C) + const temperatureScalar = raw[10]; + + // Calculated fields + const maximumAmpsDeciAmpere = + averageAmpsDeciAmpere * ((100 + maxOffset) / 100.0); + const minimumAmpsDeciAmpere = + averageAmpsDeciAmpere * ((100 - minOffset) / 100.0); + const capacitorVoltage = capacitorVoltageFactor * capacitorVoltageScalar; + const temperatureCelsius = temperatureCelsiusFactor * temperatureScalar - 40; + + if (minimumAmpsDeciAmpere < 0) { + result.warnings.push("Minimum amps is less than 0."); + } + if (capacitorVoltage < 3.4) { + result.warnings.push("Low capacitor voltage may reduce transmit interval."); + } + + result.data = { + ampHourAccumulation: ampHourAccumulationDeciAmpere * deciToUnitFactor, + averageAmps: averageAmpsDeciAmpere * deciToUnitFactor, + maximumAmps: maximumAmpsDeciAmpere * deciToUnitFactor, + minimumAmps: minimumAmpsDeciAmpere * deciToUnitFactor, + capacitorVoltage: capacitorVoltage, + temperatureCelsius: temperatureCelsius, + }; + + return result; +} + +/** + * @typedef {Object} EncodedDownlink + * @property {number[]} bytes - Array of bytes represented as numbers as it will be sent to the device + * @property {number} fPort - The Port Field on which the downlink must be sent + * @property {string[]} errors - A list of error messages while encoding the downlink object + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from encoding the downlink object + */ + +/** + * Downlink encode + * @param {Object} input - An object provided by the IoT Flow framework + * @param {Object} input.data - The higher-level object representing your downlink + * @returns {EncodedDownlink} The encoded object + */ +function encodeDownlink(input) { + let result = { + bytes: [], + errors: [], + warnings: [], + }; + + let definedDownlinkVars = 0; + if (typeof input.data.transmitIntervalSeconds !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.measurementIntervalMs !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.lowPowerThreshold !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.factoryReset !== "undefined") { + definedDownlinkVars += 1; + } + + if (definedDownlinkVars > 1) { + result.errors.push("Invalid downlink: More than one downlink type defined"); + delete result.bytes; + return result; + } + + if (typeof input.data.transmitIntervalSeconds !== "undefined") { + if (input.data.transmitIntervalSeconds < 60) { + result.errors.push( + "Invalid downlink: transmit interval cannot be less than 1 min" + ); + delete result.bytes; + return result; + } + if (input.data.transmitIntervalSeconds > 1800) { + result.errors.push( + "Invalid downlink: transmit interval cannot be greater than 30 min" + ); + delete result.bytes; + return result; + } + var downlink = Buffer.alloc(10); + downlink.writeUInt16LE(0x0054, 0); + downlink.writeFloatLE(input.data.transmitIntervalSeconds, 2); + downlink.writeFloatLE(0, 6); + result.bytes = Array.from(new Uint8Array(downlink.buffer)); + result.fPort = 3; + return result; + } + + if (typeof input.data.measurementIntervalMs !== "undefined") { + if (input.data.measurementIntervalMs < 200) { + result.errors.push( + "Invalid downlink: measurement interval cannot be less than 200 ms" + ); + delete result.bytes; + return result; + } + if (input.data.measurementIntervalMs > 10000) { + result.errors.push( + "Invalid downlink: measurement interval cannot be greater than 10000 ms" + ); + delete result.bytes; + return result; + } + + var downlink = Buffer.alloc(10); + downlink.writeUInt16LE(0x004d, 0); + downlink.writeFloatLE(input.data.measurementIntervalMs, 2); + downlink.writeFloatLE(0, 6); + result.bytes = Array.from(new Uint8Array(downlink.buffer)); + result.fPort = 3; + return result; + } + + if (typeof input.data.lowPowerThreshold !== "undefined") { + var lowPowerTolerance = 0.000001; + // Have leniant lower tolerance due to floating point + if (input.data.lowPowerThreshold + lowPowerTolerance < 2.1) { + result.errors.push( + "Invalid downlink: low power threshold cannot be less than 2.1 v" + ); + delete result.bytes; + return result; + } + // Have leniant upper tolerance due to floating point + if (input.data.lowPowerThreshold - lowPowerTolerance > 3.9) { + result.errors.push( + "Invalid downlink: low power threshold cannot be greater than 3.9 v" + ); + delete result.bytes; + return result; + } + + var downlink = Buffer.alloc(10); + downlink.writeUInt16LE(0x0050, 0); + downlink.writeFloatLE(input.data.lowPowerThreshold, 2); + downlink.writeFloatLE(0, 6); + result.bytes = Array.from(new Uint8Array(downlink.buffer)); + result.fPort = 3; + return result; + } + + if (typeof input.data.factoryReset !== "undefined") { + if (input.data.factoryReset === true) { + result.bytes = [ + 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + result.fPort = 3; + return result; + } else { + result.errors.push("Invalid downlink: valid factoryReset value is true"); + delete result.bytes; + return result; + } + } + + result.errors.push("Invalid downlink: invalid downlink parameter name"); + delete result.bytes; + return result; +} + +/** + * @typedef {Object} DecodedDownlink + * @property {Object} data - The open JavaScript object representing the decoded downlink payload when no errors occurred + * @property {string[]} errors - A list of error messages while decoding the downlink payload + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from decoding the downlink payload + */ + +/** + * Downlink decode + * @param {Object} input - An object provided by the IoT Flow framework + * @param {number[]} input.bytes - Array of bytes represented as numbers as it will be sent to the device + * @param {number} input.fPort - The Port Field on which the downlink must be sent + * @param {Date} input.recvTime - The uplink message time computed by the IoT Flow framework + * @returns {DecodedDownlink} The decoded object + */ +function decodeDownlink(input) { + let result = { + data: {}, + errors: [], + warnings: [], + }; + + var raw = Buffer.from(input.bytes); + + if (raw.length !== 10) { + result.errors.push("Invalid downlink: downlink must be 10 bytes"); + delete result.data; + return result; + } + + var type = raw.readUInt16LE(0); + switch (type) { + case 0x54: // transmit interval + var transmitIntervalSeconds = raw.readFloatLE(2); + // Not currently in use for this decoder + var transmitIntervalVarianceSeconds = raw.readFloatLE(6); + if (transmitIntervalVarianceSeconds !== 0) { + result.warnings.push("Warning: Byte index 6-9 are not 0"); + } + if (transmitIntervalSeconds < 60) { + result.errors.push( + "Invalid downlink: transmit interval cannot be less than 1 min" + ); + delete result.data; + return result; + } + if (transmitIntervalSeconds > 1800) { + result.errors.push( + "Invalid downlink: transmit interval cannot be greater than 30 min" + ); + delete result.bytes; + return result; + } + result.data.transmitIntervalSeconds = transmitIntervalSeconds; + break; + case 0x4d: // measurement interval + var measurementIntervalMs = raw.readFloatLE(2); + var reserved = raw.readFloatLE(6); + if (reserved !== 0) { + result.warnings.push( + "Warning: Measurement interval reserved bytes are not equal to 0" + ); + } + if (measurementIntervalMs < 200) { + result.errors.push( + "Invalid downlink: measurement interval cannot be less than 200 ms" + ); + delete result.bytes; + return result; + } + if (measurementIntervalMs > 10000) { + result.errors.push( + "Invalid downlink: measurement interval cannot be greater than 10000 ms" + ); + delete result.bytes; + return result; + } + result.data.measurementIntervalMs = measurementIntervalMs; + break; + case 0x50: // low power threshold + var lowPowerTolerance = 0.000001; + var lowPowerThreshold = raw.readFloatLE(2); + var reserved = raw.readFloatLE(6); + if (reserved !== 0) { + result.warnings.push( + "Warning: Low power threshold reserved bytes are not equal to 0" + ); + } + // Have leniant lower tolerance due to floating point + if (lowPowerThreshold + lowPowerTolerance < 2.1) { + result.errors.push( + "Invalid downlink: low power threshold cannot be less than 2.1 v" + ); + delete result.bytes; + return result; + } + // Have leniant upper tolerance due to floating point + if (lowPowerThreshold - lowPowerTolerance > 3.9) { + result.errors.push( + "Invalid downlink: low power threshold cannot be greater than 3.9 v" + ); + delete result.bytes; + return result; + } + result.data.lowPowerThreshold = lowPowerThreshold; + break; + case 0x46: // factory reset + if ( + raw[2] !== 0 || + raw[3] !== 0 || + raw[4] !== 0 || + raw[5] !== 0 || + raw[6] !== 0 || + raw[7] !== 0 || + raw[8] !== 0 || + raw[9] !== 0 + ) { + result.errors.push( + "Invalid downlink: Factory reset reserved bytes are not equal to 0" + ); + delete result.data; + return result; + } + result.data.factoryReset = true; + break; + default: + result.errors.push("Invalid downlink: unknown downlink type"); + delete result.data; + return result; + } + return result; +} \ No newline at end of file diff --git a/vendors/vutility/codecs/pulsedrop.js b/vendors/vutility/codecs/pulsedrop.js new file mode 100644 index 0000000..50b459e --- /dev/null +++ b/vendors/vutility/codecs/pulsedrop.js @@ -0,0 +1,249 @@ +/** + * @typedef {Object} DecodedUplink + * @property {PulseDropDirectData} data - The open JavaScript object representing the decoded uplink payload when no errors occurred + * @property {string[]} errors - A list of error messages while decoding the uplink payload + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from decoding the uplink payload + */ + +/** + * Decode uplink + * @param {Object} input - An object provided by the IoT Flow framework + * @param {number[]} input.bytes - Array of bytes represented as numbers as it has been sent from the device + * @param {number} input.fPort - The Port Field on which the uplink has been sent + * @param {Date} input.recvTime - The uplink message time recorded by the LoRaWAN network server + * @returns {DecodedUplink} The decoded object + */ +function decodeUplink(input) { + let result = { + data: {}, + errors: [], + warnings: [], + }; + const raw = Buffer.from(input.bytes); + + // Uplink payload must be 11 bytes long. + if (raw.byteLength != 11) { + result.errors.push("Payload length must be 11 bytes"); + delete result.data; + return result; + } + + // Packet ID - 1 byte + const packetId = raw[0]; + if (packetId !== 28) { + result.errors.push("Payload packet ID is not equal to 28"); + delete result.data; + return result; + } + + // Constant factors for formulas + const capacitorVoltageFactor = 5.0 / 255.0; + const temperatureCelsiusFactor = 120.0 / 255.0; + const noResponseErrorFlag = 0x01; + const invalidResponseErrorFlag = 0x02; + + // Capacitor Voltage Scalar - 1 byte + // 8-bit unsigned integer representing the capacitor voltage. + // (as if the integer range from 0-255 is scaled to between 0.0V and 5.0V) + const capacitorVoltageScalar = raw[1]; + + // Temperature Scalar + // 8-bit unsigned integer representing the temperature. + // (as if the integer range from 0-255 is scaled to between -40C and 80C) + const temperatureScalar = raw[2]; + + // Pulse Count - 4 bytes + // 32-bit unsigned integer in network byte order (MSB/BE) + const pulseCount = raw.readUInt32BE(3); + + // Calculated fields + const capacitorVoltage = capacitorVoltageFactor * capacitorVoltageScalar; + const temperatureCelsius = temperatureCelsiusFactor * temperatureScalar - 40; + + if (capacitorVoltage < 2.5) { + result.warnings.push( + "Low capacitor voltage indicates depleted battery. System may cease operation soon." + ); + } + + // Digital AMI error flags + const errorFlags = raw[10]; + if (errorFlags & noResponseErrorFlag) { + result.errors.push( + "No response from meter" + ); + } + if (errorFlags & invalidResponseErrorFlag) { + result.errors.push( + "Invalid response from meter" + ); + } + + result.data = { + pulseCount: pulseCount, + capacitorVoltage: capacitorVoltage, + temperatureCelsius: temperatureCelsius, + }; + + return result; +} + +/** + * @typedef {Object} EncodedDownlink + * @property {number[]} bytes - Array of bytes represented as numbers as it will be sent to the device + * @property {number} fPort - The Port Field on which the downlink must be sent + * @property {string[]} errors - A list of error messages while encoding the downlink object + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from encoding the downlink object + */ + +/** + * Downlink encode + * @param {Object} input - An object provided by the IoT Flow framework + * @param {Object} input.data - The higher-level object representing your downlink + * @returns {EncodedDownlink} The encoded object + */ +function encodeDownlink(input) { + let result = { + bytes: [], + errors: [], + warnings: [], + }; + + let definedDownlinkVars = 0; + if (typeof input.data.transmitIntervalSeconds !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.factoryReset !== "undefined") { + definedDownlinkVars += 1; + } + + if (definedDownlinkVars > 1) { + result.errors.push("Invalid downlink: More than one downlink type defined"); + delete result.bytes; + return result; + } + + if (typeof input.data.transmitIntervalSeconds !== "undefined") { + if (input.data.transmitIntervalSeconds < 60) { + result.errors.push( + "Invalid downlink: transmit interval cannot be less than 1 min" + ); + delete result.bytes; + return result; + } + if (input.data.transmitIntervalSeconds > 1800) { + result.errors.push( + "Invalid downlink: transmit interval cannot be greater than 30 min" + ); + delete result.bytes; + return result; + } + var downlink = Buffer.alloc(10); + downlink.writeUInt16LE(0x0054, 0); + downlink.writeFloatLE(input.data.transmitIntervalSeconds, 2); + downlink.writeFloatLE(0, 6); + result.bytes = Array.from(new Uint8Array(downlink.buffer)); + result.fPort = 3; + return result; + } + + if (typeof input.data.factoryReset !== "undefined") { + if (input.data.factoryReset === true) { + result.bytes = [ + 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + result.fPort = 3; + return result; + } else { + result.errors.push("Invalid downlink: valid factoryReset value is true"); + delete result.bytes; + return result; + } + } + + result.errors.push("Invalid downlink: invalid downlink parameter name"); + delete result.bytes; + return result; +} + +/** + * @typedef {Object} DecodedDownlink + * @property {Object} data - The open JavaScript object representing the decoded downlink payload when no errors occurred + * @property {string[]} errors - A list of error messages while decoding the downlink payload + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from decoding the downlink payload + */ + +/** + * Downlink decode + * @param {Object} input - An object provided by the IoT Flow framework + * @param {number[]} input.bytes - Array of bytes represented as numbers as it will be sent to the device + * @param {number} input.fPort - The Port Field on which the downlink must be sent + * @param {Date} input.recvTime - The uplink message time computed by the IoT Flow framework + * @returns {DecodedDownlink} The decoded object + */ +function decodeDownlink(input) { + let result = { + data: {}, + errors: [], + warnings: [], + }; + + var raw = Buffer.from(input.bytes); + + if (raw.length !== 10) { + result.errors.push("Invalid downlink: downlink must be 10 bytes"); + delete result.data; + return result; + } + + var type = raw.readUInt16LE(0); + switch (type) { + case 0x54: // transmit interval + var transmitIntervalSeconds = raw.readFloatLE(2); + // Not currently in use for this decoder + var transmitIntervalVarianceSeconds = raw.readFloatLE(6); + if (transmitIntervalVarianceSeconds !== 0) { + result.warnings.push("Warning: Byte index 6-9 are not 0"); + } + if (transmitIntervalSeconds < 60) { + result.errors.push( + "Invalid downlink: transmit interval cannot be less than 1 min" + ); + delete result.data; + return result; + } + if (transmitIntervalSeconds > 1800) { + result.errors.push( + "Invalid downlink: transmit interval cannot be greater than 30 min" + ); + delete result.bytes; + return result; + } + result.data.transmitIntervalSeconds = transmitIntervalSeconds; + break; + case 0x46: // factory reset + if ( + raw[2] !== 0 || + raw[3] !== 0 || + raw[4] !== 0 || + raw[5] !== 0 || + raw[6] !== 0 || + raw[7] !== 0 || + raw[8] !== 0 || + raw[9] !== 0 + ) { + result.errors.push( + "Invalid downlink: Factory reset reserved bytes are not equal to 0" + ); + delete result.data; + return result; + } + result.data.factoryReset = true; + break; + default: + result.errors.push("Invalid downlink: unknown downlink type"); + delete result.data; + return result; + } + return result; +} \ No newline at end of file diff --git a/vendors/vutility/codecs/test_decode_hotdrop.json b/vendors/vutility/codecs/test_decode_hotdrop.json new file mode 100644 index 0000000..d8db979 --- /dev/null +++ b/vendors/vutility/codecs/test_decode_hotdrop.json @@ -0,0 +1,22 @@ +[ + { + "name": "standard uplink, empty", + "input": { + "bytes": [50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "fPort": 3, + "recvTime": "2023-05-02T20:00:00.000+00:00" + }, + "expected": { + "data": { + "ampHourAccumulation": 0, + "averageAmps": 0, + "maximumAmps": 0, + "minimumAmps": 0, + "capacitorVoltage": 0, + "temperatureCelsius": -40 + }, + "errors": [], + "warnings": ["Low capacitor voltage may reduce transmit interval."] + } + } +] \ No newline at end of file diff --git a/vendors/vutility/codecs/test_decode_pulsedrop.json b/vendors/vutility/codecs/test_decode_pulsedrop.json new file mode 100644 index 0000000..1059e30 --- /dev/null +++ b/vendors/vutility/codecs/test_decode_pulsedrop.json @@ -0,0 +1,19 @@ +[ + { + "name": "standard uplink, fractional numbers", + "input": { + "bytes": [28, 197, 125, 0, 0, 13, 232, 0, 0, 0, 0], + "fPort": 3, + "recvTime": "2023-05-02T20:00:00.000+00:00" + }, + "expected": { + "data": { + "pulseCount": 3560, + "capacitorVoltage": 3.8627450980392157, + "temperatureCelsius": 18.823529411764703 + }, + "errors": [], + "warnings": [] + } + } +] \ No newline at end of file diff --git a/vendors/vutility/codecs/test_decode_voltdrop.json b/vendors/vutility/codecs/test_decode_voltdrop.json new file mode 100644 index 0000000..aea5e9a --- /dev/null +++ b/vendors/vutility/codecs/test_decode_voltdrop.json @@ -0,0 +1,30 @@ +[ + { + "name": "Consolidated Voltage, Phase Angle, Amperage, and Max Amperage All Ones", + "input": { + "bytes": [38, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + "fPort": 3, + "recvTime": "2023-05-02T20:00:00.000+00:00" + }, + "expected": { + "data": { + "voltageL1": 1023.984375, + "voltageL2": 1023.984375, + "voltageL3": 1023.984375, + "phaseAngleL1": 360.0, + "phaseAngleL2": 360.0, + "phaseAngleL3": 360.0, + "capacitorVoltage": 5, + "currentL1": 4095.9375, + "currentL2": 4095.9375, + "currentL3": 4095.9375, + "maxCurrentL1": 36735.439453125, + "maxCurrentL2": 36735.439453125, + "maxCurrentL3": 36735.439453125, + "temperatureCelsius": 80 + }, + "errors": [], + "warnings": [] + } + } +] \ No newline at end of file diff --git a/vendors/vutility/codecs/test_encode_hotdrop.json b/vendors/vutility/codecs/test_encode_hotdrop.json new file mode 100644 index 0000000..e675bb3 --- /dev/null +++ b/vendors/vutility/codecs/test_encode_hotdrop.json @@ -0,0 +1,16 @@ +[ + { + "name": "factory reset", + "input": { + "data": { + "factoryReset": true + } + }, + "expected": { + "bytes": [70, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "errors": [], + "warnings": [], + "fPort": 3 + } + } +] \ No newline at end of file diff --git a/vendors/vutility/codecs/test_encode_pulsedrop.json b/vendors/vutility/codecs/test_encode_pulsedrop.json new file mode 100644 index 0000000..7206fcf --- /dev/null +++ b/vendors/vutility/codecs/test_encode_pulsedrop.json @@ -0,0 +1,16 @@ +[ + { + "name": "downlink transmit interval (30 min)", + "input": { + "data": { + "transmitIntervalSeconds": 1800 + } + }, + "expected": { + "bytes": [84, 0, 0, 0, 225, 68, 0, 0, 0, 0], + "errors": [], + "warnings": [], + "fPort": 3 + } + } +] \ No newline at end of file diff --git a/vendors/vutility/codecs/test_encode_voltdrop.json b/vendors/vutility/codecs/test_encode_voltdrop.json new file mode 100644 index 0000000..a3cda29 --- /dev/null +++ b/vendors/vutility/codecs/test_encode_voltdrop.json @@ -0,0 +1,16 @@ +[ + { + "name": "Transmit Packet Schedule", + "input": { + "data": { + "packetTransmitSchedule": [40, 41, 40, 41, 0, 42, 43, 44, 45] + } + }, + "expected": { + "bytes": [0, 48, 9, 40, 41, 40, 41, 0, 42, 43, 44, 45], + "errors": [], + "warnings": [], + "fPort": 3 + } + }, +] \ No newline at end of file diff --git a/vendors/vutility/codecs/voltdrop.js b/vendors/vutility/codecs/voltdrop.js new file mode 100644 index 0000000..bcded5e --- /dev/null +++ b/vendors/vutility/codecs/voltdrop.js @@ -0,0 +1,551 @@ +/** + * @typedef {Object} DecodedUplink + * @property {VoltDropDirectData} data - The open JavaScript object representing the decoded uplink payload when no errors occurred + * @property {string[]} errors - A list of error messages while decoding the uplink payload + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from decoding the uplink payload + */ + +/** + * Decode uplink + * @param {Object} input - An object provided by the IoT Flow framework + * @param {number[]} input.bytes - Array of bytes represented as numbers as it has been sent from the device + * @param {number} input.fPort - The Port Field on which the uplink has been sent + * @param {Date} input.recvTime - The uplink message time recorded by the LoRaWAN network server + * @returns {DecodedUplink} The decoded object + */ +function decodeUplink(input) { + const packetList = { + VD_DIRECT_CONSOLIDATED_VOLTAGE_AMPERAGE: 38, + VD_DIRECT_VOLTAGE_ANGLE: 39, + VD_DIRECT_VOLTAGE_PF: 40, + VD_DIRECT_AMPERAGE: 41, + VD_DIRECT_ACT_ENERGY_CONF: 42, + VD_DIRECT_ACT_ENERGY_UNCONF: 43, + VD_DIRECT_APP_ENERGY_CONF: 44, + VD_DIRECT_APP_ENERGY_UNCONF: 45, + VD_DIRECT_STARTUP_DIAG: 46, + VD_DIRECT_OPERATIONAL_DIAG: 47, + }; + + // Constant factors for formulas + const phaseAngleFactor = 360.0 / 255.0; + const capacitorVoltageFactor = 5.0 / 255.0; + const temperatureCelsiusFactor = 120.0 / 255.0; + + let result = { + data: {}, + errors: [], + warnings: [], + }; + + const raw = Buffer.from(input.bytes); + let expectedLength = 0; + + // Packet must minimally contain an ID byte + if (raw.byteLength != 0) { + const packetId = raw[0]; + switch (packetId) { + // The consolidated packet is larger than typical + case packetList.VD_DIRECT_CONSOLIDATED_VOLTAGE_AMPERAGE: + expectedLength = 21; + break; + default: // All other packets are currently 11 bytes long + expectedLength = 11; + break; + } + + if (raw.byteLength != expectedLength) { + result.errors.push("Invalid payload length for data type"); + delete result.data; + return result; + } + } else { + result.errors.push("Empty payload"); + delete result.data; + return result; + } + + // Packet ID - 1 byte + const packetId = raw[0]; + switch (packetId) { + case packetList.VD_DIRECT_CONSOLIDATED_VOLTAGE_AMPERAGE: { + let currentL1 = raw.readUInt16BE(11) / 16.0; + let currentL2 = raw.readUInt16BE(13) / 16.0; + let currentL3 = raw.readUInt16BE(15) / 16.0; + result.data = { + // Average Phase Voltages - 2 bytes each + // 16-bit unsigned integers in network byte order (MSB/BE) with 10 integer and 6 fractional bits + voltageL1: raw.readUInt16BE(1) / 64.0, + voltageL2: raw.readUInt16BE(3) / 64.0, + voltageL3: raw.readUInt16BE(5) / 64.0, + // Phase Angle Scalars - 1 byte each + // 8-bit unsigned integer representing the phase angle between voltage and current. + // (as if the integer range from 0-255 is scaled to between 0° and 358.59375°) + phaseAngleL1: raw[7] * phaseAngleFactor, + phaseAngleL2: raw[8] * phaseAngleFactor, + phaseAngleL3: raw[9] * phaseAngleFactor, + // Capacitor Voltage Scalar - 1 byte + // 8-bit unsigned integer representing the capacitor voltage. + // (as if the integer range from 0-255 is scaled to between 0.0V and 5.0V) + capacitorVoltage: raw[10] * capacitorVoltageFactor, + // Average Phase Current - 2 bytes each + // 16-bit unsigned integers in network byte order (MSB/BE) with 12 integer and 4 fractional bits + currentL1: currentL1, + currentL2: currentL2, + currentL3: currentL3, + // Maximum Phase Current - 1 byte each + // 8-bit unsigned integer with 3 integer and 5 fractional bits (expressed as percentage in addition to 100%) + maxCurrentL1: (raw[17] / 32.0 + 1.0) * currentL1, + maxCurrentL2: (raw[18] / 32.0 + 1.0) * currentL2, + maxCurrentL3: (raw[19] / 32.0 + 1.0) * currentL3, + // Temperature Scalar + // 8-bit unsigned integer representing the temperature. + // (as if the integer range from 0-255 is scaled to between -40C and 80C) + temperatureCelsius: raw[20] * temperatureCelsiusFactor - 40.0, + }; + break; + } + case packetList.VD_DIRECT_VOLTAGE_ANGLE: + result.data = { + // Average Phase Voltages - 2 bytes each + // 16-bit unsigned integers in network byte order (MSB/BE) with 10 integer and 6 fractional bits + voltageL1: raw.readUInt16BE(1) / 64.0, + voltageL2: raw.readUInt16BE(3) / 64.0, + voltageL3: raw.readUInt16BE(5) / 64.0, + // Phase Angle Scalars - 1 byte each + // 8-bit unsigned integer representing the phase angle between voltage and current. + // (as if the integer range from 0-255 is scaled to between 0° and 358.59375°) + phaseAngleL1: raw[7] * phaseAngleFactor, + phaseAngleL2: raw[8] * phaseAngleFactor, + phaseAngleL3: raw[9] * phaseAngleFactor, + // Capacitor Voltage Scalar - 1 byte + // 8-bit unsigned integer representing the capacitor voltage. + // (as if the integer range from 0-255 is scaled to between 0.0V and 5.0V) + capacitorVoltage: raw[10] * capacitorVoltageFactor, + }; + break; + case packetList.VD_DIRECT_VOLTAGE_PF: + result.data = { + // Average Phase Voltages - 2 bytes each + // 16-bit unsigned integers in network byte order (MSB/BE) with 10 integer and 6 fractional bits + voltageL1: raw.readUInt16BE(1) / 64.0, + voltageL2: raw.readUInt16BE(3) / 64.0, + voltageL3: raw.readUInt16BE(5) / 64.0, + // Phase Power Factors - 1 byte each, 8-bit signed integers as percentage + powerFactorL1: raw.readInt8(7), + powerFactorL2: raw.readInt8(8), + powerFactorL3: raw.readInt8(9), + // Capacitor Voltage Scalar - 1 byte + // 8-bit unsigned integer representing the capacitor voltage. + // (as if the integer range from 0-255 is scaled to between 0.0V and 5.0V) + capacitorVoltage: raw[10] * capacitorVoltageFactor, + }; + break; + case packetList.VD_DIRECT_AMPERAGE: { + let currentL1 = raw.readUInt16BE(1) / 16.0; + let currentL2 = raw.readUInt16BE(3) / 16.0; + let currentL3 = raw.readUInt16BE(5) / 16.0; + result.data = { + // Average Phase Current - 2 bytes each + // 16-bit unsigned integers in network byte order (MSB/BE) with 12 integer and 4 fractional bits + currentL1: currentL1, + currentL2: currentL2, + currentL3: currentL3, + // Maximum Phase Current - 1 byte each + // 8-bit unsigned integer with 3 integer and 5 fractional bits (expressed as percentage in addition to 100%) + maxCurrentL1: (raw[7] / 32.0 + 1.0) * currentL1, + maxCurrentL2: (raw[8] / 32.0 + 1.0) * currentL2, + maxCurrentL3: (raw[9] / 32.0 + 1.0) * currentL3, + // Temperature Scalar + // 8-bit unsigned integer representing the temperature. + // (as if the integer range from 0-255 is scaled to between -40C and 80C) + temperatureCelsius: raw[10] * temperatureCelsiusFactor - 40.0, + }; + break; + } + case packetList.VD_DIRECT_ACT_ENERGY_CONF: // Intentional fall-through + case packetList.VD_DIRECT_ACT_ENERGY_UNCONF: + result.data = { + // Total Forward Active Energy - 8 bytes + // Sum of all phases active energy accumulated in Watt-Hours since last factory reset downlink. + // 64-bit signed integer in network byte order (MSB/BE) + // + // NOTE: Due to the limitations of Javascript and JSON this codec only uses 53 bits + // (max of standard number type) This still gives an effective range of 102,821 years + // at 10 Mega-Watts which is more than the Voltdrop is reasonably capable of measuring. + // Test and truncate the BigInt type into a normal number for JSON compatibility to the + // ES5 version of the codec. + activeEnergyAccumulation: Number( + BigInt.asIntN(53, raw.readBigInt64BE(1)) + ), + // Average Power Factor over all Phases - 2 bytes + // 16-bit signed integer in network byte order (MSB/BE) expressed as percentage with 8 integer and 7 fractional bits + averagePowerFactor: raw.readInt16BE(9) / 128.0, + }; + break; + case packetList.VD_DIRECT_APP_ENERGY_CONF: // Intentional fall-through + case packetList.VD_DIRECT_APP_ENERGY_UNCONF: + result.data = { + // Total Forward Active Energy - 8 bytes + // Sum of all phases active energy accumulated in Watt-Hours since last factory reset downlink. + // 64-bit unsigned integer in network byte order (MSB/BE) + // + // NOTE: See info about activeEnergyAccumulation and 53 bit numeric limitation + apparentEnergyAccumulation: Number( + BigInt.asUintN(53, raw.readBigUInt64BE(1)) + ), + // Average Power Factor over all Phases - 2 bytes + // 16-bit signed integer in network byte order (MSB/BE) expressed as percentage with 8 integer and 7 fractional bits + averagePowerFactor: raw.readInt16BE(9) / 128.0, + }; + break; + case packetList.VD_DIRECT_STARTUP_DIAG: + let resetReason = "Invalid"; + switch (raw.readUInt8(1)) { + case 0: + resetReason = "Power Loss"; + break; + case 1: + resetReason = "Hardware Reset"; + break; + case 2: + resetReason = "Watchdog Timer"; + break; + case 3: + resetReason = "Unknown Software Request"; + break; + case 4: + resetReason = "CPU Lock-Up"; + break; + case 5: + resetReason = "Hard-Fault"; + break; + case 6: + resetReason = "Application Request"; + break; + case 7: + resetReason = "Factory Reset"; + break; + case 8: + resetReason = "Reader Non-Responsive"; + break; + case 9: + resetReason = "Failed Link-Check"; + break; + case 10: + resetReason = "Assertion Failure"; + break; + } + // Format hashes to hexadecimal and pad to correct length if leading zeroes are needed + let coreFirmwareHash = raw.readUInt32BE(2).toString(16).toUpperCase(); + coreFirmwareHash = + "0".repeat(Math.max(0, 8 - coreFirmwareHash.length)) + coreFirmwareHash; + let readerFirmwareHash = raw.readUInt16BE(6).toString(16).toUpperCase(); + readerFirmwareHash = + "0".repeat(Math.max(0, 4 - readerFirmwareHash.length)) + + readerFirmwareHash; + result.data = { + resetReason: resetReason, + coreFirmwareHash: "0x" + coreFirmwareHash, + readerFirmwareHash: "0x" + readerFirmwareHash, + }; + break; + case packetList.VD_DIRECT_OPERATIONAL_DIAG: + const systemErrorConditionsList = [ + "Invalid Downlink", + "Core Voltage Drop", + "EEPROM Fail", + "Reader Timeout", + "Reader NACK", + "Reader Overvoltage", + "Reader Not Calibrated", + "Phase Sequence Error", + "Transmit Duty-Cycle Restricted", + ]; + let rawErrorConditions = raw.readUInt16BE(1); + let systemErrorConditions = []; + for (let idx = 0; idx < systemErrorConditionsList.length; idx++) { + if (rawErrorConditions & (1 << idx)) { + systemErrorConditions.push(systemErrorConditionsList[idx]); + } + } + result.data = { + systemErrorConditions: systemErrorConditions, + registerID: "0x" + raw.readUInt16BE(3).toString(16).toUpperCase(), + }; + break; + default: + result.errors.push("Unsupported packet ID"); + delete result.data; + return result; + } + return result; +} + +/** + * @typedef {Object} EncodedDownlink + * @property {number[]} bytes - Array of bytes represented as numbers as it will be sent to the device + * @property {number} fPort - The Port Field on which the downlink must be sent + * @property {string[]} errors - A list of error messages while encoding the downlink object + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from encoding the downlink object + */ + +/** + * Downlink encode + * @param {Object} input - An object provided by the IoT Flow framework + * @param {Object} input.data - The higher-level object representing your downlink + * @returns {EncodedDownlink} The encoded object + */ +function encodeDownlink(input) { + let result = { + bytes: [], + errors: [], + warnings: [], + }; + + let definedDownlinkVars = 0; + if (typeof input.data.transmitIntervalSeconds !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.packetTransmitSchedule !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.factoryReset !== "undefined") { + definedDownlinkVars += 1; + } + if (typeof input.data.softReset !== "undefined") { + definedDownlinkVars += 1; + } + + if (definedDownlinkVars > 1) { + result.errors.push("Invalid downlink: More than one downlink type defined"); + delete result.bytes; + return result; + } + + if (typeof input.data.transmitIntervalSeconds !== "undefined") { + if (input.data.transmitIntervalSeconds < 60) { + result.errors.push( + "Invalid downlink: transmit interval cannot be less than 1 min" + ); + delete result.bytes; + return result; + } + if (input.data.transmitIntervalSeconds > 1800) { + result.errors.push( + "Invalid downlink: transmit interval cannot be greater than 30 min" + ); + delete result.bytes; + return result; + } + let downlink = Buffer.alloc(6); + downlink.writeUInt16BE(0x0031, 0); + downlink.writeUInt32BE(input.data.transmitIntervalSeconds, 2); + result.bytes = Array.from(new Uint8Array(downlink.buffer)); + result.fPort = 3; + return result; + } + + if (typeof input.data.packetTransmitSchedule !== "undefined") { + if (input.data.packetTransmitSchedule.length < 1) { + result.errors.push( + "Invalid downlink: Packet transmit schedule must contain at least one element" + ); + delete result.bytes; + return result; + } + if (input.data.packetTransmitSchedule.length > 60) { + result.errors.push( + "Invalid downlink: Packet transmit schedule cannot be longer than 60 elements" + ); + delete result.bytes; + return result; + } + + const validIDList = [0, 40, 41, 42, 43, 44, 45]; + let scheduleValid = false; + let downlink = Buffer.alloc(input.data.packetTransmitSchedule.length + 3); + downlink.writeUInt16BE(0x0030, 0); + downlink.writeUInt8(input.data.packetTransmitSchedule.length, 2); + for ( + let index = 0; + index < input.data.packetTransmitSchedule.length; + index++ + ) { + let id = input.data.packetTransmitSchedule[index]; + if (!validIDList.includes(id)) { + result.warnings.push( + "Invalid packet ID " + id + " replaced by gap in transmit schedule" + ); + downlink.writeUInt8(0, index + 3); + } else { + downlink.writeUInt8(id, index + 3); + if (id != 0) { + // Enforce that there is at least one non-gap packet in schedule + scheduleValid = true; + } + } + } + + if (scheduleValid) { + result.bytes = Array.from(new Uint8Array(downlink.buffer)); + result.fPort = 3; + } else { + result.errors.push( + "Invalid downlink: Packet transmit schedule must contain at least one transmitting element" + ); + delete result.bytes; + } + return result; + } + + if (typeof input.data.factoryReset !== "undefined") { + if (input.data.factoryReset === true) { + result.bytes = [0x00, 0x46]; + result.fPort = 3; + return result; + } else { + result.errors.push("Invalid downlink: valid factoryReset value is true"); + delete result.bytes; + return result; + } + } + + if (typeof input.data.softReset !== "undefined") { + if (input.data.softReset === true) { + result.bytes = [0x00, 0x5a]; + result.fPort = 3; + return result; + } else { + result.errors.push("Invalid downlink: valid softReset value is true"); + delete result.bytes; + return result; + } + } + + result.errors.push("Invalid downlink: invalid downlink parameter name"); + delete result.bytes; + return result; +} + +/** + * @typedef {Object} DecodedDownlink + * @property {Object} data - The open JavaScript object representing the decoded downlink payload when no errors occurred + * @property {string[]} errors - A list of error messages while decoding the downlink payload + * @property {string[]} warnings - A list of warning messages that do not prevent the driver from decoding the downlink payload + */ + +/** + * Downlink decode + * @param {Object} input - An object provided by the IoT Flow framework + * @param {number[]} input.bytes - Array of bytes represented as numbers as it will be sent to the device + * @param {number} input.fPort - The Port Field on which the downlink must be sent + * @param {Date} input.recvTime - The uplink message time computed by the IoT Flow framework + * @returns {DecodedDownlink} The decoded object + */ +function decodeDownlink(input) { + let result = { + data: {}, + errors: [], + warnings: [], + }; + + let raw = Buffer.from(input.bytes); + + if (raw.length < 2) { + result.errors.push("Invalid downlink: downlink must be 2 bytes or greater"); + delete result.data; + return result; + } + + let type = raw.readUInt16BE(0); + switch (type) { + case 0x31: // transmit interval + if (raw.length < 6) { + result.errors.push( + "Invalid downlink: transmit interval downlink must be 6 bytes or greater" + ); + delete result.data; + return result; + } + let transmitIntervalSeconds = raw.readUInt32BE(2); + if (transmitIntervalSeconds < 60) { + result.errors.push( + "Invalid downlink: transmit interval cannot be less than 1 min" + ); + delete result.data; + return result; + } + if (transmitIntervalSeconds > 1800) { + result.errors.push( + "Invalid downlink: transmit interval cannot be greater than 30 min" + ); + delete result.data; + return result; + } + result.data.transmitIntervalSeconds = transmitIntervalSeconds; + break; + case 0x30: // packet transmit schedule + if (raw.length < 4) { + result.errors.push( + "Invalid downlink: transmit packet schedule downlink must be 4 bytes or greater" + ); + delete result.data; + return result; + } + let dataLength = raw.readUInt8(2); + if (dataLength > raw.length - 3) { + result.errors.push( + "Invalid downlink: transmit packet schedule length field larger than input data" + ); + delete result.data; + return result; + } else if (dataLength == 0) { + result.errors.push( + "Invalid downlink: Packet transmit schedule length field cannot be zero" + ); + delete result.data; + return result; + } else if (dataLength > 60) { + result.errors.push( + "Invalid downlink: Packet transmit schedule cannot be longer than 60 elements" + ); + delete result.data; + return result; + } + const validIDList = [0, 40, 41, 42, 43, 44, 45]; + let scheduleValid = false; + result.data.packetTransmitSchedule = []; + for (let index = 0; index < dataLength; index++) { + let id = raw.readUint8(index + 3); + if (!validIDList.includes(id)) { + result.warnings.push( + "Invalid packet ID " + + id + + " in transmit schedule will be replaced by gap on device" + ); + } else if (id != 0) { + // Enforce that there is at least one non-gap packet in schedule + scheduleValid = true; + } + result.data.packetTransmitSchedule.push(id); + } + if (!scheduleValid) { + result.errors.push( + "Invalid downlink: Packet transmit schedule must contain at least one transmitting element" + ); + } + break; + case 0x46: // factory reset + result.data.factoryReset = true; + break; + case 0x5a: // soft reset + result.data.softReset = true; + break; + default: + result.errors.push("Invalid downlink: unknown downlink type"); + delete result.data; + break; + } + return result; +} \ No newline at end of file diff --git a/vendors/vutility/devices/hotdrop.toml b/vendors/vutility/devices/hotdrop.toml new file mode 100644 index 0000000..ab6a2a6 --- /dev/null +++ b/vendors/vutility/devices/hotdrop.toml @@ -0,0 +1,19 @@ +[device] +id = "8d125ac1-6dd7-4518-b093-e18323fe26c5" +name = "Hotdrop" +description = "The HotDrop is a wireless, self-powered energy monitoring device created and developed by Vutility. Ideal for monitoring precise current measurements, such as amperage and amp-hours, the HotDrop offers a scalable solution for managing energy consumption across commercial and industrial environments." + +[[device.firmware]] +version = "30" +profiles = [ + "US915_1_0_2_B.toml", + "KR920_1_0_2_B.toml", + "EU868_1_0_2_B.toml", + "AU915_1_0_2_B.toml", + "AS923_1_0_2_B.toml", +] +codec = "hotdrop.js" + +[device.metadata] +product_url = "https://vutility.com/products/hotdrop" +documentation_url = "https://vutility.com/products/hotdrop" diff --git a/vendors/vutility/devices/pulsedrop.toml b/vendors/vutility/devices/pulsedrop.toml new file mode 100644 index 0000000..0241038 --- /dev/null +++ b/vendors/vutility/devices/pulsedrop.toml @@ -0,0 +1,19 @@ +[device] +id = "9d052aae-4d94-4982-933c-08cb6575720a" +name = "pulsedrop" +description = "PulseDrop is a robust, battery-powered device designed for real-time monitoring of pulse outputs from gas, water, and electrical meters. Ideal for use in commercial and industrial environments, PulseDrop provides long-range wireless data transmission via LoRaWAN, ensuring reliable communication over distances without the need for complex wiring." + +[[device.firmware]] +version = "2.0.0" +profiles = [ + "AS923_1_0_2_B.toml", + "AU915_1_0_2_B.toml", + "EU868_1_0_2_B.toml", + "KR920_1_0_2_B.toml", + "US915_1_0_2_B.toml", +] +codec = "pulsedrop.js" + +[device.metadata] +product_url = "https://vutility.com/products/pulsedrop" +documentation_url = "https://vutility.com/products/pulsedrop" diff --git a/vendors/vutility/devices/voltdrop.toml b/vendors/vutility/devices/voltdrop.toml new file mode 100644 index 0000000..4504429 --- /dev/null +++ b/vendors/vutility/devices/voltdrop.toml @@ -0,0 +1,19 @@ +[device] +id = "b1a68911-9fd8-4f1a-8797-59804adca52c" +name = "voltdrop" +description = "VoltDrop is a wireless, self-powered energy monitoring device designed to provide commercial and industrial facilities with real-time, granular insights into their energy consumption. Whether you're managing single-phase or complex multi-phase systems, VoltDrop offers unparalleled accuracy in measuring apparent energy, active energy, voltage, power factor, and amperage across a wide range of systems." + +[[device.firmware]] +version = "2.4" +profiles = [ + "AS923_1_0_2_B.toml", + "AU915_1_0_2_B.toml", + "EU868_1_0_2_B.toml", + "KR920_1_0_2_B.toml", + "US915_1_0_2_B.toml", +] +codec = "voltdrop.js" + +[device.metadata] +product_url = "https://vutility.com/products/voltdrop" +documentation_url = "https://vutility.com/products/voltdrop" diff --git a/vendors/vutility/profiles/AS923_1_0_2_B.toml b/vendors/vutility/profiles/AS923_1_0_2_B.toml new file mode 100644 index 0000000..aa47f2d --- /dev/null +++ b/vendors/vutility/profiles/AS923_1_0_2_B.toml @@ -0,0 +1,25 @@ +[profile] +id = "5ff4884e-8fd5-4625-98de-81eb68e29919" +vendor_profile_id = 0 +region = "AS923" +mac_version = "1.0.2" +reg_params_revision = "B" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 0 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/vutility/profiles/AU915_1_0_2_B.toml b/vendors/vutility/profiles/AU915_1_0_2_B.toml new file mode 100644 index 0000000..bb0cac9 --- /dev/null +++ b/vendors/vutility/profiles/AU915_1_0_2_B.toml @@ -0,0 +1,25 @@ +[profile] +id = "d5c1549e-9a66-436f-8a32-6a0fbf84b53a" +vendor_profile_id = 0 +region = "AU915" +mac_version = "1.0.2" +reg_params_revision = "B" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 0 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/vutility/profiles/EU868_1_0_2_B.toml b/vendors/vutility/profiles/EU868_1_0_2_B.toml new file mode 100644 index 0000000..96069cf --- /dev/null +++ b/vendors/vutility/profiles/EU868_1_0_2_B.toml @@ -0,0 +1,25 @@ +[profile] +id = "5493bf13-54d8-4a0d-9ece-6f66dde9a7c3" +vendor_profile_id = 0 +region = "EU868" +mac_version = "1.0.2" +reg_params_revision = "B" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 0 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/vutility/profiles/KR920_1_0_2_B.toml b/vendors/vutility/profiles/KR920_1_0_2_B.toml new file mode 100644 index 0000000..9cb080b --- /dev/null +++ b/vendors/vutility/profiles/KR920_1_0_2_B.toml @@ -0,0 +1,25 @@ +[profile] +id = "e8bc3333-9e80-4165-914d-e17babae9742" +vendor_profile_id = 0 +region = "KR920" +mac_version = "1.0.2" +reg_params_revision = "B" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 0 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/vutility/profiles/US915_1_0_2_B.toml b/vendors/vutility/profiles/US915_1_0_2_B.toml new file mode 100644 index 0000000..e79135b --- /dev/null +++ b/vendors/vutility/profiles/US915_1_0_2_B.toml @@ -0,0 +1,25 @@ +[profile] +id = "0a32526b-968c-4092-aa69-129835bf60c0" +vendor_profile_id = 0 +region = "US915" +mac_version = "1.0.2" +reg_params_revision = "B" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 0 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/vutility/vendor.toml b/vendors/vutility/vendor.toml new file mode 100644 index 0000000..7d9d3a7 --- /dev/null +++ b/vendors/vutility/vendor.toml @@ -0,0 +1,8 @@ +[vendor] +id = "6256cea8-29da-4b7f-af27-543bd2746c55" +name = "Vutility" +vendor_id = 0 +ouis = [] + +[vendor.metadata] +homepage = "https://vutility.com"