hat.gateway.devices.iec101.slave
IEC 60870-5-101 slave device
1"""IEC 60870-5-101 slave device""" 2 3from collections.abc import Iterable 4import asyncio 5import collections 6import contextlib 7import functools 8import itertools 9import logging 10 11from hat import aio 12from hat import json 13from hat.drivers import iec101 14from hat.drivers import serial 15from hat.drivers.iec60870 import link 16import hat.event.common 17import hat.event.eventer 18 19from hat.gateway.devices.iec101 import common 20 21 22mlog: logging.Logger = logging.getLogger(__name__) 23 24 25async def create(conf: common.DeviceConf, 26 eventer_client: hat.event.eventer.Client, 27 event_type_prefix: common.EventTypePrefix 28 ) -> 'Iec101SlaveDevice': 29 device = Iec101SlaveDevice() 30 device._conf = conf 31 device._eventer_client = eventer_client 32 device._event_type_prefix = event_type_prefix 33 device._next_conn_ids = itertools.count(1) 34 device._conns = {} 35 device._send_queues = {} 36 device._buffers = {} 37 device._data_msgs = {} 38 device._data_buffers = {} 39 device._data_with_ack = set() 40 device._broadcast_asdu_address = _get_broadcast_asdu_address( 41 iec101.AsduAddressSize[conf['asdu_address_size']]) 42 device._log = _create_logger_adapter(conf['name']) 43 44 init_buffers(buffers_conf=conf['buffers'], 45 buffers=device._buffers) 46 47 await init_data(log=device._log, 48 data_conf=conf['data'], 49 data_msgs=device._data_msgs, 50 data_buffers=device._data_buffers, 51 data_with_ack=device._data_with_ack, 52 buffers=device._buffers, 53 eventer_client=eventer_client, 54 event_type_prefix=event_type_prefix) 55 56 if conf['link_type'] == 'BALANCED': 57 create_link = link.create_balanced_link 58 59 elif conf['link_type'] == 'UNBALANCED': 60 create_link = link.create_slave_link 61 62 else: 63 raise ValueError('unsupported link type') 64 65 device._link = await create_link( 66 port=conf['port'], 67 address_size=link.AddressSize[conf['device_address_size']], 68 silent_interval=conf['silent_interval'], 69 baudrate=conf['baudrate'], 70 bytesize=serial.ByteSize[conf['bytesize']], 71 parity=serial.Parity[conf['parity']], 72 stopbits=serial.StopBits[conf['stopbits']], 73 xonxoff=conf['flow_control']['xonxoff'], 74 rtscts=conf['flow_control']['rtscts'], 75 dsrdtr=conf['flow_control']['dsrdtr'], 76 name=conf['name']) 77 78 try: 79 for device_conf in conf['devices']: 80 device.async_group.spawn(device._connection_loop, device_conf) 81 82 await device._register_connections() 83 84 except BaseException: 85 await aio.uncancellable(device.async_close()) 86 raise 87 88 return device 89 90 91info: common.DeviceInfo = common.DeviceInfo( 92 type="iec101_slave", 93 create=create, 94 json_schema_id="hat-gateway://iec101.yaml#/$defs/slave", 95 json_schema_repo=common.json_schema_repo) 96 97 98class Buffer: 99 100 def __init__(self, size: int): 101 self._size = size 102 self._data = collections.OrderedDict() 103 104 def add(self, 105 event_id: hat.event.common.EventId, 106 data_key: common.DataKey, 107 data_msg: iec101.DataMsg): 108 self._data[event_id] = data_key, data_msg 109 while len(self._data) > self._size: 110 self._data.popitem(last=False) 111 112 def remove(self, event_id: hat.event.common.EventId): 113 self._data.pop(event_id, None) 114 115 def get(self) -> Iterable[tuple[hat.event.common.EventId, 116 common.DataKey, 117 iec101.DataMsg]]: 118 return ((event_id, data_key, data_msg) 119 for event_id, (data_key, data_msg) in self._data.items()) 120 121 122def init_buffers(buffers_conf: json.Data, 123 buffers: dict[str, Buffer]): 124 for buffer_conf in buffers_conf: 125 buffers[buffer_conf['name']] = Buffer(buffer_conf['size']) 126 127 128async def init_data(log: logging.Logger, 129 data_conf: json.Data, 130 data_msgs: dict[common.DataKey, iec101.DataMsg], 131 data_buffers: dict[common.DataKey, Buffer], 132 data_with_ack: set[common.DataKey] | None, 133 buffers: dict[str, Buffer], 134 eventer_client: hat.event.eventer.Client, 135 event_type_prefix: common.EventTypePrefix): 136 for data in data_conf: 137 data_key = common.DataKey(data_type=common.DataType[data['data_type']], 138 asdu_address=data['asdu_address'], 139 io_address=data['io_address']) 140 141 data_msgs[data_key] = None 142 143 if data['buffer']: 144 data_buffers[data_key] = buffers[data['buffer']] 145 146 if data_with_ack is not None and data['with_ack']: 147 data_with_ack.add(data_key) 148 149 event_types = [(*event_type_prefix, 'system', 'data', '*')] 150 params = hat.event.common.QueryLatestParams(event_types) 151 result = await eventer_client.query(params) 152 153 for event in result.events: 154 try: 155 data_type_str, asdu_address_str, io_address_str = \ 156 event.type[len(event_type_prefix)+2:] 157 data_key = common.DataKey(data_type=common.DataType(data_type_str), 158 asdu_address=int(asdu_address_str), 159 io_address=int(io_address_str)) 160 if data_key not in data_msgs: 161 raise Exception(f'data {data_key} not configured') 162 163 data_msgs[data_key] = data_msg_from_event(data_key, event) 164 165 except Exception as e: 166 log.debug('skipping initial data: %s', e, exc_info=e) 167 168 169class Iec101SlaveDevice(common.Device): 170 171 @property 172 def async_group(self) -> aio.Group: 173 return self._link.async_group 174 175 async def process_event(self, event: hat.event.common.Event): 176 try: 177 await self._process_event(event) 178 179 except Exception as e: 180 self._log.warning('error processing event: %s', e, exc_info=e) 181 182 async def _connection_loop(self, device_conf): 183 conn = None 184 185 try: 186 if self._conf['link_type'] == 'BALANCED': 187 conn_args = { 188 'direction': link.Direction[device_conf['direction']], 189 'addr': device_conf['address'], 190 'response_timeout': device_conf['response_timeout'], 191 'send_retry_count': device_conf['send_retry_count'], 192 'status_delay': device_conf['status_delay'], 193 'name': self._conf['name']} 194 195 elif self._conf['link_type'] == 'UNBALANCED': 196 conn_args = { 197 'addr': device_conf['address'], 198 'keep_alive_timeout': device_conf['keep_alive_timeout'], 199 'name': self._conf['name']} 200 201 else: 202 raise ValueError('unsupported link type') 203 204 while True: 205 try: 206 conn = await self._link.open_connection(**conn_args) 207 208 except Exception as e: 209 self._log.error('connection error for address %s: %s', 210 device_conf['address'], e, exc_info=e) 211 await asyncio.sleep(device_conf['reconnect_delay']) 212 continue 213 214 conn = iec101.Connection( 215 conn=conn, 216 cause_size=iec101.CauseSize[self._conf['cause_size']], 217 asdu_address_size=iec101.AsduAddressSize[ 218 self._conf['asdu_address_size']], 219 io_address_size=iec101.IoAddressSize[ 220 self._conf['io_address_size']]) 221 222 conn_id = next(self._next_conn_ids) 223 self._conns[conn_id] = conn 224 225 send_queue = aio.Queue(1024) 226 self._send_queues[conn_id] = send_queue 227 228 try: 229 conn.async_group.spawn(self._connection_send_loop, conn, 230 send_queue) 231 conn.async_group.spawn(self._connection_receive_loop, conn, 232 conn_id) 233 234 await self._register_connections() 235 236 with contextlib.suppress(Exception): 237 for buffer in self._buffers.values(): 238 for event_id, data_key, data_msg in buffer.get(): 239 await self._send_data_msg( 240 conn_id=conn_id, 241 buffer=buffer, 242 event_id=event_id, 243 data_msg=data_msg, 244 with_ack=data_key in self._data_with_ack) 245 246 await conn.wait_closed() 247 248 finally: 249 send_queue.close() 250 251 self._conns.pop(conn_id, None) 252 self._send_queues.pop(conn_id, None) 253 254 with contextlib.suppress(Exception): 255 await aio.uncancellable(self._register_connections()) 256 257 await conn.async_close() 258 259 except Exception as e: 260 self._log.warning('connection loop error: %s', e, exc_info=e) 261 262 finally: 263 self._log.debug('closing connection') 264 self.close() 265 266 if conn: 267 await aio.uncancellable(conn.async_close()) 268 269 async def _connection_send_loop(self, conn, send_queue): 270 try: 271 while True: 272 msgs, sent_cb, with_ack = await send_queue.get() 273 await conn.send(msgs, 274 sent_cb=sent_cb, 275 with_ack=with_ack) 276 277 except ConnectionError: 278 self._log.debug('connection close') 279 280 except Exception as e: 281 self._log.warning('connection send loop error: %s', e, exc_info=e) 282 283 finally: 284 conn.close() 285 286 async def _connection_receive_loop(self, conn, conn_id): 287 try: 288 while True: 289 try: 290 msgs = await conn.receive() 291 292 except iec101.AsduTypeError as e: 293 self._log.warning("asdu type error: %s", e) 294 continue 295 296 for msg in msgs: 297 try: 298 self._log.debug('received message: %s', msg) 299 await self._process_msg(conn_id, msg) 300 301 except Exception as e: 302 self._log.warning('error processing message: %s', 303 e, exc_info=e) 304 305 except ConnectionError: 306 self._log.debug('connection close') 307 308 except Exception as e: 309 self._log.warning('connection receive loop error: %s', 310 e, exc_info=e) 311 312 finally: 313 conn.close() 314 315 async def _register_connections(self): 316 payload = [{'connection_id': conn_id, 317 'address': conn.info.address} 318 for conn_id, conn in self._conns.items()] 319 320 event = hat.event.common.RegisterEvent( 321 type=(*self._event_type_prefix, 'gateway', 'connections'), 322 source_timestamp=None, 323 payload=hat.event.common.EventPayloadJson(payload)) 324 325 await self._eventer_client.register([event]) 326 327 async def _process_event(self, event): 328 suffix = event.type[len(self._event_type_prefix):] 329 330 if suffix[:2] == ('system', 'data'): 331 data_type_str, asdu_address_str, io_address_str = suffix[2:] 332 data_key = common.DataKey(data_type=common.DataType(data_type_str), 333 asdu_address=int(asdu_address_str), 334 io_address=int(io_address_str)) 335 336 await self._process_data_event(data_key, event) 337 338 elif suffix[:2] == ('system', 'command'): 339 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 340 cmd_key = common.CommandKey( 341 cmd_type=common.CommandType(cmd_type_str), 342 asdu_address=int(asdu_address_str), 343 io_address=int(io_address_str)) 344 345 await self._process_command_event(cmd_key, event) 346 347 else: 348 raise Exception('unsupported event type') 349 350 async def _process_data_event(self, data_key, event): 351 if data_key not in self._data_msgs: 352 raise Exception('data not configured') 353 354 data_msg = data_msg_from_event(data_key, event) 355 self._data_msgs[data_key] = data_msg 356 357 buffer = self._data_buffers.get(data_key) 358 if buffer: 359 buffer.add(event.id, data_key, data_msg) 360 361 with_ack = data_key in self._data_with_ack 362 363 for conn_id in self._conns.keys(): 364 await self._send_data_msg(conn_id=conn_id, 365 buffer=buffer, 366 event_id=event.id, 367 data_msg=data_msg, 368 with_ack=with_ack) 369 370 async def _process_command_event(self, cmd_key, event): 371 cmd_msg = cmd_msg_from_event(cmd_key, event) 372 conn_id = event.payload.data['connection_id'] 373 await self._send(conn_id, [cmd_msg]) 374 375 async def _process_msg(self, conn_id, msg): 376 if isinstance(msg, iec101.CommandMsg): 377 await self._process_command_msg(conn_id, msg) 378 379 elif isinstance(msg, iec101.InterrogationMsg): 380 await self._process_interrogation_msg(conn_id, msg) 381 382 elif isinstance(msg, iec101.CounterInterrogationMsg): 383 await self._process_counter_interrogation_msg(conn_id, msg) 384 385 elif isinstance(msg, iec101.ReadMsg): 386 await self._process_read_msg(conn_id, msg) 387 388 elif isinstance(msg, iec101.ClockSyncMsg): 389 await self._process_clock_sync_msg(conn_id, msg) 390 391 elif isinstance(msg, iec101.TestMsg): 392 await self._process_test_msg(conn_id, msg) 393 394 elif isinstance(msg, iec101.ResetMsg): 395 await self._process_reset_msg(conn_id, msg) 396 397 elif isinstance(msg, iec101.ParameterMsg): 398 await self._process_parameter_msg(conn_id, msg) 399 400 elif isinstance(msg, iec101.ParameterActivationMsg): 401 await self._process_parameter_activation_msg(conn_id, msg) 402 403 else: 404 raise Exception('unsupported message') 405 406 async def _process_command_msg(self, conn_id, msg): 407 if isinstance(msg.cause, iec101.CommandReqCause): 408 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 409 await self._eventer_client.register([event]) 410 411 else: 412 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 413 is_negative_confirm=True) 414 await self._send(conn_id, [res]) 415 416 async def _process_interrogation_msg(self, conn_id, msg): 417 if msg.cause == iec101.CommandReqCause.ACTIVATION: 418 asdu_data_msgs = collections.defaultdict(collections.deque) 419 420 for data_key, data_msg in self._data_msgs.items(): 421 if data_key.data_type == common.DataType.BINARY_COUNTER: 422 continue 423 424 if (msg.asdu_address != self._broadcast_asdu_address and 425 msg.asdu_address != data_key.asdu_address): 426 continue 427 428 asdu_data_msgs[data_key.asdu_address].append(data_msg) 429 430 if msg.asdu_address != self._broadcast_asdu_address: 431 asdu_data_msgs[msg.asdu_address].append(None) 432 433 for asdu_address, data_msgs in asdu_data_msgs.items(): 434 res = msg._replace( 435 asdu_address=asdu_address, 436 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 437 is_negative_confirm=False) 438 await self._send(conn_id, [res]) 439 440 msgs = [ 441 data_msg._replace( 442 is_test=msg.is_test, 443 cause=iec101.DataResCause.INTERROGATED_STATION) 444 for data_msg in data_msgs 445 if data_msg] 446 if msgs: 447 await self._send(conn_id, msgs) 448 449 res = msg._replace( 450 asdu_address=asdu_address, 451 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 452 is_negative_confirm=False) 453 await self._send(conn_id, [res]) 454 455 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 456 res = msg._replace( 457 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 458 is_negative_confirm=True) 459 await self._send(conn_id, [res]) 460 461 else: 462 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 463 is_negative_confirm=True) 464 await self._send(conn_id, [res]) 465 466 async def _process_counter_interrogation_msg(self, conn_id, msg): 467 if msg.cause == iec101.CommandReqCause.ACTIVATION: 468 asdu_data_msgs = collections.defaultdict(collections.deque) 469 470 for data_key, data_msg in self._data_msgs.items(): 471 if data_key.data_type != common.DataType.BINARY_COUNTER: 472 continue 473 474 if (msg.asdu_address != self._broadcast_asdu_address and 475 msg.asdu_address != data_key.asdu_address): 476 continue 477 478 asdu_data_msgs[data_key.asdu_address].append(data_msg) 479 480 if msg.asdu_address != self._broadcast_asdu_address: 481 asdu_data_msgs[msg.asdu_address].append(None) 482 483 for asdu_address, data_msgs in asdu_data_msgs.items(): 484 res = msg._replace( 485 asdu_address=asdu_address, 486 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 487 is_negative_confirm=False) 488 await self._send(conn_id, [res]) 489 490 msgs = [ 491 data_msg._replace( 492 is_test=msg.is_test, 493 cause=iec101.DataResCause.INTERROGATED_COUNTER) 494 for data_msg in data_msgs 495 if data_msg] 496 if msgs: 497 await self._send(conn_id, msgs) 498 499 res = msg._replace( 500 asdu_address=asdu_address, 501 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 502 is_negative_confirm=False) 503 await self._send(conn_id, [res]) 504 505 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 506 res = msg._replace( 507 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 508 is_negative_confirm=True) 509 await self._send(conn_id, [res]) 510 511 else: 512 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 513 is_negative_confirm=True) 514 await self._send(conn_id, [res]) 515 516 async def _process_read_msg(self, conn_id, msg): 517 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 518 await self._send(conn_id, [res]) 519 520 async def _process_clock_sync_msg(self, conn_id, msg): 521 if isinstance(msg.cause, iec101.ClockSyncReqCause): 522 res = msg._replace( 523 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 524 is_negative_confirm=True) 525 await self._send(conn_id, [res]) 526 527 else: 528 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 529 is_negative_confirm=True) 530 await self._send(conn_id, [res]) 531 532 async def _process_test_msg(self, conn_id, msg): 533 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 534 await self._send(conn_id, [res]) 535 536 async def _process_reset_msg(self, conn_id, msg): 537 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 538 await self._send(conn_id, [res]) 539 540 async def _process_parameter_msg(self, conn_id, msg): 541 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 542 await self._send(conn_id, [res]) 543 544 async def _process_parameter_activation_msg(self, conn_id, msg): 545 res = msg._replace( 546 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 547 await self._send(conn_id, [res]) 548 549 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg, 550 with_ack): 551 sent_cb = (functools.partial(buffer.remove, event_id) 552 if buffer else None) 553 554 await self._send(conn_id=conn_id, 555 msgs=[data_msg], 556 sent_cb=sent_cb, 557 with_ack=with_ack) 558 559 async def _send(self, conn_id, msgs, sent_cb=None, with_ack=True): 560 send_queue = self._send_queues.get(conn_id) 561 if send_queue is None: 562 return 563 564 try: 565 send_queue.put_nowait((msgs, sent_cb, with_ack)) 566 567 except aio.QueueFullError: 568 self._log.warning('send queue full') 569 570 conn = self._conns.get(conn_id) 571 if conn: 572 conn.close() 573 574 except aio.QueueClosedError: 575 pass 576 577 578def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 579 conn_id: int, 580 msg: iec101.CommandMsg 581 ) -> hat.event.common.RegisterEvent: 582 command_type = common.get_command_type(msg.command) 583 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 584 command = common.command_to_json(msg.command) 585 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 586 str(msg.asdu_address), str(msg.io_address)) 587 588 return hat.event.common.RegisterEvent( 589 type=event_type, 590 source_timestamp=None, 591 payload=hat.event.common.EventPayloadJson({ 592 'connection_id': conn_id, 593 'is_test': msg.is_test, 594 'cause': cause, 595 'command': command})) 596 597 598def data_msg_from_event(data_key: common.DataKey, 599 event: hat.event.common.Event 600 ) -> iec101.DataMsg: 601 time = common.time_from_source_timestamp(event.source_timestamp) 602 cause = common.cause_from_json(iec101.DataResCause, 603 event.payload.data['cause']) 604 data = common.data_from_json(data_key.data_type, 605 event.payload.data['data']) 606 607 return iec101.DataMsg(is_test=event.payload.data['is_test'], 608 originator_address=0, 609 asdu_address=data_key.asdu_address, 610 io_address=data_key.io_address, 611 data=data, 612 time=time, 613 cause=cause) 614 615 616def cmd_msg_from_event(cmd_key: common.CommandKey, 617 event: hat.event.common.Event 618 ) -> iec101.CommandMsg: 619 cause = common.cause_from_json(iec101.CommandResCause, 620 event.payload.data['cause']) 621 command = common.command_from_json(cmd_key.cmd_type, 622 event.payload.data['command']) 623 is_negative_confirm = event.payload.data['is_negative_confirm'] 624 625 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 626 originator_address=0, 627 asdu_address=cmd_key.asdu_address, 628 io_address=cmd_key.io_address, 629 command=command, 630 is_negative_confirm=is_negative_confirm, 631 cause=cause) 632 633 634def _get_broadcast_asdu_address(asdu_address_size): 635 if asdu_address_size == iec101.AsduAddressSize.ONE: 636 return 0xFF 637 638 if asdu_address_size == iec101.AsduAddressSize.TWO: 639 return 0xFFFF 640 641 raise ValueError('unsupported asdu address size') 642 643 644def _create_logger_adapter(name): 645 extra = {'meta': {'type': 'Iec101SlaveDevice', 646 'name': name}} 647 648 return logging.LoggerAdapter(mlog, extra)
async def
create( conf: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]) -> Iec101SlaveDevice:
26async def create(conf: common.DeviceConf, 27 eventer_client: hat.event.eventer.Client, 28 event_type_prefix: common.EventTypePrefix 29 ) -> 'Iec101SlaveDevice': 30 device = Iec101SlaveDevice() 31 device._conf = conf 32 device._eventer_client = eventer_client 33 device._event_type_prefix = event_type_prefix 34 device._next_conn_ids = itertools.count(1) 35 device._conns = {} 36 device._send_queues = {} 37 device._buffers = {} 38 device._data_msgs = {} 39 device._data_buffers = {} 40 device._data_with_ack = set() 41 device._broadcast_asdu_address = _get_broadcast_asdu_address( 42 iec101.AsduAddressSize[conf['asdu_address_size']]) 43 device._log = _create_logger_adapter(conf['name']) 44 45 init_buffers(buffers_conf=conf['buffers'], 46 buffers=device._buffers) 47 48 await init_data(log=device._log, 49 data_conf=conf['data'], 50 data_msgs=device._data_msgs, 51 data_buffers=device._data_buffers, 52 data_with_ack=device._data_with_ack, 53 buffers=device._buffers, 54 eventer_client=eventer_client, 55 event_type_prefix=event_type_prefix) 56 57 if conf['link_type'] == 'BALANCED': 58 create_link = link.create_balanced_link 59 60 elif conf['link_type'] == 'UNBALANCED': 61 create_link = link.create_slave_link 62 63 else: 64 raise ValueError('unsupported link type') 65 66 device._link = await create_link( 67 port=conf['port'], 68 address_size=link.AddressSize[conf['device_address_size']], 69 silent_interval=conf['silent_interval'], 70 baudrate=conf['baudrate'], 71 bytesize=serial.ByteSize[conf['bytesize']], 72 parity=serial.Parity[conf['parity']], 73 stopbits=serial.StopBits[conf['stopbits']], 74 xonxoff=conf['flow_control']['xonxoff'], 75 rtscts=conf['flow_control']['rtscts'], 76 dsrdtr=conf['flow_control']['dsrdtr'], 77 name=conf['name']) 78 79 try: 80 for device_conf in conf['devices']: 81 device.async_group.spawn(device._connection_loop, device_conf) 82 83 await device._register_connections() 84 85 except BaseException: 86 await aio.uncancellable(device.async_close()) 87 raise 88 89 return device
info: hat.gateway.common.DeviceInfo =
DeviceInfo(type='iec101_slave', create=<function create>, json_schema_id='hat-gateway://iec101.yaml#/$defs/slave', json_schema_repo={'hat-json://path.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-json://path.yaml', 'title': 'JSON Path', 'oneOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'array', 'items': {'$ref': 'hat-json://path.yaml'}}]}, 'hat-json://logging.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-json://logging.yaml', 'title': 'Logging', 'description': 'Logging configuration', 'type': 'object', 'required': ['version'], 'properties': {'version': {'title': 'Version', 'type': 'integer', 'default': 1}, 'formatters': {'title': 'Formatters', 'type': 'object', 'patternProperties': {'.+': {'title': 'Formatter', 'type': 'object', 'properties': {'format': {'title': 'Format', 'type': 'string', 'default': None}, 'datefmt': {'title': 'Date format', 'type': 'string', 'default': None}}}}}, 'filters': {'title': 'Filters', 'type': 'object', 'patternProperties': {'.+': {'title': 'Filter', 'type': 'object', 'properties': {'name': {'title': 'Logger name', 'type': 'string', 'default': ''}}}}}, 'handlers': {'title': 'Handlers', 'type': 'object', 'patternProperties': {'.+': {'title': 'Handler', 'type': 'object', 'description': 'Additional properties are passed as keyword arguments to\nconstructor\n', 'required': ['class'], 'properties': {'class': {'title': 'Class', 'type': 'string'}, 'level': {'title': 'Level', 'type': 'string'}, 'formatter': {'title': 'Formatter', 'type': 'string'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}}}}}, 'loggers': {'title': 'Loggers', 'type': 'object', 'patternProperties': {'.+': {'title': 'Logger', 'type': 'object', 'properties': {'level': {'title': 'Level', 'type': 'string'}, 'propagate': {'title': 'Propagate', 'type': 'boolean'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}, 'handlers': {'title': 'Handlers', 'type': 'array', 'items': {'title': 'Handler id', 'type': 'string'}}}}}}, 'root': {'title': 'Root logger', 'type': 'object', 'properties': {'level': {'title': 'Level', 'type': 'string'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}, 'handlers': {'title': 'Handlers', 'type': 'array', 'items': {'title': 'Handler id', 'type': 'string'}}}}, 'incremental': {'title': 'Incremental configuration', 'type': 'boolean', 'default': False}, 'disable_existing_loggers': {'title': 'Disable existing loggers', 'type': 'boolean', 'default': True}}}, 'hat-gateway://iec103.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec103.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'reconnect_delay', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['cause', 'value'], 'properties': {'cause': {'oneOf': [{'enum': ['SPONTANEOUS', 'CYCLIC', 'TEST_MODE', 'GENERAL_INTERROGATION', 'LOCAL_OPERATION', 'REMOTE_OPERATION']}, {'type': 'integer', 'description': 'other cause in range [0, 255]\n'}]}, 'value': {'oneOf': [{'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}, {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/measurand'}]}}}, 'command': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'enable': {'type': 'boolean'}, 'command': {'type': 'object', 'required': ['session_id', 'value'], 'properties': {'value': {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}}}}}}, 'values': {'double': {'enum': ['TRANSIENT', 'OFF', 'ON', 'ERROR']}, 'measurand': {'type': 'object', 'required': ['overflow', 'invalid', 'value'], 'properties': {'overflow': {'type': 'boolean'}, 'invalid': {'type': 'boolean'}, 'value': {'type': 'number'}}}}}}, 'hat-gateway://modbus.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://modbus.yaml', 'title': 'Modbus devices', '$defs': {'master': {'type': 'object', 'title': 'Modbus master', 'required': ['name', 'connection', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['modbus_type', 'transport', 'connect_timeout', 'connect_delay', 'request_timeout', 'request_delay', 'request_retry_immediate_count', 'request_retry_delayed_count', 'request_retry_delay'], 'properties': {'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'host', 'port'], 'properties': {'type': {'const': 'TCP'}, 'host': {'type': 'string', 'description': 'Remote host name\n'}, 'port': {'type': 'integer', 'description': 'Remote host TCP port\n', 'default': 502}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silent interval\n'}}}]}, 'connect_timeout': {'type': 'number', 'description': 'Maximum number of seconds available to single connection\nattempt\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of read or write\nrequest/response exchange.\n'}, 'request_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive requests\n(minimal duration between response and next request)\n'}, 'request_retry_immediate_count': {'type': 'integer', 'description': 'Number of immediate request retries before remote\ndata is considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delayed_count': {'type': 'integer', 'description': 'Number of delayed request retries before remote data\nis considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive delayed\nrequest retries\n'}}}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['device_id', 'timeout_poll_delay', 'data'], 'properties': {'device_id': {'type': 'integer', 'description': 'Modbus device identifier\n'}, 'timeout_poll_delay': {'type': 'number', 'description': 'Delay (in seconds) after read timeout and\nbefore device polling is resumed\n'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'interval', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'interval': {'type': ['number', 'null'], 'description': 'Polling interval in seconds or\nnull if polling is disabled\n'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}}}}, 'slave': {'type': 'object', 'required': ['name', 'modbus_type', 'transport', 'data'], 'properties': {'name': {'type': 'string'}, 'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'local_host', 'local_port', 'remote_hosts', 'max_connections', 'response_timeout', 'keep_alive_timeout'], 'properties': {'type': {'const': 'TCP'}, 'local_host': {'type': 'string', 'description': 'Local host name\n'}, 'local_port': {'type': 'integer', 'description': 'Local host TCP port\n', 'default': 502}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'response_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of write\nrequest/response exchange.\n'}, 'keep_alive_timeout': {'type': ['number', 'null']}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'response_timeout', 'keep_alive_timeout'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silent interval\n'}, 'response_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of write\nrequest/response exchange.\n'}, 'keep_alive_timeout': {'type': 'number'}}}]}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'device_id', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'device_id': {'type': 'integer'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['DISCONNECTED', 'CONNECTING', 'CONNECTED']}, 'remote_device_status': {'enum': ['DISABLED', 'CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['result'], 'properties': {'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND']}, 'value': {'type': 'integer'}, 'cause': {'enum': ['INTERROGATE', 'CHANGE']}}}, 'write': {'type': 'object', 'required': ['request_id', 'result'], 'properties': {'request_id': {'type': 'string'}, 'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND', 'TIMEOUT']}}}}, 'system': {'enable': {'type': 'boolean'}, 'write': {'type': 'object', 'required': ['request_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'value': {'type': 'integer'}}}}}, 'slave': {'gateway': {'connections': {'type': 'array', 'items': {'oneOf': [{'type': 'object', 'required': ['type', 'connection_id'], 'properties': {'type': {'const': 'SERIAL'}, 'connection_id': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'connection_id', 'local', 'remote'], 'properties': {'type': {'const': 'TCP'}, 'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}]}}, 'write': {'type': 'object', 'required': ['request_id', 'connection_id', 'data'], 'properties': {'request_id': {'type': 'string'}, 'connection_id': {'type': 'integer'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'value'], 'properties': {'name': {'type': 'string'}, 'value': {'type': 'integer'}}}}}}}, 'system': {'data': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}}}, 'write': {'type': 'object', 'required': ['request_id', 'success'], 'properties': {'request_id': {'type': 'string'}, 'success': {'type': 'boolean'}}}}}}}}, 'hat-gateway://smpp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://smpp.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'remote_address', 'ssl', 'system_id', 'password', 'enquire_link_delay', 'enquire_link_timeout', 'connect_timeout', 'reconnect_delay', 'short_message', 'priority', 'data_coding', 'message_encoding', 'message_timeout'], 'properties': {'name': {'type': 'string'}, 'remote_address': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'ssl': {'type': 'boolean'}, 'system_id': {'type': 'string'}, 'password': {'type': 'string'}, 'enquire_link_delay': {'type': ['null', 'number']}, 'enquire_link_timeout': {'type': 'number'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'short_message': {'type': 'boolean'}, 'priority': {'enum': ['BULK', 'NORMAL', 'URGENT', 'VERY_URGENT']}, 'data_coding': {'enum': ['DEFAULT', 'ASCII', 'UNSPECIFIED_1', 'LATIN_1', 'UNSPECIFIED_2', 'JIS', 'CYRLLIC', 'LATIN_HEBREW', 'UCS2', 'PICTOGRAM', 'MUSIC', 'EXTENDED_KANJI', 'KS']}, 'message_encoding': {'type': 'string'}, 'message_timeout': {'type': 'number'}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}}, 'system': {'message': {'type': 'object', 'required': ['address', 'message'], 'properties': {'address': {'type': 'string'}, 'message': {'type': 'string'}}}}}}}}, 'hat-gateway://main.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://main.yaml', 'title': 'Gateway', 'description': "Gateway's configuration", 'type': 'object', 'required': ['name', 'event_server', 'devices'], 'properties': {'type': {'const': 'gateway', 'description': 'configuration type identification'}, 'version': {'type': 'string', 'description': 'component version'}, 'log': {'$ref': 'hat-json://logging.yaml'}, 'name': {'type': 'string', 'description': 'component name'}, 'event_server': {'allOf': [{'type': 'object', 'properties': {'require_operational': {'type': 'boolean'}}}, {'oneOf': [{'type': 'object', 'required': ['monitor_component'], 'properties': {'monitor_component': {'type': 'object', 'required': ['host', 'port', 'gateway_group', 'event_server_group'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23010}, 'gateway_group': {'type': 'string'}, 'event_server_group': {'type': 'string'}}}}}, {'type': 'object', 'required': ['eventer_server'], 'properties': {'eventer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23012}}}}}]}]}, 'devices': {'type': 'array', 'items': {'$ref': 'hat-gateway://main.yaml#/$defs/device'}}, 'adminer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23016}}}}, '$defs': {'device': {'type': 'object', 'description': 'structure of device configuration depends on device type\n', 'required': ['module', 'name'], 'properties': {'module': {'type': 'string', 'description': 'full python module name that implements device\n'}, 'name': {'type': 'string'}}}}}, 'hat-gateway://iec101.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec101.yaml', '$defs': {'master': {'allOf': [{'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'cause_size', 'asdu_address_size', 'io_address_size', 'reconnect_delay'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'reconnect_delay': {'type': 'number'}}}, {'oneOf': [{'type': 'object', 'required': ['link_type', 'device_address_size', 'remote_devices'], 'properties': {'link_type': {'const': 'BALANCED'}, 'device_address_size': {'enum': ['ZERO', 'ONE', 'TWO']}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['direction', 'address', 'response_timeout', 'send_retry_count', 'status_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'direction': {'enum': ['A_TO_B', 'B_TO_A']}, 'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'status_delay': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, {'type': 'object', 'required': ['link_type', 'device_address_size', 'remote_devices'], 'properties': {'link_type': {'const': 'UNBALANCED'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}]}]}, 'slave': {'allOf': [{'type': 'object', 'required': ['port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'cause_size', 'asdu_address_size', 'io_address_size', 'buffers', 'data'], 'properties': {'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer', 'with_ack'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}, 'with_ack': {'type': 'boolean'}}}}}}, {'oneOf': [{'type': 'object', 'required': ['link_type', 'device_address_size', 'devices'], 'properties': {'link_type': {'const': 'BALANCED'}, 'device_address_size': {'enum': ['ZERO', 'ONE', 'TWO']}, 'devices': {'type': 'array', 'items': {'type': 'object', 'required': ['direction', 'address', 'response_timeout', 'send_retry_count', 'status_delay', 'reconnect_delay'], 'properties': {'direction': {'enum': ['A_TO_B', 'B_TO_A']}, 'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'status_delay': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}}}}}}, {'type': 'object', 'required': ['link_type', 'device_address_size', 'devices'], 'properties': {'link_type': {'const': 'UNBALANCED'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'keep_alive_timeout', 'reconnect_delay'], 'properties': {'address': {'type': 'integer'}, 'keep_alive_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}}}}}}]}]}, 'events': {'master': {'gateway': {'status': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/status'}, 'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/data/res'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/res'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/res'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/counter_interrogation/res'}}, 'system': {'enable': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/enable'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/req'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/req'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/counter_interrogation/req'}}}, 'slave': {'gateway': {'connections': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/connections'}, 'command': {'allOf': [{'type': 'object', 'required': ['connection_id'], 'properties': {'connection_id': {'type': 'integer'}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/req'}]}}, 'system': {'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/data/res'}, 'command': {'allOf': [{'type': 'object', 'required': ['connection_id'], 'properties': {'connection_id': {'type': 'integer'}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/res'}]}}}}, 'messages': {'enable': {'type': 'boolean'}, 'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'connections': {'type': 'array', 'items': {'type': 'object', 'required': ['connection_id', 'address'], 'properties': {'connection_id': {'type': 'integer'}, 'address': {'type': 'integer'}}}}, 'data': {'res': {'type': 'object', 'required': ['is_test', 'cause', 'data'], 'properties': {'is_test': {'type': 'boolean'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/data/res'}, 'data': {'oneOf': [{'$ref': 'hat-gateway://iec101.yaml#/$defs/data/single'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/double'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/step_position'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/bitstring'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/normalized'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/scaled'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/floating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/binary_counter'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/protection'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/protection_start'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/protection_command'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/status'}]}}}}, 'command': {'req': {'type': 'object', 'required': ['is_test', 'cause', 'command'], 'properties': {'is_test': {'type': 'boolean'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/req'}, 'command': {'oneOf': [{'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/single'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/double'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/regulating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/normalized'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/scaled'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/floating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/bitstring'}]}}}, 'res': {'type': 'object', 'required': ['is_test', 'is_negative_confirm', 'cause', 'command'], 'properties': {'is_test': {'type': 'boolean'}, 'is_negative_confirm': {'type': 'boolean'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/res'}, 'command': {'oneOf': [{'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/single'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/double'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/regulating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/normalized'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/scaled'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/floating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/bitstring'}]}}}}, 'interrogation': {'req': {'type': 'object', 'required': ['is_test', 'request', 'cause'], 'properties': {'is_test': {'type': 'boolean'}, 'request': {'type': 'integer', 'description': 'request in range [0, 255]\n'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/req'}}}, 'res': {'type': 'object', 'required': ['is_test', 'is_negative_confirm', 'request', 'cause'], 'properties': {'is_test': {'type': 'boolean'}, 'is_negative_confirm': {'type': 'boolean'}, 'request': {'type': 'integer', 'description': 'request in range [0, 255]\n'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/res'}}}}, 'counter_interrogation': {'req': {'allOf': [{'type': 'object', 'required': ['freeze'], 'properties': {'freeze': {'enum': ['READ', 'FREEZE', 'FREEZE_AND_RESET', 'RESET']}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/req'}]}, 'res': {'allOf': [{'type': 'object', 'required': ['freeze'], 'properties': {'freeze': {'enum': ['READ', 'FREEZE', 'FREEZE_AND_RESET', 'RESET']}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/res'}]}}}, 'data': {'single': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/single'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/indication'}}}, 'double': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/double'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/indication'}}}, 'step_position': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/step_position'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'bitstring': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/bitstring'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'normalized': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/normalized'}, 'quality': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}]}}}, 'scaled': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/scaled'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'floating': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/floating'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'binary_counter': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/binary_counter'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/counter'}}}, 'protection': {'type': 'object', 'required': ['value', 'quality', 'elapsed_time'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/protection'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/protection'}, 'elapsed_time': {'type': 'integer', 'description': 'elapsed_time in range [0, 65535]\n'}}}, 'protection_start': {'type': 'object', 'required': ['value', 'quality', 'duration_time'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/protection_start'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/protection'}, 'duration_time': {'type': 'integer', 'description': 'duration_time in range [0, 65535]\n'}}}, 'protection_command': {'type': 'object', 'required': ['value', 'quality', 'operating_time'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/protection_command'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/protection'}, 'operating_time': {'type': 'integer', 'description': 'operating_time in range [0, 65535]\n'}}}, 'status': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/status'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}}, 'commands': {'single': {'type': 'object', 'required': ['value', 'select', 'qualifier'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/single'}, 'select': {'type': 'boolean'}, 'qualifier': {'type': 'integer', 'description': 'qualifier in range [0, 31]\n'}}}, 'double': {'type': 'object', 'required': ['value', 'select', 'qualifier'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/double'}, 'select': {'type': 'boolean'}, 'qualifier': {'type': 'integer', 'description': 'qualifier in range [0, 31]\n'}}}, 'regulating': {'type': 'object', 'required': ['value', 'select', 'qualifier'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/regulating'}, 'select': {'type': 'boolean'}, 'qualifier': {'type': 'integer', 'description': 'qualifier in range [0, 31]\n'}}}, 'normalized': {'type': 'object', 'required': ['value', 'select'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/normalized'}, 'select': {'type': 'boolean'}}}, 'scaled': {'type': 'object', 'required': ['value', 'select'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/scaled'}, 'select': {'type': 'boolean'}}}, 'floating': {'type': 'object', 'required': ['value', 'select'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/floating'}, 'select': {'type': 'boolean'}}}, 'bitstring': {'type': 'object', 'required': ['value'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/bitstring'}}}}, 'values': {'single': {'enum': ['OFF', 'ON']}, 'double': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'FAULT']}, 'regulating': {'enum': ['LOWER', 'HIGHER']}, 'step_position': {'type': 'object', 'required': ['value', 'transient'], 'properties': {'value': {'type': 'integer', 'description': 'value in range [-64, 63]\n'}, 'transient': {'type': 'boolean'}}}, 'bitstring': {'type': 'array', 'description': 'bitstring encoded as 4 bytes\n', 'items': {'type': 'integer'}}, 'normalized': {'type': 'number', 'description': 'value in range [-1.0, 1.0)\n'}, 'scaled': {'type': 'integer', 'description': 'value in range [-2^15, 2^15-1]\n'}, 'floating': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'binary_counter': {'type': 'integer', 'description': 'value in range [-2^31, 2^31-1]\n'}, 'protection': {'enum': ['OFF', 'ON']}, 'protection_start': {'type': 'object', 'required': ['general', 'l1', 'l2', 'l3', 'ie', 'reverse'], 'properties': {'general': {'type': 'boolean'}, 'l1': {'type': 'boolean'}, 'l2': {'type': 'boolean'}, 'l3': {'type': 'boolean'}, 'ie': {'type': 'boolean'}, 'reverse': {'type': 'boolean'}}}, 'protection_command': {'type': 'object', 'required': ['general', 'l1', 'l2', 'l3'], 'properties': {'general': {'type': 'boolean'}, 'l1': {'type': 'boolean'}, 'l2': {'type': 'boolean'}, 'l3': {'type': 'boolean'}}}, 'status': {'type': 'object', 'required': ['value', 'change'], 'properties': {'value': {'type': 'array', 'description': 'value length is 16\n', 'items': {'type': 'boolean'}}, 'change': {'type': 'array', 'description': 'change length is 16\n', 'items': {'type': 'boolean'}}}}}, 'qualities': {'indication': {'type': 'object', 'required': ['invalid', 'not_topical', 'substituted', 'blocked'], 'properties': {'invalid': {'type': 'boolean'}, 'not_topical': {'type': 'boolean'}, 'substituted': {'type': 'boolean'}, 'blocked': {'type': 'boolean'}}}, 'measurement': {'type': 'object', 'required': ['invalid', 'not_topical', 'substituted', 'blocked', 'overflow'], 'properties': {'invalid': {'type': 'boolean'}, 'not_topical': {'type': 'boolean'}, 'substituted': {'type': 'boolean'}, 'blocked': {'type': 'boolean'}, 'overflow': {'type': 'boolean'}}}, 'counter': {'type': 'object', 'required': ['invalid', 'adjusted', 'overflow', 'sequence'], 'properties': {'invalid': {'type': 'boolean'}, 'adjusted': {'type': 'boolean'}, 'overflow': {'type': 'boolean'}, 'sequence': {'type': 'boolean'}}}, 'protection': {'type': 'object', 'required': ['invalid', 'not_topical', 'substituted', 'blocked', 'time_invalid'], 'properties': {'invalid': {'type': 'boolean'}, 'not_topical': {'type': 'boolean'}, 'substituted': {'type': 'boolean'}, 'blocked': {'type': 'boolean'}, 'time_invalid': {'type': 'boolean'}}}}, 'causes': {'data': {'res': {'oneOf': [{'enum': ['PERIODIC', 'BACKGROUND_SCAN', 'SPONTANEOUS', 'REQUEST', 'REMOTE_COMMAND', 'LOCAL_COMMAND', 'INTERROGATED_STATION', 'INTERROGATED_GROUP01', 'INTERROGATED_GROUP02', 'INTERROGATED_GROUP03', 'INTERROGATED_GROUP04', 'INTERROGATED_GROUP05', 'INTERROGATED_GROUP06', 'INTERROGATED_GROUP07', 'INTERROGATED_GROUP08', 'INTERROGATED_GROUP09', 'INTERROGATED_GROUP10', 'INTERROGATED_GROUP11', 'INTERROGATED_GROUP12', 'INTERROGATED_GROUP13', 'INTERROGATED_GROUP14', 'INTERROGATED_GROUP15', 'INTERROGATED_GROUP16', 'INTERROGATED_COUNTER', 'INTERROGATED_COUNTER01', 'INTERROGATED_COUNTER02', 'INTERROGATED_COUNTER03', 'INTERROGATED_COUNTER04']}, {'type': 'integer', 'description': 'other cause in range [0, 63]\n'}]}}, 'command': {'req': {'oneOf': [{'enum': ['ACTIVATION', 'DEACTIVATION']}, {'type': 'integer', 'description': 'other cause in range [0, 63]\n'}]}, 'res': {'oneOf': [{'enum': ['ACTIVATION_CONFIRMATION', 'DEACTIVATION_CONFIRMATION', 'ACTIVATION_TERMINATION', 'UNKNOWN_TYPE', 'UNKNOWN_CAUSE', 'UNKNOWN_ASDU_ADDRESS', 'UNKNOWN_IO_ADDRESS']}, {'type': 'integer', 'description': 'other cause in range [0, 63]\n'}]}}}}}, 'hat-gateway://iec104.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec104.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'remote_addresses', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'reconnect_delay', 'time_sync_delay', 'security'], 'properties': {'name': {'type': 'string'}, 'remote_addresses': {'type': 'array', 'items': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}}}, 'slave': {'type': 'object', 'required': ['local_host', 'local_port', 'remote_hosts', 'max_connections', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'security', 'buffers', 'data'], 'properties': {'local_host': {'type': 'string'}, 'local_port': {'type': 'integer'}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}}}}}}, 'events': {'master': {'gateway': {'status': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/status'}, 'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/counter_interrogation'}}, 'system': {'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/counter_interrogation'}}}, 'slave': {'gateway': {'connections': {'$ref': 'hat-gateway://iec104.yaml#/$defs/messages/connections'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/gateway/command'}}, 'system': {'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/command'}}}}, 'messages': {'connections': {'type': 'array', 'items': {'type': 'object', 'required': ['connection_id', 'local', 'remote'], 'properties': {'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}}}, 'security': {'type': 'object', 'required': ['cert_path', 'key_path', 'verify_cert', 'ca_path'], 'properties': {'cert_path': {'type': 'string'}, 'key_path': {'type': ['null', 'string']}, 'verify_cert': {'type': 'boolean'}, 'ca_path': {'type': ['null', 'string']}, 'strict_mode': {'type': 'boolean'}, 'renegotiate_delay': {'type': ['null', 'number']}}}}}, 'hat-gateway://ping.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://ping.yaml', '$defs': {'device': {'type': 'object', 'required': ['name', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'host', 'ping_delay', 'ping_timeout', 'retry_count', 'retry_delay'], 'properties': {'name': {'type': 'string'}, 'host': {'type': 'string'}, 'ping_delay': {'type': 'number'}, 'ping_timeout': {'type': 'number'}, 'retry_count': {'type': 'number'}, 'retry_delay': {'type': 'number'}}}}}}, 'events': {'status': {'enum': ['AVAILABLE', 'NOT_AVAILABLE']}}}}, 'hat-gateway://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'rcb', 'value'], 'properties': {'name': {'type': 'string'}, 'rcb': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}, 'commands': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'model', 'ref', 'with_operate_time'], 'properties': {'name': {'type': 'string'}, 'model': {'enum': ['DIRECT_WITH_NORMAL_SECURITY', 'SBO_WITH_NORMAL_SECURITY', 'DIRECT_WITH_ENHANCED_SECURITY', 'SBO_WITH_ENHANCED_SECURITY']}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/command'}, 'with_operate_time': {'type': 'boolean'}}}}, 'changes': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['reasons'], 'properties': {'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}}}, 'command': {'allOf': [{'type': 'object', 'required': ['session_id', 'action'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE', 'TERMINATION']}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'service_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}, 'additional_cause': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/additional_cause'}, 'test_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/test_error'}}}]}]}, 'change': {'allOf': [{'type': 'object', 'required': ['session_id'], 'properties': {'session_id': {'type': 'string'}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}}}]}]}, 'entry_id': {'type': ['string', 'null'], 'description': 'hex encoded bytes'}}, 'system': {'command': {'type': 'object', 'required': ['session_id', 'action', 'value', 'origin', 'test', 'checks'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE']}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'origin': {'type': 'object', 'required': ['category', 'identification'], 'properties': {'category': {'enum': ['BAY_CONTROL', 'STATION_CONTROL', 'REMOTE_CONTROL', 'AUTOMATIC_BAY', 'AUTOMATIC_STATION', 'AUTOMATIC_REMOTE', 'MAINTENANCE', 'PROCESS']}, 'identification': {'type': 'string'}}}, 'test': {'type': 'boolean'}, 'checks': {'type': 'array', 'items': {'enum': ['SYNCHRO', 'INTERLOCK']}}}}, 'change': {'type': 'object', 'requried': ['session_id', 'value'], 'properties': {'session_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}}}}, 'value': {'anyOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/integer'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/unsigned'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/float'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/bit_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/octet_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/visible_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/mms_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/array'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/struct'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/double_point'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/direction'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/severity'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/vector'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/step_position'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/binary_control'}], '$defs': {'boolean': {'type': 'boolean'}, 'integer': {'type': 'integer'}, 'unsigned': {'type': 'integer'}, 'float': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'bit_string': {'type': 'array', 'items': {'type': 'boolean'}}, 'octet_string': {'type': 'string', 'description': 'hex encoded bytes'}, 'visible_string': {'type': 'string'}, 'mms_string': {'type': 'string'}, 'array': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}, 'struct': {'type': 'object', 'patternProperties': {'.+': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}, 'quality': {'type': 'object', 'required': ['validity', 'details', 'source', 'test', 'operator_blocked'], 'properties': {'validity': {'enum': ['GOOD', 'INVALID', 'RESERVED', 'QUESTIONABLE']}, 'details': {'type': 'array', 'items': {'enum': ['OVERFLOW', 'OUT_OF_RANGE', 'BAD_REFERENCE', 'OSCILLATORY', 'FAILURE', 'OLD_DATA', 'INCONSISTENT', 'INACCURATE']}}, 'source': {'enum': ['PROCESS', 'SUBSTITUTED']}, 'test': {'type': 'boolean'}, 'operator_blocked': {'type': 'boolean'}}}, 'timestamp': {'type': 'object', 'required': ['value', 'leap_second', 'clock_failure', 'not_synchronized'], 'properties': {'value': {'type': 'number', 'description': 'seconds since 1970-01-01'}, 'leap_second': {'type': 'boolean'}, 'clock_failure': {'type': 'boolean'}, 'not_synchronized': {'type': 'boolean'}, 'accuracy': {'type': 'integer'}}}, 'double_point': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'BAD']}, 'direction': {'enum': ['UNKNOWN', 'FORWARD', 'BACKWARD', 'BOTH']}, 'severity': {'enum': ['UNKNOWN', 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING']}, 'analogue': {'type': 'object', 'properties': {'i': {'type': 'integer'}, 'f': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}}}, 'vector': {'type': 'object', 'required': ['magnitude'], 'properties': {'magnitude': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, 'angle': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}}}, 'step_position': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}, 'transient': {'type': 'boolean'}}}, 'binary_control': {'enum': ['STOP', 'LOWER', 'HIGHER', 'RESERVED']}}}, 'value_type': {'oneOf': [{'enum': ['BOOLEAN', 'INTEGER', 'UNSIGNED', 'FLOAT', 'BIT_STRING', 'OCTET_STRING', 'VISIBLE_STRING', 'MMS_STRING', 'QUALITY', 'TIMESTAMP', 'DOUBLE_POINT', 'DIRECTION', 'SEVERITY', 'ANALOGUE', 'VECTOR', 'STEP_POSITION', 'BINARY_CONTROL']}, {'type': 'object', 'required': ['type', 'element_type', 'length'], 'properties': {'type': {'const': 'ARRAY'}, 'element_type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}, 'length': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'elements'], 'properties': {'type': {'const': 'STRUCT'}, 'elements': {'type': 'array', 'items': {'type': 'object', 'requried': ['name', 'type'], 'properties': {'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}}}]}, 'refs': {'value': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, 'command': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}, 'rcb': {'type': 'object', 'required': ['logical_device', 'logical_node', 'type', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'type': {'enum': ['BUFFERED', 'UNBUFFERED']}, 'name': {'type': 'string'}}}, 'dataset': {'oneOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/persisted'}], '$defs': {'nonpersisted': {'type': 'string'}, 'persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, 'hat-gateway://snmp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://snmp.yaml', '$defs': {'manager': {'allOf': [{'oneOf': [{'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v1'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v2c'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v3'}]}, {'type': 'object', 'required': ['name', 'remote_host', 'remote_port', 'connect_delay', 'request_timeout', 'request_retry_count', 'request_retry_delay', 'polling_delay', 'polling_oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'Device name\n'}, 'remote_host': {'type': 'string', 'description': 'Remote hostname or IP address\n'}, 'remote_port': {'type': 'integer', 'description': 'Remote UDP port\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of request/response\nexchange\n'}, 'request_retry_count': {'type': 'integer', 'description': 'Number of request retries before remote data is\nconsidered unavailable\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive request\nretries\n'}, 'polling_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive polling\ncycles\n'}, 'polling_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID read during polling cycle formated as integers\nseparated by '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value formated as\nintegers separated by '.'\n"}}}}]}, 'trap_listener': {'type': 'object', 'required': ['name', 'local_host', 'local_port', 'users', 'remote_devices'], 'properties': {'name': {'type': 'string', 'description': 'Device name\n'}, 'local_host': {'type': 'string', 'description': 'Local listening hostname or IP address\n'}, 'local_port': {'type': 'integer', 'description': 'Local listening UDP port\n'}, 'users': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'authentication', 'privacy'], 'properties': {'name': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'remote_devices': {'type': 'array', 'items': {'allOf': [{'oneOf': [{'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'enum': ['V1', 'V2C']}, 'community': {'type': ['null', 'string']}}}, {'type': 'object', 'required': ['version', 'context'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal\ndigits\n'}, 'name': {'type': 'string'}}}]}}}]}, {'type': 'object', 'required': ['name', 'oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'remote device name\n'}, 'oids': {'type': 'array', 'items': {'type': 'string', 'description': "data OID formated as integers separated\nby '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value\nformated as integers separated by '.'\n"}}}}]}}}}, 'managers': {'v1': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V1'}, 'community': {'type': 'string'}}}, 'v2c': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V2C'}, 'community': {'type': 'string'}}}, 'v3': {'type': 'object', 'required': ['version', 'context', 'user', 'authentication', 'privacy'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal digits\n'}, 'name': {'type': 'string'}}}]}, 'user': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'events': {'manager': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['session_id', 'cause', 'data'], 'properties': {'session_id': {'oneOf': [{'type': 'null', 'description': 'In case of INTERROGATE or CHANGE cause\n'}, {'description': 'In case of REQUESTED cause\n'}]}, 'cause': ['INTERROGATE', 'CHANGE', 'REQUESTED'], 'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}, 'write': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'read': {'type': 'object', 'required': ['session_id']}, 'write': {'type': 'object', 'required': ['session_id', 'data'], 'properties': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}}, 'trap_listener': {'gateway': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}, 'data': {'oneOf': [{'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['INTEGER', 'UNSIGNED', 'COUNTER', 'BIG_COUNTER', 'TIME_TICKS']}, 'value': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['STRING', 'STRING_HEX', 'OBJECT_ID', 'IP_ADDRESS', 'ARBITRARY']}, 'value': {'type': 'string'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'const': 'ERROR'}, 'value': {'enum': ['TOO_BIG', 'NO_SUCH_NAME', 'BAD_VALUE', 'READ_ONLY', 'GEN_ERR', 'NO_ACCESS', 'WRONG_TYPE', 'WRONG_LENGTH', 'WRONG_ENCODING', 'WRONG_VALUE', 'NO_CREATION', 'INCONSISTENT_VALUE', 'RESOURCE_UNAVAILABLE', 'COMMIT_FAILED', 'UNDO_FAILED', 'AUTHORIZATION_ERROR', 'NOT_WRITABLE', 'INCONSISTENT_NAME', 'EMPTY', 'UNSPECIFIED', 'NO_SUCH_OBJECT', 'NO_SUCH_INSTANCE', 'END_OF_MIB_VIEW', 'NOT_IN_TIME_WINDOWS', 'UNKNOWN_USER_NAMES', 'UNKNOWN_ENGINE_IDS', 'WRONG_DIGESTS', 'DECRYPTION_ERRORS']}}}]}}}})
class
Buffer:
99class Buffer: 100 101 def __init__(self, size: int): 102 self._size = size 103 self._data = collections.OrderedDict() 104 105 def add(self, 106 event_id: hat.event.common.EventId, 107 data_key: common.DataKey, 108 data_msg: iec101.DataMsg): 109 self._data[event_id] = data_key, data_msg 110 while len(self._data) > self._size: 111 self._data.popitem(last=False) 112 113 def remove(self, event_id: hat.event.common.EventId): 114 self._data.pop(event_id, None) 115 116 def get(self) -> Iterable[tuple[hat.event.common.EventId, 117 common.DataKey, 118 iec101.DataMsg]]: 119 return ((event_id, data_key, data_msg) 120 for event_id, (data_key, data_msg) in self._data.items())
def
add( self, event_id: hat.event.common.common.EventId, data_key: hat.gateway.devices.iec101.common.DataKey, data_msg: hat.drivers.iec101.common.DataMsg):
def
get( self) -> Iterable[tuple[hat.event.common.common.EventId, hat.gateway.devices.iec101.common.DataKey, hat.drivers.iec101.common.DataMsg]]:
def
init_buffers( buffers_conf: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')], buffers: dict[str, Buffer]):
async def
init_data( log: logging.Logger, data_conf: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')], data_msgs: dict[hat.gateway.devices.iec101.common.DataKey, hat.drivers.iec101.common.DataMsg], data_buffers: dict[hat.gateway.devices.iec101.common.DataKey, Buffer], data_with_ack: set[hat.gateway.devices.iec101.common.DataKey] | None, buffers: dict[str, Buffer], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]):
129async def init_data(log: logging.Logger, 130 data_conf: json.Data, 131 data_msgs: dict[common.DataKey, iec101.DataMsg], 132 data_buffers: dict[common.DataKey, Buffer], 133 data_with_ack: set[common.DataKey] | None, 134 buffers: dict[str, Buffer], 135 eventer_client: hat.event.eventer.Client, 136 event_type_prefix: common.EventTypePrefix): 137 for data in data_conf: 138 data_key = common.DataKey(data_type=common.DataType[data['data_type']], 139 asdu_address=data['asdu_address'], 140 io_address=data['io_address']) 141 142 data_msgs[data_key] = None 143 144 if data['buffer']: 145 data_buffers[data_key] = buffers[data['buffer']] 146 147 if data_with_ack is not None and data['with_ack']: 148 data_with_ack.add(data_key) 149 150 event_types = [(*event_type_prefix, 'system', 'data', '*')] 151 params = hat.event.common.QueryLatestParams(event_types) 152 result = await eventer_client.query(params) 153 154 for event in result.events: 155 try: 156 data_type_str, asdu_address_str, io_address_str = \ 157 event.type[len(event_type_prefix)+2:] 158 data_key = common.DataKey(data_type=common.DataType(data_type_str), 159 asdu_address=int(asdu_address_str), 160 io_address=int(io_address_str)) 161 if data_key not in data_msgs: 162 raise Exception(f'data {data_key} not configured') 163 164 data_msgs[data_key] = data_msg_from_event(data_key, event) 165 166 except Exception as e: 167 log.debug('skipping initial data: %s', e, exc_info=e)
170class Iec101SlaveDevice(common.Device): 171 172 @property 173 def async_group(self) -> aio.Group: 174 return self._link.async_group 175 176 async def process_event(self, event: hat.event.common.Event): 177 try: 178 await self._process_event(event) 179 180 except Exception as e: 181 self._log.warning('error processing event: %s', e, exc_info=e) 182 183 async def _connection_loop(self, device_conf): 184 conn = None 185 186 try: 187 if self._conf['link_type'] == 'BALANCED': 188 conn_args = { 189 'direction': link.Direction[device_conf['direction']], 190 'addr': device_conf['address'], 191 'response_timeout': device_conf['response_timeout'], 192 'send_retry_count': device_conf['send_retry_count'], 193 'status_delay': device_conf['status_delay'], 194 'name': self._conf['name']} 195 196 elif self._conf['link_type'] == 'UNBALANCED': 197 conn_args = { 198 'addr': device_conf['address'], 199 'keep_alive_timeout': device_conf['keep_alive_timeout'], 200 'name': self._conf['name']} 201 202 else: 203 raise ValueError('unsupported link type') 204 205 while True: 206 try: 207 conn = await self._link.open_connection(**conn_args) 208 209 except Exception as e: 210 self._log.error('connection error for address %s: %s', 211 device_conf['address'], e, exc_info=e) 212 await asyncio.sleep(device_conf['reconnect_delay']) 213 continue 214 215 conn = iec101.Connection( 216 conn=conn, 217 cause_size=iec101.CauseSize[self._conf['cause_size']], 218 asdu_address_size=iec101.AsduAddressSize[ 219 self._conf['asdu_address_size']], 220 io_address_size=iec101.IoAddressSize[ 221 self._conf['io_address_size']]) 222 223 conn_id = next(self._next_conn_ids) 224 self._conns[conn_id] = conn 225 226 send_queue = aio.Queue(1024) 227 self._send_queues[conn_id] = send_queue 228 229 try: 230 conn.async_group.spawn(self._connection_send_loop, conn, 231 send_queue) 232 conn.async_group.spawn(self._connection_receive_loop, conn, 233 conn_id) 234 235 await self._register_connections() 236 237 with contextlib.suppress(Exception): 238 for buffer in self._buffers.values(): 239 for event_id, data_key, data_msg in buffer.get(): 240 await self._send_data_msg( 241 conn_id=conn_id, 242 buffer=buffer, 243 event_id=event_id, 244 data_msg=data_msg, 245 with_ack=data_key in self._data_with_ack) 246 247 await conn.wait_closed() 248 249 finally: 250 send_queue.close() 251 252 self._conns.pop(conn_id, None) 253 self._send_queues.pop(conn_id, None) 254 255 with contextlib.suppress(Exception): 256 await aio.uncancellable(self._register_connections()) 257 258 await conn.async_close() 259 260 except Exception as e: 261 self._log.warning('connection loop error: %s', e, exc_info=e) 262 263 finally: 264 self._log.debug('closing connection') 265 self.close() 266 267 if conn: 268 await aio.uncancellable(conn.async_close()) 269 270 async def _connection_send_loop(self, conn, send_queue): 271 try: 272 while True: 273 msgs, sent_cb, with_ack = await send_queue.get() 274 await conn.send(msgs, 275 sent_cb=sent_cb, 276 with_ack=with_ack) 277 278 except ConnectionError: 279 self._log.debug('connection close') 280 281 except Exception as e: 282 self._log.warning('connection send loop error: %s', e, exc_info=e) 283 284 finally: 285 conn.close() 286 287 async def _connection_receive_loop(self, conn, conn_id): 288 try: 289 while True: 290 try: 291 msgs = await conn.receive() 292 293 except iec101.AsduTypeError as e: 294 self._log.warning("asdu type error: %s", e) 295 continue 296 297 for msg in msgs: 298 try: 299 self._log.debug('received message: %s', msg) 300 await self._process_msg(conn_id, msg) 301 302 except Exception as e: 303 self._log.warning('error processing message: %s', 304 e, exc_info=e) 305 306 except ConnectionError: 307 self._log.debug('connection close') 308 309 except Exception as e: 310 self._log.warning('connection receive loop error: %s', 311 e, exc_info=e) 312 313 finally: 314 conn.close() 315 316 async def _register_connections(self): 317 payload = [{'connection_id': conn_id, 318 'address': conn.info.address} 319 for conn_id, conn in self._conns.items()] 320 321 event = hat.event.common.RegisterEvent( 322 type=(*self._event_type_prefix, 'gateway', 'connections'), 323 source_timestamp=None, 324 payload=hat.event.common.EventPayloadJson(payload)) 325 326 await self._eventer_client.register([event]) 327 328 async def _process_event(self, event): 329 suffix = event.type[len(self._event_type_prefix):] 330 331 if suffix[:2] == ('system', 'data'): 332 data_type_str, asdu_address_str, io_address_str = suffix[2:] 333 data_key = common.DataKey(data_type=common.DataType(data_type_str), 334 asdu_address=int(asdu_address_str), 335 io_address=int(io_address_str)) 336 337 await self._process_data_event(data_key, event) 338 339 elif suffix[:2] == ('system', 'command'): 340 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 341 cmd_key = common.CommandKey( 342 cmd_type=common.CommandType(cmd_type_str), 343 asdu_address=int(asdu_address_str), 344 io_address=int(io_address_str)) 345 346 await self._process_command_event(cmd_key, event) 347 348 else: 349 raise Exception('unsupported event type') 350 351 async def _process_data_event(self, data_key, event): 352 if data_key not in self._data_msgs: 353 raise Exception('data not configured') 354 355 data_msg = data_msg_from_event(data_key, event) 356 self._data_msgs[data_key] = data_msg 357 358 buffer = self._data_buffers.get(data_key) 359 if buffer: 360 buffer.add(event.id, data_key, data_msg) 361 362 with_ack = data_key in self._data_with_ack 363 364 for conn_id in self._conns.keys(): 365 await self._send_data_msg(conn_id=conn_id, 366 buffer=buffer, 367 event_id=event.id, 368 data_msg=data_msg, 369 with_ack=with_ack) 370 371 async def _process_command_event(self, cmd_key, event): 372 cmd_msg = cmd_msg_from_event(cmd_key, event) 373 conn_id = event.payload.data['connection_id'] 374 await self._send(conn_id, [cmd_msg]) 375 376 async def _process_msg(self, conn_id, msg): 377 if isinstance(msg, iec101.CommandMsg): 378 await self._process_command_msg(conn_id, msg) 379 380 elif isinstance(msg, iec101.InterrogationMsg): 381 await self._process_interrogation_msg(conn_id, msg) 382 383 elif isinstance(msg, iec101.CounterInterrogationMsg): 384 await self._process_counter_interrogation_msg(conn_id, msg) 385 386 elif isinstance(msg, iec101.ReadMsg): 387 await self._process_read_msg(conn_id, msg) 388 389 elif isinstance(msg, iec101.ClockSyncMsg): 390 await self._process_clock_sync_msg(conn_id, msg) 391 392 elif isinstance(msg, iec101.TestMsg): 393 await self._process_test_msg(conn_id, msg) 394 395 elif isinstance(msg, iec101.ResetMsg): 396 await self._process_reset_msg(conn_id, msg) 397 398 elif isinstance(msg, iec101.ParameterMsg): 399 await self._process_parameter_msg(conn_id, msg) 400 401 elif isinstance(msg, iec101.ParameterActivationMsg): 402 await self._process_parameter_activation_msg(conn_id, msg) 403 404 else: 405 raise Exception('unsupported message') 406 407 async def _process_command_msg(self, conn_id, msg): 408 if isinstance(msg.cause, iec101.CommandReqCause): 409 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 410 await self._eventer_client.register([event]) 411 412 else: 413 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 414 is_negative_confirm=True) 415 await self._send(conn_id, [res]) 416 417 async def _process_interrogation_msg(self, conn_id, msg): 418 if msg.cause == iec101.CommandReqCause.ACTIVATION: 419 asdu_data_msgs = collections.defaultdict(collections.deque) 420 421 for data_key, data_msg in self._data_msgs.items(): 422 if data_key.data_type == common.DataType.BINARY_COUNTER: 423 continue 424 425 if (msg.asdu_address != self._broadcast_asdu_address and 426 msg.asdu_address != data_key.asdu_address): 427 continue 428 429 asdu_data_msgs[data_key.asdu_address].append(data_msg) 430 431 if msg.asdu_address != self._broadcast_asdu_address: 432 asdu_data_msgs[msg.asdu_address].append(None) 433 434 for asdu_address, data_msgs in asdu_data_msgs.items(): 435 res = msg._replace( 436 asdu_address=asdu_address, 437 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 438 is_negative_confirm=False) 439 await self._send(conn_id, [res]) 440 441 msgs = [ 442 data_msg._replace( 443 is_test=msg.is_test, 444 cause=iec101.DataResCause.INTERROGATED_STATION) 445 for data_msg in data_msgs 446 if data_msg] 447 if msgs: 448 await self._send(conn_id, msgs) 449 450 res = msg._replace( 451 asdu_address=asdu_address, 452 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 453 is_negative_confirm=False) 454 await self._send(conn_id, [res]) 455 456 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 457 res = msg._replace( 458 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 459 is_negative_confirm=True) 460 await self._send(conn_id, [res]) 461 462 else: 463 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 464 is_negative_confirm=True) 465 await self._send(conn_id, [res]) 466 467 async def _process_counter_interrogation_msg(self, conn_id, msg): 468 if msg.cause == iec101.CommandReqCause.ACTIVATION: 469 asdu_data_msgs = collections.defaultdict(collections.deque) 470 471 for data_key, data_msg in self._data_msgs.items(): 472 if data_key.data_type != common.DataType.BINARY_COUNTER: 473 continue 474 475 if (msg.asdu_address != self._broadcast_asdu_address and 476 msg.asdu_address != data_key.asdu_address): 477 continue 478 479 asdu_data_msgs[data_key.asdu_address].append(data_msg) 480 481 if msg.asdu_address != self._broadcast_asdu_address: 482 asdu_data_msgs[msg.asdu_address].append(None) 483 484 for asdu_address, data_msgs in asdu_data_msgs.items(): 485 res = msg._replace( 486 asdu_address=asdu_address, 487 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 488 is_negative_confirm=False) 489 await self._send(conn_id, [res]) 490 491 msgs = [ 492 data_msg._replace( 493 is_test=msg.is_test, 494 cause=iec101.DataResCause.INTERROGATED_COUNTER) 495 for data_msg in data_msgs 496 if data_msg] 497 if msgs: 498 await self._send(conn_id, msgs) 499 500 res = msg._replace( 501 asdu_address=asdu_address, 502 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 503 is_negative_confirm=False) 504 await self._send(conn_id, [res]) 505 506 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 507 res = msg._replace( 508 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 509 is_negative_confirm=True) 510 await self._send(conn_id, [res]) 511 512 else: 513 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 514 is_negative_confirm=True) 515 await self._send(conn_id, [res]) 516 517 async def _process_read_msg(self, conn_id, msg): 518 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 519 await self._send(conn_id, [res]) 520 521 async def _process_clock_sync_msg(self, conn_id, msg): 522 if isinstance(msg.cause, iec101.ClockSyncReqCause): 523 res = msg._replace( 524 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 525 is_negative_confirm=True) 526 await self._send(conn_id, [res]) 527 528 else: 529 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 530 is_negative_confirm=True) 531 await self._send(conn_id, [res]) 532 533 async def _process_test_msg(self, conn_id, msg): 534 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 535 await self._send(conn_id, [res]) 536 537 async def _process_reset_msg(self, conn_id, msg): 538 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 539 await self._send(conn_id, [res]) 540 541 async def _process_parameter_msg(self, conn_id, msg): 542 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 543 await self._send(conn_id, [res]) 544 545 async def _process_parameter_activation_msg(self, conn_id, msg): 546 res = msg._replace( 547 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 548 await self._send(conn_id, [res]) 549 550 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg, 551 with_ack): 552 sent_cb = (functools.partial(buffer.remove, event_id) 553 if buffer else None) 554 555 await self._send(conn_id=conn_id, 556 msgs=[data_msg], 557 sent_cb=sent_cb, 558 with_ack=with_ack) 559 560 async def _send(self, conn_id, msgs, sent_cb=None, with_ack=True): 561 send_queue = self._send_queues.get(conn_id) 562 if send_queue is None: 563 return 564 565 try: 566 send_queue.put_nowait((msgs, sent_cb, with_ack)) 567 568 except aio.QueueFullError: 569 self._log.warning('send queue full') 570 571 conn = self._conns.get(conn_id) 572 if conn: 573 conn.close() 574 575 except aio.QueueClosedError: 576 pass
Device interface
async def
process_event(self, event: hat.event.common.common.Event):
176 async def process_event(self, event: hat.event.common.Event): 177 try: 178 await self._process_event(event) 179 180 except Exception as e: 181 self._log.warning('error processing event: %s', e, exc_info=e)
Process received event
This method can be coroutine or regular function.
def
cmd_msg_to_event( event_type_prefix: tuple[str, ...], conn_id: int, msg: hat.drivers.iec101.common.CommandMsg) -> hat.event.common.common.RegisterEvent:
579def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 580 conn_id: int, 581 msg: iec101.CommandMsg 582 ) -> hat.event.common.RegisterEvent: 583 command_type = common.get_command_type(msg.command) 584 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 585 command = common.command_to_json(msg.command) 586 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 587 str(msg.asdu_address), str(msg.io_address)) 588 589 return hat.event.common.RegisterEvent( 590 type=event_type, 591 source_timestamp=None, 592 payload=hat.event.common.EventPayloadJson({ 593 'connection_id': conn_id, 594 'is_test': msg.is_test, 595 'cause': cause, 596 'command': command}))
def
data_msg_from_event( data_key: hat.gateway.devices.iec101.common.DataKey, event: hat.event.common.common.Event) -> hat.drivers.iec101.common.DataMsg:
599def data_msg_from_event(data_key: common.DataKey, 600 event: hat.event.common.Event 601 ) -> iec101.DataMsg: 602 time = common.time_from_source_timestamp(event.source_timestamp) 603 cause = common.cause_from_json(iec101.DataResCause, 604 event.payload.data['cause']) 605 data = common.data_from_json(data_key.data_type, 606 event.payload.data['data']) 607 608 return iec101.DataMsg(is_test=event.payload.data['is_test'], 609 originator_address=0, 610 asdu_address=data_key.asdu_address, 611 io_address=data_key.io_address, 612 data=data, 613 time=time, 614 cause=cause)
def
cmd_msg_from_event( cmd_key: hat.gateway.devices.iec101.common.CommandKey, event: hat.event.common.common.Event) -> hat.drivers.iec101.common.CommandMsg:
617def cmd_msg_from_event(cmd_key: common.CommandKey, 618 event: hat.event.common.Event 619 ) -> iec101.CommandMsg: 620 cause = common.cause_from_json(iec101.CommandResCause, 621 event.payload.data['cause']) 622 command = common.command_from_json(cmd_key.cmd_type, 623 event.payload.data['command']) 624 is_negative_confirm = event.payload.data['is_negative_confirm'] 625 626 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 627 originator_address=0, 628 asdu_address=cmd_key.asdu_address, 629 io_address=cmd_key.io_address, 630 command=command, 631 is_negative_confirm=is_negative_confirm, 632 cause=cause)