// Copyright (C) 2026 Hynek Moravec (gpl-3.0-or-later)
//
// Zenner Caltos E (D1605) heat cost allocator, manufacturer ZRI, version 0xFC,
// device type 0x08 (heat cost allocation). The same meter is also marketed
// generically as "Zenner HCA"; this driver covers both. It supersedes the
// earlier duplicate "zennerhca" driver, which detected the identical
// ZRI,FC,08 fingerprint and caused ambiguous detection. Driver named after the
// device model (Caltos E), matching the project convention (c5isf, minomess, ...).
//
// Status comes from the manufacturer bits of the TPL status byte (see the
// mfct_tpl_status_bits block) per the Zenner Caltos E / D1605-HCA spec:
// 0x04 BATTERY_WARNING, 0x08 INVALID_METROLOGICAL_TABLE, 0x20 REMOVAL,
// 0x40 MODE_2F (commissioning/learning mode), 0x80 PRODUCT_SCALE.
driver {
name = caltose
aliases = zennerhca
meter_type = HeatCostAllocationMeter
default_fields = name,id,status,consumption_hca,target_hca,target_date,timestamp
detect {
mvt = ZRI,FC,08
}
mfct_tpl_status_bits {
mask_bits = 0xff
default_message = OK
map {
name = BATTERY_WARNING
value = 0x04
test = set
}
map {
name = INVALID_METROLOGICAL_TABLE
value = 0x08
test = set
}
map {
name = REMOVAL
value = 0x20
test = set
}
map {
name = MODE_2F
value = 0x40
test = set
}
map {
name = PRODUCT_SCALE
value = 0x80
test = set
}
}
library {
use = consumption_hca
use = target_hca
use = target_date
}
fields {
field {
name = status
quantity = Text
info = 'Status and error flags.'
attributes = STATUS,INCLUDE_TPL_STATUS
}
field {
name = base
quantity = PointInTime
info = 'Base date of compact profile (most recent monthly value).'
display_unit = date
match {
measurement_type = Instantaneous
vif_range = Date
storage_nr = 8
}
}
field {
name = base
quantity = HCA
info = 'Base HCA value of compact profile.'
match {
measurement_type = Instantaneous
vif_range = HeatCostAllocation
storage_nr = 8
}
}
field {
name = 'consumption_{storage_counter-8counter}'
quantity = HCA
info = 'Compact-profile history values (most recent first after base month).'
match {
measurement_type = Instantaneous
vif_range = HeatCostAllocation
add_combinable = Synthetic
storage_nr = 9,40
}
}
field {
name = 'consumption_{storage_counter-8counter}'
quantity = Text
info = 'Billing period date for each compact-profile history slot.'
match {
measurement_type = Instantaneous
vif_range = Date
add_combinable = Synthetic
storage_nr = 9,40
}
}
}
tests {
test {
args = 'Caltos caltose 80081991 6B6B5EB80884328A7B1E45043D39FAAD'
telegram = 5e44496a91190880fc087a744050052f05bf0c6baf613a078098e048ae47ffb93083b455065e03de9b7878c8b3bb063637ea5693160e831cc3d4e0470fabbcd4fedb09d289a0025ca7960d5c7b8064b1093037e7a0a57908edb554d17ad587
json = '{"_":"telegram","media":"heat cost allocation","meter":"caltose","name":"Caltos","id":"80081991","base_date":"2026-02-01","base_hca":203,"consumption_hca":225,"consumption_1_hca":99,"consumption_1":"2026-01-01","consumption_2_hca":52,"consumption_2":"2025-12-01","consumption_3_hca":10,"consumption_3":"2025-11-01","consumption_4_hca":0,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":99,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Caltos;80081991;MODE_2F;225;99;2026-01-01;1111-11-11 11:11.11'
}
test {
args = 'Caltos2 caltose 80081809 EB7CA876CAA641DAC39239326F6DD7EA'
telegram = 5e44496a09180880fc087a84405005760039129d1ce51ce54ef7b4aa5383f1fd525f87046d4f869a3189e5b0c400ecf9c41ae6ed6cf94810f09f4f113c04cc731e379e62ea0331ec7f8b09511357a7f7a484851c3de2ec414dcfdebd4af67d
json = '{"_":"telegram","media":"heat cost allocation","meter":"caltose","name":"Caltos2","id":"80081809","base_date":"2026-02-01","base_hca":0,"consumption_hca":0,"consumption_1_hca":0,"consumption_1":"2026-01-01","consumption_2_hca":0,"consumption_2":"2025-12-01","consumption_3_hca":0,"consumption_3":"2025-11-01","consumption_4_hca":0,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":0,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Caltos2;80081809;MODE_2F;0;0;2026-01-01;1111-11-11 11:11.11'
}
test {
comment = 'Real meter previously covered by the removed zennerhca driver'
args = 'Caltos3 caltose 80081812 DC7C9EF16126348CDFD52CE6567A9FFD'
telegram = 5e44496a12180880fc087a7040500584a63344b5af071a23539c6020cbba81fa6adcfe738682a0924d4f6f7d89d165c9144e4918cdc2d5f86be7b5bd8143528273accc131a13d0b100f2540dc1f2c379dde4984236d3bca424113cf0ee1bbd
json = '{"_":"telegram","media":"heat cost allocation","meter":"caltose","name":"Caltos3","id":"80081812","base_date":"2026-02-01","base_hca":272,"consumption_hca":339,"consumption_1_hca":627,"consumption_1":"2026-01-01","consumption_2_hca":395,"consumption_2":"2025-12-01","consumption_3_hca":176,"consumption_3":"2025-11-01","consumption_4_hca":7,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":627,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Caltos3;80081812;MODE_2F;339;627;2026-01-01;1111-11-11 11:11.11'
}
test {
comment = 'Real meter previously covered by the removed zennerhca driver'
args = 'Caltos4 caltose 80081907 750381240D0A7E371D4CB8D1869D8F9B'
telegram = 5e44496a07190880fc087a714050058922c9598bdddfbb5bbf44b9d54830830eb6be3ba8117dfc88ae1b251837dfecb04e071554125366ef9d72dce87c9a099ad0cd9bf70f4a2e9c4c58d780444219c3f546c887fbc2d93c272e314f925473
json = '{"_":"telegram","media":"heat cost allocation","meter":"caltose","name":"Caltos4","id":"80081907","base_date":"2026-02-01","base_hca":0,"consumption_hca":0,"consumption_1_hca":2,"consumption_1":"2026-01-01","consumption_2_hca":0,"consumption_2":"2025-12-01","consumption_3_hca":0,"consumption_3":"2025-11-01","consumption_4_hca":0,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":2,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Caltos4;80081907;MODE_2F;0;2;2026-01-01;1111-11-11 11:11.11'
}
}
}