Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions vendor/axioma/qalcosonic-w1-codec.js
Original file line number Diff line number Diff line change
@@ -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
};
}
102 changes: 102 additions & 0 deletions vendor/axioma/qalcosonic-w1-codec.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion vendor/axioma/qalcosonic-w1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading