hat.drivers.iec61850
1from hat.drivers.iec61850.client import (ReportCb, 2 TerminationCb, 3 connect, 4 Client) 5from hat.drivers.iec61850.common import (EntryTime, 6 ReportId, 7 PersistedDatasetRef, 8 NonPersistedDatasetRef, 9 DatasetRef, 10 DataRef, 11 CommandRef, 12 RcbType, 13 RcbRef, 14 BasicValueType, 15 AcsiValueType, 16 ArrayValueType, 17 StructValueType, 18 ValueType, 19 Timestamp, 20 QualityValidity, 21 QualityDetail, 22 QualitySource, 23 Quality, 24 DoublePoint, 25 Direction, 26 Severity, 27 Analogue, 28 Vector, 29 StepPosition, 30 BinaryControl, 31 BasicValue, 32 AcsiValue, 33 ArrayValue, 34 StructValue, 35 Value, 36 ServiceError, 37 AdditionalCause, 38 TestError, 39 CommandError, 40 OptionalField, 41 TriggerCondition, 42 RcbAttrType, 43 ReportIdRcbAttrValue, 44 ReportEnableRcbAttrValue, 45 DatasetRcbAttrValue, 46 ConfRevisionRcbAttrValue, 47 OptionalFieldsRcbAttrValue, 48 BufferTimeRcbAttrValue, 49 SequenceNumberRcbAttrValue, 50 TriggerOptionsRcbAttrValue, 51 IntegrityPeriodRcbAttrValue, 52 GiRcbAttrValue, 53 PurgeBufferRcbAttrValue, 54 EntryIdRcbAttrValue, 55 TimeOfEntryRcbAttrValue, 56 ReservationTimeRcbAttrValue, 57 ReserveRcbAttrValue, 58 RcbAttrValue, 59 Reason, 60 ReportData, 61 Report, 62 ControlModel, 63 OriginCategory, 64 Origin, 65 Check, 66 Command, 67 Termination) 68 69 70__all__ = ['ReportCb', 71 'TerminationCb', 72 'connect', 73 'Client', 74 'EntryTime', 75 'ReportId', 76 'PersistedDatasetRef', 77 'NonPersistedDatasetRef', 78 'DatasetRef', 79 'DataRef', 80 'CommandRef', 81 'RcbType', 82 'RcbRef', 83 'BasicValueType', 84 'AcsiValueType', 85 'ArrayValueType', 86 'StructValueType', 87 'ValueType', 88 'Timestamp', 89 'QualityValidity', 90 'QualityDetail', 91 'QualitySource', 92 'Quality', 93 'DoublePoint', 94 'Direction', 95 'Severity', 96 'Analogue', 97 'Vector', 98 'StepPosition', 99 'BinaryControl', 100 'BasicValue', 101 'AcsiValue', 102 'ArrayValue', 103 'StructValue', 104 'Value', 105 'ServiceError', 106 'AdditionalCause', 107 'TestError', 108 'CommandError', 109 'OptionalField', 110 'TriggerCondition', 111 'RcbAttrType', 112 'ReportIdRcbAttrValue', 113 'ReportEnableRcbAttrValue', 114 'DatasetRcbAttrValue', 115 'ConfRevisionRcbAttrValue', 116 'OptionalFieldsRcbAttrValue', 117 'BufferTimeRcbAttrValue', 118 'SequenceNumberRcbAttrValue', 119 'TriggerOptionsRcbAttrValue', 120 'IntegrityPeriodRcbAttrValue', 121 'GiRcbAttrValue', 122 'PurgeBufferRcbAttrValue', 123 'EntryIdRcbAttrValue', 124 'TimeOfEntryRcbAttrValue', 125 'ReservationTimeRcbAttrValue', 126 'ReserveRcbAttrValue', 127 'RcbAttrValue', 128 'Reason', 129 'ReportData', 130 'Report', 131 'ControlModel', 132 'OriginCategory', 133 'Origin', 134 'Check', 135 'Command', 136 'Termination']
28async def connect(addr: tcp.Address, 29 data_value_types: dict[common.DataRef, 30 common.ValueType] = {}, 31 cmd_value_types: dict[common.CommandRef, 32 common.ValueType] = {}, 33 report_data_refs: dict[common.ReportId, 34 Collection[common.DataRef]] = {}, 35 report_cb: ReportCb | None = None, 36 termination_cb: TerminationCb | None = None, 37 status_delay: float | None = None, 38 status_timeout: float = 30, 39 **kwargs 40 ) -> 'Client': 41 """Connect to IEC61850 server 42 43 `data_value_types` include value types used in report processing and 44 writing data. 45 46 `cmd_value_types` include value types used in command actions and 47 termination processing. 48 49 Only reports that are specified by `report_data_refs` are notified with 50 `report_cb`. 51 52 If `status_delay` is ``None``, periodical sending of status requests is 53 disabled. 54 55 Additional arguments are passed directly to `hat.drivers.mms.connect` 56 (`request_cb` and `unconfirmed_cb` are set by this coroutine). 57 58 """ 59 client = Client() 60 client._data_value_types = data_value_types 61 client._cmd_value_types = cmd_value_types 62 client._report_cb = report_cb 63 client._termination_cb = termination_cb 64 client._loop = asyncio.get_running_loop() 65 client._status_event = asyncio.Event() 66 client._last_appl_errors = {} 67 client._report_data_defs = { 68 report_id: [encoder.DataDef(ref=data_ref, 69 value_type=data_value_types[data_ref]) 70 for data_ref in data_refs] 71 for report_id, data_refs in report_data_refs.items()} 72 73 client._conn = await mms.connect(addr=addr, 74 request_cb=None, 75 unconfirmed_cb=client._on_unconfirmed, 76 **kwargs) 77 78 client._log = _create_logger_adapter(False, client._conn.info) 79 client._comm_log = _create_logger_adapter(True, client._conn.info) 80 81 if client.is_open and status_delay is not None: 82 client.async_group.spawn(client._status_loop, status_delay, 83 status_timeout) 84 85 return client
Connect to IEC61850 server
data_value_types include value types used in report processing and
writing data.
cmd_value_types include value types used in command actions and
termination processing.
Only reports that are specified by report_data_refs are notified with
report_cb.
If status_delay is None, periodical sending of status requests is
disabled.
Additional arguments are passed directly to hat.drivers.mms.connect
(request_cb and unconfirmed_cb are set by this coroutine).
88class Client(aio.Resource): 89 """Client""" 90 91 @property 92 def async_group(self): 93 """Async group""" 94 return self._conn.async_group 95 96 @property 97 def info(self) -> acse.ConnectionInfo: 98 """Connection info""" 99 return self._conn.info 100 101 async def create_dataset(self, 102 ref: common.DatasetRef, 103 data: Iterable[common.DataRef] 104 ) -> common.ServiceError | None: 105 """Create dataset""" 106 req = mms.DefineNamedVariableListRequest( 107 name=encoder.dataset_ref_to_object_name(ref), 108 specification=[ 109 mms.NameVariableSpecification( 110 encoder.data_ref_to_object_name(i)) 111 for i in data]) 112 113 res = await self._send(req) 114 115 if isinstance(res, mms.Error): 116 if res == mms.AccessError.OBJECT_NON_EXISTENT: 117 return common.ServiceError.INSTANCE_NOT_AVAILABLE 118 119 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 120 return common.ServiceError.ACCESS_VIOLATION 121 122 if res == mms.DefinitionError.OBJECT_EXISTS: 123 return common.ServiceError.INSTANCE_IN_USE 124 125 if res == mms.DefinitionError.OBJECT_UNDEFINED: 126 return common.ServiceError.PARAMETER_VALUE_INCONSISTENT 127 128 if res == mms.ResourceError.CAPABILITY_UNAVAILABLE: 129 return common.ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT 130 131 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 132 133 if not isinstance(res, mms.DefineNamedVariableListResponse): 134 raise Exception('unsupported response type') 135 136 async def delete_dataset(self, 137 ref: common.DatasetRef 138 ) -> common.ServiceError | None: 139 """Delete dataset""" 140 req = mms.DeleteNamedVariableListRequest([ 141 encoder.dataset_ref_to_object_name(ref)]) 142 143 res = await self._send(req) 144 145 if isinstance(res, mms.Error): 146 if res == mms.AccessError.OBJECT_NON_EXISTENT: 147 return common.ServiceError.INSTANCE_NOT_AVAILABLE 148 149 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 150 return common.ServiceError.ACCESS_VIOLATION 151 152 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 153 154 if not isinstance(res, mms.DeleteNamedVariableListResponse): 155 raise Exception('unsupported response type') 156 157 if res.matched == 0 and res.deleted == 0: 158 return common.ServiceError.INSTANCE_NOT_AVAILABLE 159 160 if res.matched != res.deleted: 161 return common.ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT 162 163 async def get_persisted_dataset_refs(self, 164 logical_device: str 165 ) -> Collection[common.PersistedDatasetRef] | common.ServiceError: # NOQA 166 """Get persisted dataset references associated with logical device""" 167 identifiers = await self._get_name_list( 168 object_class=mms.ObjectClass.NAMED_VARIABLE_LIST, 169 object_scope=mms.DomainSpecificObjectScope(logical_device)) 170 171 if isinstance(identifiers, common.ServiceError): 172 return identifiers 173 174 refs = collections.deque() 175 for identifier in identifiers: 176 logical_node, name = identifier.split('$') 177 refs.append( 178 common.PersistedDatasetRef(logical_device=logical_device, 179 logical_node=logical_node, 180 name=name)) 181 182 return refs 183 184 async def get_dataset_data_refs(self, 185 ref: common.DatasetRef 186 ) -> Collection[common.DataRef] | common.ServiceError: # NOQA 187 """Get data references associated with single dataset""" 188 req = mms.GetNamedVariableListAttributesRequest( 189 encoder.dataset_ref_to_object_name(ref)) 190 191 res = await self._send(req) 192 193 if isinstance(res, mms.Error): 194 if res == mms.AccessError.OBJECT_NON_EXISTENT: 195 return common.ServiceError.INSTANCE_NOT_AVAILABLE 196 197 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 198 return common.ServiceError.ACCESS_VIOLATION 199 200 if res == mms.ServiceError.PDU_SIZE: 201 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 202 203 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 204 205 if not isinstance(res, mms.GetNamedVariableListAttributesResponse): 206 raise Exception('unsupported response type') 207 208 refs = collections.deque() 209 210 for i in res.specification: 211 if not isinstance(i, mms.NameVariableSpecification): 212 raise Exception('unsupported specification type') 213 214 refs.append(encoder.data_ref_from_object_name(i.name)) 215 216 return refs 217 218 async def get_rcb_attrs(self, 219 ref: common.RcbRef, 220 attr_types: Collection[common.RcbAttrType] 221 ) -> dict[common.RcbAttrType, 222 common.RcbAttrValue | common.ServiceError]: # NOQA 223 """Get RCB attribute value""" 224 specification = collections.deque() 225 226 for attr_type in attr_types: 227 if ref.type == common.RcbType.BUFFERED: 228 if attr_type == common.RcbAttrType.RESERVE: 229 raise ValueError('unsupported attribute type') 230 231 elif ref.type == common.RcbType.UNBUFFERED: 232 if attr_type in (common.RcbAttrType.PURGE_BUFFER, 233 common.RcbAttrType.ENTRY_ID, 234 common.RcbAttrType.TIME_OF_ENTRY, 235 common.RcbAttrType.RESERVATION_TIME): 236 raise ValueError('unsupported attribute type') 237 238 else: 239 raise TypeError('unsupported rcb type') 240 241 specification.append( 242 mms.NameVariableSpecification( 243 encoder.data_ref_to_object_name( 244 common.DataRef(logical_device=ref.logical_device, 245 logical_node=ref.logical_node, 246 fc=ref.type.value, 247 names=(ref.name, attr_type.value))))) 248 249 req = mms.ReadRequest(specification) 250 251 res = await self._send(req) 252 253 if isinstance(res, mms.Error): 254 return {attr_type: common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 255 for attr_type in attr_types} 256 257 if not isinstance(res, mms.ReadResponse): 258 raise Exception('unsupported response type') 259 260 if len(res.results) != len(attr_types): 261 raise Exception('invalid results length') 262 263 results = {} 264 265 for attr_type, mms_data in zip(attr_types, res.results): 266 if isinstance(mms_data, mms.DataAccessError): 267 if mms_data == mms.DataAccessError.OBJECT_ACCESS_DENIED: 268 results[attr_type] = common.ServiceError.ACCESS_VIOLATION 269 270 elif mms_data == mms.DataAccessError.OBJECT_NON_EXISTENT: 271 results[attr_type] = common.ServiceError.INSTANCE_NOT_AVAILABLE # NOQA 272 273 else: 274 results[attr_type] = common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 275 276 else: 277 results[attr_type] = encoder.rcb_attr_value_from_mms_data( 278 mms_data, attr_type) 279 280 return results 281 282 async def set_rcb_attrs(self, 283 ref: common.RcbRef, 284 attrs: Collection[tuple[common.RcbAttrType, 285 common.RcbAttrValue]] 286 ) -> dict[common.RcbAttrType, 287 common.ServiceError | None]: 288 """Set RCB attribute values""" 289 specification = collections.deque() 290 data = collections.deque() 291 292 for attr_type, attr_value in attrs: 293 if ref.type == common.RcbType.BUFFERED: 294 if attr_type == common.RcbAttrType.RESERVE: 295 raise ValueError('unsupported attribute type') 296 297 elif ref.type == common.RcbType.UNBUFFERED: 298 if attr_type in (common.RcbAttrType.PURGE_BUFFER, 299 common.RcbAttrType.ENTRY_ID, 300 common.RcbAttrType.TIME_OF_ENTRY, 301 common.RcbAttrType.RESERVATION_TIME): 302 raise ValueError('unsupported attribute type') 303 304 else: 305 raise TypeError('unsupported rcb type') 306 307 specification.append( 308 mms.NameVariableSpecification( 309 encoder.data_ref_to_object_name( 310 common.DataRef(logical_device=ref.logical_device, 311 logical_node=ref.logical_node, 312 fc=ref.type.value, 313 names=(ref.name, attr_type.value))))) 314 data.append( 315 encoder.rcb_attr_value_to_mms_data(attr_value, attr_type)) 316 317 req = mms.WriteRequest(specification=specification, 318 data=data) 319 320 res = await self._send(req) 321 322 if isinstance(res, mms.Error): 323 return {attr_type: common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 324 for attr_type, _ in attrs} 325 326 if not isinstance(res, mms.WriteResponse): 327 raise Exception('unsupported response type') 328 329 if len(res.results) != len(attrs): 330 raise Exception('invalid results length') 331 332 results = {} 333 334 for (attr_type, _), mms_data in zip(attrs, res.results): 335 if mms_data is None: 336 results[attr_type] = None 337 338 elif mms_data == mms.DataAccessError.OBJECT_ACCESS_DENIED: 339 results[attr_type] = common.ServiceError.ACCESS_VIOLATION 340 341 elif mms_data == mms.DataAccessError.OBJECT_NON_EXISTENT: 342 results[attr_type] = common.ServiceError.INSTANCE_NOT_AVAILABLE 343 344 elif mms_data == mms.DataAccessError.TEMPORARILY_UNAVAILABLE: 345 results[attr_type] = common.ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT # NOQA 346 347 elif mms_data == mms.DataAccessError.TYPE_INCONSISTENT: 348 results[attr_type] = common.ServiceError.TYPE_CONFLICT 349 350 elif mms_data == mms.DataAccessError.OBJECT_VALUE_INVALID: 351 results[attr_type] = common.ServiceError.PARAMETER_VALUE_INCONSISTENT # NOQA 352 353 else: 354 results[attr_type] = common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 355 356 return results 357 358 async def write_data(self, 359 ref: common.DataRef, 360 value: common.Value 361 ) -> common.ServiceError | None: 362 """Write data""" 363 value_type = self._data_value_types[ref] 364 365 req = mms.WriteRequest( 366 specification=[ 367 mms.NameVariableSpecification( 368 encoder.data_ref_to_object_name(ref))], 369 data=[encoder.value_to_mms_data(value, value_type)]) 370 371 res = await self._send(req) 372 373 if isinstance(res, mms.Error): 374 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 375 376 if not isinstance(res, mms.WriteResponse): 377 raise Exception('unsupported response type') 378 379 if len(res.results) != 1: 380 raise Exception('invalid results size') 381 382 if res.results[0] is not None: 383 if res.results[0] == mms.DataAccessError.OBJECT_ACCESS_DENIED: 384 return common.ServiceError.ACCESS_VIOLATION 385 386 if res.results[0] == mms.DataAccessError.OBJECT_NON_EXISTENT: 387 return common.ServiceError.INSTANCE_NOT_AVAILABLE 388 389 if res.results[0] == mms.DataAccessError.TEMPORARILY_UNAVAILABLE: 390 return common.ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT 391 392 if res.results[0] == mms.DataAccessError.TYPE_INCONSISTENT: 393 return common.ServiceError.TYPE_CONFLICT 394 395 if res.results[0] == mms.DataAccessError.OBJECT_VALUE_INVALID: 396 return common.ServiceError.PARAMETER_VALUE_INCONSISTENT 397 398 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 399 400 async def select(self, 401 ref: common.CommandRef, 402 cmd: common.Command | None 403 ) -> common.CommandError | None: 404 """Select command""" 405 if cmd is not None: 406 return await self._command_with_last_appl_error(ref=ref, 407 cmd=cmd, 408 attr='SBOw', 409 with_checks=True) 410 411 req = mms.ReadRequest([ 412 mms.NameVariableSpecification( 413 encoder.data_ref_to_object_name( 414 common.DataRef(logical_device=ref.logical_device, 415 logical_node=ref.logical_node, 416 fc='CO', 417 names=(ref.name, 'SBO'))))]) 418 419 res = await self._send(req) 420 421 if isinstance(res, mms.Error): 422 return _create_command_error( 423 service_error=common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, # NOQA 424 last_appl_error=None) 425 426 if not isinstance(res, mms.ReadResponse): 427 raise Exception('unsupported response type') 428 429 if len(res.results) != 1: 430 raise Exception('invalid results size') 431 432 if not isinstance(res.results[0], mms.VisibleStringData): 433 if res.results[0] == mms.DataAccessError.OBJECT_ACCESS_DENIED: 434 service_error = common.ServiceError.ACCESS_VIOLATION 435 436 elif res.results[0] == mms.DataAccessError.OBJECT_NON_EXISTENT: 437 service_error = common.ServiceError.INSTANCE_NOT_AVAILABLE 438 439 else: 440 service_error = common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 441 442 return _create_command_error(service_error=service_error, 443 last_appl_error=None) 444 445 if res.results[0].value == '': 446 return _create_command_error( 447 service_error=common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, # NOQA 448 last_appl_error=None) 449 450 async def cancel(self, 451 ref: common.CommandRef, 452 cmd: common.Command 453 ) -> common.CommandError | None: 454 """Cancel command""" 455 return await self._command_with_last_appl_error(ref=ref, 456 cmd=cmd, 457 attr='Cancel', 458 with_checks=False) 459 460 async def operate(self, 461 ref: common.CommandRef, 462 cmd: common.Command 463 ) -> common.CommandError | None: 464 """Operate command""" 465 return await self._command_with_last_appl_error(ref=ref, 466 cmd=cmd, 467 attr='Oper', 468 with_checks=True) 469 470 async def _on_unconfirmed(self, conn, unconfirmed): 471 self._comm_log.debug('received %s', unconfirmed) 472 473 self._status_event.set() 474 475 if _is_unconfirmed_report(unconfirmed): 476 try: 477 await self._process_report(unconfirmed.data) 478 479 except Exception as e: 480 self._log.error("error processing report: %s", e, exc_info=e) 481 482 elif _is_unconfirmed_last_appl_error(unconfirmed): 483 try: 484 self._process_last_appl_error(unconfirmed.data[0]) 485 486 except Exception as e: 487 self._log.error("error processing last application error: %s", 488 e, exc_info=e) 489 490 elif _is_unconfirmed_termination(unconfirmed): 491 names = [i.name for i in unconfirmed.specification] 492 data = list(unconfirmed.data) 493 494 if len(names) != len(data): 495 self._log.warning("names/data size mismatch") 496 return 497 498 data_ref = encoder.data_ref_from_object_name(names[-1]) 499 500 try: 501 await self._process_termination( 502 ref=common.CommandRef( 503 logical_device=data_ref.logical_device, 504 logical_node=data_ref.logical_node, 505 name=data_ref.names[0]), 506 cmd_mms_data=data[-1], 507 last_appl_error_mms_data=(data[0] if len(data) > 1 508 else None)) 509 510 except Exception as e: 511 self._log.error("error processing termination: %s", 512 e, exc_info=e) 513 514 else: 515 self._log.info("received unprocessed unconfirmed message") 516 517 async def _process_report(self, mms_data): 518 if not self._report_cb: 519 self._log.info("report callback not defined - skipping report") 520 return 521 522 report_id = encoder.value_from_mms_data( 523 mms_data[0], common.BasicValueType.VISIBLE_STRING) 524 525 data_defs = self._report_data_defs.get(report_id) 526 if data_defs is None: 527 self._log.info("report id not defined - skipping report") 528 return 529 530 report = encoder.report_from_mms_data(mms_data, data_defs) 531 532 await aio.call(self._report_cb, report) 533 534 def _process_last_appl_error(self, mms_data): 535 last_appl_error = encoder.last_appl_error_from_mms_data(mms_data) 536 537 key = last_appl_error.name, last_appl_error.control_number 538 if key in self._last_appl_errors: 539 self._last_appl_errors[key] = last_appl_error 540 541 async def _process_termination(self, ref, cmd_mms_data, 542 last_appl_error_mms_data): 543 if not self._termination_cb: 544 self._log.info("termination callback not defined - " 545 "skipping termination") 546 return 547 548 value_type = self._cmd_value_types.get(ref) 549 if value_type is None: 550 self._log.info("command value type not defined - " 551 "skipping termination") 552 return 553 554 cmd = encoder.command_from_mms_data(mms_data=cmd_mms_data, 555 value_type=value_type, 556 with_checks=True) 557 558 if last_appl_error_mms_data: 559 error = _create_command_error( 560 service_error=None, 561 last_appl_error=encoder.last_appl_error_from_mms_data( 562 last_appl_error_mms_data)) 563 564 else: 565 error = None 566 567 termination = common.Termination(ref=ref, 568 cmd=cmd, 569 error=error) 570 571 await aio.call(self._termination_cb, termination) 572 573 async def _send(self, req): 574 self._comm_log.debug('sending %s', req) 575 576 res = await self._conn.send_confirmed(req) 577 self._status_event.set() 578 579 self._comm_log.debug('received %s', res) 580 581 return res 582 583 async def _status_loop(self, delay, timeout): 584 try: 585 self._log.debug("starting status loop") 586 while True: 587 self._status_event.clear() 588 589 with contextlib.suppress(asyncio.TimeoutError): 590 await aio.wait_for(self._status_event.wait(), delay) 591 continue 592 593 self._log.debug("sending status request") 594 await aio.wait_for(self._send(mms.StatusRequest()), timeout) 595 596 except asyncio.TimeoutError: 597 self._log.warning("status timeout") 598 599 except ConnectionError: 600 pass 601 602 except Exception as e: 603 self._log.error("status loop error: %s", e, exc_info=e) 604 605 finally: 606 self._log.debug("stopping status loop") 607 self.close() 608 609 async def _get_name_list(self, object_class, object_scope): 610 identifiers = collections.deque() 611 continue_after = None 612 613 while True: 614 req = mms.GetNameListRequest( 615 object_class=object_class, 616 object_scope=object_scope, 617 continue_after=continue_after) 618 619 res = await self._send(req) 620 621 if isinstance(res, mms.Error): 622 if res == mms.AccessError.OBJECT_NON_EXISTENT: 623 return common.ServiceError.INSTANCE_NOT_AVAILABLE 624 625 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 626 return common.ServiceError.ACCESS_VIOLATION 627 628 if res == mms.ServiceError.OBJECT_CONSTRAINT_CONFLICT: 629 return common.ServiceError.PARAMETER_VALUE_INCONSISTENT 630 631 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 632 633 if not isinstance(res, mms.GetNameListResponse): 634 raise Exception('unsupported response type') 635 636 identifiers.extend(res.identifiers) 637 638 if not res.more_follows: 639 break 640 641 if not res.identifiers: 642 raise Exception('invalid more follows value') 643 644 continue_after = identifiers[-1] 645 646 return identifiers 647 648 async def _command_with_last_appl_error(self, ref, cmd, attr, with_checks): 649 value_type = self._cmd_value_types[ref] 650 651 req = mms.WriteRequest( 652 specification=[ 653 mms.NameVariableSpecification( 654 encoder.data_ref_to_object_name( 655 common.DataRef(logical_device=ref.logical_device, 656 logical_node=ref.logical_node, 657 fc='CO', 658 names=(ref.name, attr))))], 659 data=[encoder.command_to_mms_data(cmd=cmd, 660 value_type=value_type, 661 with_checks=with_checks)]) 662 663 name = (f'{req.specification[0].name.domain_id}/' 664 f'{req.specification[0].name.item_id}') 665 key = name, cmd.control_number 666 667 if key in self._last_appl_errors: 668 raise Exception('active control number duplicate') 669 670 self._last_appl_errors[key] = None 671 672 try: 673 res = await self._send(req) 674 675 finally: 676 last_appl_error = self._last_appl_errors.pop(key, None) 677 678 if isinstance(res, mms.Error): 679 return _create_command_error( 680 service_error=common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, # NOQA 681 last_appl_error=last_appl_error) 682 683 if not isinstance(res, mms.WriteResponse): 684 raise Exception('unsupported response type') 685 686 if len(res.results) != 1: 687 raise Exception('invalid results size') 688 689 if res.results[0] is not None: 690 return _create_command_error(service_error=None, 691 last_appl_error=last_appl_error)
Client
96 @property 97 def info(self) -> acse.ConnectionInfo: 98 """Connection info""" 99 return self._conn.info
Connection info
101 async def create_dataset(self, 102 ref: common.DatasetRef, 103 data: Iterable[common.DataRef] 104 ) -> common.ServiceError | None: 105 """Create dataset""" 106 req = mms.DefineNamedVariableListRequest( 107 name=encoder.dataset_ref_to_object_name(ref), 108 specification=[ 109 mms.NameVariableSpecification( 110 encoder.data_ref_to_object_name(i)) 111 for i in data]) 112 113 res = await self._send(req) 114 115 if isinstance(res, mms.Error): 116 if res == mms.AccessError.OBJECT_NON_EXISTENT: 117 return common.ServiceError.INSTANCE_NOT_AVAILABLE 118 119 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 120 return common.ServiceError.ACCESS_VIOLATION 121 122 if res == mms.DefinitionError.OBJECT_EXISTS: 123 return common.ServiceError.INSTANCE_IN_USE 124 125 if res == mms.DefinitionError.OBJECT_UNDEFINED: 126 return common.ServiceError.PARAMETER_VALUE_INCONSISTENT 127 128 if res == mms.ResourceError.CAPABILITY_UNAVAILABLE: 129 return common.ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT 130 131 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 132 133 if not isinstance(res, mms.DefineNamedVariableListResponse): 134 raise Exception('unsupported response type')
Create dataset
136 async def delete_dataset(self, 137 ref: common.DatasetRef 138 ) -> common.ServiceError | None: 139 """Delete dataset""" 140 req = mms.DeleteNamedVariableListRequest([ 141 encoder.dataset_ref_to_object_name(ref)]) 142 143 res = await self._send(req) 144 145 if isinstance(res, mms.Error): 146 if res == mms.AccessError.OBJECT_NON_EXISTENT: 147 return common.ServiceError.INSTANCE_NOT_AVAILABLE 148 149 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 150 return common.ServiceError.ACCESS_VIOLATION 151 152 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 153 154 if not isinstance(res, mms.DeleteNamedVariableListResponse): 155 raise Exception('unsupported response type') 156 157 if res.matched == 0 and res.deleted == 0: 158 return common.ServiceError.INSTANCE_NOT_AVAILABLE 159 160 if res.matched != res.deleted: 161 return common.ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT
Delete dataset
163 async def get_persisted_dataset_refs(self, 164 logical_device: str 165 ) -> Collection[common.PersistedDatasetRef] | common.ServiceError: # NOQA 166 """Get persisted dataset references associated with logical device""" 167 identifiers = await self._get_name_list( 168 object_class=mms.ObjectClass.NAMED_VARIABLE_LIST, 169 object_scope=mms.DomainSpecificObjectScope(logical_device)) 170 171 if isinstance(identifiers, common.ServiceError): 172 return identifiers 173 174 refs = collections.deque() 175 for identifier in identifiers: 176 logical_node, name = identifier.split('$') 177 refs.append( 178 common.PersistedDatasetRef(logical_device=logical_device, 179 logical_node=logical_node, 180 name=name)) 181 182 return refs
Get persisted dataset references associated with logical device
184 async def get_dataset_data_refs(self, 185 ref: common.DatasetRef 186 ) -> Collection[common.DataRef] | common.ServiceError: # NOQA 187 """Get data references associated with single dataset""" 188 req = mms.GetNamedVariableListAttributesRequest( 189 encoder.dataset_ref_to_object_name(ref)) 190 191 res = await self._send(req) 192 193 if isinstance(res, mms.Error): 194 if res == mms.AccessError.OBJECT_NON_EXISTENT: 195 return common.ServiceError.INSTANCE_NOT_AVAILABLE 196 197 if res == mms.AccessError.OBJECT_ACCESS_DENIED: 198 return common.ServiceError.ACCESS_VIOLATION 199 200 if res == mms.ServiceError.PDU_SIZE: 201 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 202 203 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 204 205 if not isinstance(res, mms.GetNamedVariableListAttributesResponse): 206 raise Exception('unsupported response type') 207 208 refs = collections.deque() 209 210 for i in res.specification: 211 if not isinstance(i, mms.NameVariableSpecification): 212 raise Exception('unsupported specification type') 213 214 refs.append(encoder.data_ref_from_object_name(i.name)) 215 216 return refs
Get data references associated with single dataset
218 async def get_rcb_attrs(self, 219 ref: common.RcbRef, 220 attr_types: Collection[common.RcbAttrType] 221 ) -> dict[common.RcbAttrType, 222 common.RcbAttrValue | common.ServiceError]: # NOQA 223 """Get RCB attribute value""" 224 specification = collections.deque() 225 226 for attr_type in attr_types: 227 if ref.type == common.RcbType.BUFFERED: 228 if attr_type == common.RcbAttrType.RESERVE: 229 raise ValueError('unsupported attribute type') 230 231 elif ref.type == common.RcbType.UNBUFFERED: 232 if attr_type in (common.RcbAttrType.PURGE_BUFFER, 233 common.RcbAttrType.ENTRY_ID, 234 common.RcbAttrType.TIME_OF_ENTRY, 235 common.RcbAttrType.RESERVATION_TIME): 236 raise ValueError('unsupported attribute type') 237 238 else: 239 raise TypeError('unsupported rcb type') 240 241 specification.append( 242 mms.NameVariableSpecification( 243 encoder.data_ref_to_object_name( 244 common.DataRef(logical_device=ref.logical_device, 245 logical_node=ref.logical_node, 246 fc=ref.type.value, 247 names=(ref.name, attr_type.value))))) 248 249 req = mms.ReadRequest(specification) 250 251 res = await self._send(req) 252 253 if isinstance(res, mms.Error): 254 return {attr_type: common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 255 for attr_type in attr_types} 256 257 if not isinstance(res, mms.ReadResponse): 258 raise Exception('unsupported response type') 259 260 if len(res.results) != len(attr_types): 261 raise Exception('invalid results length') 262 263 results = {} 264 265 for attr_type, mms_data in zip(attr_types, res.results): 266 if isinstance(mms_data, mms.DataAccessError): 267 if mms_data == mms.DataAccessError.OBJECT_ACCESS_DENIED: 268 results[attr_type] = common.ServiceError.ACCESS_VIOLATION 269 270 elif mms_data == mms.DataAccessError.OBJECT_NON_EXISTENT: 271 results[attr_type] = common.ServiceError.INSTANCE_NOT_AVAILABLE # NOQA 272 273 else: 274 results[attr_type] = common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 275 276 else: 277 results[attr_type] = encoder.rcb_attr_value_from_mms_data( 278 mms_data, attr_type) 279 280 return results
Get RCB attribute value
282 async def set_rcb_attrs(self, 283 ref: common.RcbRef, 284 attrs: Collection[tuple[common.RcbAttrType, 285 common.RcbAttrValue]] 286 ) -> dict[common.RcbAttrType, 287 common.ServiceError | None]: 288 """Set RCB attribute values""" 289 specification = collections.deque() 290 data = collections.deque() 291 292 for attr_type, attr_value in attrs: 293 if ref.type == common.RcbType.BUFFERED: 294 if attr_type == common.RcbAttrType.RESERVE: 295 raise ValueError('unsupported attribute type') 296 297 elif ref.type == common.RcbType.UNBUFFERED: 298 if attr_type in (common.RcbAttrType.PURGE_BUFFER, 299 common.RcbAttrType.ENTRY_ID, 300 common.RcbAttrType.TIME_OF_ENTRY, 301 common.RcbAttrType.RESERVATION_TIME): 302 raise ValueError('unsupported attribute type') 303 304 else: 305 raise TypeError('unsupported rcb type') 306 307 specification.append( 308 mms.NameVariableSpecification( 309 encoder.data_ref_to_object_name( 310 common.DataRef(logical_device=ref.logical_device, 311 logical_node=ref.logical_node, 312 fc=ref.type.value, 313 names=(ref.name, attr_type.value))))) 314 data.append( 315 encoder.rcb_attr_value_to_mms_data(attr_value, attr_type)) 316 317 req = mms.WriteRequest(specification=specification, 318 data=data) 319 320 res = await self._send(req) 321 322 if isinstance(res, mms.Error): 323 return {attr_type: common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 324 for attr_type, _ in attrs} 325 326 if not isinstance(res, mms.WriteResponse): 327 raise Exception('unsupported response type') 328 329 if len(res.results) != len(attrs): 330 raise Exception('invalid results length') 331 332 results = {} 333 334 for (attr_type, _), mms_data in zip(attrs, res.results): 335 if mms_data is None: 336 results[attr_type] = None 337 338 elif mms_data == mms.DataAccessError.OBJECT_ACCESS_DENIED: 339 results[attr_type] = common.ServiceError.ACCESS_VIOLATION 340 341 elif mms_data == mms.DataAccessError.OBJECT_NON_EXISTENT: 342 results[attr_type] = common.ServiceError.INSTANCE_NOT_AVAILABLE 343 344 elif mms_data == mms.DataAccessError.TEMPORARILY_UNAVAILABLE: 345 results[attr_type] = common.ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT # NOQA 346 347 elif mms_data == mms.DataAccessError.TYPE_INCONSISTENT: 348 results[attr_type] = common.ServiceError.TYPE_CONFLICT 349 350 elif mms_data == mms.DataAccessError.OBJECT_VALUE_INVALID: 351 results[attr_type] = common.ServiceError.PARAMETER_VALUE_INCONSISTENT # NOQA 352 353 else: 354 results[attr_type] = common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 355 356 return results
Set RCB attribute values
358 async def write_data(self, 359 ref: common.DataRef, 360 value: common.Value 361 ) -> common.ServiceError | None: 362 """Write data""" 363 value_type = self._data_value_types[ref] 364 365 req = mms.WriteRequest( 366 specification=[ 367 mms.NameVariableSpecification( 368 encoder.data_ref_to_object_name(ref))], 369 data=[encoder.value_to_mms_data(value, value_type)]) 370 371 res = await self._send(req) 372 373 if isinstance(res, mms.Error): 374 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT 375 376 if not isinstance(res, mms.WriteResponse): 377 raise Exception('unsupported response type') 378 379 if len(res.results) != 1: 380 raise Exception('invalid results size') 381 382 if res.results[0] is not None: 383 if res.results[0] == mms.DataAccessError.OBJECT_ACCESS_DENIED: 384 return common.ServiceError.ACCESS_VIOLATION 385 386 if res.results[0] == mms.DataAccessError.OBJECT_NON_EXISTENT: 387 return common.ServiceError.INSTANCE_NOT_AVAILABLE 388 389 if res.results[0] == mms.DataAccessError.TEMPORARILY_UNAVAILABLE: 390 return common.ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT 391 392 if res.results[0] == mms.DataAccessError.TYPE_INCONSISTENT: 393 return common.ServiceError.TYPE_CONFLICT 394 395 if res.results[0] == mms.DataAccessError.OBJECT_VALUE_INVALID: 396 return common.ServiceError.PARAMETER_VALUE_INCONSISTENT 397 398 return common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT
Write data
400 async def select(self, 401 ref: common.CommandRef, 402 cmd: common.Command | None 403 ) -> common.CommandError | None: 404 """Select command""" 405 if cmd is not None: 406 return await self._command_with_last_appl_error(ref=ref, 407 cmd=cmd, 408 attr='SBOw', 409 with_checks=True) 410 411 req = mms.ReadRequest([ 412 mms.NameVariableSpecification( 413 encoder.data_ref_to_object_name( 414 common.DataRef(logical_device=ref.logical_device, 415 logical_node=ref.logical_node, 416 fc='CO', 417 names=(ref.name, 'SBO'))))]) 418 419 res = await self._send(req) 420 421 if isinstance(res, mms.Error): 422 return _create_command_error( 423 service_error=common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, # NOQA 424 last_appl_error=None) 425 426 if not isinstance(res, mms.ReadResponse): 427 raise Exception('unsupported response type') 428 429 if len(res.results) != 1: 430 raise Exception('invalid results size') 431 432 if not isinstance(res.results[0], mms.VisibleStringData): 433 if res.results[0] == mms.DataAccessError.OBJECT_ACCESS_DENIED: 434 service_error = common.ServiceError.ACCESS_VIOLATION 435 436 elif res.results[0] == mms.DataAccessError.OBJECT_NON_EXISTENT: 437 service_error = common.ServiceError.INSTANCE_NOT_AVAILABLE 438 439 else: 440 service_error = common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT # NOQA 441 442 return _create_command_error(service_error=service_error, 443 last_appl_error=None) 444 445 if res.results[0].value == '': 446 return _create_command_error( 447 service_error=common.ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, # NOQA 448 last_appl_error=None)
Select command
450 async def cancel(self, 451 ref: common.CommandRef, 452 cmd: common.Command 453 ) -> common.CommandError | None: 454 """Cancel command""" 455 return await self._command_with_last_appl_error(ref=ref, 456 cmd=cmd, 457 attr='Cancel', 458 with_checks=False)
Cancel command
17class PersistedDatasetRef(typing.NamedTuple): 18 logical_device: str 19 logical_node: str 20 name: str
PersistedDatasetRef(logical_device, logical_node, name)
NonPersistedDatasetRef(name,)
30class DataRef(typing.NamedTuple): 31 logical_device: str 32 logical_node: str 33 fc: str 34 names: tuple[str | int, ...]
DataRef(logical_device, logical_node, fc, names)
CommandRef(logical_device, logical_node, name)
48class RcbRef(typing.NamedTuple): 49 logical_device: str 50 logical_node: str 51 type: RcbType 52 name: str
RcbRef(logical_device, logical_node, type, name)
Create new instance of RcbRef(logical_device, logical_node, type, name)
57class BasicValueType(enum.Enum): 58 BOOLEAN = 'BOOLEAN' # bool 59 INTEGER = 'INTEGER' # int 60 UNSIGNED = 'UNSIGNED' # int 61 FLOAT = 'FLOAT' # float 62 BIT_STRING = 'BIT_STRING' # Collection[bool] 63 OCTET_STRING = 'OCTET_STRING' # util.Bytes 64 VISIBLE_STRING = 'VISIBLE_STRING' # str 65 MMS_STRING = 'MMS_STRING' # str
68class AcsiValueType(enum.Enum): 69 QUALITY = 'QUALITY' 70 TIMESTAMP = 'TIMESTAMP' 71 DOUBLE_POINT = 'DOUBLE_POINT' 72 DIRECTION = 'DIRECTION' 73 SEVERITY = 'SEVERITY' 74 ANALOGUE = 'ANALOGUE' 75 VECTOR = 'VECTOR' 76 STEP_POSITION = 'STEP_POSITION' 77 BINARY_CONTROL = 'BINARY_CONTROL'
ArrayValueType(type, length)
StructValueType(elements,)
Create new instance of StructValueType(elements,)
Alias for field number 0
97class Timestamp(typing.NamedTuple): 98 value: datetime.datetime 99 leap_second: bool 100 clock_failure: bool 101 not_synchronized: bool 102 accuracy: int | None 103 """accurate fraction bits [0,24]"""
Timestamp(value, leap_second, clock_failure, not_synchronized, accuracy)
106class QualityValidity(enum.Enum): 107 GOOD = 0 108 INVALID = 1 109 RESERVED = 2 110 QUESTIONABLE = 3
113class QualityDetail(enum.Enum): 114 OVERFLOW = 2 115 OUT_OF_RANGE = 3 116 BAD_REFERENCE = 4 117 OSCILLATORY = 5 118 FAILURE = 6 119 OLD_DATA = 7 120 INCONSISTENT = 8 121 INACCURATE = 9
129class Quality(typing.NamedTuple): 130 validity: QualityValidity 131 details: set[QualityDetail] 132 source: QualitySource 133 test: bool 134 operator_blocked: bool
Quality(validity, details, source, test, operator_blocked)
Create new instance of Quality(validity, details, source, test, operator_blocked)
151class Severity(enum.Enum): 152 UNKNOWN = 0 153 CRITICAL = 1 154 MAJOR = 2 155 MINOR = 3 156 WARNING = 4
Analogue(i, f)
Vector(magnitude, angle)
169class StepPosition(typing.NamedTuple): 170 value: int 171 """value in range [-64, 63]""" 172 transient: bool | None
StepPosition(value, transient)
197class ServiceError(enum.Enum): 198 NO_ERROR = 0 199 INSTANCE_NOT_AVAILABLE = 1 200 INSTANCE_IN_USE = 2 201 ACCESS_VIOLATION = 3 202 ACCESS_NOT_ALLOWED_IN_CURRENT_STATE = 4 203 PARAMETER_VALUE_INAPPROPRIATE = 5 204 PARAMETER_VALUE_INCONSISTENT = 6 205 CLASS_NOT_SUPPORTED = 7 206 INSTANCE_LOCKED_BY_OTHER_CLIENT = 8 207 CONTROL_MUST_BE_SELECTED = 9 208 TYPE_CONFLICT = 10 209 FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT = 11 210 FAILED_DUE_TO_SERVER_CONSTRAINT = 12
213class AdditionalCause(enum.Enum): 214 UNKNOWN = 0 215 NOT_SUPPORTED = 1 216 BLOCKED_BY_SWITCHING_HIERARCHY = 2 217 SELECT_FAILED = 3 218 INVALID_POSITION = 4 219 POSITION_REACHED = 5 220 PARAMETER_CHANGE_IN_EXECUTION = 6 221 STEP_LIMIT = 7 222 BLOCKED_BY_MODE = 8 223 BLOCKED_BY_PROCESS = 9 224 BLOCKED_BY_INTERLOCKING = 10 225 BLOCKED_BY_SYNCHROCHECK = 11 226 COMMAND_ALREADY_IN_EXECUTION = 12 227 BLOCKED_BY_HEALTH = 13 228 ONE_OF_N_CONTROL = 14 229 ABORTION_BY_CANCEL = 15 230 TIME_LIMIT_OVER = 16 231 ABORTION_BY_TRIP = 17 232 OBJECT_NOT_SELECTED = 18 233 OBJECT_ALREADY_SELECTED = 19 234 NO_ACCESS_AUTHORITY = 20 235 ENDED_WITH_OVERSHOOT = 21 236 ABORTION_DUE_TO_DEVIATION = 22 237 ABORTION_BY_COMMUNICATION_LOSS = 23 238 BLOCKED_BY_COMMAND = 24 239 NONE = 25 240 INCONSISTENT_PARAMETERS = 26 241 LOCKED_BY_OTHER_CLIENT = 27
244class TestError(enum.Enum): 245 NO_ERROR = 0 246 UNKNOWN = 1 247 TIMEOUT_TEST_NOT_OK = 2 248 OPERATOR_TEST_NOT_OK = 3
251class CommandError(typing.NamedTuple): 252 service_error: ServiceError | None 253 additional_cause: AdditionalCause | None 254 test_error: TestError | None
CommandError(service_error, additional_cause, test_error)
Create new instance of CommandError(service_error, additional_cause, test_error)
259class OptionalField(enum.Enum): 260 SEQUENCE_NUMBER = 1 261 REPORT_TIME_STAMP = 2 262 REASON_FOR_INCLUSION = 3 263 DATA_SET_NAME = 4 264 DATA_REFERENCE = 5 265 BUFFER_OVERFLOW = 6 266 ENTRY_ID = 7 267 CONF_REVISION = 8
270class TriggerCondition(enum.Enum): 271 DATA_CHANGE = 1 272 QUALITY_CHANGE = 2 273 DATA_UPDATE = 3 274 INTEGRITY = 4 275 GENERAL_INTERROGATION = 5
278class RcbAttrType(enum.Enum): 279 REPORT_ID = 'RptID' 280 REPORT_ENABLE = 'RptEna' 281 DATASET = 'DatSet' 282 CONF_REVISION = 'ConfRev' 283 OPTIONAL_FIELDS = 'OptFlds' 284 BUFFER_TIME = 'BufTm' 285 SEQUENCE_NUMBER = 'SqNum' 286 TRIGGER_OPTIONS = 'TrgOps' 287 INTEGRITY_PERIOD = 'IntgPd' 288 GI = 'GI' 289 PURGE_BUFFER = 'PurgeBuf' # brcb 290 ENTRY_ID = 'EntryID' # brcb 291 TIME_OF_ENTRY = 'TimeOfEntry' # brcb 292 RESERVATION_TIME = 'ResvTms' # brcb 293 RESERVE = 'Resv' # urcb
345class Reason(enum.Enum): 346 DATA_CHANGE = 1 347 QUALITY_CHANGE = 2 348 DATA_UPDATE = 3 349 INTEGRITY = 4 350 GENERAL_INTERROGATION = 5 351 APPLICATION_TRIGGER = 6
354class ReportData(typing.NamedTuple): 355 ref: DataRef 356 value: Value 357 reasons: set[Reason] | None
ReportData(ref, value, reasons)
Create new instance of ReportData(ref, value, reasons)
Alias for field number 1
360class Report(typing.NamedTuple): 361 report_id: ReportId 362 sequence_number: int | None 363 subsequence_number: int | None 364 more_segments_follow: bool | None 365 dataset: DatasetRef | None 366 buffer_overflow: bool | None 367 conf_revision: int | None 368 entry_time: EntryTime | None 369 entry_id: util.Bytes | None 370 data: Collection[ReportData]
Report(report_id, sequence_number, subsequence_number, more_segments_follow, dataset, buffer_overflow, conf_revision, entry_time, entry_id, data)
Create new instance of Report(report_id, sequence_number, subsequence_number, more_segments_follow, dataset, buffer_overflow, conf_revision, entry_time, entry_id, data)
375class ControlModel(enum.Enum): 376 DIRECT_WITH_NORMAL_SECURITY = 1 377 SBO_WITH_NORMAL_SECURITY = 2 378 DIRECT_WITH_ENHANCED_SECURITY = 3 379 SBO_WITH_ENHANCED_SECURITY = 4
382class OriginCategory(enum.Enum): 383 NOT_SUPPORTED = 0 384 BAY_CONTROL = 1 385 STATION_CONTROL = 2 386 REMOTE_CONTROL = 3 387 AUTOMATIC_BAY = 4 388 AUTOMATIC_STATION = 5 389 AUTOMATIC_REMOTE = 6 390 MAINTENANCE = 7 391 PROCESS = 8
Origin(category, identification)
Create new instance of Origin(category, identification)
404class Command(typing.NamedTuple): 405 value: Value 406 operate_time: Timestamp | None 407 origin: Origin 408 control_number: int 409 """control number in range [0, 255]""" 410 t: Timestamp 411 test: bool 412 checks: set[Check] 413 """ignored in cancel action"""
Command(value, operate_time, origin, control_number, t, test, checks)
Create new instance of Command(value, operate_time, origin, control_number, t, test, checks)
Alias for field number 0
416class Termination(typing.NamedTuple): 417 ref: CommandRef 418 cmd: Command 419 error: CommandError | None
Termination(ref, cmd, error)
Create new instance of Termination(ref, cmd, error)