diff --git a/vendor/axioma/qalcosonic-w1-codec.js b/vendor/axioma/qalcosonic-w1-codec.js new file mode 100644 index 0000000000..3573c6ac7a --- /dev/null +++ b/vendor/axioma/qalcosonic-w1-codec.js @@ -0,0 +1,197 @@ +/** + * Axioma QALCOSONIC W1 LoRaWAN decoder + * + * Supported: + * - FPort 100 + * - 51-byte Enhanced payload + * - Frame version 0x01 + * - 23 packed 14-bit hourly delta values + * + * Output: + * - timestamp_utc + * - status + * - log_volume_m3 + * - delta_1_m3 ... delta_23_m3 + * - history_total_m3 + * - current_volume_m3 + * - alarm/status flags + * + * Tested with real QALCOSONIC W1 meters manufactured in 2024. + */ + +function decodeUplink(input) { + const bytes = input.bytes; + const port = input.fPort; + + function readUInt32LE(offset) { + return ( + bytes[offset] | + (bytes[offset + 1] << 8) | + (bytes[offset + 2] << 16) | + (bytes[offset + 3] << 24) + ) >>> 0; + } + + function readBitsLE(startByte, startBit, length) { + let value = 0; + + for (let i = 0; i < length; i++) { + const bitPosition = startBit + i; + const byteIndex = startByte + Math.floor(bitPosition / 8); + const bitIndex = bitPosition % 8; + + if (byteIndex >= bytes.length) { + break; + } + + if ((bytes[byteIndex] & (1 << bitIndex)) !== 0) { + value += Math.pow(2, i); + } + } + + return value; + } + + function timestampToISO(timestamp) { + try { + return new Date(timestamp * 1000).toISOString(); + } catch (e) { + return null; + } + } + + function decodeStatus(status, data) { + data.low_battery = + (status & 0x04) !== 0; + + data.permanent_error = + (status & 0x08) !== 0; + + data.temporary_error = + (status & 0x10) !== 0; + + data.pipe_empty = + status === 0x10; + + data.backflow = + (status & 0x60) === 0x60; + + data.burst = + (status & 0xA0) === 0xA0; + + data.leakage = + (status & 0x20) !== 0 && + !data.backflow && + !data.burst; + + data.low_temperature = + (status & 0x80) !== 0 && + !data.burst; + } + + if (port !== 100) { + return { + errors: [ + "Unsupported FPort: " + port + ] + }; + } + + if (bytes.length !== 51) { + return { + errors: [ + "Unsupported payload length: " + bytes.length + ] + }; + } + + if (bytes[0] !== 0x01) { + return { + errors: [ + "Unsupported frame version: 0x" + + bytes[0].toString(16) + ] + }; + } + + const data = {}; + + const frameVersion = bytes[0]; + const timestamp = readUInt32LE(1); + const status = bytes[5]; + const logVolumeLitres = readUInt32LE(6); + + data.payload_format = + "AXIOMA_W1_ENHANCED_24H"; + + data.frame_version = + frameVersion; + + data.payload_bytes = + bytes.length; + + data.timestamp_unix = + timestamp; + + data.timestamp_utc = + timestampToISO(timestamp); + + data.status = + status; + + decodeStatus(status, data); + + data.log_volume_m3 = + logVolumeLitres / 1000.0; + + let totalDeltaLitres = 0; + + for (let i = 0; i < 23; i++) { + const deltaLitres = + readBitsLE( + 10, + i * 14, + 14 + ); + + const index = i + 1; + + data["delta_" + index + "_m3"] = + deltaLitres / 1000.0; + + totalDeltaLitres += + deltaLitres; + } + + data.history_count = + 23; + + data.history_total_m3 = + totalDeltaLitres / 1000.0; + + data.current_volume_m3 = + ( + logVolumeLitres + + totalDeltaLitres + ) / 1000.0; + + data.history_multiplier = + readBitsLE( + 10, + 23 * 14, + 6 + ); + + if (data.history_multiplier !== 0) { + return { + data: data, + warnings: [ + "Non-zero history multiplier detected. Additional scaling may be required." + ] + }; + } + + return { + data: data + }; +} diff --git a/vendor/axioma/qalcosonic-w1-codec.yaml b/vendor/axioma/qalcosonic-w1-codec.yaml new file mode 100644 index 0000000000..5fb1b17baf --- /dev/null +++ b/vendor/axioma/qalcosonic-w1-codec.yaml @@ -0,0 +1,102 @@ +uplinkDecoder: + fileName: qalcosonic-w1-codec.js + examples: + - description: Enhanced 24h payload - Lackova + input: + fPort: 100 + bytes: + - 0x01 + - 0xae + - 0xac + - 0x98 + - 0x6a + - 0x00 + - 0x32 + - 0x17 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x80 + - 0x03 + - 0x80 + - 0x00 + - 0x54 + - 0x00 + - 0x0e + - 0x40 + - 0x05 + - 0x10 + - 0x00 + - 0x48 + - 0x00 + - 0x02 + - 0x40 + - 0x02 + - 0x00 + - 0x00 + - 0x20 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + - 0x00 + output: + data: + payload_format: AXIOMA_W1_ENHANCED_24H + frame_version: 1 + payload_bytes: 51 + timestamp_unix: 1788390574 + timestamp_utc: '2026-09-02T23:09:34.000Z' + status: 0 + low_battery: false + permanent_error: false + temporary_error: false + pipe_empty: false + backflow: false + burst: false + leakage: false + low_temperature: false + log_volume_m3: 5.938 + delta_1_m3: 0 + delta_2_m3: 0 + delta_3_m3: 0 + delta_4_m3: 0 + delta_5_m3: 0 + delta_6_m3: 0.014 + delta_7_m3: 0.008 + delta_8_m3: 0.021 + delta_9_m3: 0.014 + delta_10_m3: 0.021 + delta_11_m3: 0.001 + delta_12_m3: 0.018 + delta_13_m3: 0.002 + delta_14_m3: 0.009 + delta_15_m3: 0 + delta_16_m3: 0.008 + delta_17_m3: 0 + delta_18_m3: 0 + delta_19_m3: 0 + delta_20_m3: 0 + delta_21_m3: 0 + delta_22_m3: 0 + delta_23_m3: 0 + history_count: 23 + history_total_m3: 0.116 + current_volume_m3: 6.054 + history_multiplier: 0 diff --git a/vendor/axioma/qalcosonic-w1.yaml b/vendor/axioma/qalcosonic-w1.yaml index e4472aaebd..316aa3c0d9 100644 --- a/vendor/axioma/qalcosonic-w1.yaml +++ b/vendor/axioma/qalcosonic-w1.yaml @@ -19,9 +19,9 @@ firmwareVersions: # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 profiles: EU863-870: - # Unique identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) id: eu868-profile lorawanCertified: true + codec: qalcosonic-w1-codec # Sensors that this device features (optional) # Valid values are: