Files
SolarManager/sunspec2/smdx.py
T
adminandClaude Opus 5 79843aa2ae 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>
2026-09-02 20:46:59 +02:00

394 lines
14 KiB
Python

"""
Copyright (C) 2020 SunSpec Alliance
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
import os
import xml.etree.ElementTree as ET
import sunspec2.mdef as mdef
SMDX_ROOT = 'sunSpecModels'
SMDX_MODEL = mdef.MODEL
SMDX_BLOCK = 'block'
SMDX_POINT = 'point'
SMDX_ATTR_VERS = 'v'
SMDX_ATTR_ID = 'id'
SMDX_ATTR_LEN = 'len'
SMDX_ATTR_NAME = mdef.NAME
SMDX_ATTR_TYPE = mdef.TYPE
SMDX_ATTR_COUNT = mdef.COUNT
SMDX_ATTR_VALUE = mdef.VALUE
SMDX_ATTR_TYPE_FIXED = 'fixed'
SMDX_ATTR_TYPE_REPEATING = 'repeating'
SMDX_ATTR_OFFSET = 'offset'
SMDX_ATTR_MANDATORY = mdef.MANDATORY
SMDX_ATTR_ACCESS = mdef.ACCESS
SMDX_ATTR_SF = mdef.SF
SMDX_ATTR_UNITS = mdef.UNITS
SMDX_SYMBOL = 'symbol'
SMDX_COMMENT = 'comment'
SMDX_STRINGS = 'strings'
SMDX_ATTR_LOCALE = 'locale'
SMDX_LABEL = mdef.LABEL
SMDX_DESCRIPTION = 'description'
SMDX_NOTES = 'notes'
SMDX_DETAIL = mdef.DETAIL
SMDX_TYPE_INT16 = mdef.TYPE_INT16
SMDX_TYPE_UINT16 = mdef.TYPE_UINT16
SMDX_TYPE_COUNT = mdef.TYPE_COUNT
SMDX_TYPE_ACC16 = mdef.TYPE_ACC16
SMDX_TYPE_ENUM16 = mdef.TYPE_ENUM16
SMDX_TYPE_BITFIELD16 = mdef.TYPE_BITFIELD16
SMDX_TYPE_PAD = mdef.TYPE_PAD
SMDX_TYPE_INT32 = mdef.TYPE_INT32
SMDX_TYPE_UINT32 = mdef.TYPE_UINT32
SMDX_TYPE_ACC32 = mdef.TYPE_ACC32
SMDX_TYPE_ENUM32 = mdef.TYPE_ENUM32
SMDX_TYPE_BITFIELD32 = mdef.TYPE_BITFIELD32
SMDX_TYPE_IPADDR = mdef.TYPE_IPADDR
SMDX_TYPE_INT64 = mdef.TYPE_INT64
SMDX_TYPE_UINT64 = mdef.TYPE_UINT64
SMDX_TYPE_ACC64 = mdef.TYPE_ACC64
SMDX_TYPE_IPV6ADDR = mdef.TYPE_IPV6ADDR
SMDX_TYPE_FLOAT32 = mdef.TYPE_FLOAT32
SMDX_TYPE_STRING = mdef.TYPE_STRING
SMDX_TYPE_SUNSSF = mdef.TYPE_SUNSSF
SMDX_TYPE_EUI48 = mdef.TYPE_EUI48
SMDX_ACCESS_R = 'r'
SMDX_ACCESS_RW = 'rw'
SMDX_MANDATORY_FALSE = 'false'
SMDX_MANDATORY_TRUE = 'true'
smdx_access_types = {SMDX_ACCESS_R: mdef.ACCESS_R, SMDX_ACCESS_RW: mdef.ACCESS_RW}
smdx_mandatory_types = {SMDX_MANDATORY_FALSE: mdef.MANDATORY_FALSE, SMDX_MANDATORY_TRUE: mdef.MANDATORY_TRUE}
smdx_type_types = [
SMDX_TYPE_INT16,
SMDX_TYPE_UINT16,
SMDX_TYPE_COUNT,
SMDX_TYPE_ACC16,
SMDX_TYPE_ENUM16,
SMDX_TYPE_BITFIELD16,
SMDX_TYPE_PAD,
SMDX_TYPE_INT32,
SMDX_TYPE_UINT32,
SMDX_TYPE_ACC32,
SMDX_TYPE_ENUM32,
SMDX_TYPE_BITFIELD32,
SMDX_TYPE_IPADDR,
SMDX_TYPE_INT64,
SMDX_TYPE_UINT64,
SMDX_TYPE_ACC64,
SMDX_TYPE_IPV6ADDR,
SMDX_TYPE_FLOAT32,
SMDX_TYPE_STRING,
SMDX_TYPE_SUNSSF,
SMDX_TYPE_EUI48
]
SMDX_PREFIX = 'smdx_'
SMDX_EXT = '.xml'
def to_smdx_filename(model_id):
return '%s%05d%s' % (SMDX_PREFIX, int(model_id), SMDX_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 mdef.ModelDefinitionError('Error extracting model id from filename')
return mid
'''
smdx to json mapping:
fixed block -> top level group
model 'name' attribute -> group 'name'
ID point is created for model ID and 'value' is the model ID value as a number
L point is created for model len - model len has no value specified in the model definition
fixed block points are placed in top level group
repeating block -> group with count = 0 (indicates model len shoud be used to determine number of groups)
repeating block 'name' -> group 'name', if no 'name' is defined 'name' = 'repeating'
points:
all type, access, and mandatory attributes are preserved
point symbol map to the symbol object and placed in the symbols list for the point
symbol 'name' attribute -> symbol object 'name'
symbol element content -> symbol object 'value'
strings 'label', 'description', 'notes' elements map to point attributes 'label', 'desc', 'detail'
'''
def from_smdx_file(filename):
tree = ET.parse(filename)
root = tree.getroot()
return(from_smdx(root))
def from_smdx(element):
""" Sets the model type attributes based on an element tree model type
element contained in an SMDX model definition.
Parameters:
element :
Element Tree model type element.
"""
model_def = {}
m = element.find(SMDX_MODEL)
if m is None:
raise mdef.ModelDefinitionError('Model definition not found')
try:
mid = mdef.to_number_type(m.attrib.get(SMDX_ATTR_ID))
except ValueError:
raise mdef.ModelDefinitionError('Invalid model id: %s' % m.attrib.get(SMDX_ATTR_ID))
name = m.attrib.get(SMDX_ATTR_NAME)
if name is None:
name = 'model_' + str(mid)
model_def[mdef.NAME] = name
strings = element.find(SMDX_STRINGS)
# create top level group with ID and L points
fixed_def = {mdef.NAME: name,
mdef.TYPE: mdef.TYPE_GROUP,
mdef.POINTS: [
{mdef.NAME: 'ID', mdef.VALUE: mid,
mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID', mdef.SIZE: 1,
mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16},
{mdef.NAME: 'L',
mdef.DESCRIPTION: 'Model length', mdef.LABEL: 'Model Length', mdef.SIZE: 1,
mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16}
]
}
repeating_def = None
fixed = None
repeating = None
for b in m.findall(SMDX_BLOCK):
btype = b.attrib.get(SMDX_ATTR_TYPE, SMDX_ATTR_TYPE_FIXED)
if btype == SMDX_ATTR_TYPE_FIXED:
if fixed is not None:
raise mdef.ModelDefinitionError('Duplicate fixed block type definition')
fixed = b
elif btype == SMDX_ATTR_TYPE_REPEATING:
if repeating is not None:
raise mdef.ModelDefinitionError('Duplicate repeating block type definition')
repeating = b
else:
raise mdef.ModelDefinitionError('Invalid block type: %s' % btype)
fixed_points_map = {}
if fixed is not None:
points = []
for e in fixed.findall(SMDX_POINT):
point_def = from_smdx_point(e)
if point_def[mdef.NAME] not in fixed_points_map:
fixed_points_map[point_def[mdef.NAME]] = point_def
points.append(point_def)
else:
raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME])
if points:
fixed_def[mdef.POINTS].extend(points)
repeating_points_map = {}
if repeating is not None:
name = repeating.attrib.get(SMDX_ATTR_NAME)
if name is None:
name = 'repeating'
repeating_def = {mdef.NAME: name, mdef.TYPE: mdef.TYPE_GROUP, mdef.COUNT: 0}
points = []
for e in repeating.findall(SMDX_POINT):
point_def = from_smdx_point(e)
if point_def[mdef.NAME] not in repeating_points_map:
repeating_points_map[point_def[mdef.NAME]] = point_def
points.append(point_def)
else:
raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME])
if points:
repeating_def[mdef.POINTS] = points
fixed_def[mdef.GROUPS] = [repeating_def]
e = element.find(SMDX_STRINGS)
if e.attrib.get(SMDX_ATTR_ID) == str(mid):
m = e.find(SMDX_MODEL)
if m is not None:
for a in m.findall('*'):
if a.tag == SMDX_LABEL and a.text:
fixed_def[mdef.LABEL] = a.text
elif a.tag == SMDX_DESCRIPTION and a.text:
fixed_def[mdef.DESCRIPTION] = a.text
elif a.tag == SMDX_NOTES and a.text:
fixed_def[mdef.DETAIL] = a.text
# Assign point info to point definitions
for p in e.findall(SMDX_POINT):
pid = p.attrib.get(SMDX_ATTR_ID)
label = desc = notes = None
for a in p.findall('*'):
if a.tag == SMDX_LABEL and a.text:
label = a.text
elif a.tag == SMDX_DESCRIPTION and a.text:
desc = a.text
elif a.tag == SMDX_NOTES and a.text:
notes = a.text
for points_map in [fixed_points_map, repeating_points_map]:
point_def = points_map.get(pid)
if point_def is None:
continue
if label:
point_def[mdef.LABEL] = label
if desc:
point_def[mdef.DESCRIPTION] = desc
if notes:
point_def[mdef.DETAIL] = notes
# Assign symbol info to the point's symbol definitions
for s in p.findall(SMDX_SYMBOL):
sid = s.attrib.get(SMDX_ATTR_ID)
s_label = s_desc = s_notes = None
for a in s.findall('*'):
if a.tag == SMDX_LABEL and a.text:
s_label = a.text
elif a.tag == SMDX_DESCRIPTION and a.text:
s_desc = a.text
elif a.tag == SMDX_NOTES and a.text:
s_notes = a.text
for s in point_def.get(mdef.SYMBOLS):
if s[mdef.NAME] != sid:
continue
if s_label:
s[mdef.LABEL] = s_label
if s_desc:
s[mdef.DESCRIPTION] = s_desc
if s_notes:
s[mdef.DETAIL] = s_notes
break
model_def = {'id': mid, 'group': fixed_def}
return model_def
def from_smdx_point(element):
""" Sets the point attributes based on an element tree point element
contained in an SMDX model definition.
Parameters:
element :
Element Tree point type element.
strings :
Indicates if *element* is a subelement of the 'strings'
definintion within the model definition.
"""
point_def = {}
pid = element.attrib.get(SMDX_ATTR_ID)
if pid is None:
raise mdef.ModelDefinitionError('Missing point id attribute')
point_def[mdef.NAME] = pid
ptype = element.attrib.get(SMDX_ATTR_TYPE)
if ptype is None:
raise mdef.ModelDefinitionError('Missing type attribute for point: %s' % pid)
elif ptype not in smdx_type_types:
raise mdef.ModelDefinitionError('Unknown point type %s for point %s' % (ptype, pid))
point_def[mdef.TYPE] = ptype
plen = mdef.to_number_type(element.attrib.get(SMDX_ATTR_LEN))
if ptype == SMDX_TYPE_STRING:
if plen is None:
raise mdef.ModelDefinitionError('Missing len attribute for point: %s' % pid)
point_def[mdef.SIZE] = plen
else:
point_def[mdef.SIZE] = mdef.point_type_info.get(ptype)['len']
mandatory = element.attrib.get(SMDX_ATTR_MANDATORY, SMDX_MANDATORY_FALSE)
if mandatory not in smdx_mandatory_types:
raise mdef.ModelDefinitionError('Unknown mandatory type: %s' % mandatory)
if mandatory == SMDX_MANDATORY_TRUE:
point_def[mdef.MANDATORY] = smdx_mandatory_types.get(mandatory)
access = element.attrib.get(SMDX_ATTR_ACCESS, SMDX_ACCESS_R)
if access not in smdx_access_types:
raise mdef.ModelDefinitionError('Unknown access type: %s' % access)
if access == SMDX_ACCESS_RW:
point_def[mdef.ACCESS] = smdx_access_types.get(access)
units = element.attrib.get(SMDX_ATTR_UNITS)
if units:
point_def[mdef.UNITS] = units
# if scale factor is an number, convert to correct type
sf = mdef.to_number_type(element.attrib.get(SMDX_ATTR_SF))
if sf is not None:
point_def[mdef.SF] = sf
# if scale factor is an number, convert to correct type
value = mdef.to_number_type(element.attrib.get(SMDX_ATTR_VALUE))
if value is not None:
point_def[mdef.VALUE] = value
symbols = []
for e in element.findall('*'):
if e.tag == SMDX_SYMBOL:
sid = e.attrib.get(SMDX_ATTR_ID)
value = e.text
try:
value = int(value)
except ValueError:
pass
symbols.append({mdef.NAME: sid, mdef.VALUE: value})
if symbols:
point_def[mdef.SYMBOLS] = symbols
return point_def
def indent(elem, level=0):
i = os.linesep + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indent(elem, level+1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i