DLMS-SPODES 0.87.12__py3-none-any.whl → 0.87.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DLMS_SPODES/Values/EN/__init__.py +1 -1
- DLMS_SPODES/Values/EN/actors.py +8 -8
- DLMS_SPODES/Values/EN/relation_to_obis_names.py +387 -387
- DLMS_SPODES/Values/RU/__init__.py +1 -1
- DLMS_SPODES/Values/RU/actors.py +8 -8
- DLMS_SPODES/Values/RU/relation_to_obis_names.py +396 -396
- DLMS_SPODES/__init__.py +6 -6
- DLMS_SPODES/configEN.ini +126 -126
- DLMS_SPODES/config_parser.py +53 -53
- DLMS_SPODES/cosem_interface_classes/__class_init__.py +3 -3
- DLMS_SPODES/cosem_interface_classes/__init__.py +1 -1
- DLMS_SPODES/cosem_interface_classes/a_parameter.py +20 -20
- DLMS_SPODES/cosem_interface_classes/activity_calendar.py +254 -254
- DLMS_SPODES/cosem_interface_classes/arbitrator.py +105 -105
- DLMS_SPODES/cosem_interface_classes/association_ln/abstract.py +34 -34
- DLMS_SPODES/cosem_interface_classes/association_ln/authentication_mechanism_name.py +25 -25
- DLMS_SPODES/cosem_interface_classes/association_ln/mechanism_id.py +25 -25
- DLMS_SPODES/cosem_interface_classes/association_ln/method.py +5 -5
- DLMS_SPODES/cosem_interface_classes/association_ln/ver0.py +485 -485
- DLMS_SPODES/cosem_interface_classes/association_ln/ver1.py +133 -133
- DLMS_SPODES/cosem_interface_classes/association_ln/ver2.py +36 -36
- DLMS_SPODES/cosem_interface_classes/association_ln/ver3.py +4 -4
- DLMS_SPODES/cosem_interface_classes/association_sn/ver0.py +12 -12
- DLMS_SPODES/cosem_interface_classes/attr_indexes.py +12 -12
- DLMS_SPODES/cosem_interface_classes/clock.py +131 -131
- DLMS_SPODES/cosem_interface_classes/collection.py +2122 -2122
- DLMS_SPODES/cosem_interface_classes/cosem_interface_class.py +583 -583
- DLMS_SPODES/cosem_interface_classes/data.py +21 -21
- DLMS_SPODES/cosem_interface_classes/demand_register/ver0.py +59 -59
- DLMS_SPODES/cosem_interface_classes/disconnect_control.py +74 -74
- DLMS_SPODES/cosem_interface_classes/extended_register.py +27 -27
- DLMS_SPODES/cosem_interface_classes/gprs_modem_setup.py +43 -43
- DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver0.py +103 -103
- DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver1.py +40 -40
- DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver2.py +9 -9
- DLMS_SPODES/cosem_interface_classes/iec_hdlc_setup/ver0.py +11 -11
- DLMS_SPODES/cosem_interface_classes/iec_hdlc_setup/ver1.py +53 -53
- DLMS_SPODES/cosem_interface_classes/iec_local_port_setup.py +11 -11
- DLMS_SPODES/cosem_interface_classes/image_transfer/image_transfer_status.py +15 -15
- DLMS_SPODES/cosem_interface_classes/image_transfer/ver0.py +126 -126
- DLMS_SPODES/cosem_interface_classes/implementations/__init__.py +3 -3
- DLMS_SPODES/cosem_interface_classes/implementations/arbitrator.py +19 -19
- DLMS_SPODES/cosem_interface_classes/implementations/data.py +487 -487
- DLMS_SPODES/cosem_interface_classes/implementations/profile_generic.py +83 -83
- DLMS_SPODES/cosem_interface_classes/ipv4_setup.py +72 -72
- DLMS_SPODES/cosem_interface_classes/limiter.py +111 -111
- DLMS_SPODES/cosem_interface_classes/ln_pattern.py +333 -333
- DLMS_SPODES/cosem_interface_classes/modem_configuration/ver0.py +65 -65
- DLMS_SPODES/cosem_interface_classes/modem_configuration/ver1.py +39 -39
- DLMS_SPODES/cosem_interface_classes/ntp_setup/ver0.py +67 -67
- DLMS_SPODES/cosem_interface_classes/obis.py +23 -23
- DLMS_SPODES/cosem_interface_classes/overview.py +197 -197
- DLMS_SPODES/cosem_interface_classes/parameter.py +547 -547
- DLMS_SPODES/cosem_interface_classes/parameters.py +172 -172
- DLMS_SPODES/cosem_interface_classes/profile_generic/ver0.py +122 -122
- DLMS_SPODES/cosem_interface_classes/profile_generic/ver1.py +277 -277
- DLMS_SPODES/cosem_interface_classes/push_setup/ver0.py +12 -12
- DLMS_SPODES/cosem_interface_classes/push_setup/ver1.py +10 -10
- DLMS_SPODES/cosem_interface_classes/push_setup/ver2.py +166 -166
- DLMS_SPODES/cosem_interface_classes/register.py +45 -45
- DLMS_SPODES/cosem_interface_classes/register_activation/ver0.py +80 -80
- DLMS_SPODES/cosem_interface_classes/register_monitor.py +46 -46
- DLMS_SPODES/cosem_interface_classes/reports.py +70 -70
- DLMS_SPODES/cosem_interface_classes/schedule.py +176 -176
- DLMS_SPODES/cosem_interface_classes/script_table.py +87 -87
- DLMS_SPODES/cosem_interface_classes/security_setup/ver0.py +68 -68
- DLMS_SPODES/cosem_interface_classes/security_setup/ver1.py +158 -158
- DLMS_SPODES/cosem_interface_classes/single_action_schedule.py +50 -50
- DLMS_SPODES/cosem_interface_classes/special_days_table.py +84 -84
- DLMS_SPODES/cosem_interface_classes/tcp_udp_setup.py +42 -42
- DLMS_SPODES/cosem_pdu.py +93 -93
- DLMS_SPODES/enums.py +625 -625
- DLMS_SPODES/exceptions.py +106 -106
- DLMS_SPODES/firmwares.py +99 -99
- DLMS_SPODES/hdlc/frame.py +875 -875
- DLMS_SPODES/hdlc/sub_layer.py +54 -54
- DLMS_SPODES/literals.py +17 -17
- DLMS_SPODES/obis/__init__.py +1 -1
- DLMS_SPODES/obis/media_id.py +931 -931
- DLMS_SPODES/pardata.py +22 -22
- DLMS_SPODES/pdu_enums.py +98 -98
- DLMS_SPODES/relation_to_OBIS.py +465 -463
- DLMS_SPODES/settings.py +551 -551
- DLMS_SPODES/types/choices.py +142 -142
- DLMS_SPODES/types/common_data_types.py +2401 -2399
- DLMS_SPODES/types/cosem_service_types.py +109 -109
- DLMS_SPODES/types/implementations/arrays.py +25 -25
- DLMS_SPODES/types/implementations/bitstrings.py +97 -97
- DLMS_SPODES/types/implementations/double_long_usingneds.py +35 -35
- DLMS_SPODES/types/implementations/enums.py +57 -57
- DLMS_SPODES/types/implementations/integers.py +11 -11
- DLMS_SPODES/types/implementations/long_unsigneds.py +127 -127
- DLMS_SPODES/types/implementations/octet_string.py +11 -11
- DLMS_SPODES/types/implementations/structs.py +64 -64
- DLMS_SPODES/types/useful_types.py +677 -677
- {dlms_spodes-0.87.12.dist-info → dlms_spodes-0.87.15.dist-info}/METADATA +30 -30
- dlms_spodes-0.87.15.dist-info/RECORD +117 -0
- {dlms_spodes-0.87.12.dist-info → dlms_spodes-0.87.15.dist-info}/WHEEL +1 -1
- dlms_spodes-0.87.12.dist-info/RECORD +0 -117
- {dlms_spodes-0.87.12.dist-info → dlms_spodes-0.87.15.dist-info}/top_level.txt +0 -0
|
@@ -1,2122 +1,2122 @@
|
|
|
1
|
-
""" The OBIS identification system serves as a basis for the COSEM logical names. The system of naming COSEM objects is defined in the basic
|
|
2
|
-
principles (see Clause 4 EN 62056-62:2007), the identification of real data items is specified in IEC 62056-61. The following clauses define the
|
|
3
|
-
usage of those definitions in the COSEM environment. All codes, which are not explicitly listed, but outside the manufacturer specific range are
|
|
4
|
-
reserved for future use."""
|
|
5
|
-
from struct import pack
|
|
6
|
-
import inspect
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from itertools import count, chain
|
|
9
|
-
from functools import reduce, cached_property, lru_cache
|
|
10
|
-
from typing import TypeAlias, Iterator, Type, Self, Callable, Literal, Iterable, Optional, Hashable, Protocol, cast, Annotated
|
|
11
|
-
from semver import Version as SemVer
|
|
12
|
-
from StructResult import result
|
|
13
|
-
from ..types import common_data_types as cdt, cosem_service_types as cst, useful_types as ut
|
|
14
|
-
from ..types.implementations import structs, enums, octet_string
|
|
15
|
-
from . import cosem_interface_class as ic
|
|
16
|
-
from .ln_pattern import LNPattern, LNPatterns
|
|
17
|
-
from .activity_calendar import ActivityCalendar, DayProfileAction
|
|
18
|
-
from .arbitrator import Arbitrator
|
|
19
|
-
from .association_ln import mechanism_id
|
|
20
|
-
from .association_sn.ver0 import AssociationSN as AssociationSNVer0
|
|
21
|
-
from .association_ln.ver0 import AssociationLN as AssociationLNVer0, ObjectListElement
|
|
22
|
-
from .association_ln.ver1 import AssociationLN as AssociationLNVer1
|
|
23
|
-
from .association_ln.ver2 import AssociationLN as AssociationLNVer2
|
|
24
|
-
from .push_setup.ver0 import PushSetup as PushSetupVer0
|
|
25
|
-
from .push_setup.ver1 import PushSetup as PushSetupVer1
|
|
26
|
-
from .push_setup.ver2 import PushSetup as PushSetupVer2
|
|
27
|
-
from .clock import Clock
|
|
28
|
-
from .data import Data
|
|
29
|
-
from .disconnect_control import DisconnectControl
|
|
30
|
-
from .gprs_modem_setup import GPRSModemSetup
|
|
31
|
-
from .gsm_diagnostic.ver0 import GSMDiagnostic as GSMDiagnosticVer0
|
|
32
|
-
from .gsm_diagnostic.ver1 import GSMDiagnostic as GSMDiagnosticVer1
|
|
33
|
-
from .gsm_diagnostic.ver2 import GSMDiagnostic as GSMDiagnosticVer2
|
|
34
|
-
from .iec_hdlc_setup.ver0 import IECHDLCSetup as IECHDLCSetupVer0
|
|
35
|
-
from .iec_hdlc_setup.ver1 import IECHDLCSetup as IECHDLCSetupVer1
|
|
36
|
-
from .image_transfer.ver0 import ImageTransfer
|
|
37
|
-
from .ipv4_setup import IPv4Setup
|
|
38
|
-
from .modem_configuration.ver0 import PSTNModemConfiguration
|
|
39
|
-
from .modem_configuration.ver1 import ModemConfigurationVer1
|
|
40
|
-
from .limiter import Limiter
|
|
41
|
-
from .ntp_setup.ver0 import NTPSetup
|
|
42
|
-
from .profile_generic.ver0 import ProfileGeneric as ProfileGenericVer0
|
|
43
|
-
from .profile_generic.ver1 import ProfileGeneric as ProfileGenericVer1
|
|
44
|
-
from .register import Register
|
|
45
|
-
from .extended_register import ExtendedRegister
|
|
46
|
-
from .demand_register.ver0 import DemandRegister as DemandRegisterVer0
|
|
47
|
-
from .register_activation.ver0 import RegisterActivation
|
|
48
|
-
from .register_monitor import RegisterMonitor
|
|
49
|
-
from .schedule import Schedule
|
|
50
|
-
from .security_setup.ver0 import SecuritySetup as SecuritySetupVer0
|
|
51
|
-
from .security_setup.ver1 import SecuritySetup as SecuritySetupVer1
|
|
52
|
-
from .script_table import ScriptTable
|
|
53
|
-
from .single_action_schedule import SingleActionSchedule
|
|
54
|
-
from .special_days_table import SpecialDaysTable
|
|
55
|
-
from .tcp_udp_setup import TCPUDPSetup
|
|
56
|
-
from .. import exceptions as exc
|
|
57
|
-
from ..relation_to_OBIS import get_name
|
|
58
|
-
from ..cosem_interface_classes import implementations as impl
|
|
59
|
-
from ..cosem_interface_classes.overview import ClassID, CountrySpecificIdentifiers
|
|
60
|
-
from . import obis as o, ln_pattern
|
|
61
|
-
from .. import pdu_enums as pdu
|
|
62
|
-
from ..config_parser import config, get_message
|
|
63
|
-
from ..obis import media_id
|
|
64
|
-
from .parameter import Parameter
|
|
65
|
-
from typing_extensions import deprecated, override
|
|
66
|
-
from ..settings import settings
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class CollectionMapError(exc.DLMSException):
|
|
70
|
-
""""""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
LNContaining: TypeAlias = bytes | str | cst.LogicalName | cdt.Structure | ut.CosemObjectInstanceId | ut.CosemAttributeDescriptor | ut.CosemAttributeDescriptorWithSelection \
|
|
74
|
-
| ut.CosemMethodDescriptor
|
|
75
|
-
|
|
76
|
-
AssociationSN: TypeAlias = AssociationSNVer0
|
|
77
|
-
AssociationLN: TypeAlias = AssociationLNVer0 | AssociationLNVer1 | AssociationLNVer2
|
|
78
|
-
ModemConfiguration: TypeAlias = PSTNModemConfiguration | ModemConfigurationVer1
|
|
79
|
-
SecuritySetup: TypeAlias = SecuritySetupVer0 | SecuritySetupVer1
|
|
80
|
-
PushSetup: TypeAlias = PushSetupVer0 | PushSetupVer1 | PushSetupVer2
|
|
81
|
-
ProfileGeneric: TypeAlias = ProfileGenericVer1
|
|
82
|
-
DemandRegister: TypeAlias = DemandRegisterVer0
|
|
83
|
-
IECHDLCSetup: TypeAlias = IECHDLCSetupVer0 | IECHDLCSetupVer1
|
|
84
|
-
GSMDiagnostic: TypeAlias = GSMDiagnosticVer0 | GSMDiagnosticVer1 | GSMDiagnosticVer2
|
|
85
|
-
InterfaceClass: TypeAlias = Data | Register | ExtendedRegister | DemandRegister | ProfileGeneric | Clock | ScriptTable | Schedule | SpecialDaysTable | ActivityCalendar | \
|
|
86
|
-
SingleActionSchedule | AssociationLN | IECHDLCSetup | DisconnectControl | Limiter | ModemConfiguration | PSTNModemConfiguration | ImageTransfer | \
|
|
87
|
-
GPRSModemSetup | GSMDiagnostic | SecuritySetup | TCPUDPSetup | IPv4Setup | Arbitrator | RegisterMonitor | PushSetup | AssociationSN | \
|
|
88
|
-
NTPSetup
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
type AttributeIndex = int
|
|
92
|
-
UsedAttributes: TypeAlias = dict[cst.LogicalName, set[AttributeIndex]]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
ObjectTreeMode: TypeAlias = Literal["", "m", "g", "c", "mc", "cm", "gm", "gc", "cg", "gmc"]
|
|
96
|
-
SortMode: TypeAlias = Literal["l", "n", "c", "cl", "cn"]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# todo: make new class ClassMap(for field Collection.spec_map). fields: name, version, dict(current version ClassMap)
|
|
100
|
-
class ClassMap:
|
|
101
|
-
|
|
102
|
-
def __init__(self, *values: type[InterfaceClass]):
|
|
103
|
-
self._values = values
|
|
104
|
-
|
|
105
|
-
def __hash__(self) -> int:
|
|
106
|
-
return hash(it.hash_ for it in self._values)
|
|
107
|
-
|
|
108
|
-
def get(self, ver: int) -> type[InterfaceClass]:
|
|
109
|
-
if (ver := int(ver)) < len(self._values):
|
|
110
|
-
return self._values[ver]
|
|
111
|
-
raise RuntimeError(f"got {ver=}, expected maximal={len(self._values)}")
|
|
112
|
-
|
|
113
|
-
def renew(self, ver: int, cls_: type[InterfaceClass]) -> Self:
|
|
114
|
-
"""return with one change"""
|
|
115
|
-
if ver < (l := len(self._values)):
|
|
116
|
-
tmp = list(self._values)
|
|
117
|
-
tmp.insert(ver, cls_)
|
|
118
|
-
return self.__class__(*tmp)
|
|
119
|
-
if ver == l:
|
|
120
|
-
return self.__class__(*(self._values + (cls_,)))
|
|
121
|
-
raise RuntimeError(f"got {ver=}, expected maximal={len(self._values)}")
|
|
122
|
-
|
|
123
|
-
def __str__(self) -> str:
|
|
124
|
-
return f"{self._values[0].CLASS_ID}[{len(self._values)}]"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
DataMap = ClassMap(Data)
|
|
128
|
-
DataStaticMap = ClassMap(impl.data.DataStatic)
|
|
129
|
-
DataDynamicMap = ClassMap(impl.data.DataDynamic)
|
|
130
|
-
RegisterMap = ClassMap(Register)
|
|
131
|
-
ExtendedRegisterMap = ClassMap(ExtendedRegister)
|
|
132
|
-
DemandRegisterMap = ClassMap(DemandRegisterVer0)
|
|
133
|
-
RegisterActivationMap = ClassMap(RegisterActivation)
|
|
134
|
-
ProfileGenericMap = ClassMap(ProfileGenericVer0, ProfileGenericVer1)
|
|
135
|
-
ClockMap = ClassMap(Clock)
|
|
136
|
-
ScriptTableMap = ClassMap(ScriptTable)
|
|
137
|
-
ScheduleMap = ClassMap(Schedule)
|
|
138
|
-
SpecialDaysTableMap = ClassMap(SpecialDaysTable)
|
|
139
|
-
AssociationSNMap = ClassMap(AssociationSNVer0)
|
|
140
|
-
AssociationLNMap = ClassMap(AssociationLNVer0, AssociationLNVer1, AssociationLNVer2)
|
|
141
|
-
ImageTransferMap = ClassMap(ImageTransfer)
|
|
142
|
-
ActivityCalendarMap = ClassMap(ActivityCalendar)
|
|
143
|
-
RegisterMonitorMap = ClassMap(RegisterMonitor)
|
|
144
|
-
SingleActionScheduleMap = ClassMap(SingleActionSchedule)
|
|
145
|
-
IECHDLCSetupMap = ClassMap(IECHDLCSetupVer0, IECHDLCSetupVer1)
|
|
146
|
-
ModemConfigurationMap = ClassMap(PSTNModemConfiguration, ModemConfigurationVer1)
|
|
147
|
-
TCPUDPSetupMap = ClassMap(TCPUDPSetup)
|
|
148
|
-
IPv4SetupMap = ClassMap(IPv4Setup)
|
|
149
|
-
GPRSModemSetupMap = ClassMap(GPRSModemSetup)
|
|
150
|
-
GSMDiagnosticMap = ClassMap(GSMDiagnosticVer0, GSMDiagnosticVer1, GSMDiagnosticVer2)
|
|
151
|
-
PushSetupMap = ClassMap(PushSetupVer0, PushSetupVer1, PushSetupVer2)
|
|
152
|
-
SecuritySetupMap = ClassMap(SecuritySetupVer0, SecuritySetupVer1)
|
|
153
|
-
ArbitratorMap = ClassMap(Arbitrator)
|
|
154
|
-
DisconnectControlMap = ClassMap(DisconnectControl)
|
|
155
|
-
LimiterMap = ClassMap(Limiter)
|
|
156
|
-
NTPSetupMap = ClassMap(NTPSetup)
|
|
157
|
-
|
|
158
|
-
# implementation ClassMap
|
|
159
|
-
UnsignedDataMap = ClassMap(impl.data.Unsigned)
|
|
160
|
-
|
|
161
|
-
LN_C: TypeAlias = int
|
|
162
|
-
LN_D: TypeAlias = int
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
common_interface_class_map: dict[int, ClassMap] = {
|
|
166
|
-
1: DataMap,
|
|
167
|
-
3: RegisterMap,
|
|
168
|
-
4: ExtendedRegisterMap,
|
|
169
|
-
5: DemandRegisterMap,
|
|
170
|
-
6: RegisterActivationMap,
|
|
171
|
-
7: ProfileGenericMap,
|
|
172
|
-
8: ClockMap,
|
|
173
|
-
9: ScriptTableMap,
|
|
174
|
-
10: ScheduleMap,
|
|
175
|
-
11: SpecialDaysTableMap,
|
|
176
|
-
15: AssociationLNMap,
|
|
177
|
-
18: ImageTransferMap,
|
|
178
|
-
20: ActivityCalendarMap,
|
|
179
|
-
21: RegisterMonitorMap,
|
|
180
|
-
22: SingleActionScheduleMap,
|
|
181
|
-
23: IECHDLCSetupMap,
|
|
182
|
-
27: ModemConfigurationMap,
|
|
183
|
-
41: TCPUDPSetupMap,
|
|
184
|
-
42: IPv4SetupMap,
|
|
185
|
-
45: GPRSModemSetupMap,
|
|
186
|
-
47: GSMDiagnosticMap,
|
|
187
|
-
64: SecuritySetupMap,
|
|
188
|
-
68: ArbitratorMap,
|
|
189
|
-
70: DisconnectControlMap,
|
|
190
|
-
71: LimiterMap,
|
|
191
|
-
100: NTPSetupMap,
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def get_interface_class(class_map: dict[int, ClassMap], c_id: ut.CosemClassId, ver: cdt.Unsigned) -> type[InterfaceClass]:
|
|
196
|
-
"""new version <get_type_from_class>"""
|
|
197
|
-
ret = class_map.get(int(c_id), None)
|
|
198
|
-
if isinstance(ret, ClassMap):
|
|
199
|
-
return ret.get(int(ver))
|
|
200
|
-
else:
|
|
201
|
-
if int(c_id) not in common_interface_class_map.keys():
|
|
202
|
-
raise CollectionMapError(F"unknown {c_id=}")
|
|
203
|
-
else:
|
|
204
|
-
raise CollectionMapError(F"got {c_id=}, expected {', '.join(map(str, class_map.keys()))}")
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
_CUMULATIVE = (1, 2, 11, 12, 21, 22)
|
|
208
|
-
_MAX_MIN_VALUES = (3, 6, 13, 16, 26, 51, 52, 53, 54)
|
|
209
|
-
_CURRENT_AND_LAST_AVERAGE_VALUES = (0, 4, 5, 14, 15, 24, 25, 27, 28, 49, 50, 55, 56)
|
|
210
|
-
_INSTANTANEOUS_VALUES = (7, 39, 41, 42)
|
|
211
|
-
_TIME_INTEGRAL_VALUES = (8, 9, 10, 17, 18, 19, 20, 29, 30, 58)
|
|
212
|
-
_OCCURRENCE_COUNTER = 40
|
|
213
|
-
_CONTRACTED_VALUES = (46,)
|
|
214
|
-
_UNDER_OVER_LIMIT_THRESHOLDS = (31, 35, 43, 44)
|
|
215
|
-
_UNDER_OVER_LIMIT_OCCURRENCE_COUNTERS = (32, 36)
|
|
216
|
-
_UNDER_OVER_LIMIT_DURATIONS = (33, 37)
|
|
217
|
-
_UNDER_OVER_LIMIT_MAGNITUDES = (34, 38)
|
|
218
|
-
_NOT_PROCESSING_OF_MEASUREMENT_VALUES = tuple(set(range(256)).difference((0, 93, 94, 96, 97, 98, 99))) # BlueBook DLMS UA 1000-1 Ed.14 7.5.2.1 Table 66
|
|
219
|
-
_RU_CHANGE_LIMIT_LEVEL = 134
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@lru_cache()
|
|
223
|
-
def _create_map(maps: ClassMap | tuple[ClassMap]) -> dict[int, ClassMap]:
|
|
224
|
-
if isinstance(maps, tuple):
|
|
225
|
-
return {int(map_.get(0).CLASS_ID): map_ for map_ in maps}
|
|
226
|
-
else:
|
|
227
|
-
return {int(maps.get(0).CLASS_ID): maps}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
A: TypeAlias = int
|
|
231
|
-
B: TypeAlias = int
|
|
232
|
-
C: TypeAlias = int
|
|
233
|
-
D: TypeAlias = int
|
|
234
|
-
E: TypeAlias = int
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class Xgroup(Protocol):
|
|
238
|
-
fmt: str
|
|
239
|
-
|
|
240
|
-
def get_key(self) -> Iterator[bytes]:
|
|
241
|
-
...
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
class SimpleGroup(Xgroup, Protocol):
|
|
245
|
-
def get_key(self) -> Iterator[bytes]:
|
|
246
|
-
yield pack(self.fmt, *self) # type: ignore[misc]
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class ACgroup(SimpleGroup, tuple[A, C]):
|
|
250
|
-
"""Grouping of OBIS codes by group A (media) and group C (attribute).
|
|
251
|
-
Creates a composite key for objects sharing the same media type and attribute.
|
|
252
|
-
"""
|
|
253
|
-
fmt = ">BB"
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
class ACDgroup_(Xgroup, Protocol):
|
|
257
|
-
"""Grouping of OBIS codes by groups A (media), C (attribute), and D (data) with mask support.
|
|
258
|
-
Supports exact values or masks for attribute (C) and data (D) groups.
|
|
259
|
-
Allows flexible grouping by media type with variable attribute and data patterns.
|
|
260
|
-
"""
|
|
261
|
-
fmt: str = ">BBB"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class ACDgroup(SimpleGroup, ACDgroup_, tuple[A, C, D]):
|
|
265
|
-
...
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
class ACCDgroup(ACDgroup_, tuple[A, tuple[C, ...], D]):
|
|
269
|
-
def get_key(self) -> Iterator[bytes]:
|
|
270
|
-
a, _, d = self
|
|
271
|
-
return (pack(self.fmt, a, c, d) for c in self[1])
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class ACDDgroup(ACDgroup_, tuple[A, C, tuple[D, ...]]):
|
|
275
|
-
def get_key(self) -> Iterator[bytes]:
|
|
276
|
-
a, c, _ = self
|
|
277
|
-
return (pack(self.fmt, a, c, d) for d in self[2])
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
class ACCDDgroup(ACDgroup_, tuple[A, tuple[C, ...], tuple[D, ...]]):
|
|
281
|
-
"""Grouping of OBIS codes by groups A (media), C (attribute), and D (data) with mask support.
|
|
282
|
-
Supports exact values or masks for attribute (C) and data (D) groups.
|
|
283
|
-
Allows flexible grouping by media type with variable attribute and data patterns.
|
|
284
|
-
"""
|
|
285
|
-
def get_key(self) -> Iterator[bytes]:
|
|
286
|
-
a = self[0]
|
|
287
|
-
return (pack(self.fmt, a, c, d) for c in self[1] for d in self[2])
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
class ACDEgroup_(Xgroup, Protocol):
|
|
291
|
-
"""Grouping of OBIS codes by groups A (media), C (attribute), D (data), and E (data version) with mask support.
|
|
292
|
-
Groups by fixed media and attribute with support for data and data version masks.
|
|
293
|
-
Enables precise control over data grouping with optional version filtering.
|
|
294
|
-
"""
|
|
295
|
-
fmt: str = ">BBBB"
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
class ACDEgroup(SimpleGroup, ACDEgroup_, tuple[A, C, D, E]):
|
|
299
|
-
...
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
class ACDDEgroup(ACDEgroup_, tuple[A, C, tuple[D, ...], E]):
|
|
303
|
-
def get_key(self) -> Iterator[bytes]:
|
|
304
|
-
a, c, _, e = self
|
|
305
|
-
return (pack(self.fmt, a, c, d, e) for d in self[2])
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
class ACDEEgroup(ACDEgroup_, tuple[A, C, D, tuple[E, ...]]):
|
|
310
|
-
def get_key(self) -> Iterator[bytes]:
|
|
311
|
-
a, c, d, _ = self
|
|
312
|
-
return (pack(self.fmt, a, c, d, e) for e in self[3])
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
class ACDDEEgroup(ACDEgroup_, tuple[A, C, tuple[D, ...], tuple[E, ...]]):
|
|
316
|
-
def get_key(self) -> Iterator[bytes]:
|
|
317
|
-
a, c, _, _ = self
|
|
318
|
-
return (pack(self.fmt, a, c, d, e) for d in self[2] for e in self[3])
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
class ABCDEgroup_(Xgroup, Protocol):
|
|
322
|
-
"""Grouping of OBIS codes by all OBIS groups A-E with data version mask support.
|
|
323
|
-
|
|
324
|
-
Comprehensive grouping by:
|
|
325
|
-
- A: media
|
|
326
|
-
- B: channel/interface
|
|
327
|
-
- C: attribute
|
|
328
|
-
- D: data
|
|
329
|
-
- E: data version (with mask support)
|
|
330
|
-
|
|
331
|
-
Provides complete OBIS code grouping with flexible version matching.
|
|
332
|
-
"""
|
|
333
|
-
fmt: str = ">BBBBB"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
class ABCDEgroup(SimpleGroup, ABCDEgroup_, tuple[A, B, C, D, E]):
|
|
337
|
-
...
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
class ABCCDEgroup(ABCDEgroup_, tuple[A, B, tuple[C, ...], D, E]):
|
|
341
|
-
def get_key(self) -> Iterator[bytes]:
|
|
342
|
-
a, b, _, d, e = self
|
|
343
|
-
return (pack(self.fmt, a, b, c, d, e) for c in self[2])
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
class ABCDEEgroup(ABCDEgroup_, tuple[A, B, C, D, tuple[E, ...]]):
|
|
347
|
-
def get_key(self) -> Iterator[bytes]:
|
|
348
|
-
a, b, c, d, _ = self
|
|
349
|
-
return (pack(self.fmt, a, b, c, d, e) for e in self[4])
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
type PossibleGroup = ACgroup | ACDgroup_ | ACDEgroup | ABCDEgroup
|
|
353
|
-
FUNC_MAP: TypeAlias = dict[bytes, dict[int, ClassMap]]
|
|
354
|
-
"""ln.BCDE | ln.CDE | ln.CD | ln.C: {class_id: {version: CosemInterfaceClass}}"""
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
func_maps: dict[str, FUNC_MAP] = {}
|
|
358
|
-
type PossibleClassMap = tuple[ClassMap, ...] | ClassMap
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def get_func_map(for_create_map: dict[PossibleGroup, PossibleClassMap]) -> FUNC_MAP:
|
|
362
|
-
ret: FUNC_MAP = {}
|
|
363
|
-
for g in for_create_map:
|
|
364
|
-
for k in g.get_key():
|
|
365
|
-
ret[k] = _create_map(for_create_map[g])
|
|
366
|
-
return ret
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
__func_map_for_create: dict[PossibleGroup, PossibleClassMap] = {
|
|
370
|
-
# abstract
|
|
371
|
-
ACDgroup((0, 0, 1)): DataMap,
|
|
372
|
-
ACDgroup((0, 0, 2)): DataMap,
|
|
373
|
-
ACDEgroup((0, 0, 2, 1)): ClassMap(impl.data.ActiveFirmwareId),
|
|
374
|
-
ACDgroup((0, 0, 9)): DataMap,
|
|
375
|
-
ACDgroup((0, 1, 0)): ClockMap,
|
|
376
|
-
ACDgroup((0, 1, 1)): DataMap,
|
|
377
|
-
ACDgroup((0, 1, 2)): DataMap,
|
|
378
|
-
ACDgroup((0, 1, 3)): DataMap,
|
|
379
|
-
ACDgroup((0, 1, 4)): DataMap,
|
|
380
|
-
ACDgroup((0, 1, 5)): DataMap,
|
|
381
|
-
ACDgroup((0, 1, 6)): DataMap,
|
|
382
|
-
ACDEgroup((0, 2, 0, 0)): ModemConfigurationMap,
|
|
383
|
-
#
|
|
384
|
-
ACDEEgroup((0, 10, 0, (0, 1, 125)+tuple(range(100, 112)))): ScriptTableMap,
|
|
385
|
-
ACDgroup((0, 11, 0)): SpecialDaysTableMap,
|
|
386
|
-
ACDgroup((0, 12, 0)): ScheduleMap,
|
|
387
|
-
ACDgroup((0, 13, 0)): ActivityCalendarMap,
|
|
388
|
-
ACDgroup((0, 14, 0)): RegisterActivationMap,
|
|
389
|
-
ACDEEgroup((0, 15, 0, tuple(range(0, 8)))): SingleActionScheduleMap,
|
|
390
|
-
ACDgroup((0, 16, 0)): RegisterMonitorMap,
|
|
391
|
-
ACDEEgroup((0, 16, 1, tuple(range(0, 10)))): RegisterMonitorMap,
|
|
392
|
-
#
|
|
393
|
-
ACDgroup((0, 17, 0)): LimiterMap,
|
|
394
|
-
#
|
|
395
|
-
ACDDEEgroup((0, 19, tuple(range(50, 60)), (1, 2))): DataMap,
|
|
396
|
-
#
|
|
397
|
-
ACDgroup((0, 21, 0)): (DataMap, ProfileGenericMap),
|
|
398
|
-
ACDEgroup((0, 22, 0, 0)): IECHDLCSetupMap,
|
|
399
|
-
#
|
|
400
|
-
ACDEgroup((0, 23, 2, 0)): DataMap,
|
|
401
|
-
ACDEEgroup((0, 23, 3, tuple(range(0, 10)))): (DataMap, ProfileGenericMap),
|
|
402
|
-
ACDEEgroup((0, 23, 3, tuple(range(10, 256)))): DataMap,
|
|
403
|
-
#
|
|
404
|
-
ACDgroup((0, 24, 2)): ExtendedRegisterMap,
|
|
405
|
-
ACDgroup((0, 24, 3)): ProfileGenericMap,
|
|
406
|
-
ACDEgroup((0, 24, 4, 0)): DisconnectControlMap,
|
|
407
|
-
ACDEgroup((0, 24, 5, 0)): ProfileGenericMap,
|
|
408
|
-
#
|
|
409
|
-
ACDEgroup((0, 25, 0, 0)): TCPUDPSetupMap,
|
|
410
|
-
ACDEgroup((0, 25, 1, 0)): IPv4SetupMap,
|
|
411
|
-
#
|
|
412
|
-
ACDEgroup((0, 25, 4, 0)): GPRSModemSetupMap,
|
|
413
|
-
#
|
|
414
|
-
ACDEgroup((0, 25, 6, 0)): GSMDiagnosticMap,
|
|
415
|
-
#
|
|
416
|
-
ACDEgroup((0, 25, 9, 0)): PushSetupMap,
|
|
417
|
-
ACDEgroup((0, 25, 10, 0)): NTPSetupMap,
|
|
418
|
-
#
|
|
419
|
-
ABCDEEgroup((0, 0, 40, 0, tuple(range(8)))): (AssociationSNMap, AssociationLNMap), # todo: now limit by 8 association, solve it
|
|
420
|
-
#
|
|
421
|
-
ABCDEgroup((0, 0, 42, 0, 0)): ClassMap(impl.data.LDN),
|
|
422
|
-
ABCDEEgroup((0, 0, 43, 0, tuple(range(256)))): SecuritySetupMap,
|
|
423
|
-
ACDgroup((0, 43, 1)): DataMap,
|
|
424
|
-
#
|
|
425
|
-
ABCDEEgroup((0, 0, 44, 0, tuple(range(256)))): ImageTransferMap,
|
|
426
|
-
#
|
|
427
|
-
ACDEEgroup((0, 96, 1, tuple(range(0, 11)))): ClassMap(impl.data.DLMSDeviceIDObject),
|
|
428
|
-
ACDEgroup((0, 96, 1, 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
429
|
-
ACDgroup((0, 96, 2)): DataDynamicMap,
|
|
430
|
-
ACDEEgroup((0, 96, 3, tuple(range(0, 4)))): DataMap, # todo: add StatusMapping
|
|
431
|
-
ACDEgroup((0, 96, 3, 10)): DisconnectControlMap,
|
|
432
|
-
ACDEEgroup((0, 96, 3, tuple(range(20, 29)))): ArbitratorMap,
|
|
433
|
-
ACDDEgroup((0, 96, (4, 5), 0)): (DataMap, ProfileGenericMap), # todo: add RegisterTable, StatusMapping
|
|
434
|
-
ACDDEEgroup((0, 96, (4, 5), (1, 2, 3, 4))): DataMap, # todo: add StatusMapping
|
|
435
|
-
ACDEEgroup((0, 96, 6, tuple(range(0, 7)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
436
|
-
ACDEEgroup((0, 96, 7, tuple(range(0, 22)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
437
|
-
ACDEEgroup((0, 96, 8, tuple(range(0, 64)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
438
|
-
ACDEEgroup((0, 96, 9, (0, 1, 2))): (RegisterMap, ExtendedRegisterMap),
|
|
439
|
-
ACDEEgroup((0, 96, 10, tuple(range(1, 10)))): DataMap, # todo: add StatusMapping
|
|
440
|
-
ACDEEgroup((0, 96, 11, tuple(range(100)))): (DataDynamicMap, RegisterMap, ExtendedRegisterMap),
|
|
441
|
-
ACDEEgroup((0, 96, 12, (0, 1, 2, 3, 5, 6))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
442
|
-
ACDEgroup((0, 96, 12, 4)): ClassMap(impl.data.CommunicationPortParameter),
|
|
443
|
-
ACDEEgroup((0, 96, 13, (0, 1))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
444
|
-
ACDEEgroup((0, 96, 14, tuple(range(16)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
445
|
-
ACDEEgroup((0, 96, 15, tuple(range(100)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
446
|
-
ACDEEgroup((0, 96, 16, tuple(range(10)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
447
|
-
ACDEEgroup((0, 96, 17, tuple(range(128)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
448
|
-
ACDgroup((0, 96, 20)): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
449
|
-
ACDEEgroup((0, 97, 97, tuple(range(10)))): DataMap,
|
|
450
|
-
ACDDEgroup((0, 97, (97, 98), 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
451
|
-
ACDEEgroup((0, 97, 98, tuple(range(10))+tuple(range(10, 30)))): DataMap,
|
|
452
|
-
ACgroup((0, 98)): ProfileGenericMap,
|
|
453
|
-
ACDgroup((0, 99, 98)): ProfileGenericMap,
|
|
454
|
-
# electricity
|
|
455
|
-
ACDEEgroup((1, 0, 0, tuple(range(10)))): DataMap,
|
|
456
|
-
ACDEgroup((1, 0, 0, 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
457
|
-
ACDgroup((1, 0, 1)): DataMap,
|
|
458
|
-
ACDgroup((1, 0, 2)): DataStaticMap,
|
|
459
|
-
ACDDgroup((1, 0, (3, 4, 7, 8, 9))): (DataStaticMap, RegisterMap, ExtendedRegisterMap),
|
|
460
|
-
ACDDgroup((1, 0, (6, 10))): (RegisterMap, ExtendedRegisterMap),
|
|
461
|
-
ACDEEgroup((1, 0, 11, tuple(range(1, 8)))): DataMap,
|
|
462
|
-
ACDEEgroup((1, 96, 1, tuple(range(10)))): DataMap,
|
|
463
|
-
ACDEgroup((1, 96, 1, 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
464
|
-
ACDEEgroup((1, 96, 5, (0, 1, 2, 3, 4, 5))): DataMap, # todo: add StatusMapping
|
|
465
|
-
ACDEEgroup((1, 96, 10, (0, 1, 2, 3))): DataMap, # todo: add StatusMapping
|
|
466
|
-
ACgroup((1, 98)): ProfileGenericMap,
|
|
467
|
-
ACDDgroup((1, 99, (1, 2, 11, 12, 97, 98, 99))): ProfileGenericMap,
|
|
468
|
-
ACDDEgroup((1, 99, (3, 13, 14), 0)): ProfileGenericMap,
|
|
469
|
-
ACDEEgroup((1, 99, 10, (1, 2, 3))): ProfileGenericMap,
|
|
470
|
-
ACCDgroup((1, _CUMULATIVE, _RU_CHANGE_LIMIT_LEVEL)): RegisterMap,
|
|
471
|
-
ACCDDgroup((
|
|
472
|
-
1,
|
|
473
|
-
_NOT_PROCESSING_OF_MEASUREMENT_VALUES,
|
|
474
|
-
tuple(chain(
|
|
475
|
-
_CUMULATIVE,
|
|
476
|
-
_TIME_INTEGRAL_VALUES,
|
|
477
|
-
_CONTRACTED_VALUES,
|
|
478
|
-
_UNDER_OVER_LIMIT_THRESHOLDS,
|
|
479
|
-
_UNDER_OVER_LIMIT_OCCURRENCE_COUNTERS,
|
|
480
|
-
_UNDER_OVER_LIMIT_DURATIONS,
|
|
481
|
-
_UNDER_OVER_LIMIT_MAGNITUDES
|
|
482
|
-
)))): (RegisterMap, ExtendedRegisterMap),
|
|
483
|
-
ACCDDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, _INSTANTANEOUS_VALUES)): RegisterMap,
|
|
484
|
-
ACCDDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, _MAX_MIN_VALUES)): (RegisterMap, ExtendedRegisterMap, ProfileGenericMap),
|
|
485
|
-
ACCDDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, _CURRENT_AND_LAST_AVERAGE_VALUES)): (RegisterMap, DemandRegisterMap),
|
|
486
|
-
ACCDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, 40)): (DataMap, RegisterMap),
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
func_maps["DLMS_6"] = get_func_map(__func_map_for_create)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
# SPODES3 Update
|
|
493
|
-
__func_map_for_create.update({
|
|
494
|
-
ACDgroup((0, 21, 0)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3DisplayReadout),
|
|
495
|
-
ACDEEgroup((0, 96, 1, (0, 2, 4, 5, 8, 9, 10))): ClassMap(impl.data.SPODES3IDNotSpecific),
|
|
496
|
-
ACDEgroup((0, 96, 1, 6)): ClassMap(impl.data.SPODES3SPODESVersion),
|
|
497
|
-
ACDEEgroup((0, 96, 2, (1, 2, 3, 5, 6, 7, 11, 12))): ClassMap(impl.data.AnyDateTime),
|
|
498
|
-
ACDEgroup((0, 96, 3, 20)): ClassMap(impl.arbitrator.SPODES3Arbitrator),
|
|
499
|
-
ACDEgroup((0, 96, 4, 3)): ClassMap(impl.data.SPODES3LoadLocker),
|
|
500
|
-
ACDEgroup((0, 96, 5, 1)): ClassMap(impl.data.SPODES3PowerQuality2Event),
|
|
501
|
-
ACDEgroup((0, 96, 5, 4)): ClassMap(impl.data.SPODES3PowerQuality1Event),
|
|
502
|
-
ACDEgroup((0, 96, 5, 132)): ClassMap(impl.data.Unsigned), # TODO: make according with СПОДЭС3 13.9. Контроль чередования фаз
|
|
503
|
-
ACDEgroup((0, 96, 11, 0)): ClassMap(impl.data.SPODES3VoltageEvent),
|
|
504
|
-
ACDEgroup((0, 96, 11, 1)): ClassMap(impl.data.SPODES3CurrentEvent),
|
|
505
|
-
ACDEgroup((0, 96, 11, 2)): ClassMap(impl.data.SPODES3CommutationEvent),
|
|
506
|
-
ACDEgroup((0, 96, 11, 3)): ClassMap(impl.data.SPODES3ProgrammingEvent),
|
|
507
|
-
ACDEgroup((0, 96, 11, 4)): ClassMap(impl.data.SPODES3ExternalEvent),
|
|
508
|
-
ACDEgroup((0, 96, 11, 5)): ClassMap(impl.data.SPODES3CommunicationEvent),
|
|
509
|
-
ACDEgroup((0, 96, 11, 6)): ClassMap(impl.data.SPODES3AccessEvent),
|
|
510
|
-
ACDEgroup((0, 96, 11, 7)): ClassMap(impl.data.SPODES3SelfDiagnosticEvent),
|
|
511
|
-
ACDEgroup((0, 96, 11, 8)): ClassMap(impl.data.SPODES3ReactivePowerEvent),
|
|
512
|
-
ABCDEgroup((0, 0, 96, 51, 0)): ClassMap(impl.data.OpeningBody),
|
|
513
|
-
ABCDEgroup((0, 0, 96, 51, 1)): ClassMap(impl.data.OpeningCover),
|
|
514
|
-
ABCDEgroup((0, 0, 96, 51, 3)): ClassMap(impl.data.ExposureToMagnet),
|
|
515
|
-
ABCDEgroup((0, 0, 96, 51, 4)): ClassMap(impl.data.ExposureToHSField),
|
|
516
|
-
ABCDEgroup((0, 0, 96, 51, 5)): ClassMap(impl.data.SealStatus),
|
|
517
|
-
ABCDEEgroup((0, 0, 96, 51, (6, 7))): UnsignedDataMap,
|
|
518
|
-
ABCDEEgroup((0, 0, 96, 51, (8, 9))): ClassMap(impl.data.OctetStringDateTime),
|
|
519
|
-
ABCDEEgroup((0, 0, 97, 98, (0, 10, 20))): ClassMap(impl.data.SPODES3Alarm1),
|
|
520
|
-
ABCDEEgroup((0, 0, 97, 98, (1, 11))): ClassMap(impl.data.SPODES3ControlAlarm1),
|
|
521
|
-
# electricity
|
|
522
|
-
ACDEEgroup((1, 0, 8, (4, 5))): ClassMap(impl.data.SPODES3MeasurementPeriod),
|
|
523
|
-
ACDgroup((1, 98, 1)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3MonthProfile),
|
|
524
|
-
ACDgroup((1, 98, 2)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3DailyProfile),
|
|
525
|
-
ACDDgroup((1, 99, (1, 2))): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3LoadProfile),
|
|
526
|
-
ABCDEgroup((1, 0, 131, 35, 0)): RegisterMap,
|
|
527
|
-
ABCDEgroup((1, 0, 133, 35, 0)): RegisterMap,
|
|
528
|
-
ABCDEgroup((1, 0, 147, 133, 0)): RegisterMap,
|
|
529
|
-
ABCDEgroup((1, 0, 148, 136, 0)): RegisterMap,
|
|
530
|
-
ACDEgroup((1, 94, 7, 0)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3CurrentProfile),
|
|
531
|
-
ACDEEgroup((1, 94, 7, (1, 2, 3, 4, 5, 6))): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3ScalesProfile), # Todo: RU. Scaler-profile With 1 entry and more
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
func_maps["SPODES_3"] = get_func_map(__func_map_for_create)
|
|
535
|
-
|
|
536
|
-
# KPZ Update
|
|
537
|
-
__func_map_for_create.update({
|
|
538
|
-
ACDEgroup((0, 96, 11, 4)): ClassMap(impl.data.KPZSPODES3ExternalEvent),
|
|
539
|
-
ABCDEEgroup((0, 0, 97, 98, (0, 10, 20))): ClassMap(impl.data.KPZAlarm1),
|
|
540
|
-
ABCDEgroup((0, 128, 25, 6, 0)): ClassMap(impl.data.DataStatic),
|
|
541
|
-
ABCDEEgroup((0, 128, 96, 2, (0, 1, 2))): ClassMap(impl.data.KPZAFEOffsets),
|
|
542
|
-
ABCDEgroup((0, 128, 96, 13, 1)): ClassMap(impl.data.ITEBitMap),
|
|
543
|
-
ABCDEgroup((0, 128, 154, 0, 0)): ClassMap(impl.data.KPZGSMPingIP),
|
|
544
|
-
ACDEEgroup((0, 0, 128, (100, 101, 102, 103, 150, 151, 152, 170))): DataMap,
|
|
545
|
-
ABCCDEgroup((128, 0, tuple(range(20)), 0, 0)): RegisterMap
|
|
546
|
-
})
|
|
547
|
-
func_maps["KPZ"] = get_func_map(__func_map_for_create)
|
|
548
|
-
# KPZ1 with bag in log event profiles
|
|
549
|
-
__func_map_for_create.update({
|
|
550
|
-
ACDEgroup((0, 96, 11, 0)): ClassMap(impl.data.KPZ1SPODES3VoltageEvent),
|
|
551
|
-
ACDEgroup((0, 96, 11, 1)): ClassMap(impl.data.KPZ1SPODES3CurrentEvent),
|
|
552
|
-
ACDEgroup((0, 96, 11, 2)): ClassMap(impl.data.KPZ1SPODES3CommutationEvent),
|
|
553
|
-
ACDEgroup((0, 96, 11, 3)): ClassMap(impl.data.KPZ1SPODES3ProgrammingEvent),
|
|
554
|
-
ACDEgroup((0, 96, 11, 4)): ClassMap(impl.data.KPZ1SPODES3ExternalEvent),
|
|
555
|
-
ACDEgroup((0, 96, 11, 5)): ClassMap(impl.data.KPZ1SPODES3CommunicationEvent),
|
|
556
|
-
ACDEgroup((0, 96, 11, 6)): ClassMap(impl.data.KPZ1SPODES3AccessEvent),
|
|
557
|
-
ACDEgroup((0, 96, 11, 7)): ClassMap(impl.data.KPZ1SPODES3SelfDiagnosticEvent),
|
|
558
|
-
ACDEgroup((0, 96, 11, 8)): ClassMap(impl.data.KPZ1SPODES3ReactivePowerEvent),
|
|
559
|
-
})
|
|
560
|
-
func_maps["KPZ1"] = get_func_map(__func_map_for_create)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
def get_type(class_id: ut.CosemClassId,
|
|
564
|
-
version: cdt.Unsigned,
|
|
565
|
-
ln: cst.LogicalName,
|
|
566
|
-
func_map: FUNC_MAP) -> Type[InterfaceClass]:
|
|
567
|
-
"""use DLMS UA 1000-1 Ed. 14 Table 54"""
|
|
568
|
-
c_m: Optional[dict[int, ClassMap]]
|
|
569
|
-
if (
|
|
570
|
-
(128 <= ln.b <= 199)
|
|
571
|
-
or (128 <= ln.c <= 199)
|
|
572
|
-
or ln.c == 240
|
|
573
|
-
or (128 <= ln.d <= 254)
|
|
574
|
-
or (128 <= ln.e <= 254)
|
|
575
|
-
or (128 <= ln.f <= 254)
|
|
576
|
-
):
|
|
577
|
-
# try search in ABCDE group for manufacture object before in CDE
|
|
578
|
-
c_m = func_map.get(ln.contents[:5], common_interface_class_map)
|
|
579
|
-
else:
|
|
580
|
-
# try search in ABCDE group
|
|
581
|
-
c_m = func_map.get(ln.contents[:5], None)
|
|
582
|
-
if c_m is None:
|
|
583
|
-
# try search in A-CDE group
|
|
584
|
-
c_m = func_map.get(ln.contents[:1]+ln.contents[2:5], None)
|
|
585
|
-
if c_m is None:
|
|
586
|
-
# try search in A-CD group
|
|
587
|
-
c_m = func_map.get(ln.contents[:1]+ln.contents[2:4], None)
|
|
588
|
-
if c_m is None:
|
|
589
|
-
# try search in A-C group
|
|
590
|
-
c_m = func_map.get(ln.contents[:1]+ln.contents[3:4], common_interface_class_map)
|
|
591
|
-
return get_interface_class(class_map=c_m,
|
|
592
|
-
c_id=class_id,
|
|
593
|
-
ver=version)
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
@lru_cache(20000)
|
|
597
|
-
def get_unit(class_id: ClassID, elements: Iterable[int]) -> int | None:
|
|
598
|
-
match class_id, *elements:
|
|
599
|
-
case (ClassID.LIMITER, 6 | 7) | (ClassID.LIMITER, 8, 2) | (ClassID.DEMAND_REGISTER, 8) | (ClassID.PROFILE_GENERIC, 4) | (ClassID.PUSH_SETUP, 5) |\
|
|
600
|
-
(ClassID.PUSH_SETUP, 7, _) | (ClassID.PUSH_SETUP, 12, 1) | (ClassID.COMMUNICATION_PORT_PROTECTION, 4 | 6) | (ClassID.CHARGE, 8) | (ClassID.IEC_HDLC_SETUP, 8) \
|
|
601
|
-
| (ClassID.AUTO_CONNECT, 4):
|
|
602
|
-
return 7 # second
|
|
603
|
-
case ClassID.CLOCK, 3 | 7:
|
|
604
|
-
return 6 # min
|
|
605
|
-
case (ClassID.IEC_HDLC_SETUP, 7) | (ClassID.MODEM_CONFIGURATION, 3, 2):
|
|
606
|
-
return 7 # millisecond
|
|
607
|
-
case ClassID.S_FSK_PHY_MAC_SET_UP, 7, _:
|
|
608
|
-
return 44 # HZ
|
|
609
|
-
case (ClassID.S_FSK_PHY_MAC_SET_UP, 4 | 5):
|
|
610
|
-
return 72 # Db
|
|
611
|
-
case ClassID.S_FSK_PHY_MAC_SET_UP, 6:
|
|
612
|
-
return 71 # DbmicroV
|
|
613
|
-
case _:
|
|
614
|
-
return None
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
type ObjFilteredKey = tuple[ClassID | LNPattern | LNPatterns | Channel, ...]
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
def get_filtered(objects: Iterable[InterfaceClass],
|
|
621
|
-
keys: ObjFilteredKey) -> list[InterfaceClass]:
|
|
622
|
-
c_ids: list[ut.CosemClassId] = list()
|
|
623
|
-
patterns: list[LNPattern] = list()
|
|
624
|
-
ch: Optional[Channel] = None
|
|
625
|
-
for k in keys:
|
|
626
|
-
if isinstance(k, ut.CosemClassId):
|
|
627
|
-
c_ids.append(k)
|
|
628
|
-
elif isinstance(k, LNPattern):
|
|
629
|
-
patterns.append(k)
|
|
630
|
-
elif isinstance(k, LNPatterns):
|
|
631
|
-
patterns.extend(k)
|
|
632
|
-
elif isinstance(k, Channel):
|
|
633
|
-
ch = k
|
|
634
|
-
new_list = list()
|
|
635
|
-
for obj in objects:
|
|
636
|
-
if obj.CLASS_ID in c_ids:
|
|
637
|
-
pass
|
|
638
|
-
elif obj.logical_name in patterns:
|
|
639
|
-
pass
|
|
640
|
-
else:
|
|
641
|
-
continue
|
|
642
|
-
if ch and not ch.is_approve(obj.logical_name.b):
|
|
643
|
-
continue
|
|
644
|
-
new_list.append(obj)
|
|
645
|
-
return new_list
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
@dataclass(unsafe_hash=True, frozen=True)
|
|
649
|
-
class ParameterValue:
|
|
650
|
-
par: bytes
|
|
651
|
-
value: bytes
|
|
652
|
-
|
|
653
|
-
def __str__(self) -> str:
|
|
654
|
-
return F"{'.'.join(map(str, self.par[:6]))}:{self.par[6]} - {cdt.get_instance_and_pdu_from_value(self.value)[0].__repr__()}"
|
|
655
|
-
|
|
656
|
-
def __bytes__(self) -> bytes:
|
|
657
|
-
"""par + 0x00 + value""" # todo: 00 in future other parameters
|
|
658
|
-
return self.par + b'\x00' + self.value
|
|
659
|
-
|
|
660
|
-
@classmethod
|
|
661
|
-
def parse(cls, value: bytes) -> Self:
|
|
662
|
-
if value[7] != 0:
|
|
663
|
-
raise exc.ITEApplication(F"wrong {value!r} for {cls.__name__}")
|
|
664
|
-
return cls(
|
|
665
|
-
par=value[:7],
|
|
666
|
-
value=value[8:]
|
|
667
|
-
)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
@dataclass(frozen=True, unsafe_hash=True)
|
|
671
|
-
class ID:
|
|
672
|
-
man: bytes
|
|
673
|
-
f_id: ParameterValue
|
|
674
|
-
f_ver: ParameterValue
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
class Collection:
|
|
678
|
-
__id: ID
|
|
679
|
-
__dlms_ver: int
|
|
680
|
-
__country: Optional[CountrySpecificIdentifiers]
|
|
681
|
-
__country_ver: Optional[ParameterValue]
|
|
682
|
-
__objs: dict[o.OBIS, InterfaceClass]
|
|
683
|
-
__const_objs: int
|
|
684
|
-
spec_map: str
|
|
685
|
-
|
|
686
|
-
def __init__(self,
|
|
687
|
-
id_: ID,
|
|
688
|
-
dlms_ver: int = 6,
|
|
689
|
-
country: Optional[CountrySpecificIdentifiers] = None,
|
|
690
|
-
cntr_ver: Optional[ParameterValue] = None):
|
|
691
|
-
self.__id = id_
|
|
692
|
-
self.__dlms_ver = dlms_ver
|
|
693
|
-
self.__country = country
|
|
694
|
-
self.__country_ver = cntr_ver
|
|
695
|
-
"""country version specification"""
|
|
696
|
-
self.spec_map = "DLMS_6"
|
|
697
|
-
self.__objs = {}
|
|
698
|
-
""" all DLMS objects container with obis key """
|
|
699
|
-
|
|
700
|
-
@property
|
|
701
|
-
def id(self) -> ID:
|
|
702
|
-
return self.__id
|
|
703
|
-
|
|
704
|
-
def set_id(self, value: ID) -> None:
|
|
705
|
-
if not self.__id:
|
|
706
|
-
self.__id = value
|
|
707
|
-
else:
|
|
708
|
-
if value != self.__id:
|
|
709
|
-
raise ValueError(F"got id: {value}, expected {self.__id}")
|
|
710
|
-
else:
|
|
711
|
-
"""success validation"""
|
|
712
|
-
|
|
713
|
-
def __eq__(self, other: object) -> bool:
|
|
714
|
-
if isinstance(other, Collection):
|
|
715
|
-
return hash(self) == hash(other)
|
|
716
|
-
raise NotImplementedError
|
|
717
|
-
|
|
718
|
-
def __hash__(self) -> int:
|
|
719
|
-
return hash(self.id)
|
|
720
|
-
|
|
721
|
-
def copy_object_values(self, target: ic.COSEMInterfaceClasses, association_id: int = 3) -> None:
|
|
722
|
-
"""copy object values according by association. only needed"""
|
|
723
|
-
source = self.__objs.get(target.logical_name.contents, None)
|
|
724
|
-
if source is None:
|
|
725
|
-
raise RuntimeError(f"can't find {target}")
|
|
726
|
-
for i, value in source.get_index_with_attributes():
|
|
727
|
-
el = target.get_attr_element(i)
|
|
728
|
-
if (
|
|
729
|
-
i == 1
|
|
730
|
-
or value is None
|
|
731
|
-
or (
|
|
732
|
-
not isinstance(el.DATA_TYPE, ut.CHOICE)
|
|
733
|
-
and el.classifier == ic.Classifier.DYNAMIC
|
|
734
|
-
)
|
|
735
|
-
):
|
|
736
|
-
continue
|
|
737
|
-
else:
|
|
738
|
-
if (
|
|
739
|
-
target.get_attr_element(i).classifier == ic.Classifier.STATIC
|
|
740
|
-
and not self.is_writable(
|
|
741
|
-
ln=target.logical_name,
|
|
742
|
-
index=i,
|
|
743
|
-
association_id=association_id)
|
|
744
|
-
):
|
|
745
|
-
# todo: may be callbacks inits remove?
|
|
746
|
-
if cb_func := target._cbs_attr_before_init.get(i, None):
|
|
747
|
-
cb_func(value) # Todo: 'a' as 'new_value' in set_attr are can use?
|
|
748
|
-
target._cbs_attr_before_init.pop(i)
|
|
749
|
-
target.set_attr(i, value.encoding)
|
|
750
|
-
if cb_func := target._cbs_attr_post_init.get(i, None):
|
|
751
|
-
cb_func()
|
|
752
|
-
target._cbs_attr_post_init.pop(i)
|
|
753
|
-
else:
|
|
754
|
-
if isinstance(arr := target.get_attr(i), cdt.Array):
|
|
755
|
-
arr.set_type(value.TYPE)
|
|
756
|
-
try:
|
|
757
|
-
target.set_attr(
|
|
758
|
-
index=i,
|
|
759
|
-
value=value.encoding,
|
|
760
|
-
data_type=source.get_attr(i).__class__)
|
|
761
|
-
except exc.EmptyObj as e:
|
|
762
|
-
print(F"can't copy {target} attr={i}, skipped. {e}")
|
|
763
|
-
|
|
764
|
-
def copy(self) -> result.Simple["Collection"]:
|
|
765
|
-
"""copy collection with value by Association"""
|
|
766
|
-
res = result.Simple(Collection(
|
|
767
|
-
id_=self.id,
|
|
768
|
-
dlms_ver=self.__dlms_ver,
|
|
769
|
-
country=self.__country,
|
|
770
|
-
cntr_ver=self.__country_ver)
|
|
771
|
-
)
|
|
772
|
-
res.value.spec_map = self.spec_map
|
|
773
|
-
max_ass: AssociationLN | None = None
|
|
774
|
-
"""more full association""" # todo: move to collection(from_xml)
|
|
775
|
-
for obj in self.__objs.values():
|
|
776
|
-
new_obj: InterfaceClass = obj.__class__(obj.logical_name)
|
|
777
|
-
res.value.__objs[obj.logical_name.contents] = new_obj
|
|
778
|
-
new_obj.collection = res.value # todo: remove in future
|
|
779
|
-
if obj.CLASS_ID == ClassID.ASSOCIATION_LN:
|
|
780
|
-
obj: AssociationLN
|
|
781
|
-
if obj.object_list is not None:
|
|
782
|
-
if (
|
|
783
|
-
max_ass is None
|
|
784
|
-
or len(max_ass.object_list) < len(obj.object_list)
|
|
785
|
-
):
|
|
786
|
-
max_ass = obj
|
|
787
|
-
if max_ass is None:
|
|
788
|
-
raise exc.NoObject("collection not has the AssociationLN object")
|
|
789
|
-
ln_for_set = max_ass.get_lns()
|
|
790
|
-
ass_id: int = max_ass.logical_name.e
|
|
791
|
-
while len(ln_for_set) != 0:
|
|
792
|
-
ln = ln_for_set.pop(0)
|
|
793
|
-
try:
|
|
794
|
-
new_obj = res.value.get_object(ln.contents)
|
|
795
|
-
self.copy_object_values(
|
|
796
|
-
target=new_obj,
|
|
797
|
-
association_id=ass_id)
|
|
798
|
-
except Exception as e:
|
|
799
|
-
res.append_e(e, "copy object value")
|
|
800
|
-
return res
|
|
801
|
-
|
|
802
|
-
@property
|
|
803
|
-
def dlms_ver(self) -> int:
|
|
804
|
-
return self.__dlms_ver
|
|
805
|
-
|
|
806
|
-
def set_dlms_ver(self, value: int) -> None:
|
|
807
|
-
if not self.__dlms_ver:
|
|
808
|
-
self.__dlms_ver = value
|
|
809
|
-
else:
|
|
810
|
-
if value != self.__dlms_ver:
|
|
811
|
-
raise ValueError(F"got dlms_version: {value}, expected {self.__dlms_ver}")
|
|
812
|
-
else:
|
|
813
|
-
"""success validation"""
|
|
814
|
-
|
|
815
|
-
@property
|
|
816
|
-
def country(self) -> Optional[CountrySpecificIdentifiers]:
|
|
817
|
-
return self.__country
|
|
818
|
-
|
|
819
|
-
def set_country(self, value: CountrySpecificIdentifiers):
|
|
820
|
-
if not self.__country:
|
|
821
|
-
self.__country = value
|
|
822
|
-
else:
|
|
823
|
-
if value != self.__country:
|
|
824
|
-
raise ValueError(F"got country: {value}, expected {self.__country}")
|
|
825
|
-
else:
|
|
826
|
-
"""success validation"""
|
|
827
|
-
|
|
828
|
-
@property
|
|
829
|
-
def country_ver(self):
|
|
830
|
-
return self.__country_ver
|
|
831
|
-
|
|
832
|
-
def set_country_ver(self, value: ParameterValue):
|
|
833
|
-
"""country version specification"""
|
|
834
|
-
if not self.__country_ver:
|
|
835
|
-
self.__country_ver = value
|
|
836
|
-
else:
|
|
837
|
-
if value != self.__country_ver:
|
|
838
|
-
raise ValueError(F"got country version: {value}, expected {self.__country_ver}")
|
|
839
|
-
else:
|
|
840
|
-
"""success validation"""
|
|
841
|
-
|
|
842
|
-
def __str__(self) -> str:
|
|
843
|
-
return F"[{len(self.__objs)}] DLMS version: {self.__dlms_ver}, country: {self.__country}, country specific version: {self.__country_ver}, " \
|
|
844
|
-
F"id: {self.id}, uses specification: {self.spec_map}"
|
|
845
|
-
|
|
846
|
-
def __iter__(self) -> Iterator[ic.COSEMInterfaceClasses]:
|
|
847
|
-
return iter(self.__objs.values())
|
|
848
|
-
|
|
849
|
-
def get_spec(self) -> str:
|
|
850
|
-
"""return functional map to specification by identification fields"""
|
|
851
|
-
match self.id.man:
|
|
852
|
-
case b"KPZ":
|
|
853
|
-
return "KPZ"
|
|
854
|
-
case b"101" | b"102" | b"103" | b"104":
|
|
855
|
-
return "KPZ1"
|
|
856
|
-
case _:
|
|
857
|
-
if self.country == CountrySpecificIdentifiers.RUSSIA:
|
|
858
|
-
if (
|
|
859
|
-
self.country_ver.par == b'\x00\x00`\x01\x06\xff\x02' and
|
|
860
|
-
SemVer.parse(bytes(cdt.OctetString(self.country_ver.value)), True) == SemVer(3, 0)
|
|
861
|
-
):
|
|
862
|
-
return "SPODES_3"
|
|
863
|
-
if self.dlms_ver == 6:
|
|
864
|
-
return "DLMS_6"
|
|
865
|
-
else:
|
|
866
|
-
raise exc.DLMSException("unknown specification")
|
|
867
|
-
|
|
868
|
-
def add_if_missing(self, class_id: ut.CosemClassId,
|
|
869
|
-
version: cdt.Unsigned | None,
|
|
870
|
-
logical_name: cst.LogicalName) -> InterfaceClass:
|
|
871
|
-
""" like as add method with check for missing """
|
|
872
|
-
if (res := self.__objs.get(logical_name.contents)) is None:
|
|
873
|
-
return self.add(
|
|
874
|
-
class_id=class_id,
|
|
875
|
-
version=version,
|
|
876
|
-
logical_name=logical_name)
|
|
877
|
-
else:
|
|
878
|
-
return res
|
|
879
|
-
|
|
880
|
-
def __getitem__(self, item: o.OBIS) -> InterfaceClass:
|
|
881
|
-
return self.__objs[item]
|
|
882
|
-
|
|
883
|
-
def get(self, obis: o.OBIS) -> InterfaceClass | None:
|
|
884
|
-
""" get object, return None if it absence """
|
|
885
|
-
return self.__objs.get(obis, None)
|
|
886
|
-
|
|
887
|
-
def par2obj(self, par: Parameter) -> result.SimpleOrError[InterfaceClass]:
|
|
888
|
-
"""return: DLMSObject"""
|
|
889
|
-
return self.obis2obj(par.obis)
|
|
890
|
-
|
|
891
|
-
def par2data(self, par: Parameter) -> result.Option[cdt.CommonDataType] | result.Error:
|
|
892
|
-
""":return CDT by Parameter, return None if data wasn't setting"""
|
|
893
|
-
if isinstance((res1 := self.par2obj(par)), result.Error):
|
|
894
|
-
return res1
|
|
895
|
-
res = result.Option(res1.value.get_attr(par.i))
|
|
896
|
-
if res.value is None:
|
|
897
|
-
return res
|
|
898
|
-
for el in par.elements():
|
|
899
|
-
res.value = res.value[el]
|
|
900
|
-
return res
|
|
901
|
-
|
|
902
|
-
def values(self) -> tuple[InterfaceClass]:
|
|
903
|
-
return tuple(self.__objs.values())
|
|
904
|
-
|
|
905
|
-
def __len__(self):
|
|
906
|
-
return len(self.__objs)
|
|
907
|
-
|
|
908
|
-
def add(self, class_id: ut.CosemClassId,
|
|
909
|
-
version: cdt.Unsigned | None,
|
|
910
|
-
logical_name: cst.LogicalName) -> InterfaceClass:
|
|
911
|
-
""" append new DLMS object to collection with return it"""
|
|
912
|
-
try:
|
|
913
|
-
new_object = get_type(
|
|
914
|
-
class_id=class_id,
|
|
915
|
-
version=self.find_version(class_id) if version is None else version,
|
|
916
|
-
ln=logical_name,
|
|
917
|
-
func_map=func_maps[self.spec_map])(logical_name)
|
|
918
|
-
new_object.collection = self
|
|
919
|
-
self.__objs[o.OBIS(logical_name.contents)] = new_object
|
|
920
|
-
print(F'Create {new_object}')
|
|
921
|
-
return new_object
|
|
922
|
-
except ValueError as e:
|
|
923
|
-
raise ValueError(F"error getting DLMS object instance with {class_id=} {version=} {logical_name=}: {e}")
|
|
924
|
-
except StopIteration as e:
|
|
925
|
-
raise ValueError(F"not find class version for {class_id=} {logical_name=}: {e}")
|
|
926
|
-
|
|
927
|
-
def get_class_version(self) -> dict[ut.CosemClassId, cdt.Unsigned]:
|
|
928
|
-
"""use for check all class version by unique"""
|
|
929
|
-
ret: dict[ut.CosemClassId, cdt.Unsigned] = dict()
|
|
930
|
-
for obj in self.__objs.values():
|
|
931
|
-
if ver := ret.get(obj.CLASS_ID):
|
|
932
|
-
if obj.VERSION != ver:
|
|
933
|
-
raise ValueError(F"for {obj.CLASS_ID=} exist several versions: {obj.VERSION}, {ver} in one collection")
|
|
934
|
-
else:
|
|
935
|
-
ret[obj.CLASS_ID] = obj.VERSION
|
|
936
|
-
return ret
|
|
937
|
-
|
|
938
|
-
def get_n_phases(self) -> int:
|
|
939
|
-
"""search objects with L2 phase"""
|
|
940
|
-
ret: int | None = None
|
|
941
|
-
for obj in filter(lambda obj: obj.logical_name.a == 1, self):
|
|
942
|
-
if 41 <= obj.logical_name.c <= 60:
|
|
943
|
-
return 3
|
|
944
|
-
ret = 1
|
|
945
|
-
if ret is None:
|
|
946
|
-
raise exc.NoObject("no one electricity object was find")
|
|
947
|
-
else:
|
|
948
|
-
return ret
|
|
949
|
-
|
|
950
|
-
def has_sap(self, value: enums.ClientSAP) -> bool:
|
|
951
|
-
try:
|
|
952
|
-
self.sap2association(value)
|
|
953
|
-
return True
|
|
954
|
-
except exc.NoObject:
|
|
955
|
-
return False
|
|
956
|
-
|
|
957
|
-
@lru_cache(maxsize=100) # amount of all ClassID
|
|
958
|
-
def find_version(self, class_id: ut.CosemClassId) -> cdt.Unsigned:
|
|
959
|
-
"""use for add new object from profile_generic if absence in object list"""
|
|
960
|
-
return next(filter(lambda obj: obj.CLASS_ID == class_id, self.__objs.values())).VERSION
|
|
961
|
-
|
|
962
|
-
def is_in_collection(self, value: LNContaining) -> bool:
|
|
963
|
-
obis = lnContents2obis(value)
|
|
964
|
-
return False if self.__objs.get(obis) is None else True
|
|
965
|
-
|
|
966
|
-
def get_object(self, value: LNContaining) -> InterfaceClass:
|
|
967
|
-
""" return object from obis<string> or raise exception if it absence """
|
|
968
|
-
return self.obis2obj(lnContents2obis(value)).unwrap()
|
|
969
|
-
|
|
970
|
-
@deprecated("use <par2rep>")
|
|
971
|
-
def get_report(self,
|
|
972
|
-
obj: ic.COSEMInterfaceClasses,
|
|
973
|
-
par: bytes,
|
|
974
|
-
a_val: cdt.CommonDataType | None
|
|
975
|
-
) -> cdt.Report:
|
|
976
|
-
"""par: attribute_index, par1, par2, ..."""
|
|
977
|
-
rep = cdt.Report(str(a_val))
|
|
978
|
-
try:
|
|
979
|
-
if a_val is None:
|
|
980
|
-
rep.msg = settings.report.empty
|
|
981
|
-
rep.log = cdt.EMPTY_VAL
|
|
982
|
-
elif isinstance(a_val, cdt.ReportMixin):
|
|
983
|
-
rep = a_val.get_report()
|
|
984
|
-
elif isinstance(a_val, DayProfileAction):
|
|
985
|
-
rep.msg = F"{get_message("$rate$")}-{a_val.script_selector}: {a_val.start_time}"
|
|
986
|
-
script_obj = self.get_object(a_val.script_logical_name)
|
|
987
|
-
for script in script_obj.scripts:
|
|
988
|
-
if script.script_identifier == a_val.script_selector:
|
|
989
|
-
break
|
|
990
|
-
else:
|
|
991
|
-
rep.log = cdt.Log(logging.ERROR, F"absent script with ID: {a_val.script_selector}")
|
|
992
|
-
else:
|
|
993
|
-
if unit := get_unit(obj.CLASS_ID, par):
|
|
994
|
-
rep.unit = cdt.Unit(unit).get_name()
|
|
995
|
-
else:
|
|
996
|
-
if s_u := self.get_scaler_unit(obj, par):
|
|
997
|
-
rep.msg = (settings.report.scaler_format).format(int(a_val) * 10 ** int(s_u.scaler))
|
|
998
|
-
rep.unit = s_u.unit.get_name()
|
|
999
|
-
else:
|
|
1000
|
-
match obj.CLASS_ID, *par:
|
|
1001
|
-
case (ClassID.PROFILE_GENERIC, 3, _) | (ClassID.PROFILE_GENERIC, 6):
|
|
1002
|
-
a_val: structs.CaptureObjectDefinition
|
|
1003
|
-
obj = self.get_object(a_val.logical_name)
|
|
1004
|
-
rep.msg = F"{get_name(a_val.logical_name)}.{obj.get_attr_element(int(a_val.attribute_index))}"
|
|
1005
|
-
case _:
|
|
1006
|
-
pass
|
|
1007
|
-
rep.log = cdt.Log(logging.INFO)
|
|
1008
|
-
except Exception as e:
|
|
1009
|
-
rep.log = cdt.Log(logging.ERROR, e)
|
|
1010
|
-
finally:
|
|
1011
|
-
return rep
|
|
1012
|
-
|
|
1013
|
-
def par2rep(self, par: Parameter, data: Optional[cdt.CommonDataType]) -> cdt.Report:
|
|
1014
|
-
rep = cdt.Report(str(data))
|
|
1015
|
-
try:
|
|
1016
|
-
if data is None:
|
|
1017
|
-
rep.msg = settings.report.empty
|
|
1018
|
-
rep.log = cdt.EMPTY_VAL
|
|
1019
|
-
elif isinstance(data, cdt.ReportMixin):
|
|
1020
|
-
rep = data.get_report()
|
|
1021
|
-
elif isinstance(data, DayProfileAction):
|
|
1022
|
-
rep.msg = F"{get_message("$rate$")}-{data.script_selector}: {data.start_time}"
|
|
1023
|
-
script_obj = self.get_object(data.script_logical_name)
|
|
1024
|
-
for script in script_obj.scripts:
|
|
1025
|
-
if script.script_identifier == data.script_selector:
|
|
1026
|
-
break
|
|
1027
|
-
else:
|
|
1028
|
-
rep.log = cdt.Log(logging.ERROR, F"absent script with ID: {data.script_selector}")
|
|
1029
|
-
else:
|
|
1030
|
-
obj = self.par2obj(par).unwrap()
|
|
1031
|
-
elements = tuple(par.elements())
|
|
1032
|
-
if unit := get_unit(obj.CLASS_ID, elements):
|
|
1033
|
-
rep.unit = cdt.Unit(unit).get_name()
|
|
1034
|
-
else:
|
|
1035
|
-
if s_u := self.par2su(par):
|
|
1036
|
-
rep.msg = (settings.report.scaler_format).format(int(data) * 10 ** int(s_u.scaler))
|
|
1037
|
-
rep.unit = s_u.unit.get_name()
|
|
1038
|
-
else:
|
|
1039
|
-
match obj.CLASS_ID, *elements:
|
|
1040
|
-
case (ClassID.PROFILE_GENERIC, 3, _) | (ClassID.PROFILE_GENERIC, 6):
|
|
1041
|
-
data: structs.CaptureObjectDefinition
|
|
1042
|
-
obj = self.get_object(data.logical_name)
|
|
1043
|
-
rep.msg = F"{get_name(data.logical_name)}.{obj.get_attr_element(int(data.attribute_index))}"
|
|
1044
|
-
case _:
|
|
1045
|
-
pass
|
|
1046
|
-
rep.log = cdt.Log(logging.INFO)
|
|
1047
|
-
except Exception as e:
|
|
1048
|
-
rep.log = cdt.Log(logging.ERROR, e)
|
|
1049
|
-
finally:
|
|
1050
|
-
return rep
|
|
1051
|
-
|
|
1052
|
-
@deprecated("use par2su")
|
|
1053
|
-
def get_scaler_unit(self,
|
|
1054
|
-
obj: ic.COSEMInterfaceClasses,
|
|
1055
|
-
par: bytes
|
|
1056
|
-
) -> cdt.ScalUnitType | None:
|
|
1057
|
-
match obj.CLASS_ID, *par:
|
|
1058
|
-
case (ClassID.REGISTER | ClassID.EXT_REGISTER, 2) | (ClassID.DEMAND_REGISTER, 2 | 3):
|
|
1059
|
-
obj: Register | DemandRegister
|
|
1060
|
-
if (s_u := obj.scaler_unit) is None:
|
|
1061
|
-
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1062
|
-
else:
|
|
1063
|
-
if (s := cdt.get_unit_scaler(s_u.unit.contents)) != 0:
|
|
1064
|
-
s_u = s_u.copy()
|
|
1065
|
-
s_u.scaler.set(int(s_u.scaler)-s)
|
|
1066
|
-
return s_u
|
|
1067
|
-
case ClassID.LIMITER, 3 | 4 | 5:
|
|
1068
|
-
obj: Limiter
|
|
1069
|
-
if m_v := obj.monitored_value:
|
|
1070
|
-
return self.get_scaler_unit( # recursion 1 level
|
|
1071
|
-
obj=self.get_object(m_v.logical_name),
|
|
1072
|
-
par=m_v.attribute_index.contents)
|
|
1073
|
-
else:
|
|
1074
|
-
raise ic.EmptyAttribute(obj.logical_name, 2)
|
|
1075
|
-
case ClassID.REGISTER_MONITOR, 2, _:
|
|
1076
|
-
obj: RegisterMonitor
|
|
1077
|
-
if (m_v := obj.monitored_value) is None:
|
|
1078
|
-
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1079
|
-
else:
|
|
1080
|
-
return self.get_scaler_unit( # recursion 1 level
|
|
1081
|
-
obj=self.get_object(m_v.logical_name),
|
|
1082
|
-
par=m_v.attribute_index.contents
|
|
1083
|
-
)
|
|
1084
|
-
case _:
|
|
1085
|
-
return None
|
|
1086
|
-
|
|
1087
|
-
@lru_cache(20000)
|
|
1088
|
-
def par2su(self, par: Parameter) -> Optional[cdt.ScalUnitType]:
|
|
1089
|
-
"""convert Parameter -> Optional[ScalerUnit],
|
|
1090
|
-
raise: NoObject, EmptyAttribute"""
|
|
1091
|
-
match (obj := self.par2obj(par).unwrap()).CLASS_ID, par.i:
|
|
1092
|
-
case (exc.NoObject, _):
|
|
1093
|
-
raise obj
|
|
1094
|
-
case (ClassID.REGISTER | ClassID.EXT_REGISTER, 2) | (ClassID.DEMAND_REGISTER, 2 | 3):
|
|
1095
|
-
obj: Register | DemandRegister
|
|
1096
|
-
if (s_u := obj.scaler_unit) is None:
|
|
1097
|
-
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1098
|
-
else:
|
|
1099
|
-
if (s := cdt.get_unit_scaler(s_u.unit.contents)) != 0:
|
|
1100
|
-
s_u = s_u.copy()
|
|
1101
|
-
s_u.scaler.set(int(s_u.scaler)-s)
|
|
1102
|
-
return s_u
|
|
1103
|
-
case ClassID.LIMITER, 3 | 4 | 5:
|
|
1104
|
-
obj: Limiter
|
|
1105
|
-
if m_v := obj.monitored_value:
|
|
1106
|
-
return self.par2su(Parameter(m_v.logical_name.contents).set_i(int(m_v.attribute_index))) # recursion 1 level
|
|
1107
|
-
else:
|
|
1108
|
-
raise ic.EmptyAttribute(obj.logical_name, 2)
|
|
1109
|
-
case ClassID.REGISTER_MONITOR, 2, _:
|
|
1110
|
-
obj: RegisterMonitor
|
|
1111
|
-
if (m_v := obj.monitored_value) is None:
|
|
1112
|
-
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1113
|
-
else:
|
|
1114
|
-
return self.par2su(Parameter(m_v.logical_name.contents).set_i(int(m_v.attribute_index))) # recursion 1 level
|
|
1115
|
-
case _:
|
|
1116
|
-
return None
|
|
1117
|
-
|
|
1118
|
-
def par2float(self, par: Parameter) -> float:
|
|
1119
|
-
"""try convert CDT value according with Parameter to build-in float"""
|
|
1120
|
-
data = self.par2data(par).unwrap()
|
|
1121
|
-
if hasattr(data, "__int__"):
|
|
1122
|
-
value = float(int(data))
|
|
1123
|
-
elif hasattr(data, "__float__"):
|
|
1124
|
-
value = float(data)
|
|
1125
|
-
else:
|
|
1126
|
-
raise TypeError("can't convert Parameter data to int or float")
|
|
1127
|
-
if (su := self.par2su(par)):
|
|
1128
|
-
value *= 10 ** int(su.scaler)
|
|
1129
|
-
return value
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
def filter_by_ass(self, ass_id: int) -> list[InterfaceClass]:
|
|
1133
|
-
"""return only association objects"""
|
|
1134
|
-
ret = list()
|
|
1135
|
-
for olt in self.getASSOCIATION(ass_id).object_list:
|
|
1136
|
-
ret.append(self.par2obj(Parameter(olt.logical_name.contents)).unwrap())
|
|
1137
|
-
return ret
|
|
1138
|
-
|
|
1139
|
-
def sap2objects(self, value: enums.ClientSAP) -> result.List[ic.COSEMInterfaceClasses]:
|
|
1140
|
-
res = result.List()
|
|
1141
|
-
for par in self.sap2association(value).iter_pars():
|
|
1142
|
-
if isinstance(res1 := self.par2obj(par), result.Error):
|
|
1143
|
-
res1.append_err(res.err)
|
|
1144
|
-
else:
|
|
1145
|
-
res.append(res1)
|
|
1146
|
-
return res
|
|
1147
|
-
|
|
1148
|
-
def iter_classID_objects(self,
|
|
1149
|
-
class_id: ut.CosemClassId) -> Iterator[InterfaceClass]:
|
|
1150
|
-
return (obj for obj in self.__objs.values() if obj.CLASS_ID == class_id)
|
|
1151
|
-
|
|
1152
|
-
def LNPattern2objects(self,
|
|
1153
|
-
pat: LNPattern) -> list[InterfaceClass]:
|
|
1154
|
-
ret = []
|
|
1155
|
-
for obj in self.__objs.values():
|
|
1156
|
-
if obj.logical_name in pat:
|
|
1157
|
-
ret.append(obj)
|
|
1158
|
-
return ret
|
|
1159
|
-
|
|
1160
|
-
def get_first(self, values: list[str | bytes | cst.LogicalName]) -> InterfaceClass:
|
|
1161
|
-
""" return first object from it exist in collection from value"""
|
|
1162
|
-
for val in values:
|
|
1163
|
-
if self.is_in_collection(val):
|
|
1164
|
-
return self.get_object(val)
|
|
1165
|
-
else:
|
|
1166
|
-
"""search next"""
|
|
1167
|
-
else:
|
|
1168
|
-
raise exc.NoObject(F"not found at least one DLMS Objects from collection with {values=}")
|
|
1169
|
-
|
|
1170
|
-
@deprecated("use <iter_classID_objects>")
|
|
1171
|
-
def get_objects_by_class_id(self, value: ut.CosemClassId) -> list[InterfaceClass]:
|
|
1172
|
-
return list(filter(lambda obj: obj.CLASS_ID == value, self.__objs.values()))
|
|
1173
|
-
|
|
1174
|
-
def get_writable_attr(self) -> UsedAttributes:
|
|
1175
|
-
"""return all writable {obj.ln: {attribute_index}}"""
|
|
1176
|
-
ret: UsedAttributes = dict()
|
|
1177
|
-
for ass in self.iter_classID_objects(ClassID.ASSOCIATION_LN):
|
|
1178
|
-
if (
|
|
1179
|
-
ass.logical_name.e == 0
|
|
1180
|
-
or ass.object_list is None
|
|
1181
|
-
):
|
|
1182
|
-
continue
|
|
1183
|
-
else:
|
|
1184
|
-
for list_type in ass.object_list:
|
|
1185
|
-
for attr_access in list_type.access_rights.attribute_access:
|
|
1186
|
-
if attr_access.access_mode.is_writable():
|
|
1187
|
-
if ret.get(list_type.logical_name, None) is None:
|
|
1188
|
-
ret[list_type.logical_name] = set()
|
|
1189
|
-
ret[list_type.logical_name].add(int(attr_access.attribute_id))
|
|
1190
|
-
return ret
|
|
1191
|
-
|
|
1192
|
-
def get_profile_s_u(self,
|
|
1193
|
-
obj: ProfileGeneric,
|
|
1194
|
-
mask: set[int] = None
|
|
1195
|
-
) -> list[cdt.ScalUnitType | None]:
|
|
1196
|
-
"""return container of scaler_units if possible, mask: position number in capture_objects"""
|
|
1197
|
-
res: list[cdt.ScalUnitType | None] = list()
|
|
1198
|
-
for i, obj_def in enumerate(obj.capture_objects):
|
|
1199
|
-
obj_def: structs.CaptureObjectDefinition
|
|
1200
|
-
if mask and i not in mask:
|
|
1201
|
-
continue
|
|
1202
|
-
s_u = None
|
|
1203
|
-
try:
|
|
1204
|
-
s_u = self.get_scaler_unit(
|
|
1205
|
-
obj=self.get_object(obj_def.logical_name),
|
|
1206
|
-
par=bytes([int(obj_def.attribute_index)]))
|
|
1207
|
-
except ic.EmptyAttribute as e:
|
|
1208
|
-
print(F"Can't fill Scaler and Unit for {get_name(obj_def.logical_name)}: {e}")
|
|
1209
|
-
finally:
|
|
1210
|
-
res.append(s_u)
|
|
1211
|
-
return res
|
|
1212
|
-
|
|
1213
|
-
def copy_obj_attr_values_from(self, other: InterfaceClass) -> bool:
|
|
1214
|
-
""" copy all attributes value from other and return bool result """
|
|
1215
|
-
try:
|
|
1216
|
-
obj: InterfaceClass = self.par2obj(Parameter(other.logical_name.contents)).unwrap()
|
|
1217
|
-
for i, attr in other.get_index_with_attributes(in_init_order=True):
|
|
1218
|
-
if i == 1:
|
|
1219
|
-
continue
|
|
1220
|
-
else:
|
|
1221
|
-
if attr is not None:
|
|
1222
|
-
obj.set_attr(i, attr.encoding)
|
|
1223
|
-
return True
|
|
1224
|
-
except exc.NoObject as e:
|
|
1225
|
-
return False
|
|
1226
|
-
|
|
1227
|
-
def copy_objects_attr_values_from(self, other: Self) -> bool:
|
|
1228
|
-
""" Copy collections values and return True if all was writen """
|
|
1229
|
-
if len(other) != 0:
|
|
1230
|
-
return bool(reduce(lambda a, b: a or b, map(self.copy_obj_attr_values_from, other.values())))
|
|
1231
|
-
else:
|
|
1232
|
-
return False
|
|
1233
|
-
|
|
1234
|
-
def obis2obj(self, obis: o.OBIS) -> result.SimpleOrError[InterfaceClass]:
|
|
1235
|
-
if obj := self.__objs.get(obis):
|
|
1236
|
-
return result.Simple(obj)
|
|
1237
|
-
return result.Error.from_e(ValueError(str(obis)), "no object")
|
|
1238
|
-
|
|
1239
|
-
def logicalName2obj(self, ln: cst.LogicalName) -> result.SimpleOrError[InterfaceClass]:
|
|
1240
|
-
return self.obis2obj(o.OBIS(ln.contents))
|
|
1241
|
-
|
|
1242
|
-
@cached_property
|
|
1243
|
-
def LDN(self) -> impl.data.LDN:
|
|
1244
|
-
return self.obis2obj(o.LDN).unwrap()
|
|
1245
|
-
|
|
1246
|
-
@cached_property
|
|
1247
|
-
def current_association(self) -> AssociationLN:
|
|
1248
|
-
return self.obis2obj(o.CURRENT_ASSOCIATION).unwrap()
|
|
1249
|
-
|
|
1250
|
-
def getASSOCIATION(self, instance: int) -> AssociationLN:
|
|
1251
|
-
return self.obis2obj(o.AssociationLN(instance)).unwrap()
|
|
1252
|
-
|
|
1253
|
-
@cached_property
|
|
1254
|
-
def PUBLIC_ASSOCIATION(self) -> AssociationLN:
|
|
1255
|
-
return self.obis2obj(o.PUBLIC_ASSOCIATION).unwrap()
|
|
1256
|
-
|
|
1257
|
-
@property
|
|
1258
|
-
def COMMUNICATION_PORT_PARAMETER(self) -> impl.data.CommunicationPortParameter:
|
|
1259
|
-
return self.obis2obj(bytes((0, 0, 96, 12, 4, 255))).unwrap()
|
|
1260
|
-
|
|
1261
|
-
@property
|
|
1262
|
-
def clock(self) -> Clock:
|
|
1263
|
-
return self.obis2obj(bytes((0, 0, 1, 0, 0, 255))).unwrap()
|
|
1264
|
-
|
|
1265
|
-
@property
|
|
1266
|
-
def boot_image_transfer(self) -> ImageTransfer:
|
|
1267
|
-
return self.obis2obj(bytes((0, 0, 44, 0, 128, 255))).unwrap()
|
|
1268
|
-
|
|
1269
|
-
@property
|
|
1270
|
-
def firmware_image_transfer(self) -> ImageTransfer:
|
|
1271
|
-
return self.obis2obj(bytes((0, 0, 44, 0, 0, 255))).unwrap()
|
|
1272
|
-
|
|
1273
|
-
@property
|
|
1274
|
-
def firmwares_description(self) -> Data:
|
|
1275
|
-
""" Consist from boot_version, descriptor, ex.: 0005PWRM_M2M_3_F1_5ppm_Spvq. 0.0.128.100.0.255 """
|
|
1276
|
-
return self.obis2obj(bytes((0, 0, 128, 100, 0, 255))).unwrap()
|
|
1277
|
-
|
|
1278
|
-
@property
|
|
1279
|
-
def RU_CLOSE_ELECTRIC_SEAL(self) -> Data:
|
|
1280
|
-
""" Russian. СПОДЕС Г.2 """
|
|
1281
|
-
return self.obis2obj(bytes((0, 0, 96, 51, 6, 255))).unwrap()
|
|
1282
|
-
|
|
1283
|
-
@property
|
|
1284
|
-
def RU_ERASE_MAGNETIC_EVENTS(self) -> Data:
|
|
1285
|
-
""" Russian. СПОДЕС Г.2 """
|
|
1286
|
-
return self.obis2obj(bytes((0, 0, 96, 51, 7, 255))).unwrap()
|
|
1287
|
-
|
|
1288
|
-
@property
|
|
1289
|
-
def RU_FILTER_ALARM_2(self) -> Data:
|
|
1290
|
-
""" Russian. Filter of Alarm register relay"""
|
|
1291
|
-
return self.obis2obj(bytes((0, 0, 97, 98, 11, 255))).unwrap()
|
|
1292
|
-
|
|
1293
|
-
@property
|
|
1294
|
-
def RU_DAILY_PROFILE(self) -> ProfileGeneric:
|
|
1295
|
-
""" Russian. Profile of daily values """
|
|
1296
|
-
return self.obis2obj(bytes((1, 0, 98, 2, 0, 255))).unwrap()
|
|
1297
|
-
|
|
1298
|
-
@property
|
|
1299
|
-
def RU_MAXIMUM_CURRENT_EXCESS_LIMIT(self) -> Register:
|
|
1300
|
-
""" RU. СТО 34.01-5.1-006-2021 ver3, 11.1. Maximum current excess limit before the subscriber is disconnected, % of IMAX """
|
|
1301
|
-
return self.obis2obj(bytes((1, 0, 11, 134, 0, 255))).unwrap()
|
|
1302
|
-
|
|
1303
|
-
@property
|
|
1304
|
-
def RU_MAXIMUM_VOLTAGE_EXCESS_LIMIT(self) -> Register:
|
|
1305
|
-
""" RU. СТО 34.01-5.1-006-2021 ver3, 11.1. Maximum voltage excess limit before the subscriber is disconnected, % of Unominal """
|
|
1306
|
-
return self.obis2obj(bytes((1, 0, 12, 134, 0, 255))).unwrap()
|
|
1307
|
-
|
|
1308
|
-
def getDISCONNECT_CONTROL(self, ch: int = 0) -> DisconnectControl:
|
|
1309
|
-
"""DLMS UA 1000-1 Ed 14 6.2.46 Disconnect control objects by channel"""
|
|
1310
|
-
return self.obis2obj(bytes((0, ch, 96, 3, 10, 255))).unwrap()
|
|
1311
|
-
|
|
1312
|
-
def getARBITRATOR(self, ch: int = 0) -> Arbitrator:
|
|
1313
|
-
"""DLMS UA 1000-1 Ed 14 6.2.47 Arbitrator objects objects by channel"""
|
|
1314
|
-
return self.obis2obj(bytes((0, ch, 96, 3, 20, 255))).unwrap()
|
|
1315
|
-
|
|
1316
|
-
@property
|
|
1317
|
-
def boot_version(self) -> str:
|
|
1318
|
-
try:
|
|
1319
|
-
return self.firmwares_description.value.to_str()[:4]
|
|
1320
|
-
except Exception as e:
|
|
1321
|
-
print(e)
|
|
1322
|
-
return 'unknown'
|
|
1323
|
-
|
|
1324
|
-
def get_script_names(self, ln: cst.LogicalName, selector: cdt.LongUnsigned) -> str:
|
|
1325
|
-
"""return name from script by selector"""
|
|
1326
|
-
obj = self.par2obj(Parameter(ln.contents)).unwrap()
|
|
1327
|
-
if isinstance(obj, ScriptTable):
|
|
1328
|
-
for script in obj.scripts:
|
|
1329
|
-
script: ScriptTable.scripts
|
|
1330
|
-
if script.script_identifier == selector:
|
|
1331
|
-
names: list[str] = []
|
|
1332
|
-
for action in script.actions:
|
|
1333
|
-
action_obj = self.par2obj(Parameter(action.logical_name.contents)).unwrap()
|
|
1334
|
-
if int(action_obj.CLASS_ID) != int(action.class_id):
|
|
1335
|
-
raise ValueError(F"got {action_obj.CLASS_ID}, expected {action.class_id}")
|
|
1336
|
-
match int(action.service_id):
|
|
1337
|
-
case 1: # for write
|
|
1338
|
-
if isinstance(action.parameter, cdt.NullData):
|
|
1339
|
-
names.append(str(action_obj.get_attr_element(int(action.index))))
|
|
1340
|
-
else:
|
|
1341
|
-
raise TypeError(F"not support by framework") # TODO: make it
|
|
1342
|
-
case 2: # for execute
|
|
1343
|
-
if isinstance(action.parameter, cdt.NullData):
|
|
1344
|
-
names.append(str(action_obj.get_meth_element(int(action.index))))
|
|
1345
|
-
else:
|
|
1346
|
-
raise TypeError(F"not support by framework") # TODO: make it
|
|
1347
|
-
return ", ".join(names)
|
|
1348
|
-
else:
|
|
1349
|
-
raise ValueError(F"not find {selector} in {obj}")
|
|
1350
|
-
else:
|
|
1351
|
-
raise ValueError(F"object with {ln} is not {ScriptTable.CLASS_ID}")
|
|
1352
|
-
|
|
1353
|
-
@lru_cache(4)
|
|
1354
|
-
def get_association_id(self, client_sap: enums.ClientSAP) -> int:
|
|
1355
|
-
"""return id(association instance) from it client address without current"""
|
|
1356
|
-
for ass in get_filtered(iter(self), (ln_pattern.NON_CURRENT_ASSOCIATION,)):
|
|
1357
|
-
if ass.associated_partners_id.client_SAP == client_sap:
|
|
1358
|
-
return ass.logical_name.e
|
|
1359
|
-
else:
|
|
1360
|
-
continue
|
|
1361
|
-
else:
|
|
1362
|
-
raise ValueError(F"absent association with {client_sap}")
|
|
1363
|
-
|
|
1364
|
-
def sap2association(self, sap: enums.ClientSAP) -> AssociationLN:
|
|
1365
|
-
for ass in self.iter_classID_objects(ClassID.ASSOCIATION_LN):
|
|
1366
|
-
if (
|
|
1367
|
-
ass.associated_partners_id is not None
|
|
1368
|
-
and ass.associated_partners_id.client_SAP == sap
|
|
1369
|
-
):
|
|
1370
|
-
return ass
|
|
1371
|
-
else:
|
|
1372
|
-
continue
|
|
1373
|
-
else:
|
|
1374
|
-
raise exc.NoObject(F"hasn't association with {sap}")
|
|
1375
|
-
|
|
1376
|
-
@lru_cache(maxsize=1000)
|
|
1377
|
-
def is_readable(self, ln: cst.LogicalName,
|
|
1378
|
-
index: int,
|
|
1379
|
-
association_id: int,
|
|
1380
|
-
security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
|
|
1381
|
-
) -> bool:
|
|
1382
|
-
return self.getASSOCIATION(association_id).is_readable(
|
|
1383
|
-
ln=ln,
|
|
1384
|
-
index=index,
|
|
1385
|
-
security_policy=security_policy
|
|
1386
|
-
)
|
|
1387
|
-
|
|
1388
|
-
@lru_cache(maxsize=1000)
|
|
1389
|
-
def is_writable(self, ln: cst.LogicalName,
|
|
1390
|
-
index: int,
|
|
1391
|
-
association_id: int,
|
|
1392
|
-
security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
|
|
1393
|
-
) -> bool:
|
|
1394
|
-
return self.getASSOCIATION(association_id).is_writable(
|
|
1395
|
-
ln=ln,
|
|
1396
|
-
index=index,
|
|
1397
|
-
security_policy=security_policy
|
|
1398
|
-
)
|
|
1399
|
-
|
|
1400
|
-
@lru_cache(maxsize=1000)
|
|
1401
|
-
def isnt_mutable(self,
|
|
1402
|
-
par: Parameter,
|
|
1403
|
-
association_id: int,
|
|
1404
|
-
security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
|
|
1405
|
-
) -> bool:
|
|
1406
|
-
"""is not writable and STATIC data"""
|
|
1407
|
-
if (
|
|
1408
|
-
not self.is_writable(par.ln, par.i, association_id, security_policy, security_policy)
|
|
1409
|
-
and self.par2obj(par
|
|
1410
|
-
).unwrap().getAElement(par.i
|
|
1411
|
-
).unwrap().classifier == ic.Classifier.STATIC
|
|
1412
|
-
):
|
|
1413
|
-
return True
|
|
1414
|
-
return False
|
|
1415
|
-
|
|
1416
|
-
@lru_cache(maxsize=1000)
|
|
1417
|
-
def is_accessible(self, ln: cst.LogicalName,
|
|
1418
|
-
index: int,
|
|
1419
|
-
association_id: int,
|
|
1420
|
-
m_id: mechanism_id.MechanismIdElement = None
|
|
1421
|
-
) -> bool:
|
|
1422
|
-
"""for ver 0 and 1 only"""
|
|
1423
|
-
|
|
1424
|
-
return self.getASSOCIATION(association_id).is_accessible(
|
|
1425
|
-
ln=ln,
|
|
1426
|
-
index=index,
|
|
1427
|
-
m_id=m_id
|
|
1428
|
-
)
|
|
1429
|
-
|
|
1430
|
-
@lru_cache(maxsize=100)
|
|
1431
|
-
def get_name_and_type(self, value: structs.CaptureObjectDefinition) -> tuple[list[str], Type[cdt.CommonDataType]]:
|
|
1432
|
-
""" return names and type of element from collection"""
|
|
1433
|
-
names: list[str] = list()
|
|
1434
|
-
obj = self.par2obj(Parameter(value.logical_name.contents)).unwrap()
|
|
1435
|
-
names.append(get_name(obj.logical_name))
|
|
1436
|
-
attr_index = int(value.attribute_index)
|
|
1437
|
-
data_index = int(value.data_index)
|
|
1438
|
-
data_type: Type[cdt.CommonDataType] = obj.get_attr_data_type(attr_index)
|
|
1439
|
-
names.append(str(obj.get_attr_element(attr_index)))
|
|
1440
|
-
if data_index == 0:
|
|
1441
|
-
pass
|
|
1442
|
-
elif issubclass(data_type, cdt.Structure):
|
|
1443
|
-
if len(data_type.ELEMENTS) < data_index:
|
|
1444
|
-
raise ValueError(F"can't create buffer_struct_type for {self}, got {data_index=} in struct {data_type.__name__}, expected 1..{len(data_type.ELEMENTS)}")
|
|
1445
|
-
else:
|
|
1446
|
-
el: cdt.StructElement = data_type.ELEMENTS[data_index - 1]
|
|
1447
|
-
names.append(el.NAME)
|
|
1448
|
-
data_type = el.TYPE
|
|
1449
|
-
elif isinstance(obj, ProfileGeneric) and attr_index == 2:
|
|
1450
|
-
"""according to DLMS UA 1000-1 Ed 14. ProfileGeneric.capture_object.data_index annex"""
|
|
1451
|
-
return self.get_name_and_type(obj.capture_objects[data_index - 1]) # todo: is recurse need rewrite here
|
|
1452
|
-
else:
|
|
1453
|
-
pass
|
|
1454
|
-
return names, data_type
|
|
1455
|
-
|
|
1456
|
-
def get_attr_tree(self,
|
|
1457
|
-
ass_id: int,
|
|
1458
|
-
obj_mode: ObjectTreeMode = "c",
|
|
1459
|
-
obj_filter: ObjFilteredKey = None,
|
|
1460
|
-
sort_mode: SortMode = "",
|
|
1461
|
-
af_mode: Literal["l", "r", "w", "lr", "lw", "wr", "lrw", "m", "mlrw", "mlr"] = "l",
|
|
1462
|
-
oi_filter: tuple[tuple[ClassID, tuple[int, ...]], ...] = None # todo: maybe ai_filter with LNPattern, indexes need?
|
|
1463
|
-
) -> dict[ClassID | media_id.MediaId, dict[ic.COSEMInterfaceClasses, list[int]]] | dict[ic.COSEMInterfaceClasses, list[int]]: # todo: not all ret annotation
|
|
1464
|
-
"""af_mode(attribute filter mode): l-reduce logical_name, r-show only readable, w-show only writeable,
|
|
1465
|
-
oi_filter(object attribute index filter), example: ((ClassID.REGISTER, (2,))) - is restricted for Register only Value attribute without logical_name and scaler_unit
|
|
1466
|
-
"""
|
|
1467
|
-
objects: dict[ic.COSEMInterfaceClasses, list[int]]
|
|
1468
|
-
without_ln = True if "l" in af_mode else False
|
|
1469
|
-
only_read = True if "r" in af_mode else False
|
|
1470
|
-
only_write = True if "w" in af_mode else False
|
|
1471
|
-
with_methods = True if "m" in af_mode else False
|
|
1472
|
-
oi_f = dict(oi_filter) if oi_filter else dict()
|
|
1473
|
-
"""objects indexes filter"""
|
|
1474
|
-
filtered = self.filter_by_ass(ass_id)
|
|
1475
|
-
if obj_filter:
|
|
1476
|
-
filtered = get_filtered(filtered, obj_filter)
|
|
1477
|
-
ret = get_object_tree(
|
|
1478
|
-
objects=get_sorted(
|
|
1479
|
-
objects=filtered,
|
|
1480
|
-
mode=sort_mode),
|
|
1481
|
-
mode=obj_mode)
|
|
1482
|
-
stack = [(None, None, ret)]
|
|
1483
|
-
while len(stack) != 0:
|
|
1484
|
-
d, k, v = stack.pop()
|
|
1485
|
-
if isinstance(d, dict):
|
|
1486
|
-
for k_, v_ in tuple(d.items()):
|
|
1487
|
-
if len(v_) == 0:
|
|
1488
|
-
d.pop(k_)
|
|
1489
|
-
if isinstance(v, list):
|
|
1490
|
-
objects = dict()
|
|
1491
|
-
for obj in v:
|
|
1492
|
-
obj: ic.COSEMInterfaceClasses
|
|
1493
|
-
f_i = oi_f.get(obj.CLASS_ID)
|
|
1494
|
-
"""filter indexes"""
|
|
1495
|
-
indexes = list()
|
|
1496
|
-
for i, attr in obj.get_index_with_attributes():
|
|
1497
|
-
if without_ln and i == 1:
|
|
1498
|
-
continue
|
|
1499
|
-
elif only_read and not self.is_readable(obj.logical_name, i, ass_id):
|
|
1500
|
-
continue
|
|
1501
|
-
elif only_write and not self.is_writable(obj.logical_name, i, ass_id):
|
|
1502
|
-
continue
|
|
1503
|
-
elif f_i and i not in f_i:
|
|
1504
|
-
continue
|
|
1505
|
-
else:
|
|
1506
|
-
indexes.append(i)
|
|
1507
|
-
if with_methods:
|
|
1508
|
-
i_meth = count(1)
|
|
1509
|
-
for i, m_el in zip(i_meth, obj.M_ELEMENTS):
|
|
1510
|
-
try:
|
|
1511
|
-
if not self.is_accessible(obj.logical_name, i, ass_id, mechanism_id.LOW):
|
|
1512
|
-
continue
|
|
1513
|
-
elif f_i and -i not in f_i:
|
|
1514
|
-
continue
|
|
1515
|
-
indexes.append(-i)
|
|
1516
|
-
except exc.ITEApplication as e:
|
|
1517
|
-
print(F"skip {i}... methods for {obj}: {e}")
|
|
1518
|
-
break
|
|
1519
|
-
if len(indexes) != 0:
|
|
1520
|
-
objects[obj] = indexes
|
|
1521
|
-
if d is None:
|
|
1522
|
-
return objects
|
|
1523
|
-
if len(objects) != 0:
|
|
1524
|
-
d[k] = objects
|
|
1525
|
-
else:
|
|
1526
|
-
d.pop(k)
|
|
1527
|
-
elif isinstance(v, dict):
|
|
1528
|
-
for k_, v_ in v.items():
|
|
1529
|
-
stack.append((v, k_, v_))
|
|
1530
|
-
else:
|
|
1531
|
-
raise ValueError('not support')
|
|
1532
|
-
return ret
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if config is not None:
|
|
1536
|
-
try:
|
|
1537
|
-
__collection_path = settings.collection.path
|
|
1538
|
-
except KeyError as e:
|
|
1539
|
-
raise exc.TomlKeyError(F"not find {e} in [DLMS.collection]<path>")
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
@deprecated("use lnContents2obis")
|
|
1543
|
-
def get_ln_contents(value: LNContaining) -> bytes:
|
|
1544
|
-
"""return LN as bytes[6] for use in any searching"""
|
|
1545
|
-
match value:
|
|
1546
|
-
case bytes(): return value
|
|
1547
|
-
case cst.LogicalName() | ut.CosemObjectInstanceId(): return value.contents
|
|
1548
|
-
case ut.CosemAttributeDescriptor() | ut.CosemMethodDescriptor(): return value.instance_id.contents
|
|
1549
|
-
case ut.CosemAttributeDescriptorWithSelection(): return value.cosem_attribute_descriptor.instance_id.contents
|
|
1550
|
-
case cdt.Structure(logical_name=value.logical_name): return value.logical_name.contents
|
|
1551
|
-
case cdt.Structure() as s:
|
|
1552
|
-
s: cdt.Structure
|
|
1553
|
-
for it in s:
|
|
1554
|
-
if isinstance(it, cst.LogicalName):
|
|
1555
|
-
return it.contents
|
|
1556
|
-
raise ValueError(F"can't convert {value=} to Logical Name contents. Struct {s} not content the Logical Name")
|
|
1557
|
-
case str() if value.find('.') != -1:
|
|
1558
|
-
return cst.LogicalName.from_obis(value).contents
|
|
1559
|
-
case str(): return cst.LogicalName(value).contents
|
|
1560
|
-
case _: raise ValueError(F"can't convert {value=} to Logical Name contents")
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
def lnContents2obis(value: LNContaining) -> o.OBIS:
|
|
1564
|
-
"""return LN as OBIS for use in any searching"""
|
|
1565
|
-
match value:
|
|
1566
|
-
case bytes(): return o.OBIS(value)
|
|
1567
|
-
case cst.LogicalName() | ut.CosemObjectInstanceId(): return o.OBIS(value.contents)
|
|
1568
|
-
case ut.CosemAttributeDescriptor() | ut.CosemMethodDescriptor(): return o.OBIS(value.instance_id.contents)
|
|
1569
|
-
case ut.CosemAttributeDescriptorWithSelection(): return o.OBIS(value.cosem_attribute_descriptor.instance_id.contents)
|
|
1570
|
-
case cdt.Structure(logical_name=value.logical_name): return o.OBIS(value.logical_name.contents)
|
|
1571
|
-
case cdt.Structure() as s:
|
|
1572
|
-
s: cdt.Structure
|
|
1573
|
-
for it in s:
|
|
1574
|
-
if isinstance(it, cst.LogicalName):
|
|
1575
|
-
return o.OBIS(it.contents)
|
|
1576
|
-
raise ValueError(F"can't convert {value=} to Logical Name contents. Struct {s} not content the Logical Name")
|
|
1577
|
-
case str() if value.find('.') != -1:
|
|
1578
|
-
return o.OBIS(cst.LogicalName.from_obis(value).contents)
|
|
1579
|
-
case str(): return o.OBIS(cst.LogicalName(value).contents)
|
|
1580
|
-
case _: raise ValueError(F"can't convert {value=} to Logical Name contents")
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
class AttrDesc:
|
|
1584
|
-
"""keep constant descriptors # todo: make better"""
|
|
1585
|
-
OBJECT_LIST = ut.CosemAttributeDescriptor((ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(2)))
|
|
1586
|
-
LDN_VALUE = ut.CosemAttributeDescriptor((ClassID.DATA, ut.CosemObjectInstanceId("0.0.42.0.0.255"), ut.CosemObjectAttributeId(2)))
|
|
1587
|
-
SPODES_VERSION = ut.CosemAttributeDescriptor((ClassID.DATA, ut.CosemObjectInstanceId("0.0.96.1.6.255"), ut.CosemObjectAttributeId(2)))
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
__range10_and_255: tuple = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255
|
|
1591
|
-
__range63: tuple = tuple(range(0, 64))
|
|
1592
|
-
__range120_and_124_127: tuple = tuple(chain(range(0, 121), range(124, 128)))
|
|
1593
|
-
__range100_and_255: tuple = tuple(chain(range(0, 100), (255,)))
|
|
1594
|
-
__range100_and_101_125_and_255: tuple = tuple(chain(__range100_and_255, range(101, 126)))
|
|
1595
|
-
__c1: tuple = tuple(chain(range(1, 10), (13, 14, 33, 34, 53, 54, 73, 74, 82), range(16, 31), range(36, 51), range(56, 70), range(76, 81), range(84, 90)))
|
|
1596
|
-
"""DLMS UA 1000-1 Ed 14. Table 45 c1"""
|
|
1597
|
-
__table44: tuple = (11, 12, 15, 31, 32, 35, 51, 52, 55, 71, 72, 75, 90, 91, 92)
|
|
1598
|
-
"""DLMS UA 1000-1 Ed 14. Table 44 for active power"""
|
|
1599
|
-
__c2: tuple = tuple(chain(__table44, range(100, 108)))
|
|
1600
|
-
"""DLMS UA 1000-1 Ed 14. Table 45 c2"""
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
def get_media_id(ln: cst.LogicalName) -> media_id.MediaId:
|
|
1604
|
-
return media_id.MediaId.from_int(ln.a)
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
def get_class_id(obj: InterfaceClass) -> ClassID:
|
|
1608
|
-
return obj.CLASS_ID
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
def get_map_by_obj(objects: list[InterfaceClass] | tuple[InterfaceClass], key: Callable[[InterfaceClass], ...]) -> dict[media_id.MediaId, list[InterfaceClass]]:
|
|
1612
|
-
ret = dict()
|
|
1613
|
-
for obj in objects:
|
|
1614
|
-
if ret.get(new_key := key(obj)):
|
|
1615
|
-
ret[new_key].append(obj)
|
|
1616
|
-
else:
|
|
1617
|
-
ret[new_key] = [obj]
|
|
1618
|
-
return ret
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
def get_object_tree(objects: list[InterfaceClass] | tuple[InterfaceClass],
|
|
1622
|
-
mode: ObjectTreeMode) -> dict[media_id.MediaId | ClassID, list[InterfaceClass]]:
|
|
1623
|
-
"""mode: m-media_id, g-relation_group, c-class_id"""
|
|
1624
|
-
mode = list(mode)
|
|
1625
|
-
ret = objects
|
|
1626
|
-
while mode:
|
|
1627
|
-
match mode.pop():
|
|
1628
|
-
case "m":
|
|
1629
|
-
key = lambda obj: get_media_id(obj.logical_name)
|
|
1630
|
-
case "g":
|
|
1631
|
-
key = lambda obj: get_relation_group(obj.logical_name)
|
|
1632
|
-
case "c":
|
|
1633
|
-
key = get_class_id
|
|
1634
|
-
case _:
|
|
1635
|
-
raise KeyError(F"got unknown {mode=} for get_map")
|
|
1636
|
-
stack = [(None, None, ret)]
|
|
1637
|
-
while len(stack) != 0:
|
|
1638
|
-
d, k, v = stack.pop()
|
|
1639
|
-
if isinstance(v, list):
|
|
1640
|
-
res = get_map_by_obj(v, key)
|
|
1641
|
-
if d is None:
|
|
1642
|
-
ret = res
|
|
1643
|
-
else:
|
|
1644
|
-
d[k] = res
|
|
1645
|
-
elif isinstance(v, dict):
|
|
1646
|
-
stack.extend(((v, k_, v_) for k_, v_ in v.items()))
|
|
1647
|
-
else:
|
|
1648
|
-
raise ValueError('not support')
|
|
1649
|
-
return ret
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
def get_sorted(objects: list[InterfaceClass],
|
|
1653
|
-
mode: SortMode) -> list[InterfaceClass]:
|
|
1654
|
-
"""mode: l-logical_name, n-name, c-class_id
|
|
1655
|
-
"""
|
|
1656
|
-
mode: list[str] = list(mode)
|
|
1657
|
-
while mode:
|
|
1658
|
-
match mode.pop():
|
|
1659
|
-
case "l":
|
|
1660
|
-
key = None
|
|
1661
|
-
case "n":
|
|
1662
|
-
key = lambda obj: get_name(obj.logical_name)
|
|
1663
|
-
case "c":
|
|
1664
|
-
key = lambda obj: obj.CLASS_ID
|
|
1665
|
-
case _:
|
|
1666
|
-
raise KeyError(F"got unknown {mode=} for get_map")
|
|
1667
|
-
objects = sorted(objects, key=key)
|
|
1668
|
-
return objects
|
|
1669
|
-
|
|
1670
|
-
@dataclass
|
|
1671
|
-
class Channel:
|
|
1672
|
-
"""for object filter approve"""
|
|
1673
|
-
n: int
|
|
1674
|
-
|
|
1675
|
-
def __post_init__(self) -> None:
|
|
1676
|
-
if not self.is_channel(self.n):
|
|
1677
|
-
raise ValueError(F"got value={self.n}, expected (0..64)")
|
|
1678
|
-
|
|
1679
|
-
@staticmethod
|
|
1680
|
-
def is_channel(b: int) -> bool:
|
|
1681
|
-
return True if 0 <= b <= 64 else False
|
|
1682
|
-
|
|
1683
|
-
def is_approve(self, b: int) -> bool:
|
|
1684
|
-
if self.is_channel(b):
|
|
1685
|
-
return True if b == self.n else False
|
|
1686
|
-
else:
|
|
1687
|
-
return True
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
RelationGroup: TypeAlias = media_id.Abstract | media_id.Electricity | media_id.Hca | media_id.Gas | media_id.Thermal | media_id.Water | media_id.Other
|
|
1691
|
-
RelationGroups: tuple[media_id.MediaId, ...] = (media_id.ABSTRACT, media_id.ELECTRICITY, media_id.HCA, media_id.GAS, media_id.THERMAL, media_id.WATER)
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
@lru_cache(maxsize=1000)
|
|
1695
|
-
def get_relation_group(ln: cst.LogicalName) -> RelationGroup:
|
|
1696
|
-
if ln.a == media_id.ABSTRACT:
|
|
1697
|
-
if ln.c == 0:
|
|
1698
|
-
if ln.d == 1:
|
|
1699
|
-
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES
|
|
1700
|
-
elif ln.d in (2, 9):
|
|
1701
|
-
return media_id.OTHER_ABSTRACT_GENERAL_PURPOSE_OBIS_CODES
|
|
1702
|
-
elif ln.c == 1:
|
|
1703
|
-
if ln.d in (0, 1, 2, 3, 4, 5, 6):
|
|
1704
|
-
return media_id.CLOCK_OBJECTS
|
|
1705
|
-
elif ln.c == 2:
|
|
1706
|
-
if ln.d in (0, 1, 2):
|
|
1707
|
-
return media_id.MODEM_CONFIGURATION_AND_RELATED_OBJECTS
|
|
1708
|
-
elif ln.c == 10 and ln.d == 0 and ln.e in (0, 1, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 125):
|
|
1709
|
-
return media_id.SCRIPT_TABLE_OBJECTS
|
|
1710
|
-
elif ln.c == 11 and ln.d == 0:
|
|
1711
|
-
return media_id.SPECIAL_DAYS_TABLE_OBJECTS
|
|
1712
|
-
elif ln.c == 12 and ln.d == 0:
|
|
1713
|
-
return media_id.SCHEDULE_OBJECTS
|
|
1714
|
-
elif ln.c == 13 and ln.d == 0:
|
|
1715
|
-
return media_id.ACTIVITY_CALENDAR_OBJECTS
|
|
1716
|
-
elif ln.c == 14 and ln.d == 0:
|
|
1717
|
-
return media_id.REGISTER_ACTIVATION_OBJECTS
|
|
1718
|
-
elif ln.c == 15 and ln.d == 0 and ln.e in (0, 1, 2, 3, 4, 5, 6, 7):
|
|
1719
|
-
return media_id.SINGLE_ACTION_SCHEDULE_OBJECTS
|
|
1720
|
-
elif ln.c == 16:
|
|
1721
|
-
if ln.d == 0 or (ln.d == 1 and ln.e in range(0, 10)):
|
|
1722
|
-
return media_id.REGISTER_OBJECTS_MONITOR
|
|
1723
|
-
elif ln.d == 2:
|
|
1724
|
-
return media_id.PARAMETER_MONITOR_OBJECTS
|
|
1725
|
-
elif ln.c == 17 and ln.d == 0:
|
|
1726
|
-
return media_id.LIMITER_OBJECTS
|
|
1727
|
-
elif ln.c == 18 and ln.d == 0:
|
|
1728
|
-
return media_id.ARRAY_MANAGER_OBJECT
|
|
1729
|
-
elif ln.c == 19:
|
|
1730
|
-
if (ln.d in range(0, 10) and ln.e == 0) or ln.d in range(10, 50) or ln.d in (range(50, 60) and ln.e in (1, 2)):
|
|
1731
|
-
return media_id.PAYMENT_METERING_RELATED_OBJECTS
|
|
1732
|
-
elif ln.c == 20 and ln.d == 0 and ln.e in (0, 1):
|
|
1733
|
-
return media_id.IEC_LOCAL_PORT_SETUP_OBJECTS
|
|
1734
|
-
elif ln.c == 21 and ln.d == 0:
|
|
1735
|
-
return media_id.STANDARD_READOUT_PROFILE_OBJECTS
|
|
1736
|
-
elif ln.c == 22 and ln.d == 0 and ln.e == 0:
|
|
1737
|
-
return media_id.IEC_HDLC_SETUP_OBJECTS
|
|
1738
|
-
elif ln.c == 23:
|
|
1739
|
-
if (ln.d in 0, 1, 2 and ln.e == 0) or ln.d == 3:
|
|
1740
|
-
return media_id.IEC_TWISTED_PAIR_1_SETUP_OBJECTS
|
|
1741
|
-
elif ln.c == 24:
|
|
1742
|
-
if (ln.d in (0, 1, 4, 5, 6) and ln.e == 0) or (ln.d in (2, 8, 9)):
|
|
1743
|
-
return media_id.OBJECTS_RELATED_TO_DATA_EXCHANGE_OVER_M_BUS
|
|
1744
|
-
elif ln.c == 31 and ln.d == 0 and ln.e == 0:
|
|
1745
|
-
return media_id.OBJECTS_RELATED_TO_DATA_EXCHANGE_OVER_M_BUS
|
|
1746
|
-
elif ln.c == 25:
|
|
1747
|
-
if ln.d in (0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15) and ln.e == 0:
|
|
1748
|
-
return media_id.OBJECTS_TO_SET_UP_DATA_EXCHANGE_OVER_THE_INTERNET
|
|
1749
|
-
elif ln.d == 9 and ln.e == 0:
|
|
1750
|
-
return media_id.OBJECTS_TO_SET_UP_PUSH_SETUP
|
|
1751
|
-
elif ln.c == 26 and ln.d in (0, 1, 2, 3, 5, 6) and ln.e == 0:
|
|
1752
|
-
return media_id.OBJECTS_FOR_SETTING_UP_DATA_EXCHANGE_USING_S_FSK_PLC
|
|
1753
|
-
elif ln.c == 27 and ln.d in (0, 1, 2) and ln.e == 0:
|
|
1754
|
-
return media_id.OBJECTS_FOR_SETTING_UP_THE_ISO_IEC_8802_2_LLC_LAYER
|
|
1755
|
-
elif ln.c == 28 and ln.d in (0, 1, 2, 3, 4, 5, 6, 7) and ln.e == 0:
|
|
1756
|
-
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_NARROWBAND_OFDM_PLC_FOR_PRIME_NETWORKS
|
|
1757
|
-
elif ln.c == 29 and ln.d in (0, 1, 2) and ln.e == 0:
|
|
1758
|
-
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_NARROW_BAND_OFDM_PLC_FOR_G3_PLC_NETWORKS
|
|
1759
|
-
elif ln.c == 30 and ln.d in (0, 1, 2, 3, 4):
|
|
1760
|
-
return media_id.ZIGBEE_SETUP_OBJECTS
|
|
1761
|
-
elif ln.c == 32 and ln.d in (0, 1, 2, 3) and ln.e == 0:
|
|
1762
|
-
return media_id.OBJECTS_FOR_SETTING_UP_AND_MANAGING_DATA_EXCHANGE_USING_ISO_IEC_14908_PLC_NETWORKS
|
|
1763
|
-
elif ln.c == 33 and ln.d in (0, 1, 2, 3) and ln.e == 0:
|
|
1764
|
-
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_HS_PLC_ISO_IEC_12139_1_ISO_EC_12139_1_NETWORKS
|
|
1765
|
-
elif ln.c == 34 and ln.d in (0, 1, 2, 3) and ln.e == 0:
|
|
1766
|
-
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_WI_SUN_NETWORKS
|
|
1767
|
-
elif ln.c == 40 and ln.b == 0 and ln.d == 0:
|
|
1768
|
-
return media_id.ASSOCIATION_OBJECTS
|
|
1769
|
-
elif ln.c == 41 and ln.b == 0 and ln.d == 0 and ln.e == 0:
|
|
1770
|
-
return media_id.SAP_ASSIGNMENT_OBJECT
|
|
1771
|
-
elif ln.c == 42 and ln.b == 0 and ln.d == 0 and ln.e == 0:
|
|
1772
|
-
return media_id.COSEM_LOGICAL_DEVICE_NAME_OBJECT
|
|
1773
|
-
elif ln.c == 43:
|
|
1774
|
-
if (ln.b == 0 and ln.d == 0) or ln.d in (1, 2):
|
|
1775
|
-
return media_id.INFORMATION_SECURITY_RELATED_OBJECTS
|
|
1776
|
-
elif ln.c == 44:
|
|
1777
|
-
if ln.b == 0:
|
|
1778
|
-
if ln.d == 0:
|
|
1779
|
-
return media_id.IMAGE_TRANSFER_OBJECTS
|
|
1780
|
-
elif ln.d == 1:
|
|
1781
|
-
return media_id.FUNCTION_CONTROL_OBJECTS
|
|
1782
|
-
elif ln.d == 2:
|
|
1783
|
-
return media_id.COMMUNICATION_PORT_PROTECTION_OBJECTS
|
|
1784
|
-
elif ln.c == 65 and ln.d in __range63:
|
|
1785
|
-
return media_id.UTILITY_TABLE_OBJECTS
|
|
1786
|
-
elif ln.c == 66 and ln.d == 0:
|
|
1787
|
-
return media_id.COMPACT_DATA_OBJECTS
|
|
1788
|
-
elif ln.c == 96:
|
|
1789
|
-
if ln.d == 1:
|
|
1790
|
-
if ln.e in __range10_and_255:
|
|
1791
|
-
return media_id.DEVICE_ID_OBJECTS
|
|
1792
|
-
elif ln.e == 10:
|
|
1793
|
-
return media_id.METERING_POINT_ID_OBJECTS
|
|
1794
|
-
elif ln.d == 2:
|
|
1795
|
-
return media_id.PARAMETER_CHANGES_AND_CALIBRATION_OBJECTS
|
|
1796
|
-
elif ln.d == 3:
|
|
1797
|
-
if ln.e in (0, 1, 2, 3, 4):
|
|
1798
|
-
return media_id.I_O_CONTROL_SIGNAL_OBJECTS
|
|
1799
|
-
elif ln.e == 10:
|
|
1800
|
-
return media_id.DISCONNECT_CONTROL_OBJECTS
|
|
1801
|
-
elif ln.e in range(20, 30):
|
|
1802
|
-
return media_id.ARBITRATOR_OBJECTS
|
|
1803
|
-
elif ln.d == 4 and ln.e in (0, 1, 2, 3, 4):
|
|
1804
|
-
return media_id.STATUS_OF_INTERNAL_CONTROL_SIGNALS_OBJECTS
|
|
1805
|
-
elif ln.d == 5 and ln.e in (0, 1, 2, 3, 4):
|
|
1806
|
-
return media_id.INTERNAL_OPERATING_STATUS_OBJECTS
|
|
1807
|
-
elif ln.d == 6 and ln.e in (0, 1, 2, 3, 4, 5, 6):
|
|
1808
|
-
return media_id.BATTERY_ENTRIES_OBJECTS
|
|
1809
|
-
elif ln.d == 7 and ln.e in range(0, 22):
|
|
1810
|
-
return media_id.POWER_FAILURE_MONITORING_OBJECTS
|
|
1811
|
-
elif ln.d == 8 and ln.e in __range63:
|
|
1812
|
-
return media_id.OPERATING_TIME_OBJECTS
|
|
1813
|
-
elif ln.d == 9 and ln.e in (0, 1, 2):
|
|
1814
|
-
return media_id.ENVIRONMENT_RELATED_PARAMETERS_OBJECTS
|
|
1815
|
-
elif ln.d == 10 and ln.e in range(1, 11):
|
|
1816
|
-
return media_id.STATUS_REGISTER_OBJECTS
|
|
1817
|
-
elif ln.d == 11 and ln.e in range(0, 100):
|
|
1818
|
-
return media_id.EVENT_CODE_OBJECTS
|
|
1819
|
-
elif ln.d == 12 and ln.e in range(0, 7):
|
|
1820
|
-
return media_id.COMMUNICATION_PORT_LOG_PARAMETER_OBJECTS
|
|
1821
|
-
elif ln.d == 13 and ln.e in (0, 1):
|
|
1822
|
-
return media_id.CONSUMER_MESSAGE_OBJECTS
|
|
1823
|
-
elif ln.d == 14 and ln.e in range(0, 16):
|
|
1824
|
-
return media_id.CURRENTLY_ACTIVE_TARIFF_OBJECTS
|
|
1825
|
-
elif ln.d == 15 and ln.e in range(0, 100):
|
|
1826
|
-
return media_id.EVENT_COUNTER_OBJECTS
|
|
1827
|
-
elif ln.d == 16 and ln.e in range(0, 10):
|
|
1828
|
-
return media_id.PROFILE_ENTRY_DIGITAL_SIGNATURE_OBJECTS
|
|
1829
|
-
elif ln.d == 17 and ln.e in range(0, 128):
|
|
1830
|
-
return media_id.PROFILE_ENTRY_COUNTER_OBJECTS
|
|
1831
|
-
elif ln.d == 20:
|
|
1832
|
-
return media_id.METER_TAMPER_EVENT_RELATED_OBJECTS
|
|
1833
|
-
elif ln.d in range(50, 100):
|
|
1834
|
-
return media_id.ABSTRACT_MANUFACTURER_SPECIFIC
|
|
1835
|
-
elif ln.c == 97:
|
|
1836
|
-
if ln.d == 97 and ln.e in __range10_and_255:
|
|
1837
|
-
return media_id.ERROR_REGISTER_OBJECTS
|
|
1838
|
-
elif ln.d == 98 and (ln.e in chain(range(0, 30), (255,))):
|
|
1839
|
-
return media_id.ALARM_REGISTER_FILTER_DESCRIPTOR_OBJECTS
|
|
1840
|
-
elif ln.c == 98:
|
|
1841
|
-
return media_id.GENERAL_LIST_OBJECTS
|
|
1842
|
-
elif ln.c == 99:
|
|
1843
|
-
if ln.d in (1, 2, 12, 13, 14, 15, 16, 17, 18) or (ln.d == 3 and ln.e == 0):
|
|
1844
|
-
return media_id.ABSTRACT_DATA_PROFILE_OBJECTS
|
|
1845
|
-
if ln.d == 98:
|
|
1846
|
-
return media_id.EVENT_LOG_OBJECTS
|
|
1847
|
-
elif ln.c == 127 and ln.d == 0:
|
|
1848
|
-
return media_id.INACTIVE_OBJECTS
|
|
1849
|
-
else:
|
|
1850
|
-
return media_id.ABSTRACT
|
|
1851
|
-
elif ln.a == media_id.ELECTRICITY:
|
|
1852
|
-
if ln.c == 0:
|
|
1853
|
-
if ln.d == 0 and ln.e in __range10_and_255:
|
|
1854
|
-
return media_id.ID_NUMBERS_ELECTRICITY
|
|
1855
|
-
elif ln.d == 1:
|
|
1856
|
-
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_EL
|
|
1857
|
-
elif ln.d in (2, 3, 4, 6, 7, 8, 9, 10):
|
|
1858
|
-
return media_id.OTHER_ELECTRICITY_RELATED_GENERAL_PURPOSE_OBJECTS
|
|
1859
|
-
elif ln.d == 11 and ln.e in (1, 2, 3, 4, 5, 6, 7):
|
|
1860
|
-
return media_id.MEASUREMENT_ALGORITHM
|
|
1861
|
-
elif ln.c in (1, 21, 41, 61):
|
|
1862
|
-
return media_id.ACTIVE_POWER_PLUS
|
|
1863
|
-
elif ln.c in (2, 22, 42, 62):
|
|
1864
|
-
return media_id.ACTIVE_POWER_MINUS
|
|
1865
|
-
elif ln.c in (3, 23, 43, 63):
|
|
1866
|
-
return media_id.REACTIVE_POWER_PLUS
|
|
1867
|
-
elif ln.c in (4, 24, 44, 64):
|
|
1868
|
-
return media_id.REACTIVE_POWER_MINUS
|
|
1869
|
-
elif ln.c in (5, 25, 45, 65):
|
|
1870
|
-
return media_id.REACTIVE_POWER_QI
|
|
1871
|
-
elif ln.c in (6, 26, 46, 66):
|
|
1872
|
-
return media_id.REACTIVE_POWER_QII
|
|
1873
|
-
elif ln.c in (7, 27, 47, 67):
|
|
1874
|
-
return media_id.REACTIVE_POWER_QIII
|
|
1875
|
-
elif ln.c in (8, 28, 48, 68):
|
|
1876
|
-
return media_id.REACTIVE_POWER_QIV
|
|
1877
|
-
elif ln.c in (9, 29, 49, 69):
|
|
1878
|
-
return media_id.APPARENT_POWER_PLUS
|
|
1879
|
-
elif ln.c in (10, 30, 50, 70):
|
|
1880
|
-
return media_id.APPARENT_POWER_MINUS
|
|
1881
|
-
elif ln.c in (11, 31, 51, 71):
|
|
1882
|
-
return media_id.CURRENT
|
|
1883
|
-
elif ln.c in (12, 32, 52, 72):
|
|
1884
|
-
return media_id.VOLTAGE
|
|
1885
|
-
elif ln.c in (13, 33, 53, 73):
|
|
1886
|
-
return media_id.POWER_FACTOR
|
|
1887
|
-
elif ln.c in (14, 34, 54, 74):
|
|
1888
|
-
return media_id.SUPPLY_FREQUENCY
|
|
1889
|
-
elif ln.c in (15, 35, 55, 75):
|
|
1890
|
-
return media_id.ACTIVE_POWER_SUM
|
|
1891
|
-
elif ln.c in (16, 36, 56, 76):
|
|
1892
|
-
return media_id.ACTIVE_POWER_DIFF
|
|
1893
|
-
elif ln.c in (17, 37, 57, 77):
|
|
1894
|
-
return media_id.ACTIVE_POWER_QI
|
|
1895
|
-
elif ln.c in (18, 38, 58, 78):
|
|
1896
|
-
return media_id.ACTIVE_POWER_QII
|
|
1897
|
-
elif ln.c in (19, 39, 59, 79):
|
|
1898
|
-
return media_id.ACTIVE_POWER_QIII
|
|
1899
|
-
elif ln.c in (20, 40, 60, 80):
|
|
1900
|
-
return media_id.ACTIVE_POWER_QIV
|
|
1901
|
-
elif ln.c == 96:
|
|
1902
|
-
if ln.d == 1 and ln.e in __range10_and_255:
|
|
1903
|
-
return media_id.ELECTRICITY_METERING_POINT_ID_OBJECTS
|
|
1904
|
-
elif ln.d == 5 and ln.e in (0, 1, 2, 3, 4, 5):
|
|
1905
|
-
return media_id.ELECTRICITY_RELATED_STATUS_OBJECTS
|
|
1906
|
-
elif ln.d == 10 and ln.e in (0, 1, 2, 3):
|
|
1907
|
-
return media_id.ELECTRICITY_RELATED_STATUS_OBJECTS
|
|
1908
|
-
elif ln.c == 98:
|
|
1909
|
-
return media_id.LIST_OBJECTS_ELECTRICITY
|
|
1910
|
-
elif ln.d in range(31, 46) and ln.f in __range100_and_255:
|
|
1911
|
-
if ln.c in __c1 and ln.e in __range63:
|
|
1912
|
-
return media_id.THRESHOLD_VALUES
|
|
1913
|
-
elif ln.c in __table44 and ln.e in __range120_and_124_127:
|
|
1914
|
-
return media_id.THRESHOLD_VALUES
|
|
1915
|
-
elif ln.f in __range100_and_255:
|
|
1916
|
-
if ln.d in (31, 35, 39, 4, 5, 14, 15, 24, 25):
|
|
1917
|
-
if ln.c in __c1 and ln.e in __range63:
|
|
1918
|
-
return media_id.REGISTER_MONITOR_OBJECTS
|
|
1919
|
-
elif ln.c in __c2 and ln.e in __range120_and_124_127:
|
|
1920
|
-
return media_id.REGISTER_MONITOR_OBJECTS
|
|
1921
|
-
else:
|
|
1922
|
-
return media_id.ELECTRICITY
|
|
1923
|
-
elif ln.a == media_id.HCA:
|
|
1924
|
-
if ln.c == 0:
|
|
1925
|
-
if ln.d == 0 and ln.e in __range10_and_255:
|
|
1926
|
-
return media_id.ID_NUMBERS_HCA
|
|
1927
|
-
elif ln.d == 1 and ln.e in (1, 2, 10, 11):
|
|
1928
|
-
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_HCA
|
|
1929
|
-
elif ln.d == 2 and ln.e in (0, 1, 2, 3):
|
|
1930
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1931
|
-
elif ln.d == 4 and ln.e in (0, 1, 2, 3, 4, 5, 6):
|
|
1932
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1933
|
-
elif ln.d == 5 and ln.e in (10, 11):
|
|
1934
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1935
|
-
elif ln.d == 8 and ln.e in (0, 4, 6):
|
|
1936
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1937
|
-
elif ln.d == 9 and ln.e in (1, 2, 3):
|
|
1938
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1939
|
-
elif ln.c in (1, 2) and ln.e == 0:
|
|
1940
|
-
if ln.d in (0, 6) and ln.f == 255:
|
|
1941
|
-
return media_id.MEASURED_VALUES_HCA_CONSUMPTION
|
|
1942
|
-
elif ln.d in (1, 2, 3, 4, 5) and ln.f in __range100_and_101_125_and_255:
|
|
1943
|
-
return media_id.MEASURED_VALUES_HCA_CONSUMPTION
|
|
1944
|
-
elif ln.c in range(3, 8) and ln.d in (0, 4, 5, 6) and ln.e == 255 and ln.f == 255:
|
|
1945
|
-
return media_id.MEASURED_VALUES_HCA_TEMPERATURE
|
|
1946
|
-
elif ln.c == 97 and ln.d == 97:
|
|
1947
|
-
return media_id.ERROR_REGISTER_OBJECTS_HCA
|
|
1948
|
-
elif ln.c == 98:
|
|
1949
|
-
return media_id.LIST_OBJECTS_HCA
|
|
1950
|
-
elif ln.c == 99 and ln.d == 1:
|
|
1951
|
-
return media_id.DATA_PROFILE_OBJECTS_HCA
|
|
1952
|
-
else:
|
|
1953
|
-
return media_id.HCA
|
|
1954
|
-
elif ln.a == media_id.THERMAL:
|
|
1955
|
-
if ln.c == 0:
|
|
1956
|
-
if ln.d == 0 and ln.e in __range10_and_255:
|
|
1957
|
-
return media_id.ID_NUMBERS_THERMAL
|
|
1958
|
-
elif ln.d == 1 and ln.e in (1, 2, 10, 11):
|
|
1959
|
-
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_THERMAL
|
|
1960
|
-
elif ln.d == 2 and ln.e in chain(range(0, 5), range(10, 14)):
|
|
1961
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1962
|
-
elif ln.d == 4 and ln.e in (1, 2, 3):
|
|
1963
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1964
|
-
elif ln.d == 5 and ln.e in chain(range(1, 10), range(21, 25)):
|
|
1965
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1966
|
-
elif ln.d == 8 and ln.e in chain(range(0, 8), range(11, 15), range(21, 26), range(31, 35)):
|
|
1967
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1968
|
-
elif ln.d == 9 and ln.e in (1, 2, 3):
|
|
1969
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1970
|
-
elif ln.c in range(1, 8):
|
|
1971
|
-
if ln.e in range(10):
|
|
1972
|
-
if ln.d in (0, 1, 2, 3, 7) and ln.f == 255:
|
|
1973
|
-
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1974
|
-
elif ln.d in (3, 8, 9) and ln.f in __range100_and_101_125_and_255:
|
|
1975
|
-
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1976
|
-
elif ln.d in (1, 2, 4, 5, 12, 13, 14, 15) and ln.f in chain(range(100), range(100, 126)):
|
|
1977
|
-
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1978
|
-
elif ln.d == 6 and ln.e == 255 and ln.f == 255:
|
|
1979
|
-
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1980
|
-
elif ln.e in range(10):
|
|
1981
|
-
if ln.f in __range100_and_101_125_and_255:
|
|
1982
|
-
if ln.c in range(1, 8) and ln.d in (5, 15):
|
|
1983
|
-
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1984
|
-
elif ln.c in (8, 9) and ln.d in (1, 4, 5, 12, 13, 14, 15):
|
|
1985
|
-
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1986
|
-
elif ln.c in range(10, 14):
|
|
1987
|
-
if ln.d == 0 and ln.f == 255:
|
|
1988
|
-
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1989
|
-
elif ln.d in (4, 5, 14, 15) and ln.f in chain(range(100), range(101, 126)):
|
|
1990
|
-
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1991
|
-
elif ln.d in (6, 7, 10, 11) and ln.f == 255:
|
|
1992
|
-
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1993
|
-
elif ln.c in range(1, 14) and (ln.d in range(20, 26)) and ln.f == 255:
|
|
1994
|
-
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1995
|
-
elif ln.c == 97 and ln.d == 97 and ln.e in (0, 1, 2):
|
|
1996
|
-
return media_id.ERROR_REGISTER_OBJECTS_THERMAL
|
|
1997
|
-
elif ln.c == 98:
|
|
1998
|
-
return media_id.LIST_OBJECTS_THERMAL
|
|
1999
|
-
elif ln.c == 99 and ln.f == 255:
|
|
2000
|
-
if ln.d in (1, 2) and ln.e in (1, 2, 3):
|
|
2001
|
-
return media_id.DATA_PROFILE_OBJECTS_THERMAL
|
|
2002
|
-
elif ln.d == 3 and ln.e == 1:
|
|
2003
|
-
return media_id.DATA_PROFILE_OBJECTS_THERMAL
|
|
2004
|
-
elif ln.d == 99:
|
|
2005
|
-
return media_id.DATA_PROFILE_OBJECTS_THERMAL
|
|
2006
|
-
else:
|
|
2007
|
-
return media_id.THERMAL
|
|
2008
|
-
elif media_id.GAS == ln.a:
|
|
2009
|
-
if ln.c == 0:
|
|
2010
|
-
if ln.d == 0 and ln.e in __range10_and_255:
|
|
2011
|
-
return media_id.ID_NUMBERS_GAS
|
|
2012
|
-
elif ln.d == 1:
|
|
2013
|
-
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_GAS
|
|
2014
|
-
elif ln.d in (2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15):
|
|
2015
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_GAS
|
|
2016
|
-
elif ln.c == 96 and ln.d == 5 and (ln.e in range(10)):
|
|
2017
|
-
return media_id.INTERNAL_OPERATING_STATUS_OBJECTS_GAS
|
|
2018
|
-
elif ln.c in chain(range(1, 9), range(11, 17), range(21, 27), range(31, 36), range(61, 66)) and ln.e in __range63:
|
|
2019
|
-
if ln.d in (24, 25, 26, 42, 43, 44, 63, 64, 65, 81, 82, 83) and ln.f in chain(range(100), range(101, 127)):
|
|
2020
|
-
return media_id.MEASURED_VALUES_GAS_INDEXES_AND_INDEX_DIFFERENCES
|
|
2021
|
-
elif ln.d in chain(range(6, 24), range(27, 33), range(45, 51), range(66, 72), range(84, 90)) and ln.f == 255:
|
|
2022
|
-
return media_id.MEASURED_VALUES_GAS_INDEXES_AND_INDEX_DIFFERENCES
|
|
2023
|
-
elif ln.d in chain(range(33, 42), range(52, 63), range(72, 81), range(90, 99)) and ln.f in chain(__range100_and_101_125_and_255, (126,)):
|
|
2024
|
-
return media_id.MEASURED_VALUES_GAS_INDEXES_AND_INDEX_DIFFERENCES
|
|
2025
|
-
elif ln.c == 42 and ln.e == 0:
|
|
2026
|
-
if ln.d in chain(0, 1, 2, 13, range(15, 19), range(19, 31), range(35, 51), range(55, 71)) and ln.f == 255:
|
|
2027
|
-
return media_id.MEASURED_VALUES_GAS_FLOW_RATE
|
|
2028
|
-
elif ln.d in chain(range(31, 35), range(51, 55)) and ln.f in chain(__range100_and_101_125_and_255, (126,)):
|
|
2029
|
-
return media_id.MEASURED_VALUES_GAS_FLOW_RATE
|
|
2030
|
-
elif ln.c in chain((41, 42), range(44, 50)) and ln.d in (0, 2, 3, 10, 11, 13, range(15, 92)) and ln.e == 0 and ln.f == 255:
|
|
2031
|
-
return media_id.MEASURED_VALUES_GAS_PROCESS_VALUES
|
|
2032
|
-
elif ln.c in range(51, 56):
|
|
2033
|
-
if ln.d in (0, 2, 3, 10, 11) and ln.e in chain((0, 1), range(11, 29)) and ln.f == 255:
|
|
2034
|
-
return media_id.CONVERSION_RELATED_FACTORS_AND_COEFFICIENTS_GAS
|
|
2035
|
-
elif ln.d == 12 and ln.e in range(20) and ln.f == 255:
|
|
2036
|
-
return media_id.CALCULATION_METHODS_GAS
|
|
2037
|
-
elif ln.c == 70 and ln.f == 255:
|
|
2038
|
-
if ln.d in (8, 9) and ln.e == 0:
|
|
2039
|
-
return media_id.NATURAL_GAS_ANALYSIS
|
|
2040
|
-
elif ln.d in chain(range(10, 21), range(60, 85)) and ln.e in chain((0, 1), range(11, 29)):
|
|
2041
|
-
return media_id.NATURAL_GAS_ANALYSIS
|
|
2042
|
-
elif ln.c == 98:
|
|
2043
|
-
return media_id.LIST_OBJECTS_GAS
|
|
2044
|
-
else:
|
|
2045
|
-
return media_id.GAS
|
|
2046
|
-
elif ln.a == media_id.WATER:
|
|
2047
|
-
if ln.c == 0:
|
|
2048
|
-
if ln.d == 0 and ln.e in __range10_and_255:
|
|
2049
|
-
return media_id.ID_NUMBERS_WATER
|
|
2050
|
-
elif ln.d == 1 and ln.e in (1, 2, 10, 11, 12):
|
|
2051
|
-
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_WATER
|
|
2052
|
-
elif ln.d == 2 and ln.e in (0, 3):
|
|
2053
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2054
|
-
elif ln.d in (5, 7) and ln.e == 1:
|
|
2055
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2056
|
-
elif ln.d == 8 and ln.e in (1, 6):
|
|
2057
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2058
|
-
elif ln.d == 9 and ln.e in (1, 2, 3):
|
|
2059
|
-
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2060
|
-
elif ln.c == 1 and ln.e in range(0, 13):
|
|
2061
|
-
if ln.d in (0, 1, 2, 3, 6) and ln.f == 255:
|
|
2062
|
-
return media_id.MEASURED_VALUES_WATER_CONSUMPTION
|
|
2063
|
-
elif ln.d in range(1, 6) and ln.f in chain(range(100), range(101, 126)):
|
|
2064
|
-
return media_id.MEASURED_VALUES_WATER_CONSUMPTION
|
|
2065
|
-
elif ln.c in (2, 3) and ln.e in range(0, 13):
|
|
2066
|
-
if ln.d in (0, 1, 2, 3, 6) and ln.f == 255:
|
|
2067
|
-
return media_id.MEASURED_VALUES_WATER_MONITORING_VALUES
|
|
2068
|
-
elif ln.d in range(1, 6) and ln.f in chain(range(100), range(101, 126)):
|
|
2069
|
-
return media_id.MEASURED_VALUES_WATER_MONITORING_VALUES
|
|
2070
|
-
elif ln.c == 97 and ln.d == 97:
|
|
2071
|
-
return media_id.ERROR_REGISTER_OBJECTS_WATER
|
|
2072
|
-
elif ln.c == 98:
|
|
2073
|
-
return media_id.LIST_OBJECTS_WATER
|
|
2074
|
-
elif ln.c == 99 and ln.d == 1:
|
|
2075
|
-
return media_id.DATA_PROFILE_OBJECTS_WATER
|
|
2076
|
-
else:
|
|
2077
|
-
return media_id.WATER
|
|
2078
|
-
return media_id.OTHER_MEDIA
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
DLMSObjectContainer: TypeAlias = Collection | list[InterfaceClass] | filter
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
@dataclass
|
|
2085
|
-
class Template:
|
|
2086
|
-
name: str
|
|
2087
|
-
collections: list[Collection]
|
|
2088
|
-
used: UsedAttributes
|
|
2089
|
-
description: str = ""
|
|
2090
|
-
verified: bool = False
|
|
2091
|
-
|
|
2092
|
-
def get_not_contains(self, cols: Iterable[Collection]) -> list[Collection]:
|
|
2093
|
-
"""return of collections not contains in template"""
|
|
2094
|
-
ret = list()
|
|
2095
|
-
for i, col in enumerate(cols):
|
|
2096
|
-
if col not in self.collections:
|
|
2097
|
-
ret.append(col)
|
|
2098
|
-
return ret
|
|
2099
|
-
|
|
2100
|
-
def get_not_valid(self, col: Collection) -> list[Exception]:
|
|
2101
|
-
"""with update col"""
|
|
2102
|
-
attr: cdt.CommonDataType
|
|
2103
|
-
ret = list()
|
|
2104
|
-
use_col = self.collections[0]
|
|
2105
|
-
"""temporary used first collection"""
|
|
2106
|
-
for ln, indexes in self.used.items():
|
|
2107
|
-
try:
|
|
2108
|
-
obj = use_col.get_object(ln)
|
|
2109
|
-
obj_col = col.get_object(ln)
|
|
2110
|
-
except exc.NoObject as e:
|
|
2111
|
-
ret.append(e)
|
|
2112
|
-
print(F"<add_collection> skip obj{self}: {e}")
|
|
2113
|
-
continue
|
|
2114
|
-
for i in indexes:
|
|
2115
|
-
if (attr := obj.get_attr(i)) is not None:
|
|
2116
|
-
try:
|
|
2117
|
-
attr.validate()
|
|
2118
|
-
except ValueError as e:
|
|
2119
|
-
ret.append(ValueError(F"can't decode value {attr} for {ln}:{i}"))
|
|
2120
|
-
else:
|
|
2121
|
-
ret.append(exc.NoObject(F"has't attribute {i} for {ln}"))
|
|
2122
|
-
return ret
|
|
1
|
+
""" The OBIS identification system serves as a basis for the COSEM logical names. The system of naming COSEM objects is defined in the basic
|
|
2
|
+
principles (see Clause 4 EN 62056-62:2007), the identification of real data items is specified in IEC 62056-61. The following clauses define the
|
|
3
|
+
usage of those definitions in the COSEM environment. All codes, which are not explicitly listed, but outside the manufacturer specific range are
|
|
4
|
+
reserved for future use."""
|
|
5
|
+
from struct import pack
|
|
6
|
+
import inspect
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import count, chain
|
|
9
|
+
from functools import reduce, cached_property, lru_cache
|
|
10
|
+
from typing import TypeAlias, Iterator, Type, Self, Callable, Literal, Iterable, Optional, Hashable, Protocol, cast, Annotated
|
|
11
|
+
from semver import Version as SemVer
|
|
12
|
+
from StructResult import result
|
|
13
|
+
from ..types import common_data_types as cdt, cosem_service_types as cst, useful_types as ut
|
|
14
|
+
from ..types.implementations import structs, enums, octet_string
|
|
15
|
+
from . import cosem_interface_class as ic
|
|
16
|
+
from .ln_pattern import LNPattern, LNPatterns
|
|
17
|
+
from .activity_calendar import ActivityCalendar, DayProfileAction
|
|
18
|
+
from .arbitrator import Arbitrator
|
|
19
|
+
from .association_ln import mechanism_id
|
|
20
|
+
from .association_sn.ver0 import AssociationSN as AssociationSNVer0
|
|
21
|
+
from .association_ln.ver0 import AssociationLN as AssociationLNVer0, ObjectListElement
|
|
22
|
+
from .association_ln.ver1 import AssociationLN as AssociationLNVer1
|
|
23
|
+
from .association_ln.ver2 import AssociationLN as AssociationLNVer2
|
|
24
|
+
from .push_setup.ver0 import PushSetup as PushSetupVer0
|
|
25
|
+
from .push_setup.ver1 import PushSetup as PushSetupVer1
|
|
26
|
+
from .push_setup.ver2 import PushSetup as PushSetupVer2
|
|
27
|
+
from .clock import Clock
|
|
28
|
+
from .data import Data
|
|
29
|
+
from .disconnect_control import DisconnectControl
|
|
30
|
+
from .gprs_modem_setup import GPRSModemSetup
|
|
31
|
+
from .gsm_diagnostic.ver0 import GSMDiagnostic as GSMDiagnosticVer0
|
|
32
|
+
from .gsm_diagnostic.ver1 import GSMDiagnostic as GSMDiagnosticVer1
|
|
33
|
+
from .gsm_diagnostic.ver2 import GSMDiagnostic as GSMDiagnosticVer2
|
|
34
|
+
from .iec_hdlc_setup.ver0 import IECHDLCSetup as IECHDLCSetupVer0
|
|
35
|
+
from .iec_hdlc_setup.ver1 import IECHDLCSetup as IECHDLCSetupVer1
|
|
36
|
+
from .image_transfer.ver0 import ImageTransfer
|
|
37
|
+
from .ipv4_setup import IPv4Setup
|
|
38
|
+
from .modem_configuration.ver0 import PSTNModemConfiguration
|
|
39
|
+
from .modem_configuration.ver1 import ModemConfigurationVer1
|
|
40
|
+
from .limiter import Limiter
|
|
41
|
+
from .ntp_setup.ver0 import NTPSetup
|
|
42
|
+
from .profile_generic.ver0 import ProfileGeneric as ProfileGenericVer0
|
|
43
|
+
from .profile_generic.ver1 import ProfileGeneric as ProfileGenericVer1
|
|
44
|
+
from .register import Register
|
|
45
|
+
from .extended_register import ExtendedRegister
|
|
46
|
+
from .demand_register.ver0 import DemandRegister as DemandRegisterVer0
|
|
47
|
+
from .register_activation.ver0 import RegisterActivation
|
|
48
|
+
from .register_monitor import RegisterMonitor
|
|
49
|
+
from .schedule import Schedule
|
|
50
|
+
from .security_setup.ver0 import SecuritySetup as SecuritySetupVer0
|
|
51
|
+
from .security_setup.ver1 import SecuritySetup as SecuritySetupVer1
|
|
52
|
+
from .script_table import ScriptTable
|
|
53
|
+
from .single_action_schedule import SingleActionSchedule
|
|
54
|
+
from .special_days_table import SpecialDaysTable
|
|
55
|
+
from .tcp_udp_setup import TCPUDPSetup
|
|
56
|
+
from .. import exceptions as exc
|
|
57
|
+
from ..relation_to_OBIS import get_name
|
|
58
|
+
from ..cosem_interface_classes import implementations as impl
|
|
59
|
+
from ..cosem_interface_classes.overview import ClassID, CountrySpecificIdentifiers
|
|
60
|
+
from . import obis as o, ln_pattern
|
|
61
|
+
from .. import pdu_enums as pdu
|
|
62
|
+
from ..config_parser import config, get_message
|
|
63
|
+
from ..obis import media_id
|
|
64
|
+
from .parameter import Parameter
|
|
65
|
+
from typing_extensions import deprecated, override
|
|
66
|
+
from ..settings import settings
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CollectionMapError(exc.DLMSException):
|
|
70
|
+
""""""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
LNContaining: TypeAlias = bytes | str | cst.LogicalName | cdt.Structure | ut.CosemObjectInstanceId | ut.CosemAttributeDescriptor | ut.CosemAttributeDescriptorWithSelection \
|
|
74
|
+
| ut.CosemMethodDescriptor
|
|
75
|
+
|
|
76
|
+
AssociationSN: TypeAlias = AssociationSNVer0
|
|
77
|
+
AssociationLN: TypeAlias = AssociationLNVer0 | AssociationLNVer1 | AssociationLNVer2
|
|
78
|
+
ModemConfiguration: TypeAlias = PSTNModemConfiguration | ModemConfigurationVer1
|
|
79
|
+
SecuritySetup: TypeAlias = SecuritySetupVer0 | SecuritySetupVer1
|
|
80
|
+
PushSetup: TypeAlias = PushSetupVer0 | PushSetupVer1 | PushSetupVer2
|
|
81
|
+
ProfileGeneric: TypeAlias = ProfileGenericVer1
|
|
82
|
+
DemandRegister: TypeAlias = DemandRegisterVer0
|
|
83
|
+
IECHDLCSetup: TypeAlias = IECHDLCSetupVer0 | IECHDLCSetupVer1
|
|
84
|
+
GSMDiagnostic: TypeAlias = GSMDiagnosticVer0 | GSMDiagnosticVer1 | GSMDiagnosticVer2
|
|
85
|
+
InterfaceClass: TypeAlias = Data | Register | ExtendedRegister | DemandRegister | ProfileGeneric | Clock | ScriptTable | Schedule | SpecialDaysTable | ActivityCalendar | \
|
|
86
|
+
SingleActionSchedule | AssociationLN | IECHDLCSetup | DisconnectControl | Limiter | ModemConfiguration | PSTNModemConfiguration | ImageTransfer | \
|
|
87
|
+
GPRSModemSetup | GSMDiagnostic | SecuritySetup | TCPUDPSetup | IPv4Setup | Arbitrator | RegisterMonitor | PushSetup | AssociationSN | \
|
|
88
|
+
NTPSetup
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
type AttributeIndex = int
|
|
92
|
+
UsedAttributes: TypeAlias = dict[cst.LogicalName, set[AttributeIndex]]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
ObjectTreeMode: TypeAlias = Literal["", "m", "g", "c", "mc", "cm", "gm", "gc", "cg", "gmc"]
|
|
96
|
+
SortMode: TypeAlias = Literal["l", "n", "c", "cl", "cn"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# todo: make new class ClassMap(for field Collection.spec_map). fields: name, version, dict(current version ClassMap)
|
|
100
|
+
class ClassMap:
|
|
101
|
+
|
|
102
|
+
def __init__(self, *values: type[InterfaceClass]):
|
|
103
|
+
self._values = values
|
|
104
|
+
|
|
105
|
+
def __hash__(self) -> int:
|
|
106
|
+
return hash(it.hash_ for it in self._values)
|
|
107
|
+
|
|
108
|
+
def get(self, ver: int) -> type[InterfaceClass]:
|
|
109
|
+
if (ver := int(ver)) < len(self._values):
|
|
110
|
+
return self._values[ver]
|
|
111
|
+
raise RuntimeError(f"got {ver=}, expected maximal={len(self._values)}")
|
|
112
|
+
|
|
113
|
+
def renew(self, ver: int, cls_: type[InterfaceClass]) -> Self:
|
|
114
|
+
"""return with one change"""
|
|
115
|
+
if ver < (l := len(self._values)):
|
|
116
|
+
tmp = list(self._values)
|
|
117
|
+
tmp.insert(ver, cls_)
|
|
118
|
+
return self.__class__(*tmp)
|
|
119
|
+
if ver == l:
|
|
120
|
+
return self.__class__(*(self._values + (cls_,)))
|
|
121
|
+
raise RuntimeError(f"got {ver=}, expected maximal={len(self._values)}")
|
|
122
|
+
|
|
123
|
+
def __str__(self) -> str:
|
|
124
|
+
return f"{self._values[0].CLASS_ID}[{len(self._values)}]"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
DataMap = ClassMap(Data)
|
|
128
|
+
DataStaticMap = ClassMap(impl.data.DataStatic)
|
|
129
|
+
DataDynamicMap = ClassMap(impl.data.DataDynamic)
|
|
130
|
+
RegisterMap = ClassMap(Register)
|
|
131
|
+
ExtendedRegisterMap = ClassMap(ExtendedRegister)
|
|
132
|
+
DemandRegisterMap = ClassMap(DemandRegisterVer0)
|
|
133
|
+
RegisterActivationMap = ClassMap(RegisterActivation)
|
|
134
|
+
ProfileGenericMap = ClassMap(ProfileGenericVer0, ProfileGenericVer1)
|
|
135
|
+
ClockMap = ClassMap(Clock)
|
|
136
|
+
ScriptTableMap = ClassMap(ScriptTable)
|
|
137
|
+
ScheduleMap = ClassMap(Schedule)
|
|
138
|
+
SpecialDaysTableMap = ClassMap(SpecialDaysTable)
|
|
139
|
+
AssociationSNMap = ClassMap(AssociationSNVer0)
|
|
140
|
+
AssociationLNMap = ClassMap(AssociationLNVer0, AssociationLNVer1, AssociationLNVer2)
|
|
141
|
+
ImageTransferMap = ClassMap(ImageTransfer)
|
|
142
|
+
ActivityCalendarMap = ClassMap(ActivityCalendar)
|
|
143
|
+
RegisterMonitorMap = ClassMap(RegisterMonitor)
|
|
144
|
+
SingleActionScheduleMap = ClassMap(SingleActionSchedule)
|
|
145
|
+
IECHDLCSetupMap = ClassMap(IECHDLCSetupVer0, IECHDLCSetupVer1)
|
|
146
|
+
ModemConfigurationMap = ClassMap(PSTNModemConfiguration, ModemConfigurationVer1)
|
|
147
|
+
TCPUDPSetupMap = ClassMap(TCPUDPSetup)
|
|
148
|
+
IPv4SetupMap = ClassMap(IPv4Setup)
|
|
149
|
+
GPRSModemSetupMap = ClassMap(GPRSModemSetup)
|
|
150
|
+
GSMDiagnosticMap = ClassMap(GSMDiagnosticVer0, GSMDiagnosticVer1, GSMDiagnosticVer2)
|
|
151
|
+
PushSetupMap = ClassMap(PushSetupVer0, PushSetupVer1, PushSetupVer2)
|
|
152
|
+
SecuritySetupMap = ClassMap(SecuritySetupVer0, SecuritySetupVer1)
|
|
153
|
+
ArbitratorMap = ClassMap(Arbitrator)
|
|
154
|
+
DisconnectControlMap = ClassMap(DisconnectControl)
|
|
155
|
+
LimiterMap = ClassMap(Limiter)
|
|
156
|
+
NTPSetupMap = ClassMap(NTPSetup)
|
|
157
|
+
|
|
158
|
+
# implementation ClassMap
|
|
159
|
+
UnsignedDataMap = ClassMap(impl.data.Unsigned)
|
|
160
|
+
|
|
161
|
+
LN_C: TypeAlias = int
|
|
162
|
+
LN_D: TypeAlias = int
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
common_interface_class_map: dict[int, ClassMap] = {
|
|
166
|
+
1: DataMap,
|
|
167
|
+
3: RegisterMap,
|
|
168
|
+
4: ExtendedRegisterMap,
|
|
169
|
+
5: DemandRegisterMap,
|
|
170
|
+
6: RegisterActivationMap,
|
|
171
|
+
7: ProfileGenericMap,
|
|
172
|
+
8: ClockMap,
|
|
173
|
+
9: ScriptTableMap,
|
|
174
|
+
10: ScheduleMap,
|
|
175
|
+
11: SpecialDaysTableMap,
|
|
176
|
+
15: AssociationLNMap,
|
|
177
|
+
18: ImageTransferMap,
|
|
178
|
+
20: ActivityCalendarMap,
|
|
179
|
+
21: RegisterMonitorMap,
|
|
180
|
+
22: SingleActionScheduleMap,
|
|
181
|
+
23: IECHDLCSetupMap,
|
|
182
|
+
27: ModemConfigurationMap,
|
|
183
|
+
41: TCPUDPSetupMap,
|
|
184
|
+
42: IPv4SetupMap,
|
|
185
|
+
45: GPRSModemSetupMap,
|
|
186
|
+
47: GSMDiagnosticMap,
|
|
187
|
+
64: SecuritySetupMap,
|
|
188
|
+
68: ArbitratorMap,
|
|
189
|
+
70: DisconnectControlMap,
|
|
190
|
+
71: LimiterMap,
|
|
191
|
+
100: NTPSetupMap,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_interface_class(class_map: dict[int, ClassMap], c_id: ut.CosemClassId, ver: cdt.Unsigned) -> type[InterfaceClass]:
|
|
196
|
+
"""new version <get_type_from_class>"""
|
|
197
|
+
ret = class_map.get(int(c_id), None)
|
|
198
|
+
if isinstance(ret, ClassMap):
|
|
199
|
+
return ret.get(int(ver))
|
|
200
|
+
else:
|
|
201
|
+
if int(c_id) not in common_interface_class_map.keys():
|
|
202
|
+
raise CollectionMapError(F"unknown {c_id=}")
|
|
203
|
+
else:
|
|
204
|
+
raise CollectionMapError(F"got {c_id=}, expected {', '.join(map(str, class_map.keys()))}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
_CUMULATIVE = (1, 2, 11, 12, 21, 22)
|
|
208
|
+
_MAX_MIN_VALUES = (3, 6, 13, 16, 26, 51, 52, 53, 54)
|
|
209
|
+
_CURRENT_AND_LAST_AVERAGE_VALUES = (0, 4, 5, 14, 15, 24, 25, 27, 28, 49, 50, 55, 56)
|
|
210
|
+
_INSTANTANEOUS_VALUES = (7, 39, 41, 42)
|
|
211
|
+
_TIME_INTEGRAL_VALUES = (8, 9, 10, 17, 18, 19, 20, 29, 30, 58)
|
|
212
|
+
_OCCURRENCE_COUNTER = 40
|
|
213
|
+
_CONTRACTED_VALUES = (46,)
|
|
214
|
+
_UNDER_OVER_LIMIT_THRESHOLDS = (31, 35, 43, 44)
|
|
215
|
+
_UNDER_OVER_LIMIT_OCCURRENCE_COUNTERS = (32, 36)
|
|
216
|
+
_UNDER_OVER_LIMIT_DURATIONS = (33, 37)
|
|
217
|
+
_UNDER_OVER_LIMIT_MAGNITUDES = (34, 38)
|
|
218
|
+
_NOT_PROCESSING_OF_MEASUREMENT_VALUES = tuple(set(range(256)).difference((0, 93, 94, 96, 97, 98, 99))) # BlueBook DLMS UA 1000-1 Ed.14 7.5.2.1 Table 66
|
|
219
|
+
_RU_CHANGE_LIMIT_LEVEL = 134
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@lru_cache()
|
|
223
|
+
def _create_map(maps: ClassMap | tuple[ClassMap]) -> dict[int, ClassMap]:
|
|
224
|
+
if isinstance(maps, tuple):
|
|
225
|
+
return {int(map_.get(0).CLASS_ID): map_ for map_ in maps}
|
|
226
|
+
else:
|
|
227
|
+
return {int(maps.get(0).CLASS_ID): maps}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
A: TypeAlias = int
|
|
231
|
+
B: TypeAlias = int
|
|
232
|
+
C: TypeAlias = int
|
|
233
|
+
D: TypeAlias = int
|
|
234
|
+
E: TypeAlias = int
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class Xgroup(Protocol):
|
|
238
|
+
fmt: str
|
|
239
|
+
|
|
240
|
+
def get_key(self) -> Iterator[bytes]:
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class SimpleGroup(Xgroup, Protocol):
|
|
245
|
+
def get_key(self) -> Iterator[bytes]:
|
|
246
|
+
yield pack(self.fmt, *self) # type: ignore[misc]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class ACgroup(SimpleGroup, tuple[A, C]):
|
|
250
|
+
"""Grouping of OBIS codes by group A (media) and group C (attribute).
|
|
251
|
+
Creates a composite key for objects sharing the same media type and attribute.
|
|
252
|
+
"""
|
|
253
|
+
fmt = ">BB"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class ACDgroup_(Xgroup, Protocol):
|
|
257
|
+
"""Grouping of OBIS codes by groups A (media), C (attribute), and D (data) with mask support.
|
|
258
|
+
Supports exact values or masks for attribute (C) and data (D) groups.
|
|
259
|
+
Allows flexible grouping by media type with variable attribute and data patterns.
|
|
260
|
+
"""
|
|
261
|
+
fmt: str = ">BBB"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ACDgroup(SimpleGroup, ACDgroup_, tuple[A, C, D]):
|
|
265
|
+
...
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class ACCDgroup(ACDgroup_, tuple[A, tuple[C, ...], D]):
|
|
269
|
+
def get_key(self) -> Iterator[bytes]:
|
|
270
|
+
a, _, d = self
|
|
271
|
+
return (pack(self.fmt, a, c, d) for c in self[1])
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class ACDDgroup(ACDgroup_, tuple[A, C, tuple[D, ...]]):
|
|
275
|
+
def get_key(self) -> Iterator[bytes]:
|
|
276
|
+
a, c, _ = self
|
|
277
|
+
return (pack(self.fmt, a, c, d) for d in self[2])
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ACCDDgroup(ACDgroup_, tuple[A, tuple[C, ...], tuple[D, ...]]):
|
|
281
|
+
"""Grouping of OBIS codes by groups A (media), C (attribute), and D (data) with mask support.
|
|
282
|
+
Supports exact values or masks for attribute (C) and data (D) groups.
|
|
283
|
+
Allows flexible grouping by media type with variable attribute and data patterns.
|
|
284
|
+
"""
|
|
285
|
+
def get_key(self) -> Iterator[bytes]:
|
|
286
|
+
a = self[0]
|
|
287
|
+
return (pack(self.fmt, a, c, d) for c in self[1] for d in self[2])
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class ACDEgroup_(Xgroup, Protocol):
|
|
291
|
+
"""Grouping of OBIS codes by groups A (media), C (attribute), D (data), and E (data version) with mask support.
|
|
292
|
+
Groups by fixed media and attribute with support for data and data version masks.
|
|
293
|
+
Enables precise control over data grouping with optional version filtering.
|
|
294
|
+
"""
|
|
295
|
+
fmt: str = ">BBBB"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class ACDEgroup(SimpleGroup, ACDEgroup_, tuple[A, C, D, E]):
|
|
299
|
+
...
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ACDDEgroup(ACDEgroup_, tuple[A, C, tuple[D, ...], E]):
|
|
303
|
+
def get_key(self) -> Iterator[bytes]:
|
|
304
|
+
a, c, _, e = self
|
|
305
|
+
return (pack(self.fmt, a, c, d, e) for d in self[2])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class ACDEEgroup(ACDEgroup_, tuple[A, C, D, tuple[E, ...]]):
|
|
310
|
+
def get_key(self) -> Iterator[bytes]:
|
|
311
|
+
a, c, d, _ = self
|
|
312
|
+
return (pack(self.fmt, a, c, d, e) for e in self[3])
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ACDDEEgroup(ACDEgroup_, tuple[A, C, tuple[D, ...], tuple[E, ...]]):
|
|
316
|
+
def get_key(self) -> Iterator[bytes]:
|
|
317
|
+
a, c, _, _ = self
|
|
318
|
+
return (pack(self.fmt, a, c, d, e) for d in self[2] for e in self[3])
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ABCDEgroup_(Xgroup, Protocol):
|
|
322
|
+
"""Grouping of OBIS codes by all OBIS groups A-E with data version mask support.
|
|
323
|
+
|
|
324
|
+
Comprehensive grouping by:
|
|
325
|
+
- A: media
|
|
326
|
+
- B: channel/interface
|
|
327
|
+
- C: attribute
|
|
328
|
+
- D: data
|
|
329
|
+
- E: data version (with mask support)
|
|
330
|
+
|
|
331
|
+
Provides complete OBIS code grouping with flexible version matching.
|
|
332
|
+
"""
|
|
333
|
+
fmt: str = ">BBBBB"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ABCDEgroup(SimpleGroup, ABCDEgroup_, tuple[A, B, C, D, E]):
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ABCCDEgroup(ABCDEgroup_, tuple[A, B, tuple[C, ...], D, E]):
|
|
341
|
+
def get_key(self) -> Iterator[bytes]:
|
|
342
|
+
a, b, _, d, e = self
|
|
343
|
+
return (pack(self.fmt, a, b, c, d, e) for c in self[2])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ABCDEEgroup(ABCDEgroup_, tuple[A, B, C, D, tuple[E, ...]]):
|
|
347
|
+
def get_key(self) -> Iterator[bytes]:
|
|
348
|
+
a, b, c, d, _ = self
|
|
349
|
+
return (pack(self.fmt, a, b, c, d, e) for e in self[4])
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
type PossibleGroup = ACgroup | ACDgroup_ | ACDEgroup | ABCDEgroup
|
|
353
|
+
FUNC_MAP: TypeAlias = dict[bytes, dict[int, ClassMap]]
|
|
354
|
+
"""ln.BCDE | ln.CDE | ln.CD | ln.C: {class_id: {version: CosemInterfaceClass}}"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
func_maps: dict[str, FUNC_MAP] = {}
|
|
358
|
+
type PossibleClassMap = tuple[ClassMap, ...] | ClassMap
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_func_map(for_create_map: dict[PossibleGroup, PossibleClassMap]) -> FUNC_MAP:
|
|
362
|
+
ret: FUNC_MAP = {}
|
|
363
|
+
for g in for_create_map:
|
|
364
|
+
for k in g.get_key():
|
|
365
|
+
ret[k] = _create_map(for_create_map[g])
|
|
366
|
+
return ret
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
__func_map_for_create: dict[PossibleGroup, PossibleClassMap] = {
|
|
370
|
+
# abstract
|
|
371
|
+
ACDgroup((0, 0, 1)): DataMap,
|
|
372
|
+
ACDgroup((0, 0, 2)): DataMap,
|
|
373
|
+
ACDEgroup((0, 0, 2, 1)): ClassMap(impl.data.ActiveFirmwareId),
|
|
374
|
+
ACDgroup((0, 0, 9)): DataMap,
|
|
375
|
+
ACDgroup((0, 1, 0)): ClockMap,
|
|
376
|
+
ACDgroup((0, 1, 1)): DataMap,
|
|
377
|
+
ACDgroup((0, 1, 2)): DataMap,
|
|
378
|
+
ACDgroup((0, 1, 3)): DataMap,
|
|
379
|
+
ACDgroup((0, 1, 4)): DataMap,
|
|
380
|
+
ACDgroup((0, 1, 5)): DataMap,
|
|
381
|
+
ACDgroup((0, 1, 6)): DataMap,
|
|
382
|
+
ACDEgroup((0, 2, 0, 0)): ModemConfigurationMap,
|
|
383
|
+
#
|
|
384
|
+
ACDEEgroup((0, 10, 0, (0, 1, 125)+tuple(range(100, 112)))): ScriptTableMap,
|
|
385
|
+
ACDgroup((0, 11, 0)): SpecialDaysTableMap,
|
|
386
|
+
ACDgroup((0, 12, 0)): ScheduleMap,
|
|
387
|
+
ACDgroup((0, 13, 0)): ActivityCalendarMap,
|
|
388
|
+
ACDgroup((0, 14, 0)): RegisterActivationMap,
|
|
389
|
+
ACDEEgroup((0, 15, 0, tuple(range(0, 8)))): SingleActionScheduleMap,
|
|
390
|
+
ACDgroup((0, 16, 0)): RegisterMonitorMap,
|
|
391
|
+
ACDEEgroup((0, 16, 1, tuple(range(0, 10)))): RegisterMonitorMap,
|
|
392
|
+
#
|
|
393
|
+
ACDgroup((0, 17, 0)): LimiterMap,
|
|
394
|
+
#
|
|
395
|
+
ACDDEEgroup((0, 19, tuple(range(50, 60)), (1, 2))): DataMap,
|
|
396
|
+
#
|
|
397
|
+
ACDgroup((0, 21, 0)): (DataMap, ProfileGenericMap),
|
|
398
|
+
ACDEgroup((0, 22, 0, 0)): IECHDLCSetupMap,
|
|
399
|
+
#
|
|
400
|
+
ACDEgroup((0, 23, 2, 0)): DataMap,
|
|
401
|
+
ACDEEgroup((0, 23, 3, tuple(range(0, 10)))): (DataMap, ProfileGenericMap),
|
|
402
|
+
ACDEEgroup((0, 23, 3, tuple(range(10, 256)))): DataMap,
|
|
403
|
+
#
|
|
404
|
+
ACDgroup((0, 24, 2)): ExtendedRegisterMap,
|
|
405
|
+
ACDgroup((0, 24, 3)): ProfileGenericMap,
|
|
406
|
+
ACDEgroup((0, 24, 4, 0)): DisconnectControlMap,
|
|
407
|
+
ACDEgroup((0, 24, 5, 0)): ProfileGenericMap,
|
|
408
|
+
#
|
|
409
|
+
ACDEgroup((0, 25, 0, 0)): TCPUDPSetupMap,
|
|
410
|
+
ACDEgroup((0, 25, 1, 0)): IPv4SetupMap,
|
|
411
|
+
#
|
|
412
|
+
ACDEgroup((0, 25, 4, 0)): GPRSModemSetupMap,
|
|
413
|
+
#
|
|
414
|
+
ACDEgroup((0, 25, 6, 0)): GSMDiagnosticMap,
|
|
415
|
+
#
|
|
416
|
+
ACDEgroup((0, 25, 9, 0)): PushSetupMap,
|
|
417
|
+
ACDEgroup((0, 25, 10, 0)): NTPSetupMap,
|
|
418
|
+
#
|
|
419
|
+
ABCDEEgroup((0, 0, 40, 0, tuple(range(8)))): (AssociationSNMap, AssociationLNMap), # todo: now limit by 8 association, solve it
|
|
420
|
+
#
|
|
421
|
+
ABCDEgroup((0, 0, 42, 0, 0)): ClassMap(impl.data.LDN),
|
|
422
|
+
ABCDEEgroup((0, 0, 43, 0, tuple(range(256)))): SecuritySetupMap,
|
|
423
|
+
ACDgroup((0, 43, 1)): DataMap,
|
|
424
|
+
#
|
|
425
|
+
ABCDEEgroup((0, 0, 44, 0, tuple(range(256)))): ImageTransferMap,
|
|
426
|
+
#
|
|
427
|
+
ACDEEgroup((0, 96, 1, tuple(range(0, 11)))): ClassMap(impl.data.DLMSDeviceIDObject),
|
|
428
|
+
ACDEgroup((0, 96, 1, 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
429
|
+
ACDgroup((0, 96, 2)): DataDynamicMap,
|
|
430
|
+
ACDEEgroup((0, 96, 3, tuple(range(0, 4)))): DataMap, # todo: add StatusMapping
|
|
431
|
+
ACDEgroup((0, 96, 3, 10)): DisconnectControlMap,
|
|
432
|
+
ACDEEgroup((0, 96, 3, tuple(range(20, 29)))): ArbitratorMap,
|
|
433
|
+
ACDDEgroup((0, 96, (4, 5), 0)): (DataMap, ProfileGenericMap), # todo: add RegisterTable, StatusMapping
|
|
434
|
+
ACDDEEgroup((0, 96, (4, 5), (1, 2, 3, 4))): DataMap, # todo: add StatusMapping
|
|
435
|
+
ACDEEgroup((0, 96, 6, tuple(range(0, 7)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
436
|
+
ACDEEgroup((0, 96, 7, tuple(range(0, 22)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
437
|
+
ACDEEgroup((0, 96, 8, tuple(range(0, 64)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
438
|
+
ACDEEgroup((0, 96, 9, (0, 1, 2))): (RegisterMap, ExtendedRegisterMap),
|
|
439
|
+
ACDEEgroup((0, 96, 10, tuple(range(1, 10)))): DataMap, # todo: add StatusMapping
|
|
440
|
+
ACDEEgroup((0, 96, 11, tuple(range(100)))): (DataDynamicMap, RegisterMap, ExtendedRegisterMap),
|
|
441
|
+
ACDEEgroup((0, 96, 12, (0, 1, 2, 3, 5, 6))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
442
|
+
ACDEgroup((0, 96, 12, 4)): ClassMap(impl.data.CommunicationPortParameter),
|
|
443
|
+
ACDEEgroup((0, 96, 13, (0, 1))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
444
|
+
ACDEEgroup((0, 96, 14, tuple(range(16)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
445
|
+
ACDEEgroup((0, 96, 15, tuple(range(100)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
446
|
+
ACDEEgroup((0, 96, 16, tuple(range(10)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
447
|
+
ACDEEgroup((0, 96, 17, tuple(range(128)))): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
448
|
+
ACDgroup((0, 96, 20)): (DataMap, RegisterMap, ExtendedRegisterMap),
|
|
449
|
+
ACDEEgroup((0, 97, 97, tuple(range(10)))): DataMap,
|
|
450
|
+
ACDDEgroup((0, 97, (97, 98), 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
451
|
+
ACDEEgroup((0, 97, 98, tuple(range(10))+tuple(range(10, 30)))): DataMap,
|
|
452
|
+
ACgroup((0, 98)): ProfileGenericMap,
|
|
453
|
+
ACDgroup((0, 99, 98)): ProfileGenericMap,
|
|
454
|
+
# electricity
|
|
455
|
+
ACDEEgroup((1, 0, 0, tuple(range(10)))): DataMap,
|
|
456
|
+
ACDEgroup((1, 0, 0, 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
457
|
+
ACDgroup((1, 0, 1)): DataMap,
|
|
458
|
+
ACDgroup((1, 0, 2)): DataStaticMap,
|
|
459
|
+
ACDDgroup((1, 0, (3, 4, 7, 8, 9))): (DataStaticMap, RegisterMap, ExtendedRegisterMap),
|
|
460
|
+
ACDDgroup((1, 0, (6, 10))): (RegisterMap, ExtendedRegisterMap),
|
|
461
|
+
ACDEEgroup((1, 0, 11, tuple(range(1, 8)))): DataMap,
|
|
462
|
+
ACDEEgroup((1, 96, 1, tuple(range(10)))): DataMap,
|
|
463
|
+
ACDEgroup((1, 96, 1, 255)): ProfileGenericMap, # todo: add RegisterTable
|
|
464
|
+
ACDEEgroup((1, 96, 5, (0, 1, 2, 3, 4, 5))): DataMap, # todo: add StatusMapping
|
|
465
|
+
ACDEEgroup((1, 96, 10, (0, 1, 2, 3))): DataMap, # todo: add StatusMapping
|
|
466
|
+
ACgroup((1, 98)): ProfileGenericMap,
|
|
467
|
+
ACDDgroup((1, 99, (1, 2, 11, 12, 97, 98, 99))): ProfileGenericMap,
|
|
468
|
+
ACDDEgroup((1, 99, (3, 13, 14), 0)): ProfileGenericMap,
|
|
469
|
+
ACDEEgroup((1, 99, 10, (1, 2, 3))): ProfileGenericMap,
|
|
470
|
+
ACCDgroup((1, _CUMULATIVE, _RU_CHANGE_LIMIT_LEVEL)): RegisterMap,
|
|
471
|
+
ACCDDgroup((
|
|
472
|
+
1,
|
|
473
|
+
_NOT_PROCESSING_OF_MEASUREMENT_VALUES,
|
|
474
|
+
tuple(chain(
|
|
475
|
+
_CUMULATIVE,
|
|
476
|
+
_TIME_INTEGRAL_VALUES,
|
|
477
|
+
_CONTRACTED_VALUES,
|
|
478
|
+
_UNDER_OVER_LIMIT_THRESHOLDS,
|
|
479
|
+
_UNDER_OVER_LIMIT_OCCURRENCE_COUNTERS,
|
|
480
|
+
_UNDER_OVER_LIMIT_DURATIONS,
|
|
481
|
+
_UNDER_OVER_LIMIT_MAGNITUDES
|
|
482
|
+
)))): (RegisterMap, ExtendedRegisterMap),
|
|
483
|
+
ACCDDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, _INSTANTANEOUS_VALUES)): RegisterMap,
|
|
484
|
+
ACCDDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, _MAX_MIN_VALUES)): (RegisterMap, ExtendedRegisterMap, ProfileGenericMap),
|
|
485
|
+
ACCDDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, _CURRENT_AND_LAST_AVERAGE_VALUES)): (RegisterMap, DemandRegisterMap),
|
|
486
|
+
ACCDgroup((1, _NOT_PROCESSING_OF_MEASUREMENT_VALUES, 40)): (DataMap, RegisterMap),
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func_maps["DLMS_6"] = get_func_map(__func_map_for_create)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# SPODES3 Update
|
|
493
|
+
__func_map_for_create.update({
|
|
494
|
+
ACDgroup((0, 21, 0)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3DisplayReadout),
|
|
495
|
+
ACDEEgroup((0, 96, 1, (0, 2, 4, 5, 8, 9, 10))): ClassMap(impl.data.SPODES3IDNotSpecific),
|
|
496
|
+
ACDEgroup((0, 96, 1, 6)): ClassMap(impl.data.SPODES3SPODESVersion),
|
|
497
|
+
ACDEEgroup((0, 96, 2, (1, 2, 3, 5, 6, 7, 11, 12))): ClassMap(impl.data.AnyDateTime),
|
|
498
|
+
ACDEgroup((0, 96, 3, 20)): ClassMap(impl.arbitrator.SPODES3Arbitrator),
|
|
499
|
+
ACDEgroup((0, 96, 4, 3)): ClassMap(impl.data.SPODES3LoadLocker),
|
|
500
|
+
ACDEgroup((0, 96, 5, 1)): ClassMap(impl.data.SPODES3PowerQuality2Event),
|
|
501
|
+
ACDEgroup((0, 96, 5, 4)): ClassMap(impl.data.SPODES3PowerQuality1Event),
|
|
502
|
+
ACDEgroup((0, 96, 5, 132)): ClassMap(impl.data.Unsigned), # TODO: make according with СПОДЭС3 13.9. Контроль чередования фаз
|
|
503
|
+
ACDEgroup((0, 96, 11, 0)): ClassMap(impl.data.SPODES3VoltageEvent),
|
|
504
|
+
ACDEgroup((0, 96, 11, 1)): ClassMap(impl.data.SPODES3CurrentEvent),
|
|
505
|
+
ACDEgroup((0, 96, 11, 2)): ClassMap(impl.data.SPODES3CommutationEvent),
|
|
506
|
+
ACDEgroup((0, 96, 11, 3)): ClassMap(impl.data.SPODES3ProgrammingEvent),
|
|
507
|
+
ACDEgroup((0, 96, 11, 4)): ClassMap(impl.data.SPODES3ExternalEvent),
|
|
508
|
+
ACDEgroup((0, 96, 11, 5)): ClassMap(impl.data.SPODES3CommunicationEvent),
|
|
509
|
+
ACDEgroup((0, 96, 11, 6)): ClassMap(impl.data.SPODES3AccessEvent),
|
|
510
|
+
ACDEgroup((0, 96, 11, 7)): ClassMap(impl.data.SPODES3SelfDiagnosticEvent),
|
|
511
|
+
ACDEgroup((0, 96, 11, 8)): ClassMap(impl.data.SPODES3ReactivePowerEvent),
|
|
512
|
+
ABCDEgroup((0, 0, 96, 51, 0)): ClassMap(impl.data.OpeningBody),
|
|
513
|
+
ABCDEgroup((0, 0, 96, 51, 1)): ClassMap(impl.data.OpeningCover),
|
|
514
|
+
ABCDEgroup((0, 0, 96, 51, 3)): ClassMap(impl.data.ExposureToMagnet),
|
|
515
|
+
ABCDEgroup((0, 0, 96, 51, 4)): ClassMap(impl.data.ExposureToHSField),
|
|
516
|
+
ABCDEgroup((0, 0, 96, 51, 5)): ClassMap(impl.data.SealStatus),
|
|
517
|
+
ABCDEEgroup((0, 0, 96, 51, (6, 7))): UnsignedDataMap,
|
|
518
|
+
ABCDEEgroup((0, 0, 96, 51, (8, 9))): ClassMap(impl.data.OctetStringDateTime),
|
|
519
|
+
ABCDEEgroup((0, 0, 97, 98, (0, 10, 20))): ClassMap(impl.data.SPODES3Alarm1),
|
|
520
|
+
ABCDEEgroup((0, 0, 97, 98, (1, 11))): ClassMap(impl.data.SPODES3ControlAlarm1),
|
|
521
|
+
# electricity
|
|
522
|
+
ACDEEgroup((1, 0, 8, (4, 5))): ClassMap(impl.data.SPODES3MeasurementPeriod),
|
|
523
|
+
ACDgroup((1, 98, 1)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3MonthProfile),
|
|
524
|
+
ACDgroup((1, 98, 2)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3DailyProfile),
|
|
525
|
+
ACDDgroup((1, 99, (1, 2))): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3LoadProfile),
|
|
526
|
+
ABCDEgroup((1, 0, 131, 35, 0)): RegisterMap,
|
|
527
|
+
ABCDEgroup((1, 0, 133, 35, 0)): RegisterMap,
|
|
528
|
+
ABCDEgroup((1, 0, 147, 133, 0)): RegisterMap,
|
|
529
|
+
ABCDEgroup((1, 0, 148, 136, 0)): RegisterMap,
|
|
530
|
+
ACDEgroup((1, 94, 7, 0)): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3CurrentProfile),
|
|
531
|
+
ACDEEgroup((1, 94, 7, (1, 2, 3, 4, 5, 6))): ProfileGenericMap.renew(1, impl.profile_generic.SPODES3ScalesProfile), # Todo: RU. Scaler-profile With 1 entry and more
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
func_maps["SPODES_3"] = get_func_map(__func_map_for_create)
|
|
535
|
+
|
|
536
|
+
# KPZ Update
|
|
537
|
+
__func_map_for_create.update({
|
|
538
|
+
ACDEgroup((0, 96, 11, 4)): ClassMap(impl.data.KPZSPODES3ExternalEvent),
|
|
539
|
+
ABCDEEgroup((0, 0, 97, 98, (0, 10, 20))): ClassMap(impl.data.KPZAlarm1),
|
|
540
|
+
ABCDEgroup((0, 128, 25, 6, 0)): ClassMap(impl.data.DataStatic),
|
|
541
|
+
ABCDEEgroup((0, 128, 96, 2, (0, 1, 2))): ClassMap(impl.data.KPZAFEOffsets),
|
|
542
|
+
ABCDEgroup((0, 128, 96, 13, 1)): ClassMap(impl.data.ITEBitMap),
|
|
543
|
+
ABCDEgroup((0, 128, 154, 0, 0)): ClassMap(impl.data.KPZGSMPingIP),
|
|
544
|
+
ACDEEgroup((0, 0, 128, (100, 101, 102, 103, 150, 151, 152, 170))): DataMap,
|
|
545
|
+
ABCCDEgroup((128, 0, tuple(range(20)), 0, 0)): RegisterMap
|
|
546
|
+
})
|
|
547
|
+
func_maps["KPZ"] = get_func_map(__func_map_for_create)
|
|
548
|
+
# KPZ1 with bag in log event profiles
|
|
549
|
+
__func_map_for_create.update({
|
|
550
|
+
ACDEgroup((0, 96, 11, 0)): ClassMap(impl.data.KPZ1SPODES3VoltageEvent),
|
|
551
|
+
ACDEgroup((0, 96, 11, 1)): ClassMap(impl.data.KPZ1SPODES3CurrentEvent),
|
|
552
|
+
ACDEgroup((0, 96, 11, 2)): ClassMap(impl.data.KPZ1SPODES3CommutationEvent),
|
|
553
|
+
ACDEgroup((0, 96, 11, 3)): ClassMap(impl.data.KPZ1SPODES3ProgrammingEvent),
|
|
554
|
+
ACDEgroup((0, 96, 11, 4)): ClassMap(impl.data.KPZ1SPODES3ExternalEvent),
|
|
555
|
+
ACDEgroup((0, 96, 11, 5)): ClassMap(impl.data.KPZ1SPODES3CommunicationEvent),
|
|
556
|
+
ACDEgroup((0, 96, 11, 6)): ClassMap(impl.data.KPZ1SPODES3AccessEvent),
|
|
557
|
+
ACDEgroup((0, 96, 11, 7)): ClassMap(impl.data.KPZ1SPODES3SelfDiagnosticEvent),
|
|
558
|
+
ACDEgroup((0, 96, 11, 8)): ClassMap(impl.data.KPZ1SPODES3ReactivePowerEvent),
|
|
559
|
+
})
|
|
560
|
+
func_maps["KPZ1"] = get_func_map(__func_map_for_create)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def get_type(class_id: ut.CosemClassId,
|
|
564
|
+
version: cdt.Unsigned,
|
|
565
|
+
ln: cst.LogicalName,
|
|
566
|
+
func_map: FUNC_MAP) -> Type[InterfaceClass]:
|
|
567
|
+
"""use DLMS UA 1000-1 Ed. 14 Table 54"""
|
|
568
|
+
c_m: Optional[dict[int, ClassMap]]
|
|
569
|
+
if (
|
|
570
|
+
(128 <= ln.b <= 199)
|
|
571
|
+
or (128 <= ln.c <= 199)
|
|
572
|
+
or ln.c == 240
|
|
573
|
+
or (128 <= ln.d <= 254)
|
|
574
|
+
or (128 <= ln.e <= 254)
|
|
575
|
+
or (128 <= ln.f <= 254)
|
|
576
|
+
):
|
|
577
|
+
# try search in ABCDE group for manufacture object before in CDE
|
|
578
|
+
c_m = func_map.get(ln.contents[:5], common_interface_class_map)
|
|
579
|
+
else:
|
|
580
|
+
# try search in ABCDE group
|
|
581
|
+
c_m = func_map.get(ln.contents[:5], None)
|
|
582
|
+
if c_m is None:
|
|
583
|
+
# try search in A-CDE group
|
|
584
|
+
c_m = func_map.get(ln.contents[:1]+ln.contents[2:5], None)
|
|
585
|
+
if c_m is None:
|
|
586
|
+
# try search in A-CD group
|
|
587
|
+
c_m = func_map.get(ln.contents[:1]+ln.contents[2:4], None)
|
|
588
|
+
if c_m is None:
|
|
589
|
+
# try search in A-C group
|
|
590
|
+
c_m = func_map.get(ln.contents[:1]+ln.contents[3:4], common_interface_class_map)
|
|
591
|
+
return get_interface_class(class_map=c_m,
|
|
592
|
+
c_id=class_id,
|
|
593
|
+
ver=version)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@lru_cache(20000)
|
|
597
|
+
def get_unit(class_id: ClassID, elements: Iterable[int]) -> int | None:
|
|
598
|
+
match class_id, *elements:
|
|
599
|
+
case (ClassID.LIMITER, 6 | 7) | (ClassID.LIMITER, 8, 2) | (ClassID.DEMAND_REGISTER, 8) | (ClassID.PROFILE_GENERIC, 4) | (ClassID.PUSH_SETUP, 5) |\
|
|
600
|
+
(ClassID.PUSH_SETUP, 7, _) | (ClassID.PUSH_SETUP, 12, 1) | (ClassID.COMMUNICATION_PORT_PROTECTION, 4 | 6) | (ClassID.CHARGE, 8) | (ClassID.IEC_HDLC_SETUP, 8) \
|
|
601
|
+
| (ClassID.AUTO_CONNECT, 4):
|
|
602
|
+
return 7 # second
|
|
603
|
+
case ClassID.CLOCK, 3 | 7:
|
|
604
|
+
return 6 # min
|
|
605
|
+
case (ClassID.IEC_HDLC_SETUP, 7) | (ClassID.MODEM_CONFIGURATION, 3, 2):
|
|
606
|
+
return 7 # millisecond
|
|
607
|
+
case ClassID.S_FSK_PHY_MAC_SET_UP, 7, _:
|
|
608
|
+
return 44 # HZ
|
|
609
|
+
case (ClassID.S_FSK_PHY_MAC_SET_UP, 4 | 5):
|
|
610
|
+
return 72 # Db
|
|
611
|
+
case ClassID.S_FSK_PHY_MAC_SET_UP, 6:
|
|
612
|
+
return 71 # DbmicroV
|
|
613
|
+
case _:
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
type ObjFilteredKey = tuple[ClassID | LNPattern | LNPatterns | Channel, ...]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def get_filtered(objects: Iterable[InterfaceClass],
|
|
621
|
+
keys: ObjFilteredKey) -> list[InterfaceClass]:
|
|
622
|
+
c_ids: list[ut.CosemClassId] = list()
|
|
623
|
+
patterns: list[LNPattern] = list()
|
|
624
|
+
ch: Optional[Channel] = None
|
|
625
|
+
for k in keys:
|
|
626
|
+
if isinstance(k, ut.CosemClassId):
|
|
627
|
+
c_ids.append(k)
|
|
628
|
+
elif isinstance(k, LNPattern):
|
|
629
|
+
patterns.append(k)
|
|
630
|
+
elif isinstance(k, LNPatterns):
|
|
631
|
+
patterns.extend(k)
|
|
632
|
+
elif isinstance(k, Channel):
|
|
633
|
+
ch = k
|
|
634
|
+
new_list = list()
|
|
635
|
+
for obj in objects:
|
|
636
|
+
if obj.CLASS_ID in c_ids:
|
|
637
|
+
pass
|
|
638
|
+
elif obj.logical_name in patterns:
|
|
639
|
+
pass
|
|
640
|
+
else:
|
|
641
|
+
continue
|
|
642
|
+
if ch and not ch.is_approve(obj.logical_name.b):
|
|
643
|
+
continue
|
|
644
|
+
new_list.append(obj)
|
|
645
|
+
return new_list
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@dataclass(unsafe_hash=True, frozen=True)
|
|
649
|
+
class ParameterValue:
|
|
650
|
+
par: bytes
|
|
651
|
+
value: bytes
|
|
652
|
+
|
|
653
|
+
def __str__(self) -> str:
|
|
654
|
+
return F"{'.'.join(map(str, self.par[:6]))}:{self.par[6]} - {cdt.get_instance_and_pdu_from_value(self.value)[0].__repr__()}"
|
|
655
|
+
|
|
656
|
+
def __bytes__(self) -> bytes:
|
|
657
|
+
"""par + 0x00 + value""" # todo: 00 in future other parameters
|
|
658
|
+
return self.par + b'\x00' + self.value
|
|
659
|
+
|
|
660
|
+
@classmethod
|
|
661
|
+
def parse(cls, value: bytes) -> Self:
|
|
662
|
+
if value[7] != 0:
|
|
663
|
+
raise exc.ITEApplication(F"wrong {value!r} for {cls.__name__}")
|
|
664
|
+
return cls(
|
|
665
|
+
par=value[:7],
|
|
666
|
+
value=value[8:]
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
@dataclass(frozen=True, unsafe_hash=True)
|
|
671
|
+
class ID:
|
|
672
|
+
man: bytes
|
|
673
|
+
f_id: ParameterValue
|
|
674
|
+
f_ver: ParameterValue
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class Collection:
|
|
678
|
+
__id: ID
|
|
679
|
+
__dlms_ver: int
|
|
680
|
+
__country: Optional[CountrySpecificIdentifiers]
|
|
681
|
+
__country_ver: Optional[ParameterValue]
|
|
682
|
+
__objs: dict[o.OBIS, InterfaceClass]
|
|
683
|
+
__const_objs: int
|
|
684
|
+
spec_map: str
|
|
685
|
+
|
|
686
|
+
def __init__(self,
|
|
687
|
+
id_: ID,
|
|
688
|
+
dlms_ver: int = 6,
|
|
689
|
+
country: Optional[CountrySpecificIdentifiers] = None,
|
|
690
|
+
cntr_ver: Optional[ParameterValue] = None):
|
|
691
|
+
self.__id = id_
|
|
692
|
+
self.__dlms_ver = dlms_ver
|
|
693
|
+
self.__country = country
|
|
694
|
+
self.__country_ver = cntr_ver
|
|
695
|
+
"""country version specification"""
|
|
696
|
+
self.spec_map = "DLMS_6"
|
|
697
|
+
self.__objs = {}
|
|
698
|
+
""" all DLMS objects container with obis key """
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def id(self) -> ID:
|
|
702
|
+
return self.__id
|
|
703
|
+
|
|
704
|
+
def set_id(self, value: ID) -> None:
|
|
705
|
+
if not self.__id:
|
|
706
|
+
self.__id = value
|
|
707
|
+
else:
|
|
708
|
+
if value != self.__id:
|
|
709
|
+
raise ValueError(F"got id: {value}, expected {self.__id}")
|
|
710
|
+
else:
|
|
711
|
+
"""success validation"""
|
|
712
|
+
|
|
713
|
+
def __eq__(self, other: object) -> bool:
|
|
714
|
+
if isinstance(other, Collection):
|
|
715
|
+
return hash(self) == hash(other)
|
|
716
|
+
raise NotImplementedError
|
|
717
|
+
|
|
718
|
+
def __hash__(self) -> int:
|
|
719
|
+
return hash(self.id)
|
|
720
|
+
|
|
721
|
+
def copy_object_values(self, target: ic.COSEMInterfaceClasses, association_id: int = 3) -> None:
|
|
722
|
+
"""copy object values according by association. only needed"""
|
|
723
|
+
source = self.__objs.get(target.logical_name.contents, None)
|
|
724
|
+
if source is None:
|
|
725
|
+
raise RuntimeError(f"can't find {target}")
|
|
726
|
+
for i, value in source.get_index_with_attributes():
|
|
727
|
+
el = target.get_attr_element(i)
|
|
728
|
+
if (
|
|
729
|
+
i == 1
|
|
730
|
+
or value is None
|
|
731
|
+
or (
|
|
732
|
+
not isinstance(el.DATA_TYPE, ut.CHOICE)
|
|
733
|
+
and el.classifier == ic.Classifier.DYNAMIC
|
|
734
|
+
)
|
|
735
|
+
):
|
|
736
|
+
continue
|
|
737
|
+
else:
|
|
738
|
+
if (
|
|
739
|
+
target.get_attr_element(i).classifier == ic.Classifier.STATIC
|
|
740
|
+
and not self.is_writable(
|
|
741
|
+
ln=target.logical_name,
|
|
742
|
+
index=i,
|
|
743
|
+
association_id=association_id)
|
|
744
|
+
):
|
|
745
|
+
# todo: may be callbacks inits remove?
|
|
746
|
+
if cb_func := target._cbs_attr_before_init.get(i, None):
|
|
747
|
+
cb_func(value) # Todo: 'a' as 'new_value' in set_attr are can use?
|
|
748
|
+
target._cbs_attr_before_init.pop(i)
|
|
749
|
+
target.set_attr(i, value.encoding)
|
|
750
|
+
if cb_func := target._cbs_attr_post_init.get(i, None):
|
|
751
|
+
cb_func()
|
|
752
|
+
target._cbs_attr_post_init.pop(i)
|
|
753
|
+
else:
|
|
754
|
+
if isinstance(arr := target.get_attr(i), cdt.Array):
|
|
755
|
+
arr.set_type(value.TYPE)
|
|
756
|
+
try:
|
|
757
|
+
target.set_attr(
|
|
758
|
+
index=i,
|
|
759
|
+
value=value.encoding,
|
|
760
|
+
data_type=source.get_attr(i).__class__)
|
|
761
|
+
except exc.EmptyObj as e:
|
|
762
|
+
print(F"can't copy {target} attr={i}, skipped. {e}")
|
|
763
|
+
|
|
764
|
+
def copy(self) -> result.Simple["Collection"]:
|
|
765
|
+
"""copy collection with value by Association"""
|
|
766
|
+
res = result.Simple(Collection(
|
|
767
|
+
id_=self.id,
|
|
768
|
+
dlms_ver=self.__dlms_ver,
|
|
769
|
+
country=self.__country,
|
|
770
|
+
cntr_ver=self.__country_ver)
|
|
771
|
+
)
|
|
772
|
+
res.value.spec_map = self.spec_map
|
|
773
|
+
max_ass: AssociationLN | None = None
|
|
774
|
+
"""more full association""" # todo: move to collection(from_xml)
|
|
775
|
+
for obj in self.__objs.values():
|
|
776
|
+
new_obj: InterfaceClass = obj.__class__(obj.logical_name)
|
|
777
|
+
res.value.__objs[obj.logical_name.contents] = new_obj
|
|
778
|
+
new_obj.collection = res.value # todo: remove in future
|
|
779
|
+
if obj.CLASS_ID == ClassID.ASSOCIATION_LN:
|
|
780
|
+
obj: AssociationLN
|
|
781
|
+
if obj.object_list is not None:
|
|
782
|
+
if (
|
|
783
|
+
max_ass is None
|
|
784
|
+
or len(max_ass.object_list) < len(obj.object_list)
|
|
785
|
+
):
|
|
786
|
+
max_ass = obj
|
|
787
|
+
if max_ass is None:
|
|
788
|
+
raise exc.NoObject("collection not has the AssociationLN object")
|
|
789
|
+
ln_for_set = max_ass.get_lns()
|
|
790
|
+
ass_id: int = max_ass.logical_name.e
|
|
791
|
+
while len(ln_for_set) != 0:
|
|
792
|
+
ln = ln_for_set.pop(0)
|
|
793
|
+
try:
|
|
794
|
+
new_obj = res.value.get_object(ln.contents)
|
|
795
|
+
self.copy_object_values(
|
|
796
|
+
target=new_obj,
|
|
797
|
+
association_id=ass_id)
|
|
798
|
+
except Exception as e:
|
|
799
|
+
res.append_e(e, "copy object value")
|
|
800
|
+
return res
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def dlms_ver(self) -> int:
|
|
804
|
+
return self.__dlms_ver
|
|
805
|
+
|
|
806
|
+
def set_dlms_ver(self, value: int) -> None:
|
|
807
|
+
if not self.__dlms_ver:
|
|
808
|
+
self.__dlms_ver = value
|
|
809
|
+
else:
|
|
810
|
+
if value != self.__dlms_ver:
|
|
811
|
+
raise ValueError(F"got dlms_version: {value}, expected {self.__dlms_ver}")
|
|
812
|
+
else:
|
|
813
|
+
"""success validation"""
|
|
814
|
+
|
|
815
|
+
@property
|
|
816
|
+
def country(self) -> Optional[CountrySpecificIdentifiers]:
|
|
817
|
+
return self.__country
|
|
818
|
+
|
|
819
|
+
def set_country(self, value: CountrySpecificIdentifiers):
|
|
820
|
+
if not self.__country:
|
|
821
|
+
self.__country = value
|
|
822
|
+
else:
|
|
823
|
+
if value != self.__country:
|
|
824
|
+
raise ValueError(F"got country: {value}, expected {self.__country}")
|
|
825
|
+
else:
|
|
826
|
+
"""success validation"""
|
|
827
|
+
|
|
828
|
+
@property
|
|
829
|
+
def country_ver(self):
|
|
830
|
+
return self.__country_ver
|
|
831
|
+
|
|
832
|
+
def set_country_ver(self, value: ParameterValue):
|
|
833
|
+
"""country version specification"""
|
|
834
|
+
if not self.__country_ver:
|
|
835
|
+
self.__country_ver = value
|
|
836
|
+
else:
|
|
837
|
+
if value != self.__country_ver:
|
|
838
|
+
raise ValueError(F"got country version: {value}, expected {self.__country_ver}")
|
|
839
|
+
else:
|
|
840
|
+
"""success validation"""
|
|
841
|
+
|
|
842
|
+
def __str__(self) -> str:
|
|
843
|
+
return F"[{len(self.__objs)}] DLMS version: {self.__dlms_ver}, country: {self.__country}, country specific version: {self.__country_ver}, " \
|
|
844
|
+
F"id: {self.id}, uses specification: {self.spec_map}"
|
|
845
|
+
|
|
846
|
+
def __iter__(self) -> Iterator[ic.COSEMInterfaceClasses]:
|
|
847
|
+
return iter(self.__objs.values())
|
|
848
|
+
|
|
849
|
+
def get_spec(self) -> str:
|
|
850
|
+
"""return functional map to specification by identification fields"""
|
|
851
|
+
match self.id.man:
|
|
852
|
+
case b"KPZ":
|
|
853
|
+
return "KPZ"
|
|
854
|
+
case b"101" | b"102" | b"103" | b"104":
|
|
855
|
+
return "KPZ1"
|
|
856
|
+
case _:
|
|
857
|
+
if self.country == CountrySpecificIdentifiers.RUSSIA:
|
|
858
|
+
if (
|
|
859
|
+
self.country_ver.par == b'\x00\x00`\x01\x06\xff\x02' and
|
|
860
|
+
SemVer.parse(bytes(cdt.OctetString(self.country_ver.value)), True) == SemVer(3, 0)
|
|
861
|
+
):
|
|
862
|
+
return "SPODES_3"
|
|
863
|
+
if self.dlms_ver == 6:
|
|
864
|
+
return "DLMS_6"
|
|
865
|
+
else:
|
|
866
|
+
raise exc.DLMSException("unknown specification")
|
|
867
|
+
|
|
868
|
+
def add_if_missing(self, class_id: ut.CosemClassId,
|
|
869
|
+
version: cdt.Unsigned | None,
|
|
870
|
+
logical_name: cst.LogicalName) -> InterfaceClass:
|
|
871
|
+
""" like as add method with check for missing """
|
|
872
|
+
if (res := self.__objs.get(logical_name.contents)) is None:
|
|
873
|
+
return self.add(
|
|
874
|
+
class_id=class_id,
|
|
875
|
+
version=version,
|
|
876
|
+
logical_name=logical_name)
|
|
877
|
+
else:
|
|
878
|
+
return res
|
|
879
|
+
|
|
880
|
+
def __getitem__(self, item: o.OBIS) -> InterfaceClass:
|
|
881
|
+
return self.__objs[item]
|
|
882
|
+
|
|
883
|
+
def get(self, obis: o.OBIS) -> InterfaceClass | None:
|
|
884
|
+
""" get object, return None if it absence """
|
|
885
|
+
return self.__objs.get(obis, None)
|
|
886
|
+
|
|
887
|
+
def par2obj(self, par: Parameter) -> result.SimpleOrError[InterfaceClass]:
|
|
888
|
+
"""return: DLMSObject"""
|
|
889
|
+
return self.obis2obj(par.obis)
|
|
890
|
+
|
|
891
|
+
def par2data(self, par: Parameter) -> result.Option[cdt.CommonDataType] | result.Error:
|
|
892
|
+
""":return CDT by Parameter, return None if data wasn't setting"""
|
|
893
|
+
if isinstance((res1 := self.par2obj(par)), result.Error):
|
|
894
|
+
return res1
|
|
895
|
+
res = result.Option(res1.value.get_attr(par.i))
|
|
896
|
+
if res.value is None:
|
|
897
|
+
return res
|
|
898
|
+
for el in par.elements():
|
|
899
|
+
res.value = res.value[el]
|
|
900
|
+
return res
|
|
901
|
+
|
|
902
|
+
def values(self) -> tuple[InterfaceClass]:
|
|
903
|
+
return tuple(self.__objs.values())
|
|
904
|
+
|
|
905
|
+
def __len__(self):
|
|
906
|
+
return len(self.__objs)
|
|
907
|
+
|
|
908
|
+
def add(self, class_id: ut.CosemClassId,
|
|
909
|
+
version: cdt.Unsigned | None,
|
|
910
|
+
logical_name: cst.LogicalName) -> InterfaceClass:
|
|
911
|
+
""" append new DLMS object to collection with return it"""
|
|
912
|
+
try:
|
|
913
|
+
new_object = get_type(
|
|
914
|
+
class_id=class_id,
|
|
915
|
+
version=self.find_version(class_id) if version is None else version,
|
|
916
|
+
ln=logical_name,
|
|
917
|
+
func_map=func_maps[self.spec_map])(logical_name)
|
|
918
|
+
new_object.collection = self
|
|
919
|
+
self.__objs[o.OBIS(logical_name.contents)] = new_object
|
|
920
|
+
print(F'Create {new_object}')
|
|
921
|
+
return new_object
|
|
922
|
+
except ValueError as e:
|
|
923
|
+
raise ValueError(F"error getting DLMS object instance with {class_id=} {version=} {logical_name=}: {e}")
|
|
924
|
+
except StopIteration as e:
|
|
925
|
+
raise ValueError(F"not find class version for {class_id=} {logical_name=}: {e}")
|
|
926
|
+
|
|
927
|
+
def get_class_version(self) -> dict[ut.CosemClassId, cdt.Unsigned]:
|
|
928
|
+
"""use for check all class version by unique"""
|
|
929
|
+
ret: dict[ut.CosemClassId, cdt.Unsigned] = dict()
|
|
930
|
+
for obj in self.__objs.values():
|
|
931
|
+
if ver := ret.get(obj.CLASS_ID):
|
|
932
|
+
if obj.VERSION != ver:
|
|
933
|
+
raise ValueError(F"for {obj.CLASS_ID=} exist several versions: {obj.VERSION}, {ver} in one collection")
|
|
934
|
+
else:
|
|
935
|
+
ret[obj.CLASS_ID] = obj.VERSION
|
|
936
|
+
return ret
|
|
937
|
+
|
|
938
|
+
def get_n_phases(self) -> int:
|
|
939
|
+
"""search objects with L2 phase"""
|
|
940
|
+
ret: int | None = None
|
|
941
|
+
for obj in filter(lambda obj: obj.logical_name.a == 1, self):
|
|
942
|
+
if 41 <= obj.logical_name.c <= 60:
|
|
943
|
+
return 3
|
|
944
|
+
ret = 1
|
|
945
|
+
if ret is None:
|
|
946
|
+
raise exc.NoObject("no one electricity object was find")
|
|
947
|
+
else:
|
|
948
|
+
return ret
|
|
949
|
+
|
|
950
|
+
def has_sap(self, value: enums.ClientSAP) -> bool:
|
|
951
|
+
try:
|
|
952
|
+
self.sap2association(value)
|
|
953
|
+
return True
|
|
954
|
+
except exc.NoObject:
|
|
955
|
+
return False
|
|
956
|
+
|
|
957
|
+
@lru_cache(maxsize=100) # amount of all ClassID
|
|
958
|
+
def find_version(self, class_id: ut.CosemClassId) -> cdt.Unsigned:
|
|
959
|
+
"""use for add new object from profile_generic if absence in object list"""
|
|
960
|
+
return next(filter(lambda obj: obj.CLASS_ID == class_id, self.__objs.values())).VERSION
|
|
961
|
+
|
|
962
|
+
def is_in_collection(self, value: LNContaining) -> bool:
|
|
963
|
+
obis = lnContents2obis(value)
|
|
964
|
+
return False if self.__objs.get(obis) is None else True
|
|
965
|
+
|
|
966
|
+
def get_object(self, value: LNContaining) -> InterfaceClass:
|
|
967
|
+
""" return object from obis<string> or raise exception if it absence """
|
|
968
|
+
return self.obis2obj(lnContents2obis(value)).unwrap()
|
|
969
|
+
|
|
970
|
+
@deprecated("use <par2rep>")
|
|
971
|
+
def get_report(self,
|
|
972
|
+
obj: ic.COSEMInterfaceClasses,
|
|
973
|
+
par: bytes,
|
|
974
|
+
a_val: cdt.CommonDataType | None
|
|
975
|
+
) -> cdt.Report:
|
|
976
|
+
"""par: attribute_index, par1, par2, ..."""
|
|
977
|
+
rep = cdt.Report(str(a_val))
|
|
978
|
+
try:
|
|
979
|
+
if a_val is None:
|
|
980
|
+
rep.msg = settings.report.empty
|
|
981
|
+
rep.log = cdt.EMPTY_VAL
|
|
982
|
+
elif isinstance(a_val, cdt.ReportMixin):
|
|
983
|
+
rep = a_val.get_report()
|
|
984
|
+
elif isinstance(a_val, DayProfileAction):
|
|
985
|
+
rep.msg = F"{get_message("$rate$")}-{a_val.script_selector}: {a_val.start_time}"
|
|
986
|
+
script_obj = self.get_object(a_val.script_logical_name)
|
|
987
|
+
for script in script_obj.scripts:
|
|
988
|
+
if script.script_identifier == a_val.script_selector:
|
|
989
|
+
break
|
|
990
|
+
else:
|
|
991
|
+
rep.log = cdt.Log(logging.ERROR, F"absent script with ID: {a_val.script_selector}")
|
|
992
|
+
else:
|
|
993
|
+
if unit := get_unit(obj.CLASS_ID, par):
|
|
994
|
+
rep.unit = cdt.Unit(unit).get_name()
|
|
995
|
+
else:
|
|
996
|
+
if s_u := self.get_scaler_unit(obj, par):
|
|
997
|
+
rep.msg = (settings.report.scaler_format).format(int(a_val) * 10 ** int(s_u.scaler))
|
|
998
|
+
rep.unit = s_u.unit.get_name()
|
|
999
|
+
else:
|
|
1000
|
+
match obj.CLASS_ID, *par:
|
|
1001
|
+
case (ClassID.PROFILE_GENERIC, 3, _) | (ClassID.PROFILE_GENERIC, 6):
|
|
1002
|
+
a_val: structs.CaptureObjectDefinition
|
|
1003
|
+
obj = self.get_object(a_val.logical_name)
|
|
1004
|
+
rep.msg = F"{get_name(a_val.logical_name)}.{obj.get_attr_element(int(a_val.attribute_index))}"
|
|
1005
|
+
case _:
|
|
1006
|
+
pass
|
|
1007
|
+
rep.log = cdt.Log(logging.INFO)
|
|
1008
|
+
except Exception as e:
|
|
1009
|
+
rep.log = cdt.Log(logging.ERROR, e)
|
|
1010
|
+
finally:
|
|
1011
|
+
return rep
|
|
1012
|
+
|
|
1013
|
+
def par2rep(self, par: Parameter, data: Optional[cdt.CommonDataType]) -> cdt.Report:
|
|
1014
|
+
rep = cdt.Report(str(data))
|
|
1015
|
+
try:
|
|
1016
|
+
if data is None:
|
|
1017
|
+
rep.msg = settings.report.empty
|
|
1018
|
+
rep.log = cdt.EMPTY_VAL
|
|
1019
|
+
elif isinstance(data, cdt.ReportMixin):
|
|
1020
|
+
rep = data.get_report()
|
|
1021
|
+
elif isinstance(data, DayProfileAction):
|
|
1022
|
+
rep.msg = F"{get_message("$rate$")}-{data.script_selector}: {data.start_time}"
|
|
1023
|
+
script_obj = self.get_object(data.script_logical_name)
|
|
1024
|
+
for script in script_obj.scripts:
|
|
1025
|
+
if script.script_identifier == data.script_selector:
|
|
1026
|
+
break
|
|
1027
|
+
else:
|
|
1028
|
+
rep.log = cdt.Log(logging.ERROR, F"absent script with ID: {data.script_selector}")
|
|
1029
|
+
else:
|
|
1030
|
+
obj = self.par2obj(par).unwrap()
|
|
1031
|
+
elements = tuple(par.elements())
|
|
1032
|
+
if unit := get_unit(obj.CLASS_ID, elements):
|
|
1033
|
+
rep.unit = cdt.Unit(unit).get_name()
|
|
1034
|
+
else:
|
|
1035
|
+
if s_u := self.par2su(par):
|
|
1036
|
+
rep.msg = (settings.report.scaler_format).format(int(data) * 10 ** int(s_u.scaler))
|
|
1037
|
+
rep.unit = s_u.unit.get_name()
|
|
1038
|
+
else:
|
|
1039
|
+
match obj.CLASS_ID, *elements:
|
|
1040
|
+
case (ClassID.PROFILE_GENERIC, 3, _) | (ClassID.PROFILE_GENERIC, 6):
|
|
1041
|
+
data: structs.CaptureObjectDefinition
|
|
1042
|
+
obj = self.get_object(data.logical_name)
|
|
1043
|
+
rep.msg = F"{get_name(data.logical_name)}.{obj.get_attr_element(int(data.attribute_index))}"
|
|
1044
|
+
case _:
|
|
1045
|
+
pass
|
|
1046
|
+
rep.log = cdt.Log(logging.INFO)
|
|
1047
|
+
except Exception as e:
|
|
1048
|
+
rep.log = cdt.Log(logging.ERROR, e)
|
|
1049
|
+
finally:
|
|
1050
|
+
return rep
|
|
1051
|
+
|
|
1052
|
+
@deprecated("use par2su")
|
|
1053
|
+
def get_scaler_unit(self,
|
|
1054
|
+
obj: ic.COSEMInterfaceClasses,
|
|
1055
|
+
par: bytes
|
|
1056
|
+
) -> cdt.ScalUnitType | None:
|
|
1057
|
+
match obj.CLASS_ID, *par:
|
|
1058
|
+
case (ClassID.REGISTER | ClassID.EXT_REGISTER, 2) | (ClassID.DEMAND_REGISTER, 2 | 3):
|
|
1059
|
+
obj: Register | DemandRegister
|
|
1060
|
+
if (s_u := obj.scaler_unit) is None:
|
|
1061
|
+
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1062
|
+
else:
|
|
1063
|
+
if (s := cdt.get_unit_scaler(s_u.unit.contents)) != 0:
|
|
1064
|
+
s_u = s_u.copy()
|
|
1065
|
+
s_u.scaler.set(int(s_u.scaler)-s)
|
|
1066
|
+
return s_u
|
|
1067
|
+
case ClassID.LIMITER, 3 | 4 | 5:
|
|
1068
|
+
obj: Limiter
|
|
1069
|
+
if m_v := obj.monitored_value:
|
|
1070
|
+
return self.get_scaler_unit( # recursion 1 level
|
|
1071
|
+
obj=self.get_object(m_v.logical_name),
|
|
1072
|
+
par=m_v.attribute_index.contents)
|
|
1073
|
+
else:
|
|
1074
|
+
raise ic.EmptyAttribute(obj.logical_name, 2)
|
|
1075
|
+
case ClassID.REGISTER_MONITOR, 2, _:
|
|
1076
|
+
obj: RegisterMonitor
|
|
1077
|
+
if (m_v := obj.monitored_value) is None:
|
|
1078
|
+
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1079
|
+
else:
|
|
1080
|
+
return self.get_scaler_unit( # recursion 1 level
|
|
1081
|
+
obj=self.get_object(m_v.logical_name),
|
|
1082
|
+
par=m_v.attribute_index.contents
|
|
1083
|
+
)
|
|
1084
|
+
case _:
|
|
1085
|
+
return None
|
|
1086
|
+
|
|
1087
|
+
@lru_cache(20000)
|
|
1088
|
+
def par2su(self, par: Parameter) -> Optional[cdt.ScalUnitType]:
|
|
1089
|
+
"""convert Parameter -> Optional[ScalerUnit],
|
|
1090
|
+
raise: NoObject, EmptyAttribute"""
|
|
1091
|
+
match (obj := self.par2obj(par).unwrap()).CLASS_ID, par.i:
|
|
1092
|
+
case (exc.NoObject, _):
|
|
1093
|
+
raise obj
|
|
1094
|
+
case (ClassID.REGISTER | ClassID.EXT_REGISTER, 2) | (ClassID.DEMAND_REGISTER, 2 | 3):
|
|
1095
|
+
obj: Register | DemandRegister
|
|
1096
|
+
if (s_u := obj.scaler_unit) is None:
|
|
1097
|
+
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1098
|
+
else:
|
|
1099
|
+
if (s := cdt.get_unit_scaler(s_u.unit.contents)) != 0:
|
|
1100
|
+
s_u = s_u.copy()
|
|
1101
|
+
s_u.scaler.set(int(s_u.scaler)-s)
|
|
1102
|
+
return s_u
|
|
1103
|
+
case ClassID.LIMITER, 3 | 4 | 5:
|
|
1104
|
+
obj: Limiter
|
|
1105
|
+
if m_v := obj.monitored_value:
|
|
1106
|
+
return self.par2su(Parameter(m_v.logical_name.contents).set_i(int(m_v.attribute_index))) # recursion 1 level
|
|
1107
|
+
else:
|
|
1108
|
+
raise ic.EmptyAttribute(obj.logical_name, 2)
|
|
1109
|
+
case ClassID.REGISTER_MONITOR, 2, _:
|
|
1110
|
+
obj: RegisterMonitor
|
|
1111
|
+
if (m_v := obj.monitored_value) is None:
|
|
1112
|
+
raise ic.EmptyAttribute(obj.logical_name, 3)
|
|
1113
|
+
else:
|
|
1114
|
+
return self.par2su(Parameter(m_v.logical_name.contents).set_i(int(m_v.attribute_index))) # recursion 1 level
|
|
1115
|
+
case _:
|
|
1116
|
+
return None
|
|
1117
|
+
|
|
1118
|
+
def par2float(self, par: Parameter) -> float:
|
|
1119
|
+
"""try convert CDT value according with Parameter to build-in float"""
|
|
1120
|
+
data = self.par2data(par).unwrap()
|
|
1121
|
+
if hasattr(data, "__int__"):
|
|
1122
|
+
value = float(int(data))
|
|
1123
|
+
elif hasattr(data, "__float__"):
|
|
1124
|
+
value = float(data)
|
|
1125
|
+
else:
|
|
1126
|
+
raise TypeError("can't convert Parameter data to int or float")
|
|
1127
|
+
if (su := self.par2su(par)):
|
|
1128
|
+
value *= 10 ** int(su.scaler)
|
|
1129
|
+
return value
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def filter_by_ass(self, ass_id: int) -> list[InterfaceClass]:
|
|
1133
|
+
"""return only association objects"""
|
|
1134
|
+
ret = list()
|
|
1135
|
+
for olt in self.getASSOCIATION(ass_id).object_list:
|
|
1136
|
+
ret.append(self.par2obj(Parameter(olt.logical_name.contents)).unwrap())
|
|
1137
|
+
return ret
|
|
1138
|
+
|
|
1139
|
+
def sap2objects(self, value: enums.ClientSAP) -> result.List[ic.COSEMInterfaceClasses]:
|
|
1140
|
+
res = result.List()
|
|
1141
|
+
for par in self.sap2association(value).iter_pars():
|
|
1142
|
+
if isinstance(res1 := self.par2obj(par), result.Error):
|
|
1143
|
+
res1.append_err(res.err)
|
|
1144
|
+
else:
|
|
1145
|
+
res.append(res1)
|
|
1146
|
+
return res
|
|
1147
|
+
|
|
1148
|
+
def iter_classID_objects(self,
|
|
1149
|
+
class_id: ut.CosemClassId) -> Iterator[InterfaceClass]:
|
|
1150
|
+
return (obj for obj in self.__objs.values() if obj.CLASS_ID == class_id)
|
|
1151
|
+
|
|
1152
|
+
def LNPattern2objects(self,
|
|
1153
|
+
pat: LNPattern) -> list[InterfaceClass]:
|
|
1154
|
+
ret = []
|
|
1155
|
+
for obj in self.__objs.values():
|
|
1156
|
+
if obj.logical_name in pat:
|
|
1157
|
+
ret.append(obj)
|
|
1158
|
+
return ret
|
|
1159
|
+
|
|
1160
|
+
def get_first(self, values: list[str | bytes | cst.LogicalName]) -> InterfaceClass:
|
|
1161
|
+
""" return first object from it exist in collection from value"""
|
|
1162
|
+
for val in values:
|
|
1163
|
+
if self.is_in_collection(val):
|
|
1164
|
+
return self.get_object(val)
|
|
1165
|
+
else:
|
|
1166
|
+
"""search next"""
|
|
1167
|
+
else:
|
|
1168
|
+
raise exc.NoObject(F"not found at least one DLMS Objects from collection with {values=}")
|
|
1169
|
+
|
|
1170
|
+
@deprecated("use <iter_classID_objects>")
|
|
1171
|
+
def get_objects_by_class_id(self, value: ut.CosemClassId) -> list[InterfaceClass]:
|
|
1172
|
+
return list(filter(lambda obj: obj.CLASS_ID == value, self.__objs.values()))
|
|
1173
|
+
|
|
1174
|
+
def get_writable_attr(self) -> UsedAttributes:
|
|
1175
|
+
"""return all writable {obj.ln: {attribute_index}}"""
|
|
1176
|
+
ret: UsedAttributes = dict()
|
|
1177
|
+
for ass in self.iter_classID_objects(ClassID.ASSOCIATION_LN):
|
|
1178
|
+
if (
|
|
1179
|
+
ass.logical_name.e == 0
|
|
1180
|
+
or ass.object_list is None
|
|
1181
|
+
):
|
|
1182
|
+
continue
|
|
1183
|
+
else:
|
|
1184
|
+
for list_type in ass.object_list:
|
|
1185
|
+
for attr_access in list_type.access_rights.attribute_access:
|
|
1186
|
+
if attr_access.access_mode.is_writable():
|
|
1187
|
+
if ret.get(list_type.logical_name, None) is None:
|
|
1188
|
+
ret[list_type.logical_name] = set()
|
|
1189
|
+
ret[list_type.logical_name].add(int(attr_access.attribute_id))
|
|
1190
|
+
return ret
|
|
1191
|
+
|
|
1192
|
+
def get_profile_s_u(self,
|
|
1193
|
+
obj: ProfileGeneric,
|
|
1194
|
+
mask: set[int] = None
|
|
1195
|
+
) -> list[cdt.ScalUnitType | None]:
|
|
1196
|
+
"""return container of scaler_units if possible, mask: position number in capture_objects"""
|
|
1197
|
+
res: list[cdt.ScalUnitType | None] = list()
|
|
1198
|
+
for i, obj_def in enumerate(obj.capture_objects):
|
|
1199
|
+
obj_def: structs.CaptureObjectDefinition
|
|
1200
|
+
if mask and i not in mask:
|
|
1201
|
+
continue
|
|
1202
|
+
s_u = None
|
|
1203
|
+
try:
|
|
1204
|
+
s_u = self.get_scaler_unit(
|
|
1205
|
+
obj=self.get_object(obj_def.logical_name),
|
|
1206
|
+
par=bytes([int(obj_def.attribute_index)]))
|
|
1207
|
+
except ic.EmptyAttribute as e:
|
|
1208
|
+
print(F"Can't fill Scaler and Unit for {get_name(obj_def.logical_name)}: {e}")
|
|
1209
|
+
finally:
|
|
1210
|
+
res.append(s_u)
|
|
1211
|
+
return res
|
|
1212
|
+
|
|
1213
|
+
def copy_obj_attr_values_from(self, other: InterfaceClass) -> bool:
|
|
1214
|
+
""" copy all attributes value from other and return bool result """
|
|
1215
|
+
try:
|
|
1216
|
+
obj: InterfaceClass = self.par2obj(Parameter(other.logical_name.contents)).unwrap()
|
|
1217
|
+
for i, attr in other.get_index_with_attributes(in_init_order=True):
|
|
1218
|
+
if i == 1:
|
|
1219
|
+
continue
|
|
1220
|
+
else:
|
|
1221
|
+
if attr is not None:
|
|
1222
|
+
obj.set_attr(i, attr.encoding)
|
|
1223
|
+
return True
|
|
1224
|
+
except exc.NoObject as e:
|
|
1225
|
+
return False
|
|
1226
|
+
|
|
1227
|
+
def copy_objects_attr_values_from(self, other: Self) -> bool:
|
|
1228
|
+
""" Copy collections values and return True if all was writen """
|
|
1229
|
+
if len(other) != 0:
|
|
1230
|
+
return bool(reduce(lambda a, b: a or b, map(self.copy_obj_attr_values_from, other.values())))
|
|
1231
|
+
else:
|
|
1232
|
+
return False
|
|
1233
|
+
|
|
1234
|
+
def obis2obj(self, obis: o.OBIS) -> result.SimpleOrError[InterfaceClass]:
|
|
1235
|
+
if obj := self.__objs.get(obis):
|
|
1236
|
+
return result.Simple(obj)
|
|
1237
|
+
return result.Error.from_e(ValueError(str(obis)), "no object")
|
|
1238
|
+
|
|
1239
|
+
def logicalName2obj(self, ln: cst.LogicalName) -> result.SimpleOrError[InterfaceClass]:
|
|
1240
|
+
return self.obis2obj(o.OBIS(ln.contents))
|
|
1241
|
+
|
|
1242
|
+
@cached_property
|
|
1243
|
+
def LDN(self) -> impl.data.LDN:
|
|
1244
|
+
return self.obis2obj(o.LDN).unwrap()
|
|
1245
|
+
|
|
1246
|
+
@cached_property
|
|
1247
|
+
def current_association(self) -> AssociationLN:
|
|
1248
|
+
return self.obis2obj(o.CURRENT_ASSOCIATION).unwrap()
|
|
1249
|
+
|
|
1250
|
+
def getASSOCIATION(self, instance: int) -> AssociationLN:
|
|
1251
|
+
return self.obis2obj(o.AssociationLN(instance)).unwrap()
|
|
1252
|
+
|
|
1253
|
+
@cached_property
|
|
1254
|
+
def PUBLIC_ASSOCIATION(self) -> AssociationLN:
|
|
1255
|
+
return self.obis2obj(o.PUBLIC_ASSOCIATION).unwrap()
|
|
1256
|
+
|
|
1257
|
+
@property
|
|
1258
|
+
def COMMUNICATION_PORT_PARAMETER(self) -> impl.data.CommunicationPortParameter:
|
|
1259
|
+
return self.obis2obj(bytes((0, 0, 96, 12, 4, 255))).unwrap()
|
|
1260
|
+
|
|
1261
|
+
@property
|
|
1262
|
+
def clock(self) -> Clock:
|
|
1263
|
+
return self.obis2obj(bytes((0, 0, 1, 0, 0, 255))).unwrap()
|
|
1264
|
+
|
|
1265
|
+
@property
|
|
1266
|
+
def boot_image_transfer(self) -> ImageTransfer:
|
|
1267
|
+
return self.obis2obj(bytes((0, 0, 44, 0, 128, 255))).unwrap()
|
|
1268
|
+
|
|
1269
|
+
@property
|
|
1270
|
+
def firmware_image_transfer(self) -> ImageTransfer:
|
|
1271
|
+
return self.obis2obj(bytes((0, 0, 44, 0, 0, 255))).unwrap()
|
|
1272
|
+
|
|
1273
|
+
@property
|
|
1274
|
+
def firmwares_description(self) -> Data:
|
|
1275
|
+
""" Consist from boot_version, descriptor, ex.: 0005PWRM_M2M_3_F1_5ppm_Spvq. 0.0.128.100.0.255 """
|
|
1276
|
+
return self.obis2obj(bytes((0, 0, 128, 100, 0, 255))).unwrap()
|
|
1277
|
+
|
|
1278
|
+
@property
|
|
1279
|
+
def RU_CLOSE_ELECTRIC_SEAL(self) -> Data:
|
|
1280
|
+
""" Russian. СПОДЕС Г.2 """
|
|
1281
|
+
return self.obis2obj(bytes((0, 0, 96, 51, 6, 255))).unwrap()
|
|
1282
|
+
|
|
1283
|
+
@property
|
|
1284
|
+
def RU_ERASE_MAGNETIC_EVENTS(self) -> Data:
|
|
1285
|
+
""" Russian. СПОДЕС Г.2 """
|
|
1286
|
+
return self.obis2obj(bytes((0, 0, 96, 51, 7, 255))).unwrap()
|
|
1287
|
+
|
|
1288
|
+
@property
|
|
1289
|
+
def RU_FILTER_ALARM_2(self) -> Data:
|
|
1290
|
+
""" Russian. Filter of Alarm register relay"""
|
|
1291
|
+
return self.obis2obj(bytes((0, 0, 97, 98, 11, 255))).unwrap()
|
|
1292
|
+
|
|
1293
|
+
@property
|
|
1294
|
+
def RU_DAILY_PROFILE(self) -> ProfileGeneric:
|
|
1295
|
+
""" Russian. Profile of daily values """
|
|
1296
|
+
return self.obis2obj(bytes((1, 0, 98, 2, 0, 255))).unwrap()
|
|
1297
|
+
|
|
1298
|
+
@property
|
|
1299
|
+
def RU_MAXIMUM_CURRENT_EXCESS_LIMIT(self) -> Register:
|
|
1300
|
+
""" RU. СТО 34.01-5.1-006-2021 ver3, 11.1. Maximum current excess limit before the subscriber is disconnected, % of IMAX """
|
|
1301
|
+
return self.obis2obj(bytes((1, 0, 11, 134, 0, 255))).unwrap()
|
|
1302
|
+
|
|
1303
|
+
@property
|
|
1304
|
+
def RU_MAXIMUM_VOLTAGE_EXCESS_LIMIT(self) -> Register:
|
|
1305
|
+
""" RU. СТО 34.01-5.1-006-2021 ver3, 11.1. Maximum voltage excess limit before the subscriber is disconnected, % of Unominal """
|
|
1306
|
+
return self.obis2obj(bytes((1, 0, 12, 134, 0, 255))).unwrap()
|
|
1307
|
+
|
|
1308
|
+
def getDISCONNECT_CONTROL(self, ch: int = 0) -> DisconnectControl:
|
|
1309
|
+
"""DLMS UA 1000-1 Ed 14 6.2.46 Disconnect control objects by channel"""
|
|
1310
|
+
return self.obis2obj(bytes((0, ch, 96, 3, 10, 255))).unwrap()
|
|
1311
|
+
|
|
1312
|
+
def getARBITRATOR(self, ch: int = 0) -> Arbitrator:
|
|
1313
|
+
"""DLMS UA 1000-1 Ed 14 6.2.47 Arbitrator objects objects by channel"""
|
|
1314
|
+
return self.obis2obj(bytes((0, ch, 96, 3, 20, 255))).unwrap()
|
|
1315
|
+
|
|
1316
|
+
@property
|
|
1317
|
+
def boot_version(self) -> str:
|
|
1318
|
+
try:
|
|
1319
|
+
return self.firmwares_description.value.to_str()[:4]
|
|
1320
|
+
except Exception as e:
|
|
1321
|
+
print(e)
|
|
1322
|
+
return 'unknown'
|
|
1323
|
+
|
|
1324
|
+
def get_script_names(self, ln: cst.LogicalName, selector: cdt.LongUnsigned) -> str:
|
|
1325
|
+
"""return name from script by selector"""
|
|
1326
|
+
obj = self.par2obj(Parameter(ln.contents)).unwrap()
|
|
1327
|
+
if isinstance(obj, ScriptTable):
|
|
1328
|
+
for script in obj.scripts:
|
|
1329
|
+
script: ScriptTable.scripts
|
|
1330
|
+
if script.script_identifier == selector:
|
|
1331
|
+
names: list[str] = []
|
|
1332
|
+
for action in script.actions:
|
|
1333
|
+
action_obj = self.par2obj(Parameter(action.logical_name.contents)).unwrap()
|
|
1334
|
+
if int(action_obj.CLASS_ID) != int(action.class_id):
|
|
1335
|
+
raise ValueError(F"got {action_obj.CLASS_ID}, expected {action.class_id}")
|
|
1336
|
+
match int(action.service_id):
|
|
1337
|
+
case 1: # for write
|
|
1338
|
+
if isinstance(action.parameter, cdt.NullData):
|
|
1339
|
+
names.append(str(action_obj.get_attr_element(int(action.index))))
|
|
1340
|
+
else:
|
|
1341
|
+
raise TypeError(F"not support by framework") # TODO: make it
|
|
1342
|
+
case 2: # for execute
|
|
1343
|
+
if isinstance(action.parameter, cdt.NullData):
|
|
1344
|
+
names.append(str(action_obj.get_meth_element(int(action.index))))
|
|
1345
|
+
else:
|
|
1346
|
+
raise TypeError(F"not support by framework") # TODO: make it
|
|
1347
|
+
return ", ".join(names)
|
|
1348
|
+
else:
|
|
1349
|
+
raise ValueError(F"not find {selector} in {obj}")
|
|
1350
|
+
else:
|
|
1351
|
+
raise ValueError(F"object with {ln} is not {ScriptTable.CLASS_ID}")
|
|
1352
|
+
|
|
1353
|
+
@lru_cache(4)
|
|
1354
|
+
def get_association_id(self, client_sap: enums.ClientSAP) -> int:
|
|
1355
|
+
"""return id(association instance) from it client address without current"""
|
|
1356
|
+
for ass in get_filtered(iter(self), (ln_pattern.NON_CURRENT_ASSOCIATION,)):
|
|
1357
|
+
if ass.associated_partners_id.client_SAP == client_sap:
|
|
1358
|
+
return ass.logical_name.e
|
|
1359
|
+
else:
|
|
1360
|
+
continue
|
|
1361
|
+
else:
|
|
1362
|
+
raise ValueError(F"absent association with {client_sap}")
|
|
1363
|
+
|
|
1364
|
+
def sap2association(self, sap: enums.ClientSAP) -> AssociationLN:
|
|
1365
|
+
for ass in self.iter_classID_objects(ClassID.ASSOCIATION_LN):
|
|
1366
|
+
if (
|
|
1367
|
+
ass.associated_partners_id is not None
|
|
1368
|
+
and ass.associated_partners_id.client_SAP == sap
|
|
1369
|
+
):
|
|
1370
|
+
return ass
|
|
1371
|
+
else:
|
|
1372
|
+
continue
|
|
1373
|
+
else:
|
|
1374
|
+
raise exc.NoObject(F"hasn't association with {sap}")
|
|
1375
|
+
|
|
1376
|
+
@lru_cache(maxsize=1000)
|
|
1377
|
+
def is_readable(self, ln: cst.LogicalName,
|
|
1378
|
+
index: int,
|
|
1379
|
+
association_id: int,
|
|
1380
|
+
security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
|
|
1381
|
+
) -> bool:
|
|
1382
|
+
return self.getASSOCIATION(association_id).is_readable(
|
|
1383
|
+
ln=ln,
|
|
1384
|
+
index=index,
|
|
1385
|
+
security_policy=security_policy
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
@lru_cache(maxsize=1000)
|
|
1389
|
+
def is_writable(self, ln: cst.LogicalName,
|
|
1390
|
+
index: int,
|
|
1391
|
+
association_id: int,
|
|
1392
|
+
security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
|
|
1393
|
+
) -> bool:
|
|
1394
|
+
return self.getASSOCIATION(association_id).is_writable(
|
|
1395
|
+
ln=ln,
|
|
1396
|
+
index=index,
|
|
1397
|
+
security_policy=security_policy
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
@lru_cache(maxsize=1000)
|
|
1401
|
+
def isnt_mutable(self,
|
|
1402
|
+
par: Parameter,
|
|
1403
|
+
association_id: int,
|
|
1404
|
+
security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
|
|
1405
|
+
) -> bool:
|
|
1406
|
+
"""is not writable and STATIC data"""
|
|
1407
|
+
if (
|
|
1408
|
+
not self.is_writable(par.ln, par.i, association_id, security_policy, security_policy)
|
|
1409
|
+
and self.par2obj(par
|
|
1410
|
+
).unwrap().getAElement(par.i
|
|
1411
|
+
).unwrap().classifier == ic.Classifier.STATIC
|
|
1412
|
+
):
|
|
1413
|
+
return True
|
|
1414
|
+
return False
|
|
1415
|
+
|
|
1416
|
+
@lru_cache(maxsize=1000)
|
|
1417
|
+
def is_accessible(self, ln: cst.LogicalName,
|
|
1418
|
+
index: int,
|
|
1419
|
+
association_id: int,
|
|
1420
|
+
m_id: mechanism_id.MechanismIdElement = None
|
|
1421
|
+
) -> bool:
|
|
1422
|
+
"""for ver 0 and 1 only"""
|
|
1423
|
+
|
|
1424
|
+
return self.getASSOCIATION(association_id).is_accessible(
|
|
1425
|
+
ln=ln,
|
|
1426
|
+
index=index,
|
|
1427
|
+
m_id=m_id
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
@lru_cache(maxsize=100)
|
|
1431
|
+
def get_name_and_type(self, value: structs.CaptureObjectDefinition) -> tuple[list[str], Type[cdt.CommonDataType]]:
|
|
1432
|
+
""" return names and type of element from collection"""
|
|
1433
|
+
names: list[str] = list()
|
|
1434
|
+
obj = self.par2obj(Parameter(value.logical_name.contents)).unwrap()
|
|
1435
|
+
names.append(get_name(obj.logical_name))
|
|
1436
|
+
attr_index = int(value.attribute_index)
|
|
1437
|
+
data_index = int(value.data_index)
|
|
1438
|
+
data_type: Type[cdt.CommonDataType] = obj.get_attr_data_type(attr_index)
|
|
1439
|
+
names.append(str(obj.get_attr_element(attr_index)))
|
|
1440
|
+
if data_index == 0:
|
|
1441
|
+
pass
|
|
1442
|
+
elif issubclass(data_type, cdt.Structure):
|
|
1443
|
+
if len(data_type.ELEMENTS) < data_index:
|
|
1444
|
+
raise ValueError(F"can't create buffer_struct_type for {self}, got {data_index=} in struct {data_type.__name__}, expected 1..{len(data_type.ELEMENTS)}")
|
|
1445
|
+
else:
|
|
1446
|
+
el: cdt.StructElement = data_type.ELEMENTS[data_index - 1]
|
|
1447
|
+
names.append(el.NAME)
|
|
1448
|
+
data_type = el.TYPE
|
|
1449
|
+
elif isinstance(obj, ProfileGeneric) and attr_index == 2:
|
|
1450
|
+
"""according to DLMS UA 1000-1 Ed 14. ProfileGeneric.capture_object.data_index annex"""
|
|
1451
|
+
return self.get_name_and_type(obj.capture_objects[data_index - 1]) # todo: is recurse need rewrite here
|
|
1452
|
+
else:
|
|
1453
|
+
pass
|
|
1454
|
+
return names, data_type
|
|
1455
|
+
|
|
1456
|
+
def get_attr_tree(self,
|
|
1457
|
+
ass_id: int,
|
|
1458
|
+
obj_mode: ObjectTreeMode = "c",
|
|
1459
|
+
obj_filter: ObjFilteredKey = None,
|
|
1460
|
+
sort_mode: SortMode = "",
|
|
1461
|
+
af_mode: Literal["l", "r", "w", "lr", "lw", "wr", "lrw", "m", "mlrw", "mlr"] = "l",
|
|
1462
|
+
oi_filter: tuple[tuple[ClassID, tuple[int, ...]], ...] = None # todo: maybe ai_filter with LNPattern, indexes need?
|
|
1463
|
+
) -> dict[ClassID | media_id.MediaId, dict[ic.COSEMInterfaceClasses, list[int]]] | dict[ic.COSEMInterfaceClasses, list[int]]: # todo: not all ret annotation
|
|
1464
|
+
"""af_mode(attribute filter mode): l-reduce logical_name, r-show only readable, w-show only writeable,
|
|
1465
|
+
oi_filter(object attribute index filter), example: ((ClassID.REGISTER, (2,))) - is restricted for Register only Value attribute without logical_name and scaler_unit
|
|
1466
|
+
"""
|
|
1467
|
+
objects: dict[ic.COSEMInterfaceClasses, list[int]]
|
|
1468
|
+
without_ln = True if "l" in af_mode else False
|
|
1469
|
+
only_read = True if "r" in af_mode else False
|
|
1470
|
+
only_write = True if "w" in af_mode else False
|
|
1471
|
+
with_methods = True if "m" in af_mode else False
|
|
1472
|
+
oi_f = dict(oi_filter) if oi_filter else dict()
|
|
1473
|
+
"""objects indexes filter"""
|
|
1474
|
+
filtered = self.filter_by_ass(ass_id)
|
|
1475
|
+
if obj_filter:
|
|
1476
|
+
filtered = get_filtered(filtered, obj_filter)
|
|
1477
|
+
ret = get_object_tree(
|
|
1478
|
+
objects=get_sorted(
|
|
1479
|
+
objects=filtered,
|
|
1480
|
+
mode=sort_mode),
|
|
1481
|
+
mode=obj_mode)
|
|
1482
|
+
stack = [(None, None, ret)]
|
|
1483
|
+
while len(stack) != 0:
|
|
1484
|
+
d, k, v = stack.pop()
|
|
1485
|
+
if isinstance(d, dict):
|
|
1486
|
+
for k_, v_ in tuple(d.items()):
|
|
1487
|
+
if len(v_) == 0:
|
|
1488
|
+
d.pop(k_)
|
|
1489
|
+
if isinstance(v, list):
|
|
1490
|
+
objects = dict()
|
|
1491
|
+
for obj in v:
|
|
1492
|
+
obj: ic.COSEMInterfaceClasses
|
|
1493
|
+
f_i = oi_f.get(obj.CLASS_ID)
|
|
1494
|
+
"""filter indexes"""
|
|
1495
|
+
indexes = list()
|
|
1496
|
+
for i, attr in obj.get_index_with_attributes():
|
|
1497
|
+
if without_ln and i == 1:
|
|
1498
|
+
continue
|
|
1499
|
+
elif only_read and not self.is_readable(obj.logical_name, i, ass_id):
|
|
1500
|
+
continue
|
|
1501
|
+
elif only_write and not self.is_writable(obj.logical_name, i, ass_id):
|
|
1502
|
+
continue
|
|
1503
|
+
elif f_i and i not in f_i:
|
|
1504
|
+
continue
|
|
1505
|
+
else:
|
|
1506
|
+
indexes.append(i)
|
|
1507
|
+
if with_methods:
|
|
1508
|
+
i_meth = count(1)
|
|
1509
|
+
for i, m_el in zip(i_meth, obj.M_ELEMENTS):
|
|
1510
|
+
try:
|
|
1511
|
+
if not self.is_accessible(obj.logical_name, i, ass_id, mechanism_id.LOW):
|
|
1512
|
+
continue
|
|
1513
|
+
elif f_i and -i not in f_i:
|
|
1514
|
+
continue
|
|
1515
|
+
indexes.append(-i)
|
|
1516
|
+
except exc.ITEApplication as e:
|
|
1517
|
+
print(F"skip {i}... methods for {obj}: {e}")
|
|
1518
|
+
break
|
|
1519
|
+
if len(indexes) != 0:
|
|
1520
|
+
objects[obj] = indexes
|
|
1521
|
+
if d is None:
|
|
1522
|
+
return objects
|
|
1523
|
+
if len(objects) != 0:
|
|
1524
|
+
d[k] = objects
|
|
1525
|
+
else:
|
|
1526
|
+
d.pop(k)
|
|
1527
|
+
elif isinstance(v, dict):
|
|
1528
|
+
for k_, v_ in v.items():
|
|
1529
|
+
stack.append((v, k_, v_))
|
|
1530
|
+
else:
|
|
1531
|
+
raise ValueError('not support')
|
|
1532
|
+
return ret
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
if config is not None:
|
|
1536
|
+
try:
|
|
1537
|
+
__collection_path = settings.collection.path
|
|
1538
|
+
except KeyError as e:
|
|
1539
|
+
raise exc.TomlKeyError(F"not find {e} in [DLMS.collection]<path>")
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
@deprecated("use lnContents2obis")
|
|
1543
|
+
def get_ln_contents(value: LNContaining) -> bytes:
|
|
1544
|
+
"""return LN as bytes[6] for use in any searching"""
|
|
1545
|
+
match value:
|
|
1546
|
+
case bytes(): return value
|
|
1547
|
+
case cst.LogicalName() | ut.CosemObjectInstanceId(): return value.contents
|
|
1548
|
+
case ut.CosemAttributeDescriptor() | ut.CosemMethodDescriptor(): return value.instance_id.contents
|
|
1549
|
+
case ut.CosemAttributeDescriptorWithSelection(): return value.cosem_attribute_descriptor.instance_id.contents
|
|
1550
|
+
case cdt.Structure(logical_name=value.logical_name): return value.logical_name.contents
|
|
1551
|
+
case cdt.Structure() as s:
|
|
1552
|
+
s: cdt.Structure
|
|
1553
|
+
for it in s:
|
|
1554
|
+
if isinstance(it, cst.LogicalName):
|
|
1555
|
+
return it.contents
|
|
1556
|
+
raise ValueError(F"can't convert {value=} to Logical Name contents. Struct {s} not content the Logical Name")
|
|
1557
|
+
case str() if value.find('.') != -1:
|
|
1558
|
+
return cst.LogicalName.from_obis(value).contents
|
|
1559
|
+
case str(): return cst.LogicalName(value).contents
|
|
1560
|
+
case _: raise ValueError(F"can't convert {value=} to Logical Name contents")
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
def lnContents2obis(value: LNContaining) -> o.OBIS:
|
|
1564
|
+
"""return LN as OBIS for use in any searching"""
|
|
1565
|
+
match value:
|
|
1566
|
+
case bytes(): return o.OBIS(value)
|
|
1567
|
+
case cst.LogicalName() | ut.CosemObjectInstanceId(): return o.OBIS(value.contents)
|
|
1568
|
+
case ut.CosemAttributeDescriptor() | ut.CosemMethodDescriptor(): return o.OBIS(value.instance_id.contents)
|
|
1569
|
+
case ut.CosemAttributeDescriptorWithSelection(): return o.OBIS(value.cosem_attribute_descriptor.instance_id.contents)
|
|
1570
|
+
case cdt.Structure(logical_name=value.logical_name): return o.OBIS(value.logical_name.contents)
|
|
1571
|
+
case cdt.Structure() as s:
|
|
1572
|
+
s: cdt.Structure
|
|
1573
|
+
for it in s:
|
|
1574
|
+
if isinstance(it, cst.LogicalName):
|
|
1575
|
+
return o.OBIS(it.contents)
|
|
1576
|
+
raise ValueError(F"can't convert {value=} to Logical Name contents. Struct {s} not content the Logical Name")
|
|
1577
|
+
case str() if value.find('.') != -1:
|
|
1578
|
+
return o.OBIS(cst.LogicalName.from_obis(value).contents)
|
|
1579
|
+
case str(): return o.OBIS(cst.LogicalName(value).contents)
|
|
1580
|
+
case _: raise ValueError(F"can't convert {value=} to Logical Name contents")
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
class AttrDesc:
|
|
1584
|
+
"""keep constant descriptors # todo: make better"""
|
|
1585
|
+
OBJECT_LIST = ut.CosemAttributeDescriptor((ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(2)))
|
|
1586
|
+
LDN_VALUE = ut.CosemAttributeDescriptor((ClassID.DATA, ut.CosemObjectInstanceId("0.0.42.0.0.255"), ut.CosemObjectAttributeId(2)))
|
|
1587
|
+
SPODES_VERSION = ut.CosemAttributeDescriptor((ClassID.DATA, ut.CosemObjectInstanceId("0.0.96.1.6.255"), ut.CosemObjectAttributeId(2)))
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
__range10_and_255: tuple = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255
|
|
1591
|
+
__range63: tuple = tuple(range(0, 64))
|
|
1592
|
+
__range120_and_124_127: tuple = tuple(chain(range(0, 121), range(124, 128)))
|
|
1593
|
+
__range100_and_255: tuple = tuple(chain(range(0, 100), (255,)))
|
|
1594
|
+
__range100_and_101_125_and_255: tuple = tuple(chain(__range100_and_255, range(101, 126)))
|
|
1595
|
+
__c1: tuple = tuple(chain(range(1, 10), (13, 14, 33, 34, 53, 54, 73, 74, 82), range(16, 31), range(36, 51), range(56, 70), range(76, 81), range(84, 90)))
|
|
1596
|
+
"""DLMS UA 1000-1 Ed 14. Table 45 c1"""
|
|
1597
|
+
__table44: tuple = (11, 12, 15, 31, 32, 35, 51, 52, 55, 71, 72, 75, 90, 91, 92)
|
|
1598
|
+
"""DLMS UA 1000-1 Ed 14. Table 44 for active power"""
|
|
1599
|
+
__c2: tuple = tuple(chain(__table44, range(100, 108)))
|
|
1600
|
+
"""DLMS UA 1000-1 Ed 14. Table 45 c2"""
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def get_media_id(ln: cst.LogicalName) -> media_id.MediaId:
|
|
1604
|
+
return media_id.MediaId.from_int(ln.a)
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def get_class_id(obj: InterfaceClass) -> ClassID:
|
|
1608
|
+
return obj.CLASS_ID
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def get_map_by_obj(objects: list[InterfaceClass] | tuple[InterfaceClass], key: Callable[[InterfaceClass], ...]) -> dict[media_id.MediaId, list[InterfaceClass]]:
|
|
1612
|
+
ret = dict()
|
|
1613
|
+
for obj in objects:
|
|
1614
|
+
if ret.get(new_key := key(obj)):
|
|
1615
|
+
ret[new_key].append(obj)
|
|
1616
|
+
else:
|
|
1617
|
+
ret[new_key] = [obj]
|
|
1618
|
+
return ret
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
def get_object_tree(objects: list[InterfaceClass] | tuple[InterfaceClass],
|
|
1622
|
+
mode: ObjectTreeMode) -> dict[media_id.MediaId | ClassID, list[InterfaceClass]]:
|
|
1623
|
+
"""mode: m-media_id, g-relation_group, c-class_id"""
|
|
1624
|
+
mode = list(mode)
|
|
1625
|
+
ret = objects
|
|
1626
|
+
while mode:
|
|
1627
|
+
match mode.pop():
|
|
1628
|
+
case "m":
|
|
1629
|
+
key = lambda obj: get_media_id(obj.logical_name)
|
|
1630
|
+
case "g":
|
|
1631
|
+
key = lambda obj: get_relation_group(obj.logical_name)
|
|
1632
|
+
case "c":
|
|
1633
|
+
key = get_class_id
|
|
1634
|
+
case _:
|
|
1635
|
+
raise KeyError(F"got unknown {mode=} for get_map")
|
|
1636
|
+
stack = [(None, None, ret)]
|
|
1637
|
+
while len(stack) != 0:
|
|
1638
|
+
d, k, v = stack.pop()
|
|
1639
|
+
if isinstance(v, list):
|
|
1640
|
+
res = get_map_by_obj(v, key)
|
|
1641
|
+
if d is None:
|
|
1642
|
+
ret = res
|
|
1643
|
+
else:
|
|
1644
|
+
d[k] = res
|
|
1645
|
+
elif isinstance(v, dict):
|
|
1646
|
+
stack.extend(((v, k_, v_) for k_, v_ in v.items()))
|
|
1647
|
+
else:
|
|
1648
|
+
raise ValueError('not support')
|
|
1649
|
+
return ret
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
def get_sorted(objects: list[InterfaceClass],
|
|
1653
|
+
mode: SortMode) -> list[InterfaceClass]:
|
|
1654
|
+
"""mode: l-logical_name, n-name, c-class_id
|
|
1655
|
+
"""
|
|
1656
|
+
mode: list[str] = list(mode)
|
|
1657
|
+
while mode:
|
|
1658
|
+
match mode.pop():
|
|
1659
|
+
case "l":
|
|
1660
|
+
key = None
|
|
1661
|
+
case "n":
|
|
1662
|
+
key = lambda obj: get_name(obj.logical_name)
|
|
1663
|
+
case "c":
|
|
1664
|
+
key = lambda obj: obj.CLASS_ID
|
|
1665
|
+
case _:
|
|
1666
|
+
raise KeyError(F"got unknown {mode=} for get_map")
|
|
1667
|
+
objects = sorted(objects, key=key)
|
|
1668
|
+
return objects
|
|
1669
|
+
|
|
1670
|
+
@dataclass
|
|
1671
|
+
class Channel:
|
|
1672
|
+
"""for object filter approve"""
|
|
1673
|
+
n: int
|
|
1674
|
+
|
|
1675
|
+
def __post_init__(self) -> None:
|
|
1676
|
+
if not self.is_channel(self.n):
|
|
1677
|
+
raise ValueError(F"got value={self.n}, expected (0..64)")
|
|
1678
|
+
|
|
1679
|
+
@staticmethod
|
|
1680
|
+
def is_channel(b: int) -> bool:
|
|
1681
|
+
return True if 0 <= b <= 64 else False
|
|
1682
|
+
|
|
1683
|
+
def is_approve(self, b: int) -> bool:
|
|
1684
|
+
if self.is_channel(b):
|
|
1685
|
+
return True if b == self.n else False
|
|
1686
|
+
else:
|
|
1687
|
+
return True
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
RelationGroup: TypeAlias = media_id.Abstract | media_id.Electricity | media_id.Hca | media_id.Gas | media_id.Thermal | media_id.Water | media_id.Other
|
|
1691
|
+
RelationGroups: tuple[media_id.MediaId, ...] = (media_id.ABSTRACT, media_id.ELECTRICITY, media_id.HCA, media_id.GAS, media_id.THERMAL, media_id.WATER)
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
@lru_cache(maxsize=1000)
|
|
1695
|
+
def get_relation_group(ln: cst.LogicalName) -> RelationGroup:
|
|
1696
|
+
if ln.a == media_id.ABSTRACT:
|
|
1697
|
+
if ln.c == 0:
|
|
1698
|
+
if ln.d == 1:
|
|
1699
|
+
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES
|
|
1700
|
+
elif ln.d in (2, 9):
|
|
1701
|
+
return media_id.OTHER_ABSTRACT_GENERAL_PURPOSE_OBIS_CODES
|
|
1702
|
+
elif ln.c == 1:
|
|
1703
|
+
if ln.d in (0, 1, 2, 3, 4, 5, 6):
|
|
1704
|
+
return media_id.CLOCK_OBJECTS
|
|
1705
|
+
elif ln.c == 2:
|
|
1706
|
+
if ln.d in (0, 1, 2):
|
|
1707
|
+
return media_id.MODEM_CONFIGURATION_AND_RELATED_OBJECTS
|
|
1708
|
+
elif ln.c == 10 and ln.d == 0 and ln.e in (0, 1, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 125):
|
|
1709
|
+
return media_id.SCRIPT_TABLE_OBJECTS
|
|
1710
|
+
elif ln.c == 11 and ln.d == 0:
|
|
1711
|
+
return media_id.SPECIAL_DAYS_TABLE_OBJECTS
|
|
1712
|
+
elif ln.c == 12 and ln.d == 0:
|
|
1713
|
+
return media_id.SCHEDULE_OBJECTS
|
|
1714
|
+
elif ln.c == 13 and ln.d == 0:
|
|
1715
|
+
return media_id.ACTIVITY_CALENDAR_OBJECTS
|
|
1716
|
+
elif ln.c == 14 and ln.d == 0:
|
|
1717
|
+
return media_id.REGISTER_ACTIVATION_OBJECTS
|
|
1718
|
+
elif ln.c == 15 and ln.d == 0 and ln.e in (0, 1, 2, 3, 4, 5, 6, 7):
|
|
1719
|
+
return media_id.SINGLE_ACTION_SCHEDULE_OBJECTS
|
|
1720
|
+
elif ln.c == 16:
|
|
1721
|
+
if ln.d == 0 or (ln.d == 1 and ln.e in range(0, 10)):
|
|
1722
|
+
return media_id.REGISTER_OBJECTS_MONITOR
|
|
1723
|
+
elif ln.d == 2:
|
|
1724
|
+
return media_id.PARAMETER_MONITOR_OBJECTS
|
|
1725
|
+
elif ln.c == 17 and ln.d == 0:
|
|
1726
|
+
return media_id.LIMITER_OBJECTS
|
|
1727
|
+
elif ln.c == 18 and ln.d == 0:
|
|
1728
|
+
return media_id.ARRAY_MANAGER_OBJECT
|
|
1729
|
+
elif ln.c == 19:
|
|
1730
|
+
if (ln.d in range(0, 10) and ln.e == 0) or ln.d in range(10, 50) or ln.d in (range(50, 60) and ln.e in (1, 2)):
|
|
1731
|
+
return media_id.PAYMENT_METERING_RELATED_OBJECTS
|
|
1732
|
+
elif ln.c == 20 and ln.d == 0 and ln.e in (0, 1):
|
|
1733
|
+
return media_id.IEC_LOCAL_PORT_SETUP_OBJECTS
|
|
1734
|
+
elif ln.c == 21 and ln.d == 0:
|
|
1735
|
+
return media_id.STANDARD_READOUT_PROFILE_OBJECTS
|
|
1736
|
+
elif ln.c == 22 and ln.d == 0 and ln.e == 0:
|
|
1737
|
+
return media_id.IEC_HDLC_SETUP_OBJECTS
|
|
1738
|
+
elif ln.c == 23:
|
|
1739
|
+
if (ln.d in 0, 1, 2 and ln.e == 0) or ln.d == 3:
|
|
1740
|
+
return media_id.IEC_TWISTED_PAIR_1_SETUP_OBJECTS
|
|
1741
|
+
elif ln.c == 24:
|
|
1742
|
+
if (ln.d in (0, 1, 4, 5, 6) and ln.e == 0) or (ln.d in (2, 8, 9)):
|
|
1743
|
+
return media_id.OBJECTS_RELATED_TO_DATA_EXCHANGE_OVER_M_BUS
|
|
1744
|
+
elif ln.c == 31 and ln.d == 0 and ln.e == 0:
|
|
1745
|
+
return media_id.OBJECTS_RELATED_TO_DATA_EXCHANGE_OVER_M_BUS
|
|
1746
|
+
elif ln.c == 25:
|
|
1747
|
+
if ln.d in (0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15) and ln.e == 0:
|
|
1748
|
+
return media_id.OBJECTS_TO_SET_UP_DATA_EXCHANGE_OVER_THE_INTERNET
|
|
1749
|
+
elif ln.d == 9 and ln.e == 0:
|
|
1750
|
+
return media_id.OBJECTS_TO_SET_UP_PUSH_SETUP
|
|
1751
|
+
elif ln.c == 26 and ln.d in (0, 1, 2, 3, 5, 6) and ln.e == 0:
|
|
1752
|
+
return media_id.OBJECTS_FOR_SETTING_UP_DATA_EXCHANGE_USING_S_FSK_PLC
|
|
1753
|
+
elif ln.c == 27 and ln.d in (0, 1, 2) and ln.e == 0:
|
|
1754
|
+
return media_id.OBJECTS_FOR_SETTING_UP_THE_ISO_IEC_8802_2_LLC_LAYER
|
|
1755
|
+
elif ln.c == 28 and ln.d in (0, 1, 2, 3, 4, 5, 6, 7) and ln.e == 0:
|
|
1756
|
+
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_NARROWBAND_OFDM_PLC_FOR_PRIME_NETWORKS
|
|
1757
|
+
elif ln.c == 29 and ln.d in (0, 1, 2) and ln.e == 0:
|
|
1758
|
+
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_NARROW_BAND_OFDM_PLC_FOR_G3_PLC_NETWORKS
|
|
1759
|
+
elif ln.c == 30 and ln.d in (0, 1, 2, 3, 4):
|
|
1760
|
+
return media_id.ZIGBEE_SETUP_OBJECTS
|
|
1761
|
+
elif ln.c == 32 and ln.d in (0, 1, 2, 3) and ln.e == 0:
|
|
1762
|
+
return media_id.OBJECTS_FOR_SETTING_UP_AND_MANAGING_DATA_EXCHANGE_USING_ISO_IEC_14908_PLC_NETWORKS
|
|
1763
|
+
elif ln.c == 33 and ln.d in (0, 1, 2, 3) and ln.e == 0:
|
|
1764
|
+
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_HS_PLC_ISO_IEC_12139_1_ISO_EC_12139_1_NETWORKS
|
|
1765
|
+
elif ln.c == 34 and ln.d in (0, 1, 2, 3) and ln.e == 0:
|
|
1766
|
+
return media_id.OBJECTS_FOR_DATA_EXCHANGE_USING_WI_SUN_NETWORKS
|
|
1767
|
+
elif ln.c == 40 and ln.b == 0 and ln.d == 0:
|
|
1768
|
+
return media_id.ASSOCIATION_OBJECTS
|
|
1769
|
+
elif ln.c == 41 and ln.b == 0 and ln.d == 0 and ln.e == 0:
|
|
1770
|
+
return media_id.SAP_ASSIGNMENT_OBJECT
|
|
1771
|
+
elif ln.c == 42 and ln.b == 0 and ln.d == 0 and ln.e == 0:
|
|
1772
|
+
return media_id.COSEM_LOGICAL_DEVICE_NAME_OBJECT
|
|
1773
|
+
elif ln.c == 43:
|
|
1774
|
+
if (ln.b == 0 and ln.d == 0) or ln.d in (1, 2):
|
|
1775
|
+
return media_id.INFORMATION_SECURITY_RELATED_OBJECTS
|
|
1776
|
+
elif ln.c == 44:
|
|
1777
|
+
if ln.b == 0:
|
|
1778
|
+
if ln.d == 0:
|
|
1779
|
+
return media_id.IMAGE_TRANSFER_OBJECTS
|
|
1780
|
+
elif ln.d == 1:
|
|
1781
|
+
return media_id.FUNCTION_CONTROL_OBJECTS
|
|
1782
|
+
elif ln.d == 2:
|
|
1783
|
+
return media_id.COMMUNICATION_PORT_PROTECTION_OBJECTS
|
|
1784
|
+
elif ln.c == 65 and ln.d in __range63:
|
|
1785
|
+
return media_id.UTILITY_TABLE_OBJECTS
|
|
1786
|
+
elif ln.c == 66 and ln.d == 0:
|
|
1787
|
+
return media_id.COMPACT_DATA_OBJECTS
|
|
1788
|
+
elif ln.c == 96:
|
|
1789
|
+
if ln.d == 1:
|
|
1790
|
+
if ln.e in __range10_and_255:
|
|
1791
|
+
return media_id.DEVICE_ID_OBJECTS
|
|
1792
|
+
elif ln.e == 10:
|
|
1793
|
+
return media_id.METERING_POINT_ID_OBJECTS
|
|
1794
|
+
elif ln.d == 2:
|
|
1795
|
+
return media_id.PARAMETER_CHANGES_AND_CALIBRATION_OBJECTS
|
|
1796
|
+
elif ln.d == 3:
|
|
1797
|
+
if ln.e in (0, 1, 2, 3, 4):
|
|
1798
|
+
return media_id.I_O_CONTROL_SIGNAL_OBJECTS
|
|
1799
|
+
elif ln.e == 10:
|
|
1800
|
+
return media_id.DISCONNECT_CONTROL_OBJECTS
|
|
1801
|
+
elif ln.e in range(20, 30):
|
|
1802
|
+
return media_id.ARBITRATOR_OBJECTS
|
|
1803
|
+
elif ln.d == 4 and ln.e in (0, 1, 2, 3, 4):
|
|
1804
|
+
return media_id.STATUS_OF_INTERNAL_CONTROL_SIGNALS_OBJECTS
|
|
1805
|
+
elif ln.d == 5 and ln.e in (0, 1, 2, 3, 4):
|
|
1806
|
+
return media_id.INTERNAL_OPERATING_STATUS_OBJECTS
|
|
1807
|
+
elif ln.d == 6 and ln.e in (0, 1, 2, 3, 4, 5, 6):
|
|
1808
|
+
return media_id.BATTERY_ENTRIES_OBJECTS
|
|
1809
|
+
elif ln.d == 7 and ln.e in range(0, 22):
|
|
1810
|
+
return media_id.POWER_FAILURE_MONITORING_OBJECTS
|
|
1811
|
+
elif ln.d == 8 and ln.e in __range63:
|
|
1812
|
+
return media_id.OPERATING_TIME_OBJECTS
|
|
1813
|
+
elif ln.d == 9 and ln.e in (0, 1, 2):
|
|
1814
|
+
return media_id.ENVIRONMENT_RELATED_PARAMETERS_OBJECTS
|
|
1815
|
+
elif ln.d == 10 and ln.e in range(1, 11):
|
|
1816
|
+
return media_id.STATUS_REGISTER_OBJECTS
|
|
1817
|
+
elif ln.d == 11 and ln.e in range(0, 100):
|
|
1818
|
+
return media_id.EVENT_CODE_OBJECTS
|
|
1819
|
+
elif ln.d == 12 and ln.e in range(0, 7):
|
|
1820
|
+
return media_id.COMMUNICATION_PORT_LOG_PARAMETER_OBJECTS
|
|
1821
|
+
elif ln.d == 13 and ln.e in (0, 1):
|
|
1822
|
+
return media_id.CONSUMER_MESSAGE_OBJECTS
|
|
1823
|
+
elif ln.d == 14 and ln.e in range(0, 16):
|
|
1824
|
+
return media_id.CURRENTLY_ACTIVE_TARIFF_OBJECTS
|
|
1825
|
+
elif ln.d == 15 and ln.e in range(0, 100):
|
|
1826
|
+
return media_id.EVENT_COUNTER_OBJECTS
|
|
1827
|
+
elif ln.d == 16 and ln.e in range(0, 10):
|
|
1828
|
+
return media_id.PROFILE_ENTRY_DIGITAL_SIGNATURE_OBJECTS
|
|
1829
|
+
elif ln.d == 17 and ln.e in range(0, 128):
|
|
1830
|
+
return media_id.PROFILE_ENTRY_COUNTER_OBJECTS
|
|
1831
|
+
elif ln.d == 20:
|
|
1832
|
+
return media_id.METER_TAMPER_EVENT_RELATED_OBJECTS
|
|
1833
|
+
elif ln.d in range(50, 100):
|
|
1834
|
+
return media_id.ABSTRACT_MANUFACTURER_SPECIFIC
|
|
1835
|
+
elif ln.c == 97:
|
|
1836
|
+
if ln.d == 97 and ln.e in __range10_and_255:
|
|
1837
|
+
return media_id.ERROR_REGISTER_OBJECTS
|
|
1838
|
+
elif ln.d == 98 and (ln.e in chain(range(0, 30), (255,))):
|
|
1839
|
+
return media_id.ALARM_REGISTER_FILTER_DESCRIPTOR_OBJECTS
|
|
1840
|
+
elif ln.c == 98:
|
|
1841
|
+
return media_id.GENERAL_LIST_OBJECTS
|
|
1842
|
+
elif ln.c == 99:
|
|
1843
|
+
if ln.d in (1, 2, 12, 13, 14, 15, 16, 17, 18) or (ln.d == 3 and ln.e == 0):
|
|
1844
|
+
return media_id.ABSTRACT_DATA_PROFILE_OBJECTS
|
|
1845
|
+
if ln.d == 98:
|
|
1846
|
+
return media_id.EVENT_LOG_OBJECTS
|
|
1847
|
+
elif ln.c == 127 and ln.d == 0:
|
|
1848
|
+
return media_id.INACTIVE_OBJECTS
|
|
1849
|
+
else:
|
|
1850
|
+
return media_id.ABSTRACT
|
|
1851
|
+
elif ln.a == media_id.ELECTRICITY:
|
|
1852
|
+
if ln.c == 0:
|
|
1853
|
+
if ln.d == 0 and ln.e in __range10_and_255:
|
|
1854
|
+
return media_id.ID_NUMBERS_ELECTRICITY
|
|
1855
|
+
elif ln.d == 1:
|
|
1856
|
+
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_EL
|
|
1857
|
+
elif ln.d in (2, 3, 4, 6, 7, 8, 9, 10):
|
|
1858
|
+
return media_id.OTHER_ELECTRICITY_RELATED_GENERAL_PURPOSE_OBJECTS
|
|
1859
|
+
elif ln.d == 11 and ln.e in (1, 2, 3, 4, 5, 6, 7):
|
|
1860
|
+
return media_id.MEASUREMENT_ALGORITHM
|
|
1861
|
+
elif ln.c in (1, 21, 41, 61):
|
|
1862
|
+
return media_id.ACTIVE_POWER_PLUS
|
|
1863
|
+
elif ln.c in (2, 22, 42, 62):
|
|
1864
|
+
return media_id.ACTIVE_POWER_MINUS
|
|
1865
|
+
elif ln.c in (3, 23, 43, 63):
|
|
1866
|
+
return media_id.REACTIVE_POWER_PLUS
|
|
1867
|
+
elif ln.c in (4, 24, 44, 64):
|
|
1868
|
+
return media_id.REACTIVE_POWER_MINUS
|
|
1869
|
+
elif ln.c in (5, 25, 45, 65):
|
|
1870
|
+
return media_id.REACTIVE_POWER_QI
|
|
1871
|
+
elif ln.c in (6, 26, 46, 66):
|
|
1872
|
+
return media_id.REACTIVE_POWER_QII
|
|
1873
|
+
elif ln.c in (7, 27, 47, 67):
|
|
1874
|
+
return media_id.REACTIVE_POWER_QIII
|
|
1875
|
+
elif ln.c in (8, 28, 48, 68):
|
|
1876
|
+
return media_id.REACTIVE_POWER_QIV
|
|
1877
|
+
elif ln.c in (9, 29, 49, 69):
|
|
1878
|
+
return media_id.APPARENT_POWER_PLUS
|
|
1879
|
+
elif ln.c in (10, 30, 50, 70):
|
|
1880
|
+
return media_id.APPARENT_POWER_MINUS
|
|
1881
|
+
elif ln.c in (11, 31, 51, 71):
|
|
1882
|
+
return media_id.CURRENT
|
|
1883
|
+
elif ln.c in (12, 32, 52, 72):
|
|
1884
|
+
return media_id.VOLTAGE
|
|
1885
|
+
elif ln.c in (13, 33, 53, 73):
|
|
1886
|
+
return media_id.POWER_FACTOR
|
|
1887
|
+
elif ln.c in (14, 34, 54, 74):
|
|
1888
|
+
return media_id.SUPPLY_FREQUENCY
|
|
1889
|
+
elif ln.c in (15, 35, 55, 75):
|
|
1890
|
+
return media_id.ACTIVE_POWER_SUM
|
|
1891
|
+
elif ln.c in (16, 36, 56, 76):
|
|
1892
|
+
return media_id.ACTIVE_POWER_DIFF
|
|
1893
|
+
elif ln.c in (17, 37, 57, 77):
|
|
1894
|
+
return media_id.ACTIVE_POWER_QI
|
|
1895
|
+
elif ln.c in (18, 38, 58, 78):
|
|
1896
|
+
return media_id.ACTIVE_POWER_QII
|
|
1897
|
+
elif ln.c in (19, 39, 59, 79):
|
|
1898
|
+
return media_id.ACTIVE_POWER_QIII
|
|
1899
|
+
elif ln.c in (20, 40, 60, 80):
|
|
1900
|
+
return media_id.ACTIVE_POWER_QIV
|
|
1901
|
+
elif ln.c == 96:
|
|
1902
|
+
if ln.d == 1 and ln.e in __range10_and_255:
|
|
1903
|
+
return media_id.ELECTRICITY_METERING_POINT_ID_OBJECTS
|
|
1904
|
+
elif ln.d == 5 and ln.e in (0, 1, 2, 3, 4, 5):
|
|
1905
|
+
return media_id.ELECTRICITY_RELATED_STATUS_OBJECTS
|
|
1906
|
+
elif ln.d == 10 and ln.e in (0, 1, 2, 3):
|
|
1907
|
+
return media_id.ELECTRICITY_RELATED_STATUS_OBJECTS
|
|
1908
|
+
elif ln.c == 98:
|
|
1909
|
+
return media_id.LIST_OBJECTS_ELECTRICITY
|
|
1910
|
+
elif ln.d in range(31, 46) and ln.f in __range100_and_255:
|
|
1911
|
+
if ln.c in __c1 and ln.e in __range63:
|
|
1912
|
+
return media_id.THRESHOLD_VALUES
|
|
1913
|
+
elif ln.c in __table44 and ln.e in __range120_and_124_127:
|
|
1914
|
+
return media_id.THRESHOLD_VALUES
|
|
1915
|
+
elif ln.f in __range100_and_255:
|
|
1916
|
+
if ln.d in (31, 35, 39, 4, 5, 14, 15, 24, 25):
|
|
1917
|
+
if ln.c in __c1 and ln.e in __range63:
|
|
1918
|
+
return media_id.REGISTER_MONITOR_OBJECTS
|
|
1919
|
+
elif ln.c in __c2 and ln.e in __range120_and_124_127:
|
|
1920
|
+
return media_id.REGISTER_MONITOR_OBJECTS
|
|
1921
|
+
else:
|
|
1922
|
+
return media_id.ELECTRICITY
|
|
1923
|
+
elif ln.a == media_id.HCA:
|
|
1924
|
+
if ln.c == 0:
|
|
1925
|
+
if ln.d == 0 and ln.e in __range10_and_255:
|
|
1926
|
+
return media_id.ID_NUMBERS_HCA
|
|
1927
|
+
elif ln.d == 1 and ln.e in (1, 2, 10, 11):
|
|
1928
|
+
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_HCA
|
|
1929
|
+
elif ln.d == 2 and ln.e in (0, 1, 2, 3):
|
|
1930
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1931
|
+
elif ln.d == 4 and ln.e in (0, 1, 2, 3, 4, 5, 6):
|
|
1932
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1933
|
+
elif ln.d == 5 and ln.e in (10, 11):
|
|
1934
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1935
|
+
elif ln.d == 8 and ln.e in (0, 4, 6):
|
|
1936
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1937
|
+
elif ln.d == 9 and ln.e in (1, 2, 3):
|
|
1938
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_HCA
|
|
1939
|
+
elif ln.c in (1, 2) and ln.e == 0:
|
|
1940
|
+
if ln.d in (0, 6) and ln.f == 255:
|
|
1941
|
+
return media_id.MEASURED_VALUES_HCA_CONSUMPTION
|
|
1942
|
+
elif ln.d in (1, 2, 3, 4, 5) and ln.f in __range100_and_101_125_and_255:
|
|
1943
|
+
return media_id.MEASURED_VALUES_HCA_CONSUMPTION
|
|
1944
|
+
elif ln.c in range(3, 8) and ln.d in (0, 4, 5, 6) and ln.e == 255 and ln.f == 255:
|
|
1945
|
+
return media_id.MEASURED_VALUES_HCA_TEMPERATURE
|
|
1946
|
+
elif ln.c == 97 and ln.d == 97:
|
|
1947
|
+
return media_id.ERROR_REGISTER_OBJECTS_HCA
|
|
1948
|
+
elif ln.c == 98:
|
|
1949
|
+
return media_id.LIST_OBJECTS_HCA
|
|
1950
|
+
elif ln.c == 99 and ln.d == 1:
|
|
1951
|
+
return media_id.DATA_PROFILE_OBJECTS_HCA
|
|
1952
|
+
else:
|
|
1953
|
+
return media_id.HCA
|
|
1954
|
+
elif ln.a == media_id.THERMAL:
|
|
1955
|
+
if ln.c == 0:
|
|
1956
|
+
if ln.d == 0 and ln.e in __range10_and_255:
|
|
1957
|
+
return media_id.ID_NUMBERS_THERMAL
|
|
1958
|
+
elif ln.d == 1 and ln.e in (1, 2, 10, 11):
|
|
1959
|
+
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_THERMAL
|
|
1960
|
+
elif ln.d == 2 and ln.e in chain(range(0, 5), range(10, 14)):
|
|
1961
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1962
|
+
elif ln.d == 4 and ln.e in (1, 2, 3):
|
|
1963
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1964
|
+
elif ln.d == 5 and ln.e in chain(range(1, 10), range(21, 25)):
|
|
1965
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1966
|
+
elif ln.d == 8 and ln.e in chain(range(0, 8), range(11, 15), range(21, 26), range(31, 35)):
|
|
1967
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1968
|
+
elif ln.d == 9 and ln.e in (1, 2, 3):
|
|
1969
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_THERMAL
|
|
1970
|
+
elif ln.c in range(1, 8):
|
|
1971
|
+
if ln.e in range(10):
|
|
1972
|
+
if ln.d in (0, 1, 2, 3, 7) and ln.f == 255:
|
|
1973
|
+
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1974
|
+
elif ln.d in (3, 8, 9) and ln.f in __range100_and_101_125_and_255:
|
|
1975
|
+
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1976
|
+
elif ln.d in (1, 2, 4, 5, 12, 13, 14, 15) and ln.f in chain(range(100), range(100, 126)):
|
|
1977
|
+
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1978
|
+
elif ln.d == 6 and ln.e == 255 and ln.f == 255:
|
|
1979
|
+
return media_id.MEASURED_VALUES_THERMAL_CONSUMPTION
|
|
1980
|
+
elif ln.e in range(10):
|
|
1981
|
+
if ln.f in __range100_and_101_125_and_255:
|
|
1982
|
+
if ln.c in range(1, 8) and ln.d in (5, 15):
|
|
1983
|
+
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1984
|
+
elif ln.c in (8, 9) and ln.d in (1, 4, 5, 12, 13, 14, 15):
|
|
1985
|
+
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1986
|
+
elif ln.c in range(10, 14):
|
|
1987
|
+
if ln.d == 0 and ln.f == 255:
|
|
1988
|
+
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1989
|
+
elif ln.d in (4, 5, 14, 15) and ln.f in chain(range(100), range(101, 126)):
|
|
1990
|
+
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1991
|
+
elif ln.d in (6, 7, 10, 11) and ln.f == 255:
|
|
1992
|
+
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1993
|
+
elif ln.c in range(1, 14) and (ln.d in range(20, 26)) and ln.f == 255:
|
|
1994
|
+
return media_id.MEASURED_VALUES_THERMAL_ENERGY
|
|
1995
|
+
elif ln.c == 97 and ln.d == 97 and ln.e in (0, 1, 2):
|
|
1996
|
+
return media_id.ERROR_REGISTER_OBJECTS_THERMAL
|
|
1997
|
+
elif ln.c == 98:
|
|
1998
|
+
return media_id.LIST_OBJECTS_THERMAL
|
|
1999
|
+
elif ln.c == 99 and ln.f == 255:
|
|
2000
|
+
if ln.d in (1, 2) and ln.e in (1, 2, 3):
|
|
2001
|
+
return media_id.DATA_PROFILE_OBJECTS_THERMAL
|
|
2002
|
+
elif ln.d == 3 and ln.e == 1:
|
|
2003
|
+
return media_id.DATA_PROFILE_OBJECTS_THERMAL
|
|
2004
|
+
elif ln.d == 99:
|
|
2005
|
+
return media_id.DATA_PROFILE_OBJECTS_THERMAL
|
|
2006
|
+
else:
|
|
2007
|
+
return media_id.THERMAL
|
|
2008
|
+
elif media_id.GAS == ln.a:
|
|
2009
|
+
if ln.c == 0:
|
|
2010
|
+
if ln.d == 0 and ln.e in __range10_and_255:
|
|
2011
|
+
return media_id.ID_NUMBERS_GAS
|
|
2012
|
+
elif ln.d == 1:
|
|
2013
|
+
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_GAS
|
|
2014
|
+
elif ln.d in (2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15):
|
|
2015
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_GAS
|
|
2016
|
+
elif ln.c == 96 and ln.d == 5 and (ln.e in range(10)):
|
|
2017
|
+
return media_id.INTERNAL_OPERATING_STATUS_OBJECTS_GAS
|
|
2018
|
+
elif ln.c in chain(range(1, 9), range(11, 17), range(21, 27), range(31, 36), range(61, 66)) and ln.e in __range63:
|
|
2019
|
+
if ln.d in (24, 25, 26, 42, 43, 44, 63, 64, 65, 81, 82, 83) and ln.f in chain(range(100), range(101, 127)):
|
|
2020
|
+
return media_id.MEASURED_VALUES_GAS_INDEXES_AND_INDEX_DIFFERENCES
|
|
2021
|
+
elif ln.d in chain(range(6, 24), range(27, 33), range(45, 51), range(66, 72), range(84, 90)) and ln.f == 255:
|
|
2022
|
+
return media_id.MEASURED_VALUES_GAS_INDEXES_AND_INDEX_DIFFERENCES
|
|
2023
|
+
elif ln.d in chain(range(33, 42), range(52, 63), range(72, 81), range(90, 99)) and ln.f in chain(__range100_and_101_125_and_255, (126,)):
|
|
2024
|
+
return media_id.MEASURED_VALUES_GAS_INDEXES_AND_INDEX_DIFFERENCES
|
|
2025
|
+
elif ln.c == 42 and ln.e == 0:
|
|
2026
|
+
if ln.d in chain(0, 1, 2, 13, range(15, 19), range(19, 31), range(35, 51), range(55, 71)) and ln.f == 255:
|
|
2027
|
+
return media_id.MEASURED_VALUES_GAS_FLOW_RATE
|
|
2028
|
+
elif ln.d in chain(range(31, 35), range(51, 55)) and ln.f in chain(__range100_and_101_125_and_255, (126,)):
|
|
2029
|
+
return media_id.MEASURED_VALUES_GAS_FLOW_RATE
|
|
2030
|
+
elif ln.c in chain((41, 42), range(44, 50)) and ln.d in (0, 2, 3, 10, 11, 13, range(15, 92)) and ln.e == 0 and ln.f == 255:
|
|
2031
|
+
return media_id.MEASURED_VALUES_GAS_PROCESS_VALUES
|
|
2032
|
+
elif ln.c in range(51, 56):
|
|
2033
|
+
if ln.d in (0, 2, 3, 10, 11) and ln.e in chain((0, 1), range(11, 29)) and ln.f == 255:
|
|
2034
|
+
return media_id.CONVERSION_RELATED_FACTORS_AND_COEFFICIENTS_GAS
|
|
2035
|
+
elif ln.d == 12 and ln.e in range(20) and ln.f == 255:
|
|
2036
|
+
return media_id.CALCULATION_METHODS_GAS
|
|
2037
|
+
elif ln.c == 70 and ln.f == 255:
|
|
2038
|
+
if ln.d in (8, 9) and ln.e == 0:
|
|
2039
|
+
return media_id.NATURAL_GAS_ANALYSIS
|
|
2040
|
+
elif ln.d in chain(range(10, 21), range(60, 85)) and ln.e in chain((0, 1), range(11, 29)):
|
|
2041
|
+
return media_id.NATURAL_GAS_ANALYSIS
|
|
2042
|
+
elif ln.c == 98:
|
|
2043
|
+
return media_id.LIST_OBJECTS_GAS
|
|
2044
|
+
else:
|
|
2045
|
+
return media_id.GAS
|
|
2046
|
+
elif ln.a == media_id.WATER:
|
|
2047
|
+
if ln.c == 0:
|
|
2048
|
+
if ln.d == 0 and ln.e in __range10_and_255:
|
|
2049
|
+
return media_id.ID_NUMBERS_WATER
|
|
2050
|
+
elif ln.d == 1 and ln.e in (1, 2, 10, 11, 12):
|
|
2051
|
+
return media_id.BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES_WATER
|
|
2052
|
+
elif ln.d == 2 and ln.e in (0, 3):
|
|
2053
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2054
|
+
elif ln.d in (5, 7) and ln.e == 1:
|
|
2055
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2056
|
+
elif ln.d == 8 and ln.e in (1, 6):
|
|
2057
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2058
|
+
elif ln.d == 9 and ln.e in (1, 2, 3):
|
|
2059
|
+
return media_id.GENERAL_PURPOSE_OBJECTS_WATER
|
|
2060
|
+
elif ln.c == 1 and ln.e in range(0, 13):
|
|
2061
|
+
if ln.d in (0, 1, 2, 3, 6) and ln.f == 255:
|
|
2062
|
+
return media_id.MEASURED_VALUES_WATER_CONSUMPTION
|
|
2063
|
+
elif ln.d in range(1, 6) and ln.f in chain(range(100), range(101, 126)):
|
|
2064
|
+
return media_id.MEASURED_VALUES_WATER_CONSUMPTION
|
|
2065
|
+
elif ln.c in (2, 3) and ln.e in range(0, 13):
|
|
2066
|
+
if ln.d in (0, 1, 2, 3, 6) and ln.f == 255:
|
|
2067
|
+
return media_id.MEASURED_VALUES_WATER_MONITORING_VALUES
|
|
2068
|
+
elif ln.d in range(1, 6) and ln.f in chain(range(100), range(101, 126)):
|
|
2069
|
+
return media_id.MEASURED_VALUES_WATER_MONITORING_VALUES
|
|
2070
|
+
elif ln.c == 97 and ln.d == 97:
|
|
2071
|
+
return media_id.ERROR_REGISTER_OBJECTS_WATER
|
|
2072
|
+
elif ln.c == 98:
|
|
2073
|
+
return media_id.LIST_OBJECTS_WATER
|
|
2074
|
+
elif ln.c == 99 and ln.d == 1:
|
|
2075
|
+
return media_id.DATA_PROFILE_OBJECTS_WATER
|
|
2076
|
+
else:
|
|
2077
|
+
return media_id.WATER
|
|
2078
|
+
return media_id.OTHER_MEDIA
|
|
2079
|
+
|
|
2080
|
+
|
|
2081
|
+
DLMSObjectContainer: TypeAlias = Collection | list[InterfaceClass] | filter
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
@dataclass
|
|
2085
|
+
class Template:
|
|
2086
|
+
name: str
|
|
2087
|
+
collections: list[Collection]
|
|
2088
|
+
used: UsedAttributes
|
|
2089
|
+
description: str = ""
|
|
2090
|
+
verified: bool = False
|
|
2091
|
+
|
|
2092
|
+
def get_not_contains(self, cols: Iterable[Collection]) -> list[Collection]:
|
|
2093
|
+
"""return of collections not contains in template"""
|
|
2094
|
+
ret = list()
|
|
2095
|
+
for i, col in enumerate(cols):
|
|
2096
|
+
if col not in self.collections:
|
|
2097
|
+
ret.append(col)
|
|
2098
|
+
return ret
|
|
2099
|
+
|
|
2100
|
+
def get_not_valid(self, col: Collection) -> list[Exception]:
|
|
2101
|
+
"""with update col"""
|
|
2102
|
+
attr: cdt.CommonDataType
|
|
2103
|
+
ret = list()
|
|
2104
|
+
use_col = self.collections[0]
|
|
2105
|
+
"""temporary used first collection"""
|
|
2106
|
+
for ln, indexes in self.used.items():
|
|
2107
|
+
try:
|
|
2108
|
+
obj = use_col.get_object(ln)
|
|
2109
|
+
obj_col = col.get_object(ln)
|
|
2110
|
+
except exc.NoObject as e:
|
|
2111
|
+
ret.append(e)
|
|
2112
|
+
print(F"<add_collection> skip obj{self}: {e}")
|
|
2113
|
+
continue
|
|
2114
|
+
for i in indexes:
|
|
2115
|
+
if (attr := obj.get_attr(i)) is not None:
|
|
2116
|
+
try:
|
|
2117
|
+
attr.validate()
|
|
2118
|
+
except ValueError as e:
|
|
2119
|
+
ret.append(ValueError(F"can't decode value {attr} for {ln}:{i}"))
|
|
2120
|
+
else:
|
|
2121
|
+
ret.append(exc.NoObject(F"has't attribute {i} for {ln}"))
|
|
2122
|
+
return ret
|