diff --git a/drivers/europe/apator/apator.c b/drivers/europe/apator/apator.c index 4b5d87a..a5f176d 100644 --- a/drivers/europe/apator/apator.c +++ b/drivers/europe/apator/apator.c @@ -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 + * 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, @@ -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[] = { diff --git a/tests/test_apator_hca.c b/tests/test_apator_hca.c index e3e4b21..319a046 100644 --- a/tests/test_apator_hca.c +++ b/tests/test_apator_hca.c @@ -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; } @@ -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"); @@ -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;