// Copyright (C) 2026 Fredrik Ohrstrom (gpl-3.0-or-later)
// Copyright (C) 2026 Ihor Kushnirenko (@SteinerOk) (gpl-3.0-or-later)
// Reverse engineering, driver architecture and implementation: Ihor Kushnirenko (@SteinerOk)
// Telegram captures and physical meter verification contributed by:
//   - @kuchar91 (Winter & Autumn captures, FW rev 0x09)
//   - @dawidcieszynski (Spring dynamics captures, FW rev 0x0D)
// Reference: wmbusmeters GitHub issue #1144
// 
// Apator Metra E-ITN 40 heat cost allocator (EN 834), two temperature sensors (tz + tL).
// Protocol: Proprietary wM-Bus, CI=0xA0 (manufacturer-specific, EN 13757-7, range A0h-B7h).
// Frame: 11-byte DLL header + 54-byte payload = 27 x uint16 LE words (w00..w26). Not OMS.
// Firmware variants handled (Version byte in DLL header):
//   0x09/0x0A: old FW (serial < 0101-00205051, before 2024-04-30). 0x09=plain, 0x0A=AES-128.
//   0x0D/0x0E: new FW (serial >= 0101-00205051, from 2024-04-30).  0x0D=plain, 0x0E=AES-128.
// Encrypted variants (0x0A, 0x0E) are detected; supply the AES key to decode.
// Date format (w01, w24): Apator proprietary 16-bit: bits 0..4=Day, 5..8=Month, 9..15=Year+2000.
//   Different from standard EN 13757-3 CP16 Type G which splits Year across byte boundaries.
// DVK keys in iXML grammar are virtual identifiers, not real DIF/VIF records.
//   All monthly HCA DVK keys encode Tariff=0 with unique Storage numbers (no collisions).
driver {
    name           = apatoreitn40
    meter_type     = HeatCostAllocationMeter
    default_fields = name,id,current_hca,previous_hca,previous_2y_hca,previous_3y_hca,current_month_hca,previous_month_hca,previous_month_02_hca,previous_month_03_hca,previous_month_04_hca,previous_month_05_hca,previous_month_06_hca,previous_month_07_hca,previous_month_08_hca,previous_month_09_hca,previous_month_10_hca,previous_month_11_hca,previous_month_12_hca,previous_month_13_hca,previous_month_14_hca,previous_month_15_hca,current_date,season_start_date,temp_radiator_c,temp_room_c,status,timestamp
    manufacturer   = Apator
    detect {
        mvt = APA,09,08
        mvt = APA,0A,08
        mvt = APA,0D,08
        mvt = APA,0E,08
        mvt = APT,09,08
        mvt = APT,0A,08
        mvt = APT,0D,08
        mvt = APT,0E,08
    }
    flags {
        flag {
            name = SENSOR_RADIATOR_OPEN
            info = 'The radiator temperature sensor is open circuited or broken.'
        }
        flag {
            name = SENSOR_RADIATOR_SHORT
            info = 'The radiator temperature sensor is short circuited.'
        }
        flag {
            name = SENSOR_ROOM_OPEN
            info = 'The ambient room temperature sensor is open circuited or broken.'
        }
        flag {
            name = SENSOR_ROOM_SHORT
            info = 'The ambient room temperature sensor is short circuited.'
        }
        flag {
            name = LOW_BATTERY
            info = 'The battery is low.'
        }
        flag {
            name = EEPROM_ERROR
            info = 'Internal EEPROM memory error.'
        }
        flag {
            name = OPTICAL_TAMPER
            info = 'Optical communication interface tampering detected.'
        }
        flag {
            name = SEAL_BROKEN
            info = 'The electronic seal has been broken.'
        }
    }
    fields {
        field {
            name     = current
            quantity = HCA
            info     = 'Cumulative energy consumption so far in current billing period (main LCD reading without SM).'
            match {
                difvifkey = 026E
            }
        }
        field {
            name     = previous
            quantity = HCA
            info     = 'Total energy consumption in previous billing period (SM symbol on LCD).'
            match {
                difvifkey = 426E
            }
        }
        field {
            name     = previous_2y
            quantity = HCA
            info     = 'Energy consumption 2 billing periods ago (historical archive).'
            match {
                difvifkey = 82016E
            }
        }
        field {
            name     = previous_3y
            quantity = HCA
            info     = 'Energy consumption 3 billing periods ago (historical archive).'
            match {
                difvifkey = 82026E
            }
        }
        field {
            name     = current_month
            quantity = HCA
            info     = 'Energy consumption in current month so far (dotted reading on LCD).'
            match {
                difvifkey = 82046E
            }
        }
        field {
            name     = previous_month
            quantity = HCA
            info     = 'Final energy consumption in previous closed month (Month -1, dotted reading with SM on LCD).'
            match {
                difvifkey = 82086E
            }
        }
        field {
            name     = previous_month_02
            quantity = HCA
            info     = 'Energy consumption 2 months ago (Month -2 in historical 15-month buffer).'
            match {
                difvifkey = 82036E
            }
        }
        field {
            name     = previous_month_03
            quantity = HCA
            info     = 'Energy consumption 3 months ago (Month -3 in historical 15-month buffer).'
            match {
                difvifkey = C2036E
            }
        }
        field {
            name     = previous_month_04
            quantity = HCA
            info     = 'Energy consumption 4 months ago (Month -4 in historical 15-month buffer).'
            match {
                difvifkey = 82056E
            }
        }
        field {
            name     = previous_month_05
            quantity = HCA
            info     = 'Energy consumption 5 months ago (Month -5 in historical 15-month buffer).'
            match {
                difvifkey = 82066E
            }
        }
        field {
            name     = previous_month_06
            quantity = HCA
            info     = 'Energy consumption 6 months ago (Month -6 in historical 15-month buffer).'
            match {
                difvifkey = 82076E
            }
        }
        field {
            name     = previous_month_07
            quantity = HCA
            info     = 'Energy consumption 7 months ago (Month -7 in historical 15-month buffer).'
            match {
                difvifkey = 82096E
            }
        }
        field {
            name     = previous_month_08
            quantity = HCA
            info     = 'Energy consumption 8 months ago (Month -8 in historical 15-month buffer).'
            match {
                difvifkey = 820A6E
            }
        }
        field {
            name     = previous_month_09
            quantity = HCA
            info     = 'Energy consumption 9 months ago (Month -9 in historical 15-month buffer).'
            match {
                difvifkey = 820B6E
            }
        }
        field {
            name     = previous_month_10
            quantity = HCA
            info     = 'Energy consumption 10 months ago (Month -10 in historical 15-month buffer).'
            match {
                difvifkey = 820C6E
            }
        }
        field {
            name     = previous_month_11
            quantity = HCA
            info     = 'Energy consumption 11 months ago (Month -11 in historical 15-month buffer).'
            match {
                difvifkey = 820D6E
            }
        }
        field {
            name     = previous_month_12
            quantity = HCA
            info     = 'Energy consumption 12 months ago (Month -12 in historical 15-month buffer).'
            match {
                difvifkey = 820E6E
            }
        }
        field {
            name     = previous_month_13
            quantity = HCA
            info     = 'Energy consumption 13 months ago (Month -13 in historical 15-month buffer).'
            match {
                difvifkey = 820F6E
            }
        }
        field {
            name     = previous_month_14
            quantity = HCA
            info     = 'Energy consumption 14 months ago (Month -14 in historical 15-month buffer).'
            match {
                difvifkey = C2016E
            }
        }
        field {
            name     = previous_month_15
            quantity = HCA
            info     = 'Energy consumption 15 months ago (Month -15 in historical 15-month buffer).'
            match {
                difvifkey = C2026E
            }
        }
        field {
            name           = temp_radiator_raw
            quantity       = Dimensionless
            attributes     = HIDE
            vif_scaling    = None
            dif_signedness = Signed
            match {
                difvifkey = 02FB03
            }
        }
        field {
            name           = temp_room_raw
            quantity       = Dimensionless
            attributes     = HIDE
            vif_scaling    = None
            dif_signedness = Signed
            match {
                difvifkey = 02FB01
            }
        }
        field {
            name       = current_date_raw
            quantity   = Dimensionless
            attributes = HIDE
            match {
                difvifkey = 02FD3A
            }
        }
        field {
            name       = season_start_date_raw
            quantity   = Dimensionless
            attributes = HIDE
            match {
                difvifkey = 42FD3A
            }
        }
        field {
            name           = day_of_week
            quantity       = Dimensionless
            attributes     = HIDE
            vif_scaling    = None
            dif_signedness = Unsigned
            match {
                difvifkey = 02FB05
            }
        }
        field {
            name       = current_day
            quantity   = Dimensionless
            attributes = HIDE
            calculate  = 'current_date_raw_counter % 32counter'
        }
        field {
            name       = current_month_num
            quantity   = Dimensionless
            attributes = HIDE
            calculate  = '(current_date_raw_counter >> 5counter) % 16counter'
        }
        field {
            name       = current_year_num
            quantity   = Dimensionless
            attributes = HIDE
            calculate  = '(current_date_raw_counter >> 9counter) % 128counter'
        }
        field {
            name       = season_start_day
            quantity   = Dimensionless
            attributes = HIDE
            calculate  = 'season_start_date_raw_counter % 32counter'
        }
        field {
            name       = season_start_month_num
            quantity   = Dimensionless
            attributes = HIDE
            calculate  = '(season_start_date_raw_counter >> 5counter) % 16counter'
        }
        field {
            name       = season_start_year_num
            quantity   = Dimensionless
            attributes = HIDE
            calculate  = '(season_start_date_raw_counter >> 9counter) % 32counter'
        }
        field {
            name      = temp_radiator
            quantity  = Temperature
            info      = 'Radiator surface sensor temperature (tz).'
            calculate = '(temp_radiator_raw_counter / 16counter) * 1c'
        }
        field {
            name      = temp_room
            quantity  = Temperature
            info      = 'Ambient room temperature (tL, SM symbol on LCD).'
            calculate = '(temp_room_raw_counter / 16counter) * 1c'
        }
        field {
            name         = current
            quantity     = PointInTime
            display_unit = date
            info         = 'Current reading date reported by meter. Apator proprietary 16-bit: bits 0..4=Day, 5..8=Month, 9..15=Year+2000.'
            calculate    = "'2000-01-01 00:00:00' +
                            (((current_year_num_counter * 12counter) + current_month_num_counter - 1counter) * 1month) +
                            ((current_day_counter - 1counter) * 24h)"
        }
        field {
            name         = season_start
            quantity     = PointInTime
            display_unit = date
            info         = 'Annual billing period start date, recurring DD.MM (u DD.MM on LCD, w01). Year bits are always 0: year 2000 in output is expected.'
            calculate    = "'2000-01-01 00:00:00' +
                            (((season_start_year_num_counter * 12counter) + season_start_month_num_counter - 1counter) * 1month) +
                            ((season_start_day_counter - 1counter) * 24h)"
        }
        field {
            name       = status
            quantity   = Flags
            info       = 'Seal and sensor status (w26). 0x0000=OK, 0xFFFF=SEAL_BROKEN. Bits 0..6 are individual sensor/hardware faults per reverse-engineering spec ss9.3 (unverified on faulty hardware).'
            attributes = STATUS
            match {
                difvifkey = 8200FD3A
            }
            lookup {
                name            = STATUS
                map_type        = BitToString
                mask_bits       = 0xffff
                default_message = OK
                map {
                    name  = SENSOR_RADIATOR_OPEN
                    value = 0x0001
                    test  = Set
                }
                map {
                    name  = SENSOR_RADIATOR_SHORT
                    value = 0x0002
                    test  = Set
                }
                map {
                    name  = SENSOR_ROOM_OPEN
                    value = 0x0004
                    test  = Set
                }
                map {
                    name  = SENSOR_ROOM_SHORT
                    value = 0x0008
                    test  = Set
                }
                map {
                    name  = LOW_BATTERY
                    value = 0x0010
                    test  = Set
                }
                map {
                    name  = EEPROM_ERROR
                    value = 0x0020
                    test  = Set
                }
                map {
                    name  = OPTICAL_TAMPER
                    value = 0x0040
                    test  = Set
                }
                map {
                    name  = SEAL_BROKEN
                    value = 0xffff
                    test  = Set
                }
            }
        }
        field {
            name                 = mfct_specific_data
            quantity             = Text
            attributes           = HIDE
            match_entire_payload = true
            ixml                 = "decode = frame_direct | frame_direct_with_a0 | frame_b6.
                                    frame_direct = pad,
                                                   SeasonStartDateRaw,
                                                   TempRadiatorRaw,
                                                   TempRoomRaw,
                                                   CurrentMonthHCA,
                                                   PrevMonth01HCA,
                                                   PrevMonth02HCA,
                                                   PrevMonth03HCA,
                                                   PrevMonth04HCA,
                                                   PrevMonth05HCA,
                                                   PrevMonth06HCA,
                                                   PrevMonth07HCA,
                                                   PrevMonth08HCA,
                                                   PrevMonth09HCA,
                                                   PrevMonth10HCA,
                                                   PrevMonth11HCA,
                                                   PrevMonth12HCA,
                                                   PrevMonth13HCA,
                                                   PrevMonth14HCA,
                                                   PrevMonth15HCA,
                                                   CurrHCA,
                                                   PrevHCA,
                                                   HCA2yAgo,
                                                   HCA3yAgo,
                                                   CurrentDateRaw,
                                                   DayOfWeek,
                                                   SealStatusRaw.
                                    frame_direct_with_a0 = -'A0', frame_direct.
                                    frame_b6 = frame_b6_00 | frame_b6_01 | frame_b6_02 | frame_b6_03 |
                                               frame_b6_04 | frame_b6_05 | frame_b6_06 | frame_b6_07 |
                                               frame_b6_08 | frame_b6_09 | frame_b6_0A | frame_b6_0B |
                                               frame_b6_0C | frame_b6_0D | frame_b6_0E | frame_b6_0F.
                                    frame_b6_00 = -'00', -'A0', frame_direct.
                                    frame_b6_01 = -'01', byte, -'A0', frame_direct.
                                    frame_b6_02 = -'02', byte, byte, -'A0', frame_direct.
                                    frame_b6_03 = -'03', byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_04 = -'04', byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_05 = -'05', byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_06 = -'06', byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_07 = -'07', byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_08 = -'08', byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_09 = -'09', byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_0A = -'0A', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_0B = -'0B', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_0C = -'0C', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_0D = -'0D', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_0E = -'0E', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
                                    frame_b6_0F = -'0F', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.

                                    pad = byte, byte.
                                    SeasonStartDateRaw = word, @DV_season_start_date_raw.
                                    TempRadiatorRaw = word, @DV_temp_radiator_raw.
                                    TempRoomRaw = word, @DV_temp_room_raw.
                                    CurrentMonthHCA = word, @DV_current_month_hca.
                                    PrevMonth01HCA = word, @DV_previous_month.
                                    PrevMonth02HCA = word, @DV_previous_month_02.
                                    PrevMonth03HCA = word, @DV_previous_month_03.
                                    PrevMonth04HCA = word, @DV_previous_month_04.
                                    PrevMonth05HCA = word, @DV_previous_month_05.
                                    PrevMonth06HCA = word, @DV_previous_month_06.
                                    PrevMonth07HCA = word, @DV_previous_month_07.
                                    PrevMonth08HCA = word, @DV_previous_month_08.
                                    PrevMonth09HCA = word, @DV_previous_month_09.
                                    PrevMonth10HCA = word, @DV_previous_month_10.
                                    PrevMonth11HCA = word, @DV_previous_month_11.
                                    PrevMonth12HCA = word, @DV_previous_month_12.
                                    PrevMonth13HCA = word, @DV_previous_month_13.
                                    PrevMonth14HCA = word, @DV_previous_month_14.
                                    PrevMonth15HCA = word, @DV_previous_month_15.
                                    CurrHCA = word, @DV_current_hca.
                                    PrevHCA = word, @DV_previous_hca.
                                    HCA2yAgo = word, @DV_previous_2y.
                                    HCA3yAgo = word, @DV_previous_3y.
                                    CurrentDateRaw = word, @DV_current_date_raw.
                                    DayOfWeek = word, @DV_day_of_week.
                                    SealStatusRaw = word, @DV_seal_status_raw.

                                    -hex  = ['A'-'F';'0'-'9'].
                                    -byte = hex, hex.
                                    -word = byte, byte.

                                    DV_current_hca>dvk = +'026E'.
                                    DV_previous_hca>dvk = +'426E'.
                                    DV_previous_2y>dvk = +'82016E'.
                                    DV_previous_3y>dvk = +'82026E'.
                                    DV_current_month_hca>dvk = +'82046E'.
                                    DV_previous_month>dvk = +'82086E'.
                                    DV_previous_month_02>dvk = +'82036E'.
                                    DV_previous_month_03>dvk = +'C2036E'.
                                    DV_previous_month_04>dvk = +'82056E'.
                                    DV_previous_month_05>dvk = +'82066E'.
                                    DV_previous_month_06>dvk = +'82076E'.
                                    DV_previous_month_07>dvk = +'82096E'.
                                    DV_previous_month_08>dvk = +'820A6E'.
                                    DV_previous_month_09>dvk = +'820B6E'.
                                    DV_previous_month_10>dvk = +'820C6E'.
                                    DV_previous_month_11>dvk = +'820D6E'.
                                    DV_previous_month_12>dvk = +'820E6E'.
                                    DV_previous_month_13>dvk = +'820F6E'.
                                    DV_previous_month_14>dvk = +'C2016E'.
                                    DV_previous_month_15>dvk = +'C2026E'.
                                    DV_temp_radiator_raw>dvk = +'02FB03'.
                                    DV_temp_room_raw>dvk = +'02FB01'.
                                    DV_current_date_raw>dvk = +'02FD3A'.
                                    DV_season_start_date_raw>dvk = +'42FD3A'.
                                    DV_day_of_week>dvk = +'02FB05'.
                                    DV_seal_status_raw>dvk = +'8200FD3A'."
        }
    }
    tests {
        test {
            args     = 'HCA1 apatoreitn40 00019913 NOKEY'
            telegram = 40440106139901000908A00000A10081016B013F00A2004E00000000000000000000000000000018001E0007007800100002002F01C700000000002D3006000000
            json     = '{"_":"telegram","current_date":"2024-01-13","current_hca":303,"current_month_hca":63,"driver":"apatoreitn40","id":"00019913","media":"heat cost allocation","name":"HCA1","previous_2y_hca":0,"previous_3y_hca":0,"previous_hca":199,"previous_month_02_hca":78,"previous_month_03_hca":0,"previous_month_04_hca":0,"previous_month_05_hca":0,"previous_month_06_hca":0,"previous_month_07_hca":0,"previous_month_08_hca":0,"previous_month_09_hca":0,"previous_month_10_hca":24,"previous_month_11_hca":30,"previous_month_12_hca":7,"previous_month_13_hca":120,"previous_month_14_hca":16,"previous_month_15_hca":2,"previous_month_hca":162,"season_start_date":"2000-05-01","status":"OK","temp_radiator_c":24.0625,"temp_room_c":22.6875,"timestamp":"1111-11-11T11:11:11Z"}'
            fields   = 'HCA1;00019913;303;199;0;0;63;162;78;0;0;0;0;0;0;0;24;30;7;120;16;2;2024-01-13;2000-05-01;24.0625;22.6875;OK;1111-11-11 11:11.11'
        }
        test {
            args     = 'HCA2 apatoreitn40 00383207 NOKEY'
            telegram = 40440106073238000D08A0000081000000F90007001000000000000000000000000000000000000000000000000000000000001700000000000000A83204000000
            json     = '{"_":"telegram","current_date":"2025-05-08","current_hca":23,"current_month_hca":7,"driver":"apatoreitn40","id":"00383207","media":"heat cost allocation","name":"HCA2","previous_2y_hca":0,"previous_3y_hca":0,"previous_hca":0,"previous_month_02_hca":0,"previous_month_03_hca":0,"previous_month_04_hca":0,"previous_month_05_hca":0,"previous_month_06_hca":0,"previous_month_07_hca":0,"previous_month_08_hca":0,"previous_month_09_hca":0,"previous_month_10_hca":0,"previous_month_11_hca":0,"previous_month_12_hca":0,"previous_month_13_hca":0,"previous_month_14_hca":0,"previous_month_15_hca":0,"previous_month_hca":16,"season_start_date":"2000-04-01","status":"OK","temp_radiator_c":0,"temp_room_c":15.5625,"timestamp":"1111-11-11T11:11:11Z"}'
            fields   = 'HCA2;00383207;23;0;0;0;7;16;0;0;0;0;0;0;0;0;0;0;0;0;0;0;2025-05-08;2000-04-01;0;15.5625;OK;1111-11-11 11:11.11'
        }
        test {
            args     = 'HCA3 apatoreitn40 00019913 NOKEY'
            telegram = 40440106139901000908A00000A10000006D0100000000000000000000000000001A002F000E0006000000000000000000000000005D003F01C7002C3305000000
            json     = '{"_":"telegram","current_date":"2025-09-12","current_hca":0,"current_month_hca":0,"driver":"apatoreitn40","id":"00019913","media":"heat cost allocation","name":"HCA3","previous_2y_hca":319,"previous_3y_hca":199,"previous_hca":93,"previous_month_02_hca":0,"previous_month_03_hca":0,"previous_month_04_hca":0,"previous_month_05_hca":0,"previous_month_06_hca":0,"previous_month_07_hca":26,"previous_month_08_hca":47,"previous_month_09_hca":14,"previous_month_10_hca":6,"previous_month_11_hca":0,"previous_month_12_hca":0,"previous_month_13_hca":0,"previous_month_14_hca":0,"previous_month_15_hca":0,"previous_month_hca":0,"season_start_date":"2000-05-01","status":"OK","temp_radiator_c":0,"temp_room_c":22.8125,"timestamp":"1111-11-11T11:11:11Z"}'
            fields   = 'HCA3;00019913;0;93;319;199;0;0;0;0;0;0;0;26;47;14;6;0;0;0;0;0;2025-09-12;2000-05-01;0;22.8125;OK;1111-11-11 11:11.11'
        }
    }
}