Skip to content
Merged
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
93 changes: 72 additions & 21 deletions drivers/europe/apator/apator.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,45 @@ const WmbusDriver wmbus_drv_apator_elf = {
.mvt = k_mvt_elf, .decode = NULL,
};

/* Apator HCA / E.ITN (heat-cost allocator). Both byte orderings appear
* in the wild: wmbusmeters' apatoreitn driver registers (mfr, 0x08,
* 0x04) which is version=0x08, medium=0x04, while older firmware uses
* the inverted (0x04, 0x08). Cover both. */
/* Apator HCA / E.ITN (heat-cost allocator) — 1:1 port of the
* wmbusmeters `apatoreitn` driver (drivers/src/apatoreitn.xmq).
*
* CI=0xA0 mfct-specific payload, 15 bytes:
* b0 season_start_date_lo (raw date = 0xA000 | lo)
* b1-2 pad
* b3-4 previous_hca uint16 LE (storagenr 1)
* b5-6 esb_date_raw uint16 LE, 0 = seal intact
* b7-8 current_hca uint16 LE (storagenr 0)
* b9-10 current_date packed date
* b11-12 temp_room_prev_avg /256 °C (matches on-meter display)
* b13-14 temp_room_avg /256 °C
*
* Packed date (all three date fields, year base 2000):
* day = raw % 32, month = (raw >> 5) % 16, year = 2000 + (raw >> 9) % 32
*
* Upstream accepts three wrappings of the same 15-byte frame:
* bare, 0xA0-prefixed, and the CI=0xB6 form
* <n> <n bytes> 0xA0 <15 bytes> with n = 0x00..0x0F. */
static uint16_t apator_hca_le16(const uint8_t* p) {
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}

static void apator_hca_date(uint16_t raw, char* buf, size_t cap) {
unsigned day = raw % 32u;
unsigned month = (raw >> 5) % 16u;
unsigned year = 2000u + ((raw >> 9) % 32u);
snprintf(buf, cap, "%04u-%02u-%02u", year, month, day);
}

/* Locate the 15-byte direct frame inside the APDU, or NULL. */
static const uint8_t* apator_hca_frame(const uint8_t* a, size_t len) {
if(len == 15) return a;
if(len == 16 && a[0] == 0xA0) return a + 1;
if(len >= 17 && a[0] <= 0x0F && len == (size_t)(2u + a[0] + 15u) && a[1 + a[0]] == 0xA0)
return a + 2 + a[0];
return NULL;
}

