Skip to content
Open
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ FILES = \
victron_regs.py \
vreglink.py \
watchdog.py \
generic_modbus_meter.py \

VELIB = \
settingsdevice.py \
Expand Down
1 change: 1 addition & 0 deletions dbus-modbus-client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import abb
import comap
import victron_em
import generic_modbus_meter

import logging
log = logging.getLogger()
Expand Down
15 changes: 11 additions & 4 deletions device.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,13 @@ def read_data_regs(self, regs, d):

start = regs[0].base
count = regs[-1].base + regs[-1].count - start
register_type = regs[0].base_register_type

rr = self.modbus.read_holding_registers(start, count, unit=self.unit)
rr = None
if(register_type == 3):
rr = self.modbus.read_input_registers(start, count, unit=self.unit)
else:
rr = self.modbus.read_holding_registers(start, count, unit=self.unit)

latency = time.time() - now

Expand Down Expand Up @@ -235,7 +240,7 @@ def pack_regs(self, regs):
rr = []
for r in regs:
rr += r if isinstance(r, list) else [r]
rr.sort(key=lambda r: r.base)
rr.sort(key=lambda r: 10000*r.base_register_type + r.base)

overhead = 5 + 2 # request + response
if self.method == 'tcp':
Expand All @@ -246,12 +251,14 @@ def pack_regs(self, regs):
overhead += 2 * (1 + 2) # address + crc

regs = []
rg = [rr.pop(0)]
r = rr.pop(0)
brt = r.base_register_type
rg = [r]

for r in rr:
end = rg[-1].base + rg[-1].count
nr = r.base + r.count - rg[0].base
if nr > 125 or 2 * (r.base - end) > overhead:
if nr > 125 or 2 * (r.base - end) > overhead or r.base_register_type != brt:
regs.append(rg)
rg = []

Expand Down
82 changes: 82 additions & 0 deletions generic_modbus_meter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import struct
import device
import probe
import logging
import json
import utils

import register

class GenericMeterRTU(device.CustomName, device.EnergyMeter):
""" Generic Meter """
def __init__(self, config, spec, modbus, model):
super().__init__(spec, modbus, model)
self.config = config
self.productid = config.get('product_id', 0)
self.min_timeout = config.get('timeout', 1.0)
self.productname = config.get('product_name', "UNAMED PRODUCT");

def init(self, dbus, enable=True):
super().init(dbus, enable)
self.dbus.add_path ('/Serial', self.config.get('serial', "UNKNOWN"))
self.dbus.add_path ('/FirmwareVersion', self.config.get('version', "UNKNOWN"))

def set_config(self, config):
self.config = config
return self

def device_init(self):
self.info_regs = []

self.data_regs = []
rjs = self.config['data_regs']
for rj in rjs:
self.data_regs.append(register.register_from_object(rj))

def get_ident(self):
return self.config['model']

class MatchWithConfig:
def __init__(self, config, **args):
self.timeout = args.get('timeout', 1)
self.methods = args.get('methods', [])
self.units = args.get('units', [])
self.rates = args.get('rates', [])
self.config = config

def probe(self, spec, modbus, timeout=2):
r = self.config['probe']['reg']
reg = register.register_from_object(r)

rr = None
with modbus, utils.timeout(modbus, timeout or self.timeout):
if not modbus.connect():
raise Exception('connection error')

if(r['type'] == 'input_register'):
rr = modbus.read_input_registers(reg.base, reg.count, unit=spec.unit)
elif(r['type'] == 'holding_register'):
rr = modbus.read_holding_registers(reg.base, reg.count, unit=spec.unit)

if rr == None or rr.isError():
log.debug('%s: %s', modbus, rr)
return None

reg.decode(rr.registers)
if(self.config['probe']['match'] == reg.value):
return GenericMeterRTU(self.config, spec, modbus, self.config['model']).set_config(self.config)

return None

try:
with open(f"/data/etc/generic_rtu_meter.json") as user_file:
file_contents = user_file.read()
configs = json.loads(file_contents)
for config in configs:
probe.add_handler(MatchWithConfig(config,
methods=['rtu'],
units=[1],
timeout=5.0))
##rates=[9600]))
except:
raise Exception('error reading config file')
38 changes: 38 additions & 0 deletions generic_rtu_meter.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"product_id": 0,
"product_name": "TestProduct",
"model": "TestName",
"version": "0.1",
"serial": "12345678",
"probe": {
"reg": {
"base": 0,
"type": "holding_register",
"data_type": "f32b"
},
"match": 9600.0
},
"data_regs": [
{
"type": "input_register",
"data_type": "f32b",
"name": "/Ac/Power",
"base": 10,
"scale": 1,
"text": "%.1f W"
},
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/Frequency", "base": 54, "scale": 1, "text": "%.1f Hz" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/Energy/Forward", "base": 256, "scale": 1, "text": "%.1f kWh" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L1/Voltage", "base": 0, "scale": 1, "text": "%.1f V" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L2/Voltage", "base": 2, "scale": 1, "text": "%.1f V" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L3/Voltage", "base": 4, "scale": 1, "text": "%.1f V" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L1/Current", "base": 8, "scale": 1, "text": "%.1f A" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L2/Current", "base": 10, "scale": 1, "text": "%.1f A" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L3/Current", "base": 12, "scale": 1, "text": "%.1f A" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L1/Power", "base": 16, "scale": 1, "text": "%.1f W" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L2/Power", "base": 18, "scale": 1, "text": "%.1f W" },
{ "type": "input_register", "data_type": "f32b", "name": "/Ac/L3/Power", "base": 20, "scale": 1, "text": "%.1f W" }
]
}
]
72 changes: 72 additions & 0 deletions register.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(self, base, count, name=None, text=None, write=False,
self.time = 0
self.max_age = max_age
self.text = text
self.base_register_type = 4

def __eq__(self, other):
if isinstance(other, type(self)):
Expand All @@ -41,6 +42,10 @@ def __str__(self):
return self.text(self.value)
return str(self.value)

def as_input_register(self):
self.base_register_type = 3
return self

def isvalid(self):
return self.value is not None

Expand Down Expand Up @@ -106,6 +111,11 @@ class Reg_f32l(Reg_num):
count = 2
rtype = float

class Reg_f32b(Reg_num):
coding = ('>f', '>2H')
count = 2
rtype = float

class Reg_e16(Reg, int):
def __init__(self, base, name, enum, **kwargs):
super().__init__(base, 1, name, **kwargs)
Expand Down Expand Up @@ -148,3 +158,65 @@ def decode(self, values):

class Reg_mapu16(Reg_map, Reg_u16):
pass


def register_from_object(obj):
try:
ref = None
if obj['data_type'] == 's16':
ref = Reg_s16
elif obj['data_type'] == 'u16 ':
ref = Reg_u16
elif obj['data_type'] == 's32b':
ref = Reg_s32b
elif obj['data_type'] == 'u32b':
ref = Reg_u32b
elif obj['data_type'] == 'u64b':
ref = Reg_u64b
elif obj['data_type'] == 's32l':
ref = Reg_s32l
elif obj['data_type'] == 'u32l':
ref = Reg_u32l
elif obj['data_type'] == 'f32l':
ref = Reg_f32l
elif obj['data_type'] == 'f32b':
ref = Reg_f32b
elif obj['data_type'] == 'e16':
ref = Reg_e16
elif obj['data_type'] == 'text':
ref = Reg_text
elif obj['data_type'] == 'mapu16':
ref = Reg_mapu16


base = obj.get('base', None)
name = obj.get('name', None)
text = obj.get('text', None)
write = obj.get('write', False)
extra = obj.get('extra', {})
reg_type = obj.get('type', 'holding_register')

ret = None
if issubclass(ref, Reg_num):
scale = obj.get('scale', 1)
invalid = obj.get('invalid', [])
ret = ref(base, name, scale, text, write, invalid, *extra)
elif issubclass(ref, Reg_e16):
enum = obj.get('enum', {})
ret = ref(base, name, enum, *extra)
elif issubclass(ref, Reg_text):
count = obj.get('count', 1)
little = obj.get('little', False)
encoding = obj.get('encoding', None)
ret = ref(base, name, count, name, little, encoding, *extra)
elif issubclass(ref, Reg_map):
args = obj.get('extra', {})
tab = obj.get('tab', {})
ret = ref(base, name, tab, args, *extra)

if reg_type == 'input_register':
ret.as_input_register();

return ret
except:
return None