// Copyright (C) 2020-2026 Fredrik Öhrström (gpl-3.0-or-later)
driver {
name = rfmtx1
meter_type = WaterMeter
default_fields = name,id,total_m3,meter_datetime,timestamp
detect {
mvt = BMT,05,07
}
fields {
// RFM-TX1 (tpl-cfg 1006) uses proprietary payload layout. The ixml rule below
// reconstructs pseudo DV entries so the rest of the driver can decode like normal fields.
field {
name = payload_decoder
quantity = Text
attributes = HIDE,REQUIRED
match_entire_frame = true
ixml = "decode = skip11, key_byte, skip3, skip2, data_raw_bytes, skip7, sec_bytes, min_bytes, hour_bytes, day_bytes, mon_bytes, year_bytes, rest.
-hex = ['A'-'F';'0'-'9'].
-byte = hex, hex.
-skip11 = byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte.
-skip3 = byte, byte, byte.
-skip2 = byte, byte.
-skip7 = byte, byte, byte, byte, byte, byte, byte.
-rest = byte*.
key_byte = byte, @DV_key_raw.
data_raw_bytes = byte, byte, byte, byte, @DV_data_raw.
sec_bytes = byte, @DV_sec_raw.
min_bytes = byte, @DV_min_raw.
hour_bytes = byte, @DV_hour_raw.
day_bytes = byte, @DV_day_raw.
mon_bytes = byte, @DV_mon_raw.
year_bytes = byte, @DV_year_raw.
DV_key_raw>dvk = +'01FF10'.
DV_data_raw>dvk = +'04FF11'.
DV_sec_raw>dvk = +'01FF13'.
DV_min_raw>dvk = +'01FF14'.
DV_hour_raw>dvk = +'01FF15'.
DV_day_raw>dvk = +'01FF16'.
DV_mon_raw>dvk = +'01FF17'.
DV_year_raw>dvk = +'01FF18'."
}
field {
name = key_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF10
}
}
field {
name = data_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 04FF11
}
}
field {
name = sec_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF13
}
}
field {
name = min_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF14
}
}
field {
name = hour_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF15
}
}
field {
name = day_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF16
}
}
field {
name = mon_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF17
}
}
field {
name = year_raw
quantity = Dimensionless
attributes = HIDE
vif_scaling = None
dif_signedness = Unsigned
match {
difvifkey = 01FF18
}
}
field {
name = row
quantity = Dimensionless
attributes = HIDE
// Row selector used by all t0..t3 lookup formulas.
// key_raw is the telegram key byte; low nibble gives a row in [0..15].
calculate = 'key_raw_counter & 15counter'
}
// t0..t3 are row-indexed lookup values. Each formula returns exactly one constant
// for the selected row (all other terms evaluate to 0).
// Keep each ((row==N)*value) term wrapped in parentheses because the formula parser
// does not apply standard operator precedence between + and *.
field {
name = t0
quantity = Dimensionless
attributes = HIDE
// LUT value for d0, where encrypted byte0 is (data_raw_counter & 255counter)
// and d0 is computed below as byte0 ^ key_raw_counter ^ t0_counter.
calculate = '((row_counter == 0counter)*122counter) + ((row_counter == 1counter)*112counter) + ((row_counter == 2counter)*185counter) + ((row_counter == 3counter)*121counter) + ((row_counter == 4counter)*7counter) + ((row_counter == 5counter)*50counter) + ((row_counter == 6counter)*156counter) + ((row_counter == 7counter)*180counter) + ((row_counter == 8counter)*10counter) + ((row_counter == 9counter)*240counter) + ((row_counter == 10counter)*22counter) + ((row_counter == 11counter)*12counter) + ((row_counter == 12counter)*74counter) + ((row_counter == 13counter)*119counter) + ((row_counter == 14counter)*189counter) + ((row_counter == 15counter)*57counter)'
}
field {
name = t1
quantity = Dimensionless
attributes = HIDE
// LUT value for d1, with byte1 = ((data_raw_counter >> 8counter) & 255counter)
// and d1 computed below as byte1 ^ key_raw_counter ^ t1_counter.
calculate = '((row_counter == 0counter)*16counter) + ((row_counter == 1counter)*19counter) + ((row_counter == 2counter)*11counter) + ((row_counter == 3counter)*7counter) + ((row_counter == 4counter)*154counter) + ((row_counter == 5counter)*161counter) + ((row_counter == 6counter)*126counter) + ((row_counter == 7counter)*196counter) + ((row_counter == 8counter)*206counter) + ((row_counter == 9counter)*5counter) + ((row_counter == 10counter)*152counter) + ((row_counter == 11counter)*201counter) + ((row_counter == 12counter)*72counter) + ((row_counter == 13counter)*46counter) + ((row_counter == 14counter)*120counter) + ((row_counter == 15counter)*111counter)'
}
field {
name = t2
quantity = Dimensionless
attributes = HIDE
// LUT value for d2, with byte2 = ((data_raw_counter >> 16counter) & 255counter)
// and d2 computed below as byte2 ^ key_raw_counter ^ t2_counter.
calculate = '((row_counter == 0counter)*26counter) + ((row_counter == 1counter)*34counter) + ((row_counter == 2counter)*142counter) + ((row_counter == 3counter)*74counter) + ((row_counter == 4counter)*203counter) + ((row_counter == 5counter)*57counter) + ((row_counter == 6counter)*96counter) + ((row_counter == 7counter)*128counter) + ((row_counter == 8counter)*25counter) + ((row_counter == 9counter)*165counter) + ((row_counter == 10counter)*17counter) + ((row_counter == 11counter)*125counter) + ((row_counter == 12counter)*228counter) + ((row_counter == 13counter)*138counter) + ((row_counter == 14counter)*87counter) + ((row_counter == 15counter)*40counter)'
}
field {
name = t3
quantity = Dimensionless
attributes = HIDE
// LUT value for d3, with byte3 = ((data_raw_counter >> 24counter) & 255counter)
// and d3 computed below as byte3 ^ key_raw_counter ^ t3_counter.
calculate = '((row_counter == 0counter)*10counter) + ((row_counter == 1counter)*19counter) + ((row_counter == 2counter)*153counter) + ((row_counter == 3counter)*22counter) + ((row_counter == 4counter)*105counter) + ((row_counter == 5counter)*14counter) + ((row_counter == 6counter)*153counter) + ((row_counter == 7counter)*163counter) + ((row_counter == 8counter)*3counter) + ((row_counter == 9counter)*134counter) + ((row_counter == 10counter)*94counter) + ((row_counter == 11counter)*162counter) + ((row_counter == 12counter)*31counter) + ((row_counter == 13counter)*232counter) + ((row_counter == 14counter)*140counter) + ((row_counter == 15counter)*5counter)'
}
field {
name = d0
quantity = Dimensionless
attributes = HIDE
calculate = '(data_raw_counter & 255counter) ^ key_raw_counter ^ t0_counter'
}
field {
name = d1
quantity = Dimensionless
attributes = HIDE
calculate = '((data_raw_counter >> 8counter) & 255counter) ^ key_raw_counter ^ t1_counter'
}
field {
name = d2
quantity = Dimensionless
attributes = HIDE
calculate = '((data_raw_counter >> 16counter) & 255counter) ^ key_raw_counter ^ t2_counter'
}
field {
name = d3
quantity = Dimensionless
attributes = HIDE
calculate = '((data_raw_counter >> 24counter) & 255counter) ^ key_raw_counter ^ t3_counter'
}
field {
name = sec
quantity = Dimensionless
attributes = HIDE
calculate = '((sec_raw_counter >> 4counter) * 10counter) + (sec_raw_counter % 16counter)'
}
field {
name = min
quantity = Dimensionless
attributes = HIDE
calculate = '((min_raw_counter >> 4counter) * 10counter) + (min_raw_counter % 16counter)'
}
field {
name = hour
quantity = Dimensionless
attributes = HIDE
calculate = '((hour_raw_counter >> 4counter) * 10counter) + (hour_raw_counter % 16counter)'
}
field {
name = day
quantity = Dimensionless
attributes = HIDE
calculate = '((day_raw_counter >> 4counter) * 10counter) + (day_raw_counter % 16counter)'
}
field {
name = mon
quantity = Dimensionless
attributes = HIDE
calculate = '((mon_raw_counter >> 4counter) * 10counter) + (mon_raw_counter % 16counter)'
}
field {
name = year
quantity = Dimensionless
attributes = HIDE
calculate = '((year_raw_counter >> 4counter) * 10counter) + (year_raw_counter % 16counter)'
}
field {
name = total
quantity = Volume
info = 'The total water consumption recorded by this meter.'
// d0..d3 are decrypted bytes; each byte stores two BCD digits. Rebuild the
// 8-digit reading and scale from liters to m3.
calculate = '((((d0_counter >> 4counter) * 10counter) + (d0_counter % 16counter)) + (100counter * (((d1_counter >> 4counter) * 10counter) + (d1_counter % 16counter))) + (10000counter * (((d2_counter >> 4counter) * 10counter) + (d2_counter % 16counter))) + (1000000counter * (((d3_counter >> 4counter) * 10counter) + (d3_counter % 16counter)))) / 1000counter * 1m3'
}
field {
name = meter
quantity = PointInTime
display_unit = datetime
info = 'Date time when meter sent this telegram.'
// Meter datetime bytes are plain BCD (yy MM dd HH mm ss) in telegram payload.
// Build local meter time from component fields, matching behavior confirmed in issue #101.
calculate = "'2000-01-01 00:00:00' +
(((year_counter * 12counter) + mon_counter - 1counter) * 1month) +
((day_counter - 1counter) * 24h) +
((((hour_counter * 3600counter) + (min_counter * 60counter) + sec_counter)) * 1s)"
}
}
tests {
test {
args = 'Wasser rfmtx1 74737271 NOKEY'
telegram = 4644B4097172737405077AA5000610_1115F78184AB0F1D1E200000005904103103208047004A4800E73C00193E00453F003E4000E64000E74100F442000144001545005B460000
json = '{"_":"telegram","media":"water","meter":"rfmtx1","name":"Wasser","id":"74737271","total_m3":188.56,"meter_datetime":"2020-03-31 10:04","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Wasser;74737271;188.56;2020-03-31 10:04;1111-11-11 11:11.11'
}
}
}