hat.gateway.devices.iec101.slave
IEC 60870-5-101 slave device
1"""IEC 60870-5-101 slave device""" 2 3from collections.abc import Collection, 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._broadcast_asdu_address = _get_broadcast_asdu_address( 40 iec101.AsduAddressSize[conf['asdu_address_size']]) 41 device._log = _create_logger_adapter(conf['name']) 42 43 init_buffers(buffers_conf=conf['buffers'], 44 buffers=device._buffers) 45 46 await init_data(log=device._log, 47 data_conf=conf['data'], 48 data_msgs=device._data_msgs, 49 data_buffers=device._data_buffers, 50 buffers=device._buffers, 51 eventer_client=eventer_client, 52 event_type_prefix=event_type_prefix) 53 54 if conf['link_type'] == 'BALANCED': 55 create_link = link.create_balanced_link 56 57 elif conf['link_type'] == 'UNBALANCED': 58 create_link = link.create_slave_link 59 60 else: 61 raise ValueError('unsupported link type') 62 63 device._link = await create_link( 64 port=conf['port'], 65 address_size=link.AddressSize[conf['device_address_size']], 66 silent_interval=conf['silent_interval'], 67 baudrate=conf['baudrate'], 68 bytesize=serial.ByteSize[conf['bytesize']], 69 parity=serial.Parity[conf['parity']], 70 stopbits=serial.StopBits[conf['stopbits']], 71 xonxoff=conf['flow_control']['xonxoff'], 72 rtscts=conf['flow_control']['rtscts'], 73 dsrdtr=conf['flow_control']['dsrdtr'], 74 name=conf['name']) 75 76 try: 77 for device_conf in conf['devices']: 78 device.async_group.spawn(device._connection_loop, device_conf) 79 80 await device._register_connections() 81 82 except BaseException: 83 await aio.uncancellable(device.async_close()) 84 raise 85 86 return device 87 88 89info: common.DeviceInfo = common.DeviceInfo( 90 type="iec101_slave", 91 create=create, 92 json_schema_id="hat-gateway://iec101.yaml#/$defs/slave", 93 json_schema_repo=common.json_schema_repo) 94 95 96class Buffer: 97 98 def __init__(self, size: int): 99 self._size = size 100 self._data = collections.OrderedDict() 101 102 def add(self, 103 event_id: hat.event.common.EventId, 104 data_msg: iec101.DataMsg): 105 self._data[event_id] = data_msg 106 while len(self._data) > self._size: 107 self._data.popitem(last=False) 108 109 def remove(self, event_id: hat.event.common.EventId): 110 self._data.pop(event_id, None) 111 112 def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId, # NOQA 113 iec101.DataMsg]]: 114 return self._data.items() 115 116 117def init_buffers(buffers_conf: json.Data, 118 buffers: dict[str, Buffer]): 119 for buffer_conf in buffers_conf: 120 buffers[buffer_conf['name']] = Buffer(buffer_conf['size']) 121 122 123async def init_data(log: logging.Logger, 124 data_conf: json.Data, 125 data_msgs: dict[common.DataKey, iec101.DataMsg], 126 data_buffers: dict[common.DataKey, Buffer], 127 buffers: dict[str, Buffer], 128 eventer_client: hat.event.eventer.Client, 129 event_type_prefix: common.EventTypePrefix): 130 for data in data_conf: 131 data_key = common.DataKey(data_type=common.DataType[data['data_type']], 132 asdu_address=data['asdu_address'], 133 io_address=data['io_address']) 134 data_msgs[data_key] = None 135 if data['buffer']: 136 data_buffers[data_key] = buffers[data['buffer']] 137 138 event_types = [(*event_type_prefix, 'system', 'data', '*')] 139 params = hat.event.common.QueryLatestParams(event_types) 140 result = await eventer_client.query(params) 141 142 for event in result.events: 143 try: 144 data_type_str, asdu_address_str, io_address_str = \ 145 event.type[len(event_type_prefix)+2:] 146 data_key = common.DataKey(data_type=common.DataType(data_type_str), 147 asdu_address=int(asdu_address_str), 148 io_address=int(io_address_str)) 149 if data_key not in data_msgs: 150 raise Exception(f'data {data_key} not configured') 151 152 data_msgs[data_key] = data_msg_from_event(data_key, event) 153 154 except Exception as e: 155 log.debug('skipping initial data: %s', e, exc_info=e) 156 157 158class Iec101SlaveDevice(common.Device): 159 160 @property 161 def async_group(self) -> aio.Group: 162 return self._link.async_group 163 164 async def process_events(self, events: Collection[hat.event.common.Event]): 165 for event in events: 166 try: 167 await self._process_event(event) 168 169 except Exception as e: 170 self._log.warning('error processing event: %s', e, exc_info=e) 171 172 async def _connection_loop(self, device_conf): 173 conn = None 174 175 try: 176 if self._conf['link_type'] == 'BALANCED': 177 conn_args = { 178 'direction': link.Direction[device_conf['direction']], 179 'addr': device_conf['address'], 180 'response_timeout': device_conf['response_timeout'], 181 'send_retry_count': device_conf['send_retry_count'], 182 'status_delay': device_conf['status_delay'], 183 'name': self._conf['name']} 184 185 elif self._conf['link_type'] == 'UNBALANCED': 186 conn_args = { 187 'addr': device_conf['address'], 188 'keep_alive_timeout': device_conf['keep_alive_timeout'], 189 'name': self._conf['name']} 190 191 else: 192 raise ValueError('unsupported link type') 193 194 while True: 195 try: 196 conn = await self._link.open_connection(**conn_args) 197 198 except Exception as e: 199 self._log.error('connection error for address %s: %s', 200 device_conf['address'], e, exc_info=e) 201 await asyncio.sleep(device_conf['reconnect_delay']) 202 continue 203 204 conn = iec101.Connection( 205 conn=conn, 206 cause_size=iec101.CauseSize[self._conf['cause_size']], 207 asdu_address_size=iec101.AsduAddressSize[ 208 self._conf['asdu_address_size']], 209 io_address_size=iec101.IoAddressSize[ 210 self._conf['io_address_size']]) 211 212 conn_id = next(self._next_conn_ids) 213 self._conns[conn_id] = conn 214 215 send_queue = aio.Queue(1024) 216 self._send_queues[conn_id] = send_queue 217 218 try: 219 conn.async_group.spawn(self._connection_send_loop, conn, 220 send_queue) 221 conn.async_group.spawn(self._connection_receive_loop, conn, 222 conn_id) 223 224 await self._register_connections() 225 226 with contextlib.suppress(Exception): 227 for buffer in self._buffers.values(): 228 for event_id, data_msg in buffer.get_event_id_data_msgs(): # NOQA 229 await self._send_data_msg(conn_id, buffer, 230 event_id, data_msg) 231 232 await conn.wait_closed() 233 234 finally: 235 send_queue.close() 236 237 self._conns.pop(conn_id, None) 238 self._send_queues.pop(conn_id, None) 239 240 with contextlib.suppress(Exception): 241 await aio.uncancellable(self._register_connections()) 242 243 await conn.async_close() 244 245 except Exception as e: 246 self._log.warning('connection loop error: %s', e, exc_info=e) 247 248 finally: 249 self._log.debug('closing connection') 250 self.close() 251 252 if conn: 253 await aio.uncancellable(conn.async_close()) 254 255 async def _connection_send_loop(self, conn, send_queue): 256 try: 257 while True: 258 msgs, sent_cb = await send_queue.get() 259 await conn.send(msgs, sent_cb=sent_cb) 260 261 except ConnectionError: 262 self._log.debug('connection close') 263 264 except Exception as e: 265 self._log.warning('connection send loop error: %s', e, exc_info=e) 266 267 finally: 268 conn.close() 269 270 async def _connection_receive_loop(self, conn, conn_id): 271 try: 272 while True: 273 msgs = await conn.receive() 274 275 for msg in msgs: 276 try: 277 self._log.debug('received message: %s', msg) 278 await self._process_msg(conn_id, msg) 279 280 except Exception as e: 281 self._log.warning('error processing message: %s', 282 e, exc_info=e) 283 284 except ConnectionError: 285 self._log.debug('connection close') 286 287 except Exception as e: 288 self._log.warning('connection receive loop error: %s', 289 e, exc_info=e) 290 291 finally: 292 conn.close() 293 294 async def _register_connections(self): 295 payload = [{'connection_id': conn_id, 296 'address': conn.info.address} 297 for conn_id, conn in self._conns.items()] 298 299 event = hat.event.common.RegisterEvent( 300 type=(*self._event_type_prefix, 'gateway', 'connections'), 301 source_timestamp=None, 302 payload=hat.event.common.EventPayloadJson(payload)) 303 304 await self._eventer_client.register([event]) 305 306 async def _process_event(self, event): 307 suffix = event.type[len(self._event_type_prefix):] 308 309 if suffix[:2] == ('system', 'data'): 310 data_type_str, asdu_address_str, io_address_str = suffix[2:] 311 data_key = common.DataKey(data_type=common.DataType(data_type_str), 312 asdu_address=int(asdu_address_str), 313 io_address=int(io_address_str)) 314 315 await self._process_data_event(data_key, event) 316 317 elif suffix[:2] == ('system', 'command'): 318 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 319 cmd_key = common.CommandKey( 320 cmd_type=common.CommandType(cmd_type_str), 321 asdu_address=int(asdu_address_str), 322 io_address=int(io_address_str)) 323 324 await self._process_command_event(cmd_key, event) 325 326 else: 327 raise Exception('unsupported event type') 328 329 async def _process_data_event(self, data_key, event): 330 if data_key not in self._data_msgs: 331 raise Exception('data not configured') 332 333 data_msg = data_msg_from_event(data_key, event) 334 self._data_msgs[data_key] = data_msg 335 336 buffer = self._data_buffers.get(data_key) 337 if buffer: 338 buffer.add(event.id, data_msg) 339 340 for conn_id in self._conns.keys(): 341 await self._send_data_msg(conn_id, buffer, event.id, data_msg) 342 343 async def _process_command_event(self, cmd_key, event): 344 cmd_msg = cmd_msg_from_event(cmd_key, event) 345 conn_id = event.payload.data['connection_id'] 346 await self._send(conn_id, [cmd_msg]) 347 348 async def _process_msg(self, conn_id, msg): 349 if isinstance(msg, iec101.CommandMsg): 350 await self._process_command_msg(conn_id, msg) 351 352 elif isinstance(msg, iec101.InterrogationMsg): 353 await self._process_interrogation_msg(conn_id, msg) 354 355 elif isinstance(msg, iec101.CounterInterrogationMsg): 356 await self._process_counter_interrogation_msg(conn_id, msg) 357 358 elif isinstance(msg, iec101.ReadMsg): 359 await self._process_read_msg(conn_id, msg) 360 361 elif isinstance(msg, iec101.ClockSyncMsg): 362 await self._process_clock_sync_msg(conn_id, msg) 363 364 elif isinstance(msg, iec101.TestMsg): 365 await self._process_test_msg(conn_id, msg) 366 367 elif isinstance(msg, iec101.ResetMsg): 368 await self._process_reset_msg(conn_id, msg) 369 370 elif isinstance(msg, iec101.ParameterMsg): 371 await self._process_parameter_msg(conn_id, msg) 372 373 elif isinstance(msg, iec101.ParameterActivationMsg): 374 await self._process_parameter_activation_msg(conn_id, msg) 375 376 else: 377 raise Exception('unsupported message') 378 379 async def _process_command_msg(self, conn_id, msg): 380 if isinstance(msg.cause, iec101.CommandReqCause): 381 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 382 await self._eventer_client.register([event]) 383 384 else: 385 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 386 is_negative_confirm=True) 387 await self._send(conn_id, [res]) 388 389 async def _process_interrogation_msg(self, conn_id, msg): 390 if msg.cause == iec101.CommandReqCause.ACTIVATION: 391 asdu_data_msgs = collections.defaultdict(collections.deque) 392 393 for data_key, data_msg in self._data_msgs.items(): 394 if data_key.data_type == common.DataType.BINARY_COUNTER: 395 continue 396 397 if (msg.asdu_address != self._broadcast_asdu_address and 398 msg.asdu_address != data_key.asdu_address): 399 continue 400 401 asdu_data_msgs[data_key.asdu_address].append(data_msg) 402 403 if msg.asdu_address != self._broadcast_asdu_address: 404 asdu_data_msgs[msg.asdu_address].append(None) 405 406 for asdu_address, data_msgs in asdu_data_msgs.items(): 407 res = msg._replace( 408 asdu_address=asdu_address, 409 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 410 is_negative_confirm=False) 411 await self._send(conn_id, [res]) 412 413 msgs = [ 414 data_msg._replace( 415 is_test=msg.is_test, 416 cause=iec101.DataResCause.INTERROGATED_STATION) 417 for data_msg in data_msgs 418 if data_msg] 419 if msgs: 420 await self._send(conn_id, msgs) 421 422 res = msg._replace( 423 asdu_address=asdu_address, 424 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 425 is_negative_confirm=False) 426 await self._send(conn_id, [res]) 427 428 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 429 res = msg._replace( 430 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 431 is_negative_confirm=True) 432 await self._send(conn_id, [res]) 433 434 else: 435 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 436 is_negative_confirm=True) 437 await self._send(conn_id, [res]) 438 439 async def _process_counter_interrogation_msg(self, conn_id, msg): 440 if msg.cause == iec101.CommandReqCause.ACTIVATION: 441 asdu_data_msgs = collections.defaultdict(collections.deque) 442 443 for data_key, data_msg in self._data_msgs.items(): 444 if data_key.data_type != common.DataType.BINARY_COUNTER: 445 continue 446 447 if (msg.asdu_address != self._broadcast_asdu_address and 448 msg.asdu_address != data_key.asdu_address): 449 continue 450 451 asdu_data_msgs[data_key.asdu_address].append(data_msg) 452 453 if msg.asdu_address != self._broadcast_asdu_address: 454 asdu_data_msgs[msg.asdu_address].append(None) 455 456 for asdu_address, data_msgs in asdu_data_msgs.items(): 457 res = msg._replace( 458 asdu_address=asdu_address, 459 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 460 is_negative_confirm=False) 461 await self._send(conn_id, [res]) 462 463 msgs = [ 464 data_msg._replace( 465 is_test=msg.is_test, 466 cause=iec101.DataResCause.INTERROGATED_COUNTER) 467 for data_msg in data_msgs 468 if data_msg] 469 if msgs: 470 await self._send(conn_id, msgs) 471 472 res = msg._replace( 473 asdu_address=asdu_address, 474 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 475 is_negative_confirm=False) 476 await self._send(conn_id, [res]) 477 478 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 479 res = msg._replace( 480 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 481 is_negative_confirm=True) 482 await self._send(conn_id, [res]) 483 484 else: 485 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 486 is_negative_confirm=True) 487 await self._send(conn_id, [res]) 488 489 async def _process_read_msg(self, conn_id, msg): 490 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 491 await self._send(conn_id, [res]) 492 493 async def _process_clock_sync_msg(self, conn_id, msg): 494 if isinstance(msg.cause, iec101.ClockSyncReqCause): 495 res = msg._replace( 496 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 497 is_negative_confirm=True) 498 await self._send(conn_id, [res]) 499 500 else: 501 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 502 is_negative_confirm=True) 503 await self._send(conn_id, [res]) 504 505 async def _process_test_msg(self, conn_id, msg): 506 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 507 await self._send(conn_id, [res]) 508 509 async def _process_reset_msg(self, conn_id, msg): 510 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 511 await self._send(conn_id, [res]) 512 513 async def _process_parameter_msg(self, conn_id, msg): 514 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 515 await self._send(conn_id, [res]) 516 517 async def _process_parameter_activation_msg(self, conn_id, msg): 518 res = msg._replace( 519 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 520 await self._send(conn_id, [res]) 521 522 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg): 523 sent_cb = (functools.partial(buffer.remove, event_id) 524 if buffer else None) 525 await self._send(conn_id, [data_msg], sent_cb=sent_cb) 526 527 async def _send(self, conn_id, msgs, sent_cb=None): 528 send_queue = self._send_queues.get(conn_id) 529 if send_queue is None: 530 return 531 532 with contextlib.suppress(aio.QueueClosedError): 533 await send_queue.put((msgs, sent_cb)) 534 535 536def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 537 conn_id: int, 538 msg: iec101.CommandMsg 539 ) -> hat.event.common.RegisterEvent: 540 command_type = common.get_command_type(msg.command) 541 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 542 command = common.command_to_json(msg.command) 543 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 544 str(msg.asdu_address), str(msg.io_address)) 545 546 return hat.event.common.RegisterEvent( 547 type=event_type, 548 source_timestamp=None, 549 payload=hat.event.common.EventPayloadJson({ 550 'connection_id': conn_id, 551 'is_test': msg.is_test, 552 'cause': cause, 553 'command': command})) 554 555 556def data_msg_from_event(data_key: common.DataKey, 557 event: hat.event.common.Event 558 ) -> iec101.DataMsg: 559 time = common.time_from_source_timestamp(event.source_timestamp) 560 cause = common.cause_from_json(iec101.DataResCause, 561 event.payload.data['cause']) 562 data = common.data_from_json(data_key.data_type, 563 event.payload.data['data']) 564 565 return iec101.DataMsg(is_test=event.payload.data['is_test'], 566 originator_address=0, 567 asdu_address=data_key.asdu_address, 568 io_address=data_key.io_address, 569 data=data, 570 time=time, 571 cause=cause) 572 573 574def cmd_msg_from_event(cmd_key: common.CommandKey, 575 event: hat.event.common.Event 576 ) -> iec101.CommandMsg: 577 cause = common.cause_from_json(iec101.CommandResCause, 578 event.payload.data['cause']) 579 command = common.command_from_json(cmd_key.cmd_type, 580 event.payload.data['command']) 581 is_negative_confirm = event.payload.data['is_negative_confirm'] 582 583 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 584 originator_address=0, 585 asdu_address=cmd_key.asdu_address, 586 io_address=cmd_key.io_address, 587 command=command, 588 is_negative_confirm=is_negative_confirm, 589 cause=cause) 590 591 592def _get_broadcast_asdu_address(asdu_address_size): 593 if asdu_address_size == iec101.AsduAddressSize.ONE: 594 return 0xFF 595 596 if asdu_address_size == iec101.AsduAddressSize.TWO: 597 return 0xFFFF 598 599 raise ValueError('unsupported asdu address size') 600 601 602def _create_logger_adapter(name): 603 extra = {'meta': {'type': 'Iec101SlaveDevice', 604 'name': name}} 605 606 return logging.LoggerAdapter(mlog, extra)
async def
create( conf: Union[NoneType, 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._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 buffers=device._buffers, 52 eventer_client=eventer_client, 53 event_type_prefix=event_type_prefix) 54 55 if conf['link_type'] == 'BALANCED': 56 create_link = link.create_balanced_link 57 58 elif conf['link_type'] == 'UNBALANCED': 59 create_link = link.create_slave_link 60 61 else: 62 raise ValueError('unsupported link type') 63 64 device._link = await create_link( 65 port=conf['port'], 66 address_size=link.AddressSize[conf['device_address_size']], 67 silent_interval=conf['silent_interval'], 68 baudrate=conf['baudrate'], 69 bytesize=serial.ByteSize[conf['bytesize']], 70 parity=serial.Parity[conf['parity']], 71 stopbits=serial.StopBits[conf['stopbits']], 72 xonxoff=conf['flow_control']['xonxoff'], 73 rtscts=conf['flow_control']['rtscts'], 74 dsrdtr=conf['flow_control']['dsrdtr'], 75 name=conf['name']) 76 77 try: 78 for device_conf in conf['devices']: 79 device.async_group.spawn(device._connection_loop, device_conf) 80 81 await device._register_connections() 82 83 except BaseException: 84 await aio.uncancellable(device.async_close()) 85 raise 86 87 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://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://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://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 silet 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', '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']}, 'keep_alive_timeout': {'type': ['number', 'null']}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', '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 silet interval\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': ['connection_id', 'value'], 'properties': {'connection': {'type': 'integer'}, 'value': {'type': 'integer'}}}}, 'system': {'data': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}}}}}}}}, '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://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']}}}]}}}, '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://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://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'], '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']}}}}}}, {'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://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'}}}}}}})
class
Buffer:
97class Buffer: 98 99 def __init__(self, size: int): 100 self._size = size 101 self._data = collections.OrderedDict() 102 103 def add(self, 104 event_id: hat.event.common.EventId, 105 data_msg: iec101.DataMsg): 106 self._data[event_id] = data_msg 107 while len(self._data) > self._size: 108 self._data.popitem(last=False) 109 110 def remove(self, event_id: hat.event.common.EventId): 111 self._data.pop(event_id, None) 112 113 def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId, # NOQA 114 iec101.DataMsg]]: 115 return self._data.items()
def
add( self, event_id: hat.event.common.common.EventId, data_msg: hat.drivers.iec101.common.DataMsg):
def
init_buffers( buffers_conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], buffers: dict[str, Buffer]):
async def
init_data( log: logging.Logger, data_conf: Union[NoneType, 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], buffers: dict[str, Buffer], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]):
124async def init_data(log: logging.Logger, 125 data_conf: json.Data, 126 data_msgs: dict[common.DataKey, iec101.DataMsg], 127 data_buffers: dict[common.DataKey, Buffer], 128 buffers: dict[str, Buffer], 129 eventer_client: hat.event.eventer.Client, 130 event_type_prefix: common.EventTypePrefix): 131 for data in data_conf: 132 data_key = common.DataKey(data_type=common.DataType[data['data_type']], 133 asdu_address=data['asdu_address'], 134 io_address=data['io_address']) 135 data_msgs[data_key] = None 136 if data['buffer']: 137 data_buffers[data_key] = buffers[data['buffer']] 138 139 event_types = [(*event_type_prefix, 'system', 'data', '*')] 140 params = hat.event.common.QueryLatestParams(event_types) 141 result = await eventer_client.query(params) 142 143 for event in result.events: 144 try: 145 data_type_str, asdu_address_str, io_address_str = \ 146 event.type[len(event_type_prefix)+2:] 147 data_key = common.DataKey(data_type=common.DataType(data_type_str), 148 asdu_address=int(asdu_address_str), 149 io_address=int(io_address_str)) 150 if data_key not in data_msgs: 151 raise Exception(f'data {data_key} not configured') 152 153 data_msgs[data_key] = data_msg_from_event(data_key, event) 154 155 except Exception as e: 156 log.debug('skipping initial data: %s', e, exc_info=e)
159class Iec101SlaveDevice(common.Device): 160 161 @property 162 def async_group(self) -> aio.Group: 163 return self._link.async_group 164 165 async def process_events(self, events: Collection[hat.event.common.Event]): 166 for event in events: 167 try: 168 await self._process_event(event) 169 170 except Exception as e: 171 self._log.warning('error processing event: %s', e, exc_info=e) 172 173 async def _connection_loop(self, device_conf): 174 conn = None 175 176 try: 177 if self._conf['link_type'] == 'BALANCED': 178 conn_args = { 179 'direction': link.Direction[device_conf['direction']], 180 'addr': device_conf['address'], 181 'response_timeout': device_conf['response_timeout'], 182 'send_retry_count': device_conf['send_retry_count'], 183 'status_delay': device_conf['status_delay'], 184 'name': self._conf['name']} 185 186 elif self._conf['link_type'] == 'UNBALANCED': 187 conn_args = { 188 'addr': device_conf['address'], 189 'keep_alive_timeout': device_conf['keep_alive_timeout'], 190 'name': self._conf['name']} 191 192 else: 193 raise ValueError('unsupported link type') 194 195 while True: 196 try: 197 conn = await self._link.open_connection(**conn_args) 198 199 except Exception as e: 200 self._log.error('connection error for address %s: %s', 201 device_conf['address'], e, exc_info=e) 202 await asyncio.sleep(device_conf['reconnect_delay']) 203 continue 204 205 conn = iec101.Connection( 206 conn=conn, 207 cause_size=iec101.CauseSize[self._conf['cause_size']], 208 asdu_address_size=iec101.AsduAddressSize[ 209 self._conf['asdu_address_size']], 210 io_address_size=iec101.IoAddressSize[ 211 self._conf['io_address_size']]) 212 213 conn_id = next(self._next_conn_ids) 214 self._conns[conn_id] = conn 215 216 send_queue = aio.Queue(1024) 217 self._send_queues[conn_id] = send_queue 218 219 try: 220 conn.async_group.spawn(self._connection_send_loop, conn, 221 send_queue) 222 conn.async_group.spawn(self._connection_receive_loop, conn, 223 conn_id) 224 225 await self._register_connections() 226 227 with contextlib.suppress(Exception): 228 for buffer in self._buffers.values(): 229 for event_id, data_msg in buffer.get_event_id_data_msgs(): # NOQA 230 await self._send_data_msg(conn_id, buffer, 231 event_id, data_msg) 232 233 await conn.wait_closed() 234 235 finally: 236 send_queue.close() 237 238 self._conns.pop(conn_id, None) 239 self._send_queues.pop(conn_id, None) 240 241 with contextlib.suppress(Exception): 242 await aio.uncancellable(self._register_connections()) 243 244 await conn.async_close() 245 246 except Exception as e: 247 self._log.warning('connection loop error: %s', e, exc_info=e) 248 249 finally: 250 self._log.debug('closing connection') 251 self.close() 252 253 if conn: 254 await aio.uncancellable(conn.async_close()) 255 256 async def _connection_send_loop(self, conn, send_queue): 257 try: 258 while True: 259 msgs, sent_cb = await send_queue.get() 260 await conn.send(msgs, sent_cb=sent_cb) 261 262 except ConnectionError: 263 self._log.debug('connection close') 264 265 except Exception as e: 266 self._log.warning('connection send loop error: %s', e, exc_info=e) 267 268 finally: 269 conn.close() 270 271 async def _connection_receive_loop(self, conn, conn_id): 272 try: 273 while True: 274 msgs = await conn.receive() 275 276 for msg in msgs: 277 try: 278 self._log.debug('received message: %s', msg) 279 await self._process_msg(conn_id, msg) 280 281 except Exception as e: 282 self._log.warning('error processing message: %s', 283 e, exc_info=e) 284 285 except ConnectionError: 286 self._log.debug('connection close') 287 288 except Exception as e: 289 self._log.warning('connection receive loop error: %s', 290 e, exc_info=e) 291 292 finally: 293 conn.close() 294 295 async def _register_connections(self): 296 payload = [{'connection_id': conn_id, 297 'address': conn.info.address} 298 for conn_id, conn in self._conns.items()] 299 300 event = hat.event.common.RegisterEvent( 301 type=(*self._event_type_prefix, 'gateway', 'connections'), 302 source_timestamp=None, 303 payload=hat.event.common.EventPayloadJson(payload)) 304 305 await self._eventer_client.register([event]) 306 307 async def _process_event(self, event): 308 suffix = event.type[len(self._event_type_prefix):] 309 310 if suffix[:2] == ('system', 'data'): 311 data_type_str, asdu_address_str, io_address_str = suffix[2:] 312 data_key = common.DataKey(data_type=common.DataType(data_type_str), 313 asdu_address=int(asdu_address_str), 314 io_address=int(io_address_str)) 315 316 await self._process_data_event(data_key, event) 317 318 elif suffix[:2] == ('system', 'command'): 319 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 320 cmd_key = common.CommandKey( 321 cmd_type=common.CommandType(cmd_type_str), 322 asdu_address=int(asdu_address_str), 323 io_address=int(io_address_str)) 324 325 await self._process_command_event(cmd_key, event) 326 327 else: 328 raise Exception('unsupported event type') 329 330 async def _process_data_event(self, data_key, event): 331 if data_key not in self._data_msgs: 332 raise Exception('data not configured') 333 334 data_msg = data_msg_from_event(data_key, event) 335 self._data_msgs[data_key] = data_msg 336 337 buffer = self._data_buffers.get(data_key) 338 if buffer: 339 buffer.add(event.id, data_msg) 340 341 for conn_id in self._conns.keys(): 342 await self._send_data_msg(conn_id, buffer, event.id, data_msg) 343 344 async def _process_command_event(self, cmd_key, event): 345 cmd_msg = cmd_msg_from_event(cmd_key, event) 346 conn_id = event.payload.data['connection_id'] 347 await self._send(conn_id, [cmd_msg]) 348 349 async def _process_msg(self, conn_id, msg): 350 if isinstance(msg, iec101.CommandMsg): 351 await self._process_command_msg(conn_id, msg) 352 353 elif isinstance(msg, iec101.InterrogationMsg): 354 await self._process_interrogation_msg(conn_id, msg) 355 356 elif isinstance(msg, iec101.CounterInterrogationMsg): 357 await self._process_counter_interrogation_msg(conn_id, msg) 358 359 elif isinstance(msg, iec101.ReadMsg): 360 await self._process_read_msg(conn_id, msg) 361 362 elif isinstance(msg, iec101.ClockSyncMsg): 363 await self._process_clock_sync_msg(conn_id, msg) 364 365 elif isinstance(msg, iec101.TestMsg): 366 await self._process_test_msg(conn_id, msg) 367 368 elif isinstance(msg, iec101.ResetMsg): 369 await self._process_reset_msg(conn_id, msg) 370 371 elif isinstance(msg, iec101.ParameterMsg): 372 await self._process_parameter_msg(conn_id, msg) 373 374 elif isinstance(msg, iec101.ParameterActivationMsg): 375 await self._process_parameter_activation_msg(conn_id, msg) 376 377 else: 378 raise Exception('unsupported message') 379 380 async def _process_command_msg(self, conn_id, msg): 381 if isinstance(msg.cause, iec101.CommandReqCause): 382 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 383 await self._eventer_client.register([event]) 384 385 else: 386 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 387 is_negative_confirm=True) 388 await self._send(conn_id, [res]) 389 390 async def _process_interrogation_msg(self, conn_id, msg): 391 if msg.cause == iec101.CommandReqCause.ACTIVATION: 392 asdu_data_msgs = collections.defaultdict(collections.deque) 393 394 for data_key, data_msg in self._data_msgs.items(): 395 if data_key.data_type == common.DataType.BINARY_COUNTER: 396 continue 397 398 if (msg.asdu_address != self._broadcast_asdu_address and 399 msg.asdu_address != data_key.asdu_address): 400 continue 401 402 asdu_data_msgs[data_key.asdu_address].append(data_msg) 403 404 if msg.asdu_address != self._broadcast_asdu_address: 405 asdu_data_msgs[msg.asdu_address].append(None) 406 407 for asdu_address, data_msgs in asdu_data_msgs.items(): 408 res = msg._replace( 409 asdu_address=asdu_address, 410 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 411 is_negative_confirm=False) 412 await self._send(conn_id, [res]) 413 414 msgs = [ 415 data_msg._replace( 416 is_test=msg.is_test, 417 cause=iec101.DataResCause.INTERROGATED_STATION) 418 for data_msg in data_msgs 419 if data_msg] 420 if msgs: 421 await self._send(conn_id, msgs) 422 423 res = msg._replace( 424 asdu_address=asdu_address, 425 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 426 is_negative_confirm=False) 427 await self._send(conn_id, [res]) 428 429 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 430 res = msg._replace( 431 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 432 is_negative_confirm=True) 433 await self._send(conn_id, [res]) 434 435 else: 436 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 437 is_negative_confirm=True) 438 await self._send(conn_id, [res]) 439 440 async def _process_counter_interrogation_msg(self, conn_id, msg): 441 if msg.cause == iec101.CommandReqCause.ACTIVATION: 442 asdu_data_msgs = collections.defaultdict(collections.deque) 443 444 for data_key, data_msg in self._data_msgs.items(): 445 if data_key.data_type != common.DataType.BINARY_COUNTER: 446 continue 447 448 if (msg.asdu_address != self._broadcast_asdu_address and 449 msg.asdu_address != data_key.asdu_address): 450 continue 451 452 asdu_data_msgs[data_key.asdu_address].append(data_msg) 453 454 if msg.asdu_address != self._broadcast_asdu_address: 455 asdu_data_msgs[msg.asdu_address].append(None) 456 457 for asdu_address, data_msgs in asdu_data_msgs.items(): 458 res = msg._replace( 459 asdu_address=asdu_address, 460 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 461 is_negative_confirm=False) 462 await self._send(conn_id, [res]) 463 464 msgs = [ 465 data_msg._replace( 466 is_test=msg.is_test, 467 cause=iec101.DataResCause.INTERROGATED_COUNTER) 468 for data_msg in data_msgs 469 if data_msg] 470 if msgs: 471 await self._send(conn_id, msgs) 472 473 res = msg._replace( 474 asdu_address=asdu_address, 475 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 476 is_negative_confirm=False) 477 await self._send(conn_id, [res]) 478 479 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 480 res = msg._replace( 481 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 482 is_negative_confirm=True) 483 await self._send(conn_id, [res]) 484 485 else: 486 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 487 is_negative_confirm=True) 488 await self._send(conn_id, [res]) 489 490 async def _process_read_msg(self, conn_id, msg): 491 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 492 await self._send(conn_id, [res]) 493 494 async def _process_clock_sync_msg(self, conn_id, msg): 495 if isinstance(msg.cause, iec101.ClockSyncReqCause): 496 res = msg._replace( 497 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 498 is_negative_confirm=True) 499 await self._send(conn_id, [res]) 500 501 else: 502 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 503 is_negative_confirm=True) 504 await self._send(conn_id, [res]) 505 506 async def _process_test_msg(self, conn_id, msg): 507 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 508 await self._send(conn_id, [res]) 509 510 async def _process_reset_msg(self, conn_id, msg): 511 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 512 await self._send(conn_id, [res]) 513 514 async def _process_parameter_msg(self, conn_id, msg): 515 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 516 await self._send(conn_id, [res]) 517 518 async def _process_parameter_activation_msg(self, conn_id, msg): 519 res = msg._replace( 520 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 521 await self._send(conn_id, [res]) 522 523 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg): 524 sent_cb = (functools.partial(buffer.remove, event_id) 525 if buffer else None) 526 await self._send(conn_id, [data_msg], sent_cb=sent_cb) 527 528 async def _send(self, conn_id, msgs, sent_cb=None): 529 send_queue = self._send_queues.get(conn_id) 530 if send_queue is None: 531 return 532 533 with contextlib.suppress(aio.QueueClosedError): 534 await send_queue.put((msgs, sent_cb))
Device interface
async def
process_events(self, events: Collection[hat.event.common.common.Event]):
165 async def process_events(self, events: Collection[hat.event.common.Event]): 166 for event in events: 167 try: 168 await self._process_event(event) 169 170 except Exception as e: 171 self._log.warning('error processing event: %s', e, exc_info=e)
Process received events
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:
537def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 538 conn_id: int, 539 msg: iec101.CommandMsg 540 ) -> hat.event.common.RegisterEvent: 541 command_type = common.get_command_type(msg.command) 542 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 543 command = common.command_to_json(msg.command) 544 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 545 str(msg.asdu_address), str(msg.io_address)) 546 547 return hat.event.common.RegisterEvent( 548 type=event_type, 549 source_timestamp=None, 550 payload=hat.event.common.EventPayloadJson({ 551 'connection_id': conn_id, 552 'is_test': msg.is_test, 553 'cause': cause, 554 '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:
557def data_msg_from_event(data_key: common.DataKey, 558 event: hat.event.common.Event 559 ) -> iec101.DataMsg: 560 time = common.time_from_source_timestamp(event.source_timestamp) 561 cause = common.cause_from_json(iec101.DataResCause, 562 event.payload.data['cause']) 563 data = common.data_from_json(data_key.data_type, 564 event.payload.data['data']) 565 566 return iec101.DataMsg(is_test=event.payload.data['is_test'], 567 originator_address=0, 568 asdu_address=data_key.asdu_address, 569 io_address=data_key.io_address, 570 data=data, 571 time=time, 572 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:
575def cmd_msg_from_event(cmd_key: common.CommandKey, 576 event: hat.event.common.Event 577 ) -> iec101.CommandMsg: 578 cause = common.cause_from_json(iec101.CommandResCause, 579 event.payload.data['cause']) 580 command = common.command_from_json(cmd_key.cmd_type, 581 event.payload.data['command']) 582 is_negative_confirm = event.payload.data['is_negative_confirm'] 583 584 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 585 originator_address=0, 586 asdu_address=cmd_key.asdu_address, 587 io_address=cmd_key.io_address, 588 command=command, 589 is_negative_confirm=is_negative_confirm, 590 cause=cause)