static size_t apator_hca_fallback(const uint8_t* a, size_t len, char* o, size_t cap) {
size_t pos = (size_t)snprintf(o, cap, "Apator HCA\n");
pos += (size_t)snprintf(o + pos, cap - pos,
Expand Down Expand Up @@ -135,23 +166,43 @@ static size_t decode_apator_hca(uint16_t m, uint8_t v, uint8_t med,
(void)v;
(void)med;

if(len != 15) return apator_hca_fallback(a, len, o, cap);

const uint16_t units = apator_hca_le16(a + 3);
const uint16_t ref_units = apator_hca_le16(a + 7);

return (size_t)snprintf(o, cap,
"Apator HCA\n"
"Units %u\n"
"RefUnits %u\n"
"TempC %u\n"
"Marker %02X%02X\n"
"Flags %02X%02X%02X%02X%02X\n",
units,
ref_units,
(unsigned)a[12],
a[9], a[10],
a[5], a[6], a[11], a[13], a[14]);
const uint8_t* p = apator_hca_frame(a, len);
if(!p) return apator_hca_fallback(a, len, o, cap);

const uint16_t previous_hca = apator_hca_le16(p + 3);
const uint16_t esb_raw = apator_hca_le16(p + 5);
const uint16_t current_hca = apator_hca_le16(p + 7);
const uint16_t date_raw = apator_hca_le16(p + 9);
const uint16_t temp_prev_raw = apator_hca_le16(p + 11);
const uint16_t temp_avg_raw = apator_hca_le16(p + 13);
const uint16_t season_raw = (uint16_t)0xA000 | p[0];

char date[12], season[12], esb[12];
apator_hca_date(date_raw, date, sizeof(date));
apator_hca_date(season_raw, season, sizeof(season));
if(esb_raw) apator_hca_date(esb_raw, esb, sizeof(esb));

size_t pos = (size_t)snprintf(o, cap,
"Apator HCA\n"
"Current %u\n"
"Previous %u\n"
"Date %s\n"
"Season %s\n"
"Seal %s\n",
current_hca,
previous_hca,
date,
season,
esb_raw ? "BROKEN" : "OK");
if(esb_raw) pos += (size_t)snprintf(o + pos, cap - pos, "ESBDate %s\n", esb);
pos += (size_t)snprintf(o + pos, cap - pos,
"TempPrev %u.%03u C\n"
"TempAvg %u.%03u C\n",
(unsigned)(temp_prev_raw >> 8),
(unsigned)((temp_prev_raw & 0xFFu) * 1000u + 128u) / 256u,
(unsigned)(temp_avg_raw >> 8),
(unsigned)((temp_avg_raw & 0xFFu) * 1000u + 128u) / 256u);
return pos;
}

static const WmbusMVT k_mvt_apator_hca[] = {
Expand Down
112 changes: 98 additions & 14 deletions tests/test_apator_hca.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,105 @@ static int expect_contains(const char* name, const char* out, const char* expect
return 1;
}

static int run_sample(void) {
static void decode(const uint8_t* apdu, size_t len, char* out, size_t cap) {
wmbus_engine_decode(MANUF('A', 'P', 'A'), 0x04, 0x08, apdu, len, out, cap);
}

/* wmbusmeters apatoreitn.xmq test HCA1: bare 15-byte frame after
* CI=0xA0. Expected: current 1, previous 89, date 2022-09-18,
* season start 2016-05-01, seal broken 2019-08-28,
* temp prev 19.890625, temp avg 21.703125. */
static int run_upstream_direct(void) {
const uint8_t apdu[] = {
0xA1, 0x00, 0x00, 0x59, 0x00, 0x1C, 0x27, 0x01,
0x00, 0x32, 0x2D, 0xE4, 0x13, 0xB4, 0x15,
};
char out[256];
int fails = 0;

decode(apdu, sizeof(apdu), out, sizeof(out));

fails += expect_contains("direct", out, "Apator HCA\n");
fails += expect_contains("direct", out, "Current 1\n");
fails += expect_contains("direct", out, "Previous 89\n");
fails += expect_contains("direct", out, "Date 2022-09-18\n");
fails += expect_contains("direct", out, "Season 2016-05-01\n");
fails += expect_contains("direct", out, "Seal BROKEN\n");
fails += expect_contains("direct", out, "ESBDate 2019-08-28\n");
fails += expect_contains("direct", out, "TempPrev 19.891 C\n");
fails += expect_contains("direct", out, "TempAvg 21.703 C\n");

return fails;
}

/* Same frame wrapped in a leading 0xA0 marker (upstream
* frame_direct_with_a0 variant). */
static int run_upstream_a0_prefixed(void) {
const uint8_t apdu[] = {
0xA0,
0xA1, 0x00, 0x00, 0x59, 0x00, 0x1C, 0x27, 0x01,
0x00, 0x32, 0x2D, 0xE4, 0x13, 0xB4, 0x15,
};
char out[256];
int fails = 0;

decode(apdu, sizeof(apdu), out, sizeof(out));

fails += expect_contains("a0", out, "Current 1\n");
fails += expect_contains("a0", out, "Previous 89\n");
fails += expect_contains("a0", out, "Date 2022-09-18\n");

return fails;
}

/* wmbusmeters apatoreitn.xmq test HCA2: CI=0xB6 wrapper — skip
* count 0x0A, ten bytes, 0xA0 marker, then the direct frame.
* Expected: current 0, previous 2424, date 2022-08-31, season
* start 2016-05-01, seal OK (esb null), temp prev 22.390625,
* temp avg 25.78125. */
static int run_upstream_b6(void) {
const uint8_t apdu[] = {
0x0A, 0xFF, 0xFF, 0xF5, 0x45, 0x01, 0x86, 0xF4,
0x1B, 0x9D, 0x58, 0xA0,
0xA1, 0x00, 0x00, 0x78, 0x09, 0x00, 0x00, 0x00,
0x00, 0x1F, 0x2D, 0x64, 0x16, 0xC8, 0x19,
};
char out[256];
int fails = 0;

decode(apdu, sizeof(apdu), out, sizeof(out));

fails += expect_contains("b6", out, "Current 0\n");
fails += expect_contains("b6", out, "Previous 2424\n");
fails += expect_contains("b6", out, "Date 2022-08-31\n");
fails += expect_contains("b6", out, "Season 2016-05-01\n");
fails += expect_contains("b6", out, "Seal OK\n");
fails += expect_contains("b6", out, "TempPrev 22.391 C\n");
fails += expect_contains("b6", out, "TempAvg 25.781 C\n");

return fails;
}

/* Field-verified live capture (APDU only, no meter ID): verified
* against wmbusmeters 3.0.0 analyze and the on-meter display
* (display shows TempPrev). Season lo 0x21 -> 2016-01-01. */
static int run_live_sample(void) {
const uint8_t apdu[] = {
0x21, 0x01, 0x00, 0x87, 0x19, 0x00, 0x00, 0x04,
0x20, 0xE7, 0x34, 0x88, 0x16, 0x08, 0x19,
0x21, 0x01, 0x00, 0xD5, 0x19, 0x00, 0x00, 0xD8,
0x28, 0xE8, 0x34, 0x94, 0x15, 0x98, 0x16,
};
char out[256];
int fails = 0;

wmbus_engine_decode(MANUF('A', 'P', 'A'), 0x04, 0x08,
apdu, sizeof(apdu), out, sizeof(out));
decode(apdu, sizeof(apdu), out, sizeof(out));

fails += expect_contains("sample", out, "Apator HCA\n");
fails += expect_contains("sample", out, "Units 6535\n");
fails += expect_contains("sample", out, "RefUnits 8196\n");
fails += expect_contains("sample", out, "TempC 22\n");
fails += expect_contains("sample", out, "Marker E734\n");
fails += expect_contains("sample", out, "Flags 0000880819\n");
fails += expect_contains("live", out, "Current 10456\n");
fails += expect_contains("live", out, "Previous 6613\n");
fails += expect_contains("live", out, "Date 2026-07-08\n");
fails += expect_contains("live", out, "Season 2016-01-01\n");
fails += expect_contains("live", out, "Seal OK\n");
fails += expect_contains("live", out, "TempPrev 21.578 C\n");
fails += expect_contains("live", out, "TempAvg 22.594 C\n");

return fails;
}
Expand All @@ -45,8 +127,7 @@ static int run_bad_length(void) {
char out[256];
int fails = 0;

wmbus_engine_decode(MANUF('A', 'P', 'A'), 0x04, 0x08,
apdu, sizeof(apdu), out, sizeof(out));
decode(apdu, sizeof(apdu), out, sizeof(out));

fails += expect_contains("bad_length", out, "Apator HCA\n");
fails += expect_contains("bad_length", out, "Bytes 3\n");
Expand All @@ -59,7 +140,10 @@ static int run_bad_length(void) {
int main(void) {
int fails = 0;

fails += run_sample();
fails += run_upstream_direct();
fails += run_upstream_a0_prefixed();
fails += run_upstream_b6();
fails += run_live_sample();
fails += run_bad_length();

if(fails) return 1;
Expand Down
Loading