SolarManager unter Versionsverwaltung
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>
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
'''
|
||||
1. JSON is used for the native encoding of information model definitions.
|
||||
2. JSON can be used to represent the values associated with information model points at a specific point in time.
|
||||
|
||||
Python support for information models:
|
||||
- Python model support is based on dictionaries and their afinity with JSON objects.
|
||||
|
||||
Model instance notes:
|
||||
|
||||
- If a model contains repeating groups, the group counts must be known to fully initialized the model. If fields are
|
||||
accessed that depend on group counts that have not been initialized, a ModelError exception is generated.
|
||||
- Points that have not been read or written contain a value of None.
|
||||
- If a point that can not be changed from the initialized value is changed, a ModelError exception is generated.
|
||||
|
||||
A model definition is represented as a dictionary using the constants defined in this file as the entry keys.
|
||||
|
||||
A model definition is required to have a single top level group.
|
||||
|
||||
A model dict
|
||||
- must contain: 'id' and 'group'
|
||||
|
||||
A group dict
|
||||
- must contain: 'name', 'type', 'points'
|
||||
- may contain: 'count', 'groups', 'label', 'description', 'notes', 'comments'
|
||||
|
||||
A point dict
|
||||
- must contain: 'name', 'type'
|
||||
- may contain: 'count', 'size', 'sf', 'units', 'mandatory', 'access', 'symbols', 'label', 'description', 'notes',
|
||||
'comments', 'standards'
|
||||
|
||||
A symbol dict
|
||||
- must contain: 'name', 'value'
|
||||
- may contain: 'label', 'description', 'notes', 'comments'
|
||||
|
||||
Example:
|
||||
model_def = {
|
||||
'id': 123,
|
||||
'group': {
|
||||
'id': model_name,
|
||||
'groups': [],
|
||||
'points': []
|
||||
}
|
||||
'''
|
||||
|
||||
DEVICE = 'device' # device (device dict) ### currently not in the spec
|
||||
MODEL = 'model' # model (model dict) ### currently not in the spec
|
||||
GROUP = 'group' # top level model group (group dict)
|
||||
GROUPS = 'groups' # groups in group (list of group dicts)
|
||||
POINTS = 'points' # points in group (list of point dicts)
|
||||
|
||||
ID = 'id' # id (int or str)
|
||||
NAME = 'name' # name (str)
|
||||
VALUE = 'value' # value (int, float, str)
|
||||
COUNT = 'count' # instance count (int or str)
|
||||
|
||||
TYPE = 'type' # point type (str of TYPE_XXX)
|
||||
MANDATORY = 'mandatory' # point mandatory (str of MANDATORY_XXX)
|
||||
ACCESS = 'access' # point access (str of ACCESS_XXX)
|
||||
STATIC = 'static' # point value is static (str of STATIC_XXX)
|
||||
SF = 'sf' # point scale factor (int)
|
||||
UNITS = 'units' # point units (str)
|
||||
SIZE = 'size' # point string length (int)
|
||||
|
||||
LABEL = 'label' # label (str)
|
||||
DESCRIPTION = 'desc' # description (str)
|
||||
NOTES = 'notes' # notes (str)
|
||||
DETAIL = 'detail' # detailed description (str)
|
||||
SYMBOLS = 'symbols' # symbols (list of symbol dicts)
|
||||
COMMENTS = 'comments' # comments (list of str)
|
||||
STANDARDS = 'standards' # standards (list of str)
|
||||
|
||||
TYPE_GROUP = 'group'
|
||||
TYPE_SYNC_GROUP = 'sync'
|
||||
|
||||
TYPE_INT16 = 'int16'
|
||||
TYPE_UINT16 = 'uint16'
|
||||
TYPE_COUNT = 'count'
|
||||
TYPE_ACC16 = 'acc16'
|
||||
TYPE_ENUM16 = 'enum16'
|
||||
TYPE_BITFIELD16 = 'bitfield16'
|
||||
TYPE_PAD = 'pad'
|
||||
TYPE_INT32 = 'int32'
|
||||
TYPE_UINT32 = 'uint32'
|
||||
TYPE_ACC32 = 'acc32'
|
||||
TYPE_ENUM32 = 'enum32'
|
||||
TYPE_BITFIELD32 = 'bitfield32'
|
||||
TYPE_IPADDR = 'ipaddr'
|
||||
TYPE_INT64 = 'int64'
|
||||
TYPE_UINT64 = 'uint64'
|
||||
TYPE_ACC64 = 'acc64'
|
||||
TYPE_IPV6ADDR = 'ipv6addr'
|
||||
TYPE_FLOAT32 = 'float32'
|
||||
TYPE_FLOAT64 = 'float64'
|
||||
TYPE_STRING = 'string'
|
||||
TYPE_SUNSSF = 'sunssf'
|
||||
TYPE_EUI48 = 'eui48'
|
||||
|
||||
ACCESS_R = 'R'
|
||||
ACCESS_RW = 'RW'
|
||||
|
||||
MANDATORY_FALSE = 'O'
|
||||
MANDATORY_TRUE = 'M'
|
||||
|
||||
STATIC_FALSE = 'D'
|
||||
STATIC_TRUE = 'S'
|
||||
|
||||
MODEL_ID_POINT_NAME = 'ID'
|
||||
MODEL_LEN_POINT_NAME = 'L'
|
||||
|
||||
END_MODEL_ID = 65535
|
||||
|
||||
MODEL_DEF_EXT = '.json'
|
||||
|
||||
|
||||
def to_int(x):
|
||||
try:
|
||||
return int(x, 0)
|
||||
except TypeError:
|
||||
return int(x)
|
||||
|
||||
|
||||
def to_str(s):
|
||||
return str(s)
|
||||
|
||||
|
||||
def to_float(f):
|
||||
try:
|
||||
return float(f)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
# valid model attributes
|
||||
model_attr = {ID: {'type': int, 'mand': True}, GROUP: {'mand': True}, COMMENTS: {}}
|
||||
|
||||
# valid group attributes
|
||||
group_attr = {NAME: {'type': str, 'mand': True}, COUNT: {'type': [int, str]}, TYPE: {'mand': True},
|
||||
GROUPS: {}, POINTS: {'mand': True}, LABEL: {'type': str},
|
||||
DESCRIPTION: {'type': str}, NOTES: {'type': str}, COMMENTS: {}, DETAIL: {'type': str}}
|
||||
|
||||
# valid point attributes
|
||||
point_attr = {NAME: {'type': str, 'mand': True}, COUNT: {'type': int}, VALUE: {}, TYPE: {'mand': True},
|
||||
SIZE: {'type': int}, SF: {}, UNITS: {'type': str},
|
||||
ACCESS: {'type': str, 'values': ['R', 'RW'], 'default': 'R'},
|
||||
MANDATORY: {'type': str, 'values': ['O', 'M'], 'default': 'O'},
|
||||
STATIC: {'type': str, 'values': ['D', 'S'], 'default': 'D'},
|
||||
LABEL: {'type': str}, DESCRIPTION: {'type': str}, NOTES: {'type': str}, SYMBOLS: {}, COMMENTS: {},
|
||||
DETAIL: {'type': str}, STANDARDS: {'type': list}}
|
||||
|
||||
# valid symbol attributes
|
||||
symbol_attr = {NAME: {'type': str, 'mand': True}, VALUE: {'mand': True}, LABEL: {'type': str},
|
||||
DESCRIPTION: {'type': str}, NOTES: {'type': str}, COMMENTS: {}, DETAIL: {'type': str}}
|
||||
|
||||
group_types = [TYPE_GROUP, TYPE_SYNC_GROUP]
|
||||
|
||||
point_type_info = {
|
||||
TYPE_INT16: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_UINT16: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_COUNT: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_ACC16: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_ENUM16: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_BITFIELD16: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_PAD: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_INT32: {'len': 2, 'to_type': to_int, 'default': 0},
|
||||
TYPE_UINT32: {'len': 2, 'to_type': to_int, 'default': 0},
|
||||
TYPE_ACC32: {'len': 2, 'to_type': to_int, 'default': 0},
|
||||
TYPE_ENUM32: {'len': 2, 'to_type': to_int, 'default': 0},
|
||||
TYPE_BITFIELD32: {'len': 2, 'to_type': to_int, 'default': 0},
|
||||
TYPE_IPADDR: {'len': 2, 'to_type': to_int, 'default': 0},
|
||||
TYPE_INT64: {'len': 4, 'to_type': to_int, 'default': 0},
|
||||
TYPE_UINT64: {'len': 4, 'to_type': to_int, 'default': 0},
|
||||
TYPE_ACC64: {'len': 4, 'to_type': to_int, 'default': 0},
|
||||
TYPE_IPV6ADDR: {'len': 8, 'to_type': to_str, 'default': 0},
|
||||
TYPE_FLOAT32: {'len': 2, 'to_type': to_float, 'default': 0},
|
||||
TYPE_FLOAT64: {'len': 4, 'to_type': to_float, 'default': 0},
|
||||
TYPE_STRING: {'len': None, 'to_type': to_str, 'default': ''},
|
||||
TYPE_SUNSSF: {'len': 1, 'to_type': to_int, 'default': 0},
|
||||
TYPE_EUI48: {'len': 4, 'to_type': to_str, 'default': 0}
|
||||
}
|
||||
|
||||
|
||||
class ModelDefinitionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def to_number_type(n):
|
||||
if isinstance(n, str):
|
||||
try:
|
||||
n = int(n)
|
||||
except ValueError:
|
||||
try:
|
||||
n = float(n)
|
||||
except ValueError:
|
||||
pass
|
||||
return n
|
||||
|
||||
|
||||
def validate_find_point(group, pname):
|
||||
points = group.get(POINTS, [])
|
||||
for p in points:
|
||||
pxname = p.get(NAME)
|
||||
if pxname:
|
||||
if p[NAME] == pname:
|
||||
return p
|
||||
|
||||
|
||||
def validate_attrs(element, attrs, result=''):
|
||||
# check for unexpected attributes
|
||||
for k in element:
|
||||
if k not in attrs:
|
||||
result += 'Unexpected model definition attribute: %s in %s\n' % (k, element.get(NAME))
|
||||
# check for missing attributes
|
||||
for k, a in attrs.items():
|
||||
if k in element and element[k] is not None:
|
||||
# check type if specified
|
||||
t = a.get('type')
|
||||
if isinstance(t, list):
|
||||
if t and type(element[k]) not in t:
|
||||
result += 'Unexpected type for model attribute %s, expected %s, found %s\n' % \
|
||||
(k, t, type(element[k]))
|
||||
else:
|
||||
if t and not isinstance(element[k], t):
|
||||
result += ('Unexpected type for model attribute %s, expected %s, found %s\n' %
|
||||
(k, t, type(element[k])))
|
||||
values = a.get('values')
|
||||
if values and element[k] not in values:
|
||||
result += 'Unexpected value for model attribute %s: %s\n' % (k, element[k])
|
||||
elif a.get('mand', False):
|
||||
result += 'Mandatory attribute missing from model definition: %s\n' % k
|
||||
return result
|
||||
|
||||
|
||||
def validate_group_point_dup(group, result=''):
|
||||
groups = group.get(GROUPS, [])
|
||||
for g in groups:
|
||||
gname = g.get(NAME)
|
||||
if gname:
|
||||
count = 0
|
||||
for gx in groups:
|
||||
gxname = gx.get(NAME)
|
||||
if gxname:
|
||||
if gx[NAME] == gname:
|
||||
count += 1
|
||||
if count > 1:
|
||||
result += 'Duplicate group id %s in group %s' % (gname, group[NAME])
|
||||
if validate_find_point(group, gname):
|
||||
result += 'Duplicate group and point id %s in group %s' % (gname, group[NAME])
|
||||
else:
|
||||
result += 'Mandatory %s attribute missing in group definition element\n' % (NAME)
|
||||
points = group.get(POINTS, [])
|
||||
for p in points:
|
||||
pname = p.get(NAME)
|
||||
if pname:
|
||||
count = 0
|
||||
for px in points:
|
||||
pxname = px.get(NAME)
|
||||
if pxname:
|
||||
if px[NAME] == pname:
|
||||
count += 1
|
||||
if count > 1:
|
||||
result += 'Duplicate point id %s in group %s' % (pname, group[NAME])
|
||||
else:
|
||||
result += 'Mandatory attribute missing in point definition element: %s\n' % (NAME)
|
||||
return result
|
||||
|
||||
|
||||
def validate_symbols(symbols, model_group, result=''):
|
||||
for symbol in symbols:
|
||||
result = validate_attrs(symbol, model_group, result)
|
||||
return result
|
||||
|
||||
|
||||
def validate_sf(point, sf, sf_groups, result=''):
|
||||
found = False
|
||||
if isinstance(sf, str):
|
||||
for group in sf_groups:
|
||||
p = validate_find_point(group, sf)
|
||||
if p:
|
||||
found = True
|
||||
if p[TYPE] != TYPE_SUNSSF:
|
||||
result += 'Scale factor %s for point %s is not scale factor type: %s\n' % (sf, point[NAME], p[TYPE])
|
||||
break
|
||||
if not found:
|
||||
result += 'Scale factor %s for point %s not found\n' % (sf, point[NAME])
|
||||
elif isinstance(sf, int):
|
||||
if sf < - 10 or sf > 10:
|
||||
result += 'Scale factor %s for point %s out of range\n' % (sf, point[NAME])
|
||||
else:
|
||||
result += 'Scale factor %s for point %s has invalid type %s\n' % (sf, point[NAME], type(sf))
|
||||
return result
|
||||
|
||||
|
||||
def validate_point_def(point, model_group, group, result=''):
|
||||
# validate general point attributes
|
||||
result = validate_attrs(point, point_attr, result)
|
||||
# validate point type
|
||||
ptype = point.get(TYPE)
|
||||
if ptype not in point_type_info:
|
||||
result += 'Unknown point type %s for point %s\n' % (ptype, point[NAME])
|
||||
# validate scale factor, if present
|
||||
sf = point.get(SF)
|
||||
if sf:
|
||||
result = validate_sf(point, sf, [model_group, group], result)
|
||||
# validate symbols
|
||||
symbols = point.get(SYMBOLS, [])
|
||||
result = validate_symbols(symbols, symbol_attr, result)
|
||||
# check for duplicate symbols
|
||||
for s in symbols:
|
||||
sname = s.get(NAME)
|
||||
if sname:
|
||||
count = 0
|
||||
for sx in symbols:
|
||||
if sx[NAME] == sname:
|
||||
count += 1
|
||||
if count > 1:
|
||||
result += 'Duplicate symbol id %s in point %s\n' % (sname, point[NAME])
|
||||
else:
|
||||
result += 'Mandatory attribute missing in symbol definition element: %s\n' % (NAME)
|
||||
return result
|
||||
|
||||
|
||||
def validate_group_def(group, model_group, result=''):
|
||||
# validate general group attributes
|
||||
result = validate_attrs(group, group_attr, result)
|
||||
# validate points
|
||||
points = group.get(POINTS, [])
|
||||
for p in points:
|
||||
result = validate_point_def(p, model_group, group, result)
|
||||
# validate groups
|
||||
groups = group.get(GROUPS, [])
|
||||
for g in groups:
|
||||
result = validate_group_def(g, model_group, result)
|
||||
# check for group and point duplicates
|
||||
result = validate_group_point_dup(group, result)
|
||||
return result
|
||||
|
||||
|
||||
def validate_model_group_def(model_def, group, result=''):
|
||||
# must contain ID and length points
|
||||
points = group.get(POINTS)
|
||||
if points:
|
||||
if len(points) >= 2:
|
||||
pname = points[0].get(NAME)
|
||||
if pname != MODEL_ID_POINT_NAME:
|
||||
result += "First point in top-level group must be %s, found: %s\n" % (MODEL_ID_POINT_NAME, pname)
|
||||
if points[0].get(VALUE) != model_def.get(ID):
|
||||
result += 'Model ID does not match top-level group ID: %s %s %s %s\n' % (
|
||||
model_def.get(ID), type(model_def.get(ID)), points[0].get(VALUE), type(points[0].get(VALUE)))
|
||||
pname = points[1].get(NAME)
|
||||
if pname != MODEL_LEN_POINT_NAME:
|
||||
result += "Second point in top-level group must be %s, found: %s\n" % (MODEL_LEN_POINT_NAME, pname)
|
||||
else:
|
||||
result += "Top-level group must contain at least two points: %s and %s\n" % (MODEL_ID_POINT_NAME,
|
||||
MODEL_LEN_POINT_NAME)
|
||||
else:
|
||||
result += 'Top-level group missing point definitions\n'
|
||||
# perform normal group validation
|
||||
result = validate_group_def(group, group, result)
|
||||
return result
|
||||
|
||||
|
||||
def validate_model_def(model_def, result=''):
|
||||
result = validate_attrs(model_def, model_attr, result)
|
||||
group = model_def.get(GROUP)
|
||||
result = validate_model_group_def(model_def, group, result)
|
||||
return result
|
||||
|
||||
|
||||
def from_json_str(s):
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def from_json_file(filename):
|
||||
f = open(filename)
|
||||
model_def = json.load(f)
|
||||
f.close()
|
||||
return model_def
|
||||
|
||||
|
||||
def to_json_str(model_def, indent=4):
|
||||
return json.dumps(model_def, indent=indent, sort_keys=True)
|
||||
|
||||
|
||||
def to_json_filename(model_id):
|
||||
return 'model_%s%s' % (model_id, MODEL_DEF_EXT)
|
||||
|
||||
|
||||
def model_filename_to_id(filename):
|
||||
f = filename
|
||||
if '.' in f:
|
||||
f = os.path.splitext(f)[0]
|
||||
try:
|
||||
mid = int(f.rsplit('_', 1)[1])
|
||||
except ValueError:
|
||||
raise ModelDefinitionError('Error extracting model id from filename')
|
||||
|
||||
return mid
|
||||
|
||||
|
||||
def to_json_file(model_def, filename=None, filedir=None, indent=4):
|
||||
if filename is None:
|
||||
filename = to_json_filename(model_def[ID])
|
||||
if filedir is not None:
|
||||
filename = os.path.join(filedir, filename)
|
||||
f = open(filename, 'w')
|
||||
json.dump(model_def, f, indent=indent, sort_keys=True)
|
||||
|
||||
|
||||
def get_group_len_points(group_def, points=None):
|
||||
if points is None:
|
||||
points = []
|
||||
groups = group_def.get(GROUPS)
|
||||
if groups:
|
||||
for g in groups:
|
||||
count = g.get(COUNT)
|
||||
if count:
|
||||
try:
|
||||
count = int(count)
|
||||
except:
|
||||
if count not in points:
|
||||
points.append(count)
|
||||
points = get_group_len_points(g, points)
|
||||
return points
|
||||
|
||||
|
||||
def get_group_len_points_index(group_def):
|
||||
index = pindex = 0
|
||||
if group_def:
|
||||
len_points = get_group_len_points(group_def)
|
||||
if len_points:
|
||||
points = group_def.get(POINTS, [])
|
||||
for p in points:
|
||||
name = p.get(NAME)
|
||||
plen = point_type_info.get(p.get(TYPE)).get('len')
|
||||
if plen is None:
|
||||
plen = point_type_info.get(p.get(SIZE))
|
||||
if not plen:
|
||||
raise ModelDefinitionError('Unable to get size of point %s' % name)
|
||||
pindex += plen
|
||||
if name in len_points:
|
||||
index = pindex
|
||||
len_points.remove(name)
|
||||
if not len_points:
|
||||
break
|
||||
if len_points:
|
||||
raise ModelDefinitionError('Expected points not found in group definition: %s' % len_points)
|
||||
return index
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
Reference in New Issue
Block a user