Erster Stand der Hintergrundprozesse, die auf der Synology unter /volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins Web-Verzeichnis. Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot), zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat. Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py, gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte. Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein pip, sie muessen neben den Skripten liegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
829 lines
30 KiB
Python
829 lines
30 KiB
Python
import json
|
|
import math
|
|
from collections import OrderedDict
|
|
import os
|
|
import sunspec2.mdef as mdef
|
|
import sunspec2.smdx as smdx
|
|
import sunspec2.mb as mb
|
|
import time
|
|
|
|
|
|
class ModelError(Exception):
|
|
pass
|
|
|
|
|
|
ACCESS_REGION_REGS = 123
|
|
|
|
this_dir, this_filename = os.path.split(__file__)
|
|
models_dir = os.path.join(this_dir, 'models')
|
|
|
|
model_defs_path = ['.', models_dir]
|
|
model_path_options = ['.', 'json', 'smdx']
|
|
model_defs_cache = {}
|
|
|
|
def get_model_defs_path():
|
|
return model_defs_path
|
|
|
|
|
|
def set_model_defs_path(path_list):
|
|
if not isinstance(path_list, list):
|
|
raise mdef.ModelDefinitionError('Invalid path list type, path list is not a list')
|
|
global model_defs_path
|
|
model_defs_path = path_list
|
|
|
|
|
|
def get_model_info(model_id):
|
|
try:
|
|
glen = 0
|
|
model_def = get_model_def(model_id)
|
|
gdef = model_def.get(mdef.GROUP)
|
|
# check if groups have a count point
|
|
has_group_count = check_group_count(gdef)
|
|
# if group has count point, compute the length of top-level points
|
|
if has_group_count:
|
|
points = gdef.get(mdef.POINTS)
|
|
if points:
|
|
for pdef in points:
|
|
info = mb.point_type_info.get(pdef[mdef.TYPE])
|
|
plen = pdef.get(mdef.SIZE, None)
|
|
if plen is not None:
|
|
glen += info.len
|
|
except:
|
|
raise
|
|
|
|
return (model_def, has_group_count, glen)
|
|
|
|
|
|
def check_group_count(gdef):
|
|
has_group_count = (gdef.get(mdef.COUNT) is not None)
|
|
if not has_group_count:
|
|
groups = gdef.get(mdef.GROUPS)
|
|
if groups:
|
|
for g in groups:
|
|
has_group_count = check_group_count(g)
|
|
if has_group_count:
|
|
break
|
|
return has_group_count
|
|
|
|
|
|
def get_model_def(model_id, mapping=True):
|
|
try:
|
|
model_id = int(model_id)
|
|
except:
|
|
raise mdef.ModelDefinitionError('Invalid model id: %s' % model_id)
|
|
|
|
global model_defs_cache
|
|
if (model_id, mapping) in model_defs_cache:
|
|
return model_defs_cache[(model_id, mapping)].copy()
|
|
|
|
model_def_file_json = mdef.to_json_filename(model_id)
|
|
model_def_file_smdx = smdx.to_smdx_filename(model_id)
|
|
model_def = None
|
|
for path in model_defs_path:
|
|
# look in directory, then json/, then smdx/
|
|
for path_option in model_path_options:
|
|
try:
|
|
model_def = mdef.from_json_file(os.path.join(path, path_option, model_def_file_json))
|
|
except FileNotFoundError:
|
|
pass
|
|
except Exception as e:
|
|
raise mdef.ModelDefinitionError('Error loading model definition for model %s: %s' %
|
|
(model_id, str(e)))
|
|
|
|
if model_def is None:
|
|
try:
|
|
model_def = smdx.from_smdx_file(os.path.join(path, path_option, model_def_file_smdx))
|
|
except FileNotFoundError:
|
|
pass
|
|
except Exception as e:
|
|
raise mdef.ModelDefinitionError('Error loading model definition for model %s: %s' %
|
|
(model_id, str(e)))
|
|
|
|
if model_def is not None:
|
|
if mapping:
|
|
add_mappings(model_def[mdef.GROUP])
|
|
model_defs_cache[(model_id, mapping)] = model_def.copy()
|
|
return model_def
|
|
raise mdef.ModelDefinitionError('Model definition not found for model %s' % model_id)
|
|
|
|
|
|
# add id mapping for points and groups for more efficient lookup by id
|
|
def add_mappings(group_def):
|
|
point_defs = {}
|
|
group_defs = {}
|
|
|
|
points = group_def.get(mdef.POINTS, None)
|
|
if points:
|
|
for p in group_def[mdef.POINTS]:
|
|
point_defs[p[mdef.NAME]] = p
|
|
|
|
groups = group_def.get(mdef.GROUPS, None)
|
|
if groups:
|
|
for g in group_def[mdef.GROUPS]:
|
|
group_defs[g[mdef.NAME]] = g
|
|
add_mappings(g)
|
|
|
|
group_def['point_defs'] = point_defs
|
|
group_def['group_defs'] = group_defs
|
|
|
|
|
|
class Point(object):
|
|
def __init__(self, pdef=None, model=None, group=None, model_offset=0, data=None, data_offset=0):
|
|
self.model = model # model object containing the point
|
|
self.group = group
|
|
self.pdef = pdef # point definition
|
|
self.len = 0 # mb register len of point
|
|
self.info = None # point def info
|
|
self.offset = model_offset # mb register offset from beginning of the model
|
|
self._value = None # value
|
|
self.dirty = False # value has been changed without being written
|
|
self.sf = None # scale factor point name
|
|
self.sf_value = None # value of scale factor
|
|
self.sf_required = False # point has a scale factor
|
|
self.detail = None # detailed description
|
|
self.standards = [] # list of standards requiring this point's implementation
|
|
self.read_func = None # function to be called on read
|
|
self.read_func_arg = None # the argument passed to the read_func
|
|
self.write_func = None # function to be called on write
|
|
self.write_func_arg = None # the argument passed to the write_func
|
|
self.static = None
|
|
|
|
if pdef:
|
|
self.sf_required = (pdef.get(mdef.SF) is not None)
|
|
if self.sf_required:
|
|
sf = self.pdef.get(mdef.SF)
|
|
try:
|
|
self.sf_value = int(sf)
|
|
except ValueError:
|
|
self.sf = sf
|
|
|
|
self.info = mb.point_type_info.get(pdef[mdef.TYPE])
|
|
plen = pdef.get(mdef.SIZE, None)
|
|
self.len = self.info.len
|
|
if plen is not None:
|
|
self.len = int(plen)
|
|
|
|
if data is not None:
|
|
self._set_data(data=data, offset=data_offset)
|
|
|
|
static = pdef.get('static', None)
|
|
if static and static == 'S':
|
|
self.static = True
|
|
|
|
standards = pdef.get('standards', None)
|
|
if standards:
|
|
if not isinstance(standards, list):
|
|
standards = [standards]
|
|
self.standards = standards
|
|
|
|
self.detail = pdef.get('detail', None)
|
|
|
|
def __str__(self):
|
|
return self.disp()
|
|
|
|
def disp(self, indent=None):
|
|
if indent is None:
|
|
indent = ''
|
|
return '%s%s: %s\n' % (indent, self.pdef[mdef.NAME], self.value)
|
|
|
|
def _set_data(self, data=None, offset=0):
|
|
if isinstance(data, (bytes, bytearray)):
|
|
byte_offset = offset * 2
|
|
if byte_offset < len(data):
|
|
self.set_mb(data=data[byte_offset:], dirty=False)
|
|
elif isinstance(data, dict):
|
|
value = data.get(self.pdef[mdef.NAME])
|
|
if value is not None:
|
|
self.set_value(data=value)
|
|
|
|
def resolve_sf(self):
|
|
pass
|
|
|
|
@property
|
|
def value(self):
|
|
return self.get_value()
|
|
|
|
@value.setter
|
|
def value(self, v):
|
|
self.set_value(v, dirty=True)
|
|
|
|
@property
|
|
def cvalue(self):
|
|
return self.get_value(computed=True)
|
|
|
|
@cvalue.setter
|
|
def cvalue(self, v):
|
|
self.set_value(v, computed=True, dirty=True)
|
|
|
|
def get_value(self, computed=False):
|
|
# call read function, if set
|
|
if self.read_func:
|
|
self.read_func(self.model, self.read_func_arg)
|
|
|
|
v = self._value
|
|
if computed and v is not None:
|
|
if self.sf_required:
|
|
if self.sf_value is None or not self.static:
|
|
if self.sf:
|
|
sf = self.group.points.get(self.sf)
|
|
if sf is None:
|
|
sf = self.model.points.get(self.sf)
|
|
if sf is not None:
|
|
self.sf_value = sf.value
|
|
else:
|
|
raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name']))
|
|
if self.sf_value:
|
|
sfv = self.sf_value
|
|
if sfv:
|
|
v = round(v * math.pow(10, sfv), -1 * sfv)
|
|
|
|
return v
|
|
|
|
def set_value(self, data=None, computed=False, dirty=None):
|
|
v = data
|
|
if dirty is not None:
|
|
self.dirty = dirty
|
|
if computed:
|
|
if self.sf_required:
|
|
if self.sf_value is None or not self.static:
|
|
if self.sf:
|
|
sf = self.group.points.get(self.sf)
|
|
if sf is None:
|
|
sf = self.model.points.get(self.sf)
|
|
if sf is not None:
|
|
self.sf_value = sf.value
|
|
if sf.value is not None:
|
|
self.sf_value = sf.value
|
|
else:
|
|
raise ModelError('SF field %s value not initialized for point %s' %
|
|
(self.sf, self.pdef['name']))
|
|
else:
|
|
raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name']))
|
|
if self.sf_value is not None:
|
|
self._value = round(round(float(v), abs(self.sf_value)) / math.pow(10, self.sf_value))
|
|
else:
|
|
self._value = v
|
|
else:
|
|
self._value = v
|
|
|
|
# call write function, if set
|
|
# should be used to set indication for subsequent processing rather than do detailed processing
|
|
if self.write_func:
|
|
self.write_func(self.model, self.write_func_arg)
|
|
|
|
def set_read_func(self, func, arg=None):
|
|
self.read_func = func
|
|
self.read_func_arg = arg
|
|
|
|
def set_write_func(self, func, arg=None):
|
|
self.write_func = func
|
|
self.write_func_arg = arg
|
|
|
|
def get_mb(self, computed=False):
|
|
v = self._value
|
|
data = err = None
|
|
if computed and v is not None:
|
|
if self.sf_required:
|
|
if self.sf_value is None or not self.static:
|
|
if self.sf:
|
|
sf = self.group.points.get(self.sf)
|
|
if sf is None:
|
|
sf = self.model.points.get(self.sf)
|
|
if sf is not None:
|
|
self.sf_value = sf.value
|
|
else:
|
|
raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name']))
|
|
if self.sf_value:
|
|
sfv = self.sf_value
|
|
if sfv:
|
|
v = int(v * math.pow(10, sfv))
|
|
try:
|
|
data = self.info.to_data(v, (int(self.len) * 2))
|
|
except Exception as e:
|
|
err = 'Error getting point value %s %s: %s' % (self.pdef[mdef.NAME], v, e)
|
|
if err:
|
|
raise ModelError(err)
|
|
elif v is None:
|
|
data = mb.create_unimpl_value(self.pdef[mdef.TYPE], len=(int(self.len) * 2))
|
|
|
|
if data is None:
|
|
try:
|
|
data = self.info.to_data(v, (int(self.len) * 2))
|
|
except Exception as e:
|
|
err = 'Error getting point value %s %s: %s' % (self.pdef[mdef.NAME], v, e)
|
|
if err:
|
|
raise ModelError(err)
|
|
return data
|
|
|
|
def set_mb(self, data=None, computed=False, dirty=None):
|
|
mb_len = self.len
|
|
try:
|
|
# if not enough data, do not set but consume the data
|
|
if len(data) < mb_len * 2:
|
|
return len(data)
|
|
self.set_value(self.info.data_to(data[:mb_len * 2]), computed=computed, dirty=dirty)
|
|
if not self.info.is_impl(self.value):
|
|
self.set_value(None)
|
|
self.sf_value = None
|
|
except Exception as e:
|
|
self.model.add_error('Error setting value for %s: %s' % (self.pdef[mdef.NAME], str(e)))
|
|
return mb_len
|
|
|
|
def is_impl(self):
|
|
impl = False
|
|
v = self.value
|
|
if v is not None:
|
|
impl = self.info.is_impl(self.value)
|
|
return impl
|
|
|
|
def get_text(self, index=None, parent_index=None):
|
|
txt = ''
|
|
name = self.pdef['name']
|
|
val = self.value
|
|
units = self.pdef.get('units')
|
|
if index:
|
|
if parent_index:
|
|
group_index = f'{parent_index:02}'
|
|
group_index += ':' + f'{index:02}'
|
|
txt += '%6s' % (group_index + ':')
|
|
else:
|
|
group_index = f'{index:02}'
|
|
txt += '%6s' % (group_index + ':')
|
|
else:
|
|
txt += ' ' * 6
|
|
|
|
txt += name
|
|
length = 55 - len(name)
|
|
txt += '%*s' % (length, str(val))
|
|
if units:
|
|
txt += ' ' + units + '\n'
|
|
else:
|
|
txt += '\n'
|
|
|
|
return txt
|
|
|
|
|
|
class Group(object):
|
|
def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None, data_offset=0, group_class=None,
|
|
point_class=None, index=None):
|
|
self.gdef = gdef
|
|
self.model = model
|
|
self.gname = None
|
|
self.offset = model_offset
|
|
self.len = group_len
|
|
self.points = OrderedDict()
|
|
self.groups = OrderedDict()
|
|
self.points_len = 0
|
|
self.group_class = group_class
|
|
self.index = index
|
|
self.access_regions = []
|
|
|
|
if group_class is None:
|
|
self.group_class = self.__class__
|
|
if point_class is None:
|
|
point_class = Point
|
|
|
|
if gdef is not None:
|
|
self.gname = gdef[mdef.NAME]
|
|
self.gdef = gdef
|
|
|
|
# initialize points and point values, if present
|
|
points = self.gdef.get(mdef.POINTS)
|
|
if points:
|
|
for pdef in points:
|
|
# allow legacy model 1 to have an alternate length of 65 registers
|
|
if self.len != 65 or model.model_id != 1 or pdef[mdef.NAME] != 'Pad':
|
|
p = point_class(pdef, model=self.model, group=self, model_offset=model_offset, data=data,
|
|
data_offset=data_offset)
|
|
self.points_len += p.len
|
|
model_offset += p.len
|
|
data_offset += p.len
|
|
self.points[pdef[mdef.NAME]] = p
|
|
# initialize groups
|
|
groups = self.gdef.get(mdef.GROUPS)
|
|
if groups:
|
|
for gdef in groups:
|
|
gdata = self._group_data(data=data, name=gdef[mdef.NAME])
|
|
if gdef.get(mdef.COUNT) is not None:
|
|
g = self._init_repeating_group(gdef=gdef, model_offset=model_offset, data=gdata,
|
|
data_offset=data_offset)
|
|
group_count = len(g)
|
|
if group_count:
|
|
self.groups[gdef[mdef.NAME]] = g
|
|
glen = g[0].len * group_count
|
|
model_offset += glen
|
|
data_offset += glen
|
|
else:
|
|
g = self.group_class(gdef, model=self.model, model_offset=model_offset, data=gdata,
|
|
data_offset=data_offset)
|
|
self.groups[gdef[mdef.NAME]] = g
|
|
model_offset += g.len
|
|
data_offset += g.len
|
|
mlen = model_offset - self.offset
|
|
if self.len:
|
|
if self.len + 2 != mlen:
|
|
self.model.add_error('Model length %s not equal to calculated model length %s for model %s' %
|
|
(self.len + 2, mlen, self.model.model_id))
|
|
self.len = mlen
|
|
|
|
# check if group fits in access region
|
|
if self.len > ACCESS_REGION_REGS:
|
|
index, count = self._init_access(self.access_regions, index=0, count=0)
|
|
# add last region if pending
|
|
if count > 0:
|
|
self.access_regions.append((index, count))
|
|
|
|
len_point = self.points.get('L')
|
|
if len_point:
|
|
len_point.set_value(self.len - 2)
|
|
|
|
id_point = self.points.get('ID')
|
|
if id_point:
|
|
id_val = id_point.pdef.get('value')
|
|
if id_val:
|
|
id_point.set_value(id_point.pdef['value'])
|
|
|
|
def _init_access(self, access_regions, index, count):
|
|
# add points
|
|
if self.points:
|
|
for p, point in self.points.items():
|
|
if count + point.len > ACCESS_REGION_REGS:
|
|
access_regions.append((index, count))
|
|
index += count
|
|
count = 0
|
|
count += point.len
|
|
# add groups
|
|
if self.groups:
|
|
for g, groups in self.groups.items():
|
|
if isinstance(groups, list) and len(groups) > 0:
|
|
for group in groups:
|
|
index, count = group._init_access(access_regions, index, count)
|
|
else:
|
|
index, count = groups._init_access(access_regions, index, count)
|
|
|
|
return index, count
|
|
|
|
def __getattr__(self, attr):
|
|
v = self.points.get(attr)
|
|
if v is None:
|
|
v = self.groups.get(attr)
|
|
if v is None:
|
|
raise AttributeError("%s object has no attribute %s" % (self.group_class.__name__, attr))
|
|
return v
|
|
|
|
def __str__(self):
|
|
return self.disp()
|
|
|
|
def disp(self, indent=None):
|
|
if indent is None:
|
|
indent = ''
|
|
if self.index is not None:
|
|
index = '(%s)' % self.index
|
|
else:
|
|
index = ''
|
|
s = '%s%s%s:\n' % (indent, self.gdef[mdef.NAME], index)
|
|
|
|
indent += ' '
|
|
for k, p in self.points.items():
|
|
s += p.disp(indent)
|
|
|
|
for k, g in self.groups.items():
|
|
if isinstance(g, list):
|
|
for i in range(len(g)):
|
|
s += g[i].disp(indent=indent)
|
|
else:
|
|
s += g.disp(indent)
|
|
|
|
return s
|
|
|
|
def _group_data(self, data=None, name=None, index=None):
|
|
if isinstance(data, dict):
|
|
data = data.get(name)
|
|
elif isinstance(data, list):
|
|
if index is not None and len(data) > index:
|
|
data = data[index]
|
|
else:
|
|
data = None
|
|
return data
|
|
|
|
# check group count in dict data
|
|
def _get_data_group_count(self, data=None):
|
|
if isinstance(data, list):
|
|
return len(data)
|
|
|
|
def _init_repeating_group(self, gdef=None, model_offset=None, data=None, data_offset=0):
|
|
groups = []
|
|
# get group count as a constant
|
|
count = None
|
|
try:
|
|
count = int(gdef[mdef.COUNT])
|
|
except ValueError:
|
|
pass
|
|
except AttributeError:
|
|
self.model.add_error('Count definition %s missing for group %s' % (gdef[mdef.COUNT], gdef[mdef.NAME]))
|
|
if count is None:
|
|
# get count as model point
|
|
count_attr = getattr(self.model, gdef[mdef.COUNT], None)
|
|
if count_attr is None:
|
|
raise ModelError('Count field %s undefined for group %s' % (gdef[mdef.COUNT], gdef[mdef.NAME]))
|
|
count = count_attr.value
|
|
if count is None:
|
|
raise ModelError('Count field %s value not initialized for group %s ' %
|
|
(gdef[mdef.COUNT], gdef[mdef.NAME]))
|
|
|
|
data_group_count = self._get_data_group_count(data=data)
|
|
model_len = self.model.len
|
|
if model_len <= self.model.points_len:
|
|
# if legacy model definition but it is defined in format that number of groups are known, use that count
|
|
# to avoid having to figure out the length in the model data
|
|
if count == 0 and data_group_count:
|
|
count = data_group_count
|
|
|
|
# allocate the group entries if the count is available
|
|
if count > 0:
|
|
for i in range(count):
|
|
gdata = self._group_data(data=data, index=i)
|
|
g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata,
|
|
data_offset=data_offset, index=i+1)
|
|
model_offset += g.len
|
|
data_offset += g.len
|
|
groups.append(g)
|
|
elif count == 0:
|
|
data_group_count = self._get_data_group_count(data=data)
|
|
# legacy model definition - need to calculate repeating count by model length
|
|
# compute count based on model len if present, otherwise allocate when set
|
|
model_len = self.model.len
|
|
if model_len:
|
|
gdata = self._group_data(data=data, name=gdef[mdef.NAME], index=0)
|
|
g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata,
|
|
data_offset=data_offset, index=1)
|
|
group_points_len = g.points_len
|
|
# count is model.len-model.points_len/group_points_len
|
|
# (ID and L points are not included in model length)
|
|
repeating_len = model_len - (self.model.points_len - 2)
|
|
if repeating_len > 0:
|
|
remaining = repeating_len % group_points_len
|
|
if remaining != 0:
|
|
raise ModelError('Repeating group count not consistent with model length for model %s,'
|
|
'model repeating len = %s, model repeating group len = %s' %
|
|
(self.model.model_id, repeating_len, group_points_len))
|
|
|
|
count = int(repeating_len / group_points_len)
|
|
if count > 0:
|
|
groups.append(g)
|
|
model_offset += g.len
|
|
data_offset += g.len
|
|
for i in range(count - 1):
|
|
gdata = self._group_data(data=data, index=(i+1))
|
|
g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata,
|
|
data_offset=data_offset, index=i+2)
|
|
model_offset += g.len
|
|
data_offset += g.len
|
|
groups.append(g)
|
|
return groups
|
|
|
|
def get_dict(self, computed=False):
|
|
d = {}
|
|
for pid, p in self.points.items():
|
|
d[pid] = p.get_value(computed=computed)
|
|
for gid, group in self.groups.items():
|
|
if isinstance(group, list):
|
|
glist = []
|
|
for g in group:
|
|
glist.append(g.get_dict(computed=computed))
|
|
d[gid] = glist
|
|
else:
|
|
d[gid] = group.get_dict(computed=computed)
|
|
return d
|
|
|
|
def set_dict(self, data=None, computed=False, dirty=None):
|
|
groups = []
|
|
group_def = self.gdef
|
|
for k, v in data.items():
|
|
if k in group_def['point_defs']:
|
|
self.points[k].set_value(data=v, computed=computed, dirty=dirty)
|
|
elif k in group_def['group_defs']:
|
|
# write points first as group initialization may depend on point value for group counts
|
|
groups.append(k)
|
|
for k in groups:
|
|
if isinstance(self.groups[k], list):
|
|
i = 0
|
|
for rg in self.groups[k]:
|
|
rg.set_dict(data[k][i], computed=computed, dirty=dirty)
|
|
i += 1
|
|
else:
|
|
self.groups[k].set_dict(data[k], computed=computed, dirty=dirty)
|
|
|
|
def get_json(self, computed=False):
|
|
return json.dumps(self.get_dict(computed=computed))
|
|
|
|
def set_json(self, data=None, computed=False, dirty=None):
|
|
if data is not None:
|
|
d = json.loads(data)
|
|
self.set_dict(d, computed=computed, dirty=dirty)
|
|
|
|
def get_mb(self, computed=False):
|
|
data = bytearray()
|
|
for pid, point in self.points.items():
|
|
data.extend(point.get_mb(computed=computed))
|
|
for gid, group in self.groups.items():
|
|
if isinstance(group, list):
|
|
for g in group:
|
|
data.extend(g.get_mb(computed=computed))
|
|
else:
|
|
data.extend(group.get_mb(computed=computed))
|
|
return bytes(data)
|
|
|
|
def set_mb(self, data=None, computed=False, dirty=None):
|
|
if data:
|
|
data_len = len(data)
|
|
else:
|
|
data_len = 0
|
|
offset = 0
|
|
for pid, point in self.points.items():
|
|
if data_len > offset:
|
|
mb_len = point.set_mb(data[offset:], computed=computed, dirty=dirty)
|
|
if mb_len is not None:
|
|
offset += mb_len * 2
|
|
|
|
for gid, group in self.groups.items():
|
|
if isinstance(group, list):
|
|
for g in group:
|
|
if data_len > offset:
|
|
mb_len = g.set_mb(data[offset:], computed=computed, dirty=dirty)
|
|
if mb_len is not None:
|
|
offset += mb_len * 2
|
|
else:
|
|
return None
|
|
else:
|
|
if data_len > offset:
|
|
mb_len = group.set_mb(data[offset:], computed=computed, dirty=dirty)
|
|
if mb_len is not None:
|
|
offset += mb_len * 2
|
|
else:
|
|
return None
|
|
return int(offset/2)
|
|
|
|
def get_text(self, index=None, parent_index=None):
|
|
txt = ''
|
|
tmp_txt = ''
|
|
for p in self.points:
|
|
tmp_txt = self.points[p].get_text(index, parent_index)
|
|
if tmp_txt:
|
|
txt += tmp_txt
|
|
for g in self.groups:
|
|
if isinstance(self.groups[g], list):
|
|
for i in range(len(self.groups[g])):
|
|
if index:
|
|
txt += self.groups[g][i].get_text(i+1, parent_index=index)
|
|
else:
|
|
txt += self.groups[g][i].get_text(i+1)
|
|
else:
|
|
txt += self.groups[g].get_text(index)
|
|
return txt
|
|
|
|
|
|
class Model(Group):
|
|
def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, group_class=Group):
|
|
self.model_id = model_id
|
|
self.model_addr = model_addr
|
|
self.model_len = model_len
|
|
self.model_def = model_def
|
|
self.error_info = ''
|
|
self.mid = None
|
|
self.device = None
|
|
self.model = self
|
|
|
|
gdef = None
|
|
try:
|
|
if self.model_def is None and model_id is not None:
|
|
self.model_def = get_model_def(model_id)
|
|
if self.model_def is not None:
|
|
gdef = self.model_def.get(mdef.GROUP)
|
|
except Exception as e:
|
|
self.add_error(str(e))
|
|
|
|
Group.__init__(self, gdef=gdef, model=self.model, model_offset=0, group_len=self.model_len, data=data,
|
|
data_offset=0, group_class=group_class)
|
|
|
|
def add_error(self, error_info):
|
|
self.error_info = '%s%s\n' % (self.error_info, error_info)
|
|
|
|
def get_dict(self, computed=False):
|
|
d = Group.get_dict(self, computed=computed)
|
|
d['mid'] = self.mid
|
|
d['error'] = self.error_info
|
|
d['model_id'] = self.model_id
|
|
return d
|
|
|
|
|
|
class Device(object):
|
|
def __init__(self, model_class=Model):
|
|
self.name = None
|
|
self.did = None
|
|
self.models = {}
|
|
self.model_list = []
|
|
self.model_class = model_class
|
|
|
|
def __getattr__(self, attr):
|
|
v = self.models.get(attr)
|
|
if v is None:
|
|
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr))
|
|
return v
|
|
|
|
def scan(self, data=None):
|
|
pass
|
|
|
|
def add_model(self, model):
|
|
# add by model id
|
|
model_id = model.model_id
|
|
model_list = self.models.get(model_id)
|
|
if model_list is None:
|
|
model_list = []
|
|
self.models[model_id] = model_list
|
|
model_list.append(model)
|
|
# add by group id
|
|
gname = model.gname
|
|
if gname is not None:
|
|
model_list = self.models.get(gname)
|
|
if model_list is None:
|
|
model_list = []
|
|
self.models[gname] = model_list
|
|
model_list.append(model)
|
|
# add to model list
|
|
self.model_list.append(model)
|
|
|
|
model.device = self
|
|
|
|
def delete_models(self):
|
|
self.models = {}
|
|
self.model_list = []
|
|
|
|
def get_dict(self, computed=False):
|
|
d = {'name': self.name, 'did': self.did, 'models': []}
|
|
for m in self.model_list:
|
|
d['models'].append(m.get_dict(computed=computed))
|
|
return d
|
|
|
|
def get_json(self, computed=False):
|
|
return json.dumps(self.get_dict(computed=computed))
|
|
|
|
def get_mb(self, computed=False):
|
|
data = bytearray()
|
|
for m in self.model_list:
|
|
data.extend(m.get_mb(computed=computed))
|
|
return bytes(data)
|
|
|
|
def set_mb(self, data=None, computed=False, dirty=None):
|
|
if data:
|
|
data_len = len(data)
|
|
else:
|
|
data_len = 0
|
|
offset = 0
|
|
for m in self.model_list:
|
|
if data_len > offset:
|
|
mb_len = m.set_mb(data[offset:], dirty=dirty, computed=computed)
|
|
if mb_len is not None:
|
|
offset += mb_len * 2
|
|
else:
|
|
return None
|
|
return int(offset/2)
|
|
|
|
def find_mid(self, mid=None):
|
|
if mid is not None:
|
|
for m in self.model_list:
|
|
if m.mid == mid:
|
|
return m
|
|
|
|
# assumes data should be used to create and initialize the models, does not currently update an initialized device
|
|
def _set_dict(self, data, computed=False, detail=False):
|
|
if self.model_list:
|
|
raise ModelError('Device already initialized')
|
|
self.name = data.get('name')
|
|
models = data.get('models')
|
|
for m in models:
|
|
if detail:
|
|
model_id = m['ID']['value']
|
|
else:
|
|
model_id = m['ID']
|
|
if model_id != mdef.END_MODEL_ID:
|
|
model_def = model_len = None
|
|
try:
|
|
model_def = get_model_def(model_id)
|
|
except:
|
|
model_len = m.get('L')
|
|
if not model_len:
|
|
model_len = 0
|
|
model = Model(model_def=model_def, data=m, model_id=m['ID'], model_len=model_len)
|
|
self.add_model(model=model)
|
|
|
|
def get_text(self):
|
|
txt = 'Timestamp: %s\n' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
|
|
for m in self.model_list:
|
|
if m.error_info:
|
|
txt += '\nError: ' + m.error_info + '\n'
|
|
continue
|
|
txt += '\nModel: %s (%s)\n\n' % (m.model_def['group']['name'], m.model_id)
|
|
txt += m.get_text()
|
|
return txt
|