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 try: 274 msgs = await conn.receive() 275 276 except iec101.AsduTypeError as e: 277 self._log.warning("asdu type error: %s", e) 278 continue 279 280 for msg in msgs: 281 try: 282 self._log.debug('received message: %s', msg) 283 await self._process_msg(conn_id, msg) 284 285 except Exception as e: 286 self._log.warning('error processing message: %s', 287 e, exc_info=e) 288 289 except ConnectionError: 290 self._log.debug('connection close') 291 292 except Exception as e: 293 self._log.warning('connection receive loop error: %s', 294 e, exc_info=e) 295 296 finally: 297 conn.close() 298 299 async def _register_connections(self): 300 payload = [{'connection_id': conn_id, 301 'address': conn.info.address} 302 for conn_id, conn in self._conns.items()] 303 304 event = hat.event.common.RegisterEvent( 305 type=(*self._event_type_prefix, 'gateway', 'connections'), 306 source_timestamp=None, 307 payload=hat.event.common.EventPayloadJson(payload)) 308 309 await self._eventer_client.register([event]) 310 311 async def _process_event(self, event): 312 suffix = event.type[len(self._event_type_prefix):] 313 314 if suffix[:2] == ('system', 'data'): 315 data_type_str, asdu_address_str, io_address_str = suffix[2:] 316 data_key = common.DataKey(data_type=common.DataType(data_type_str), 317 asdu_address=int(asdu_address_str), 318 io_address=int(io_address_str)) 319 320 await self._process_data_event(data_key, event) 321 322 elif suffix[:2] == ('system', 'command'): 323 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 324 cmd_key = common.CommandKey( 325 cmd_type=common.CommandType(cmd_type_str), 326 asdu_address=int(asdu_address_str), 327 io_address=int(io_address_str)) 328 329 await self._process_command_event(cmd_key, event) 330 331 else: 332 raise Exception('unsupported event type') 333 334 async def _process_data_event(self, data_key, event): 335 if data_key not in self._data_msgs: 336 raise Exception('data not configured') 337 338 data_msg = data_msg_from_event(data_key, event) 339 self._data_msgs[data_key] = data_msg 340 341 buffer = self._data_buffers.get(data_key) 342 if buffer: 343 buffer.add(event.id, data_msg) 344 345 for conn_id in self._conns.keys(): 346 await self._send_data_msg(conn_id, buffer, event.id, data_msg) 347 348 async def _process_command_event(self, cmd_key, event): 349 cmd_msg = cmd_msg_from_event(cmd_key, event) 350 conn_id = event.payload.data['connection_id'] 351 await self._send(conn_id, [cmd_msg]) 352 353 async def _process_msg(self, conn_id, msg): 354 if isinstance(msg, iec101.CommandMsg): 355 await self._process_command_msg(conn_id, msg) 356 357 elif isinstance(msg, iec101.InterrogationMsg): 358 await self._process_interrogation_msg(conn_id, msg) 359 360 elif isinstance(msg, iec101.CounterInterrogationMsg): 361 await self._process_counter_interrogation_msg(conn_id, msg) 362 363 elif isinstance(msg, iec101.ReadMsg): 364 await self._process_read_msg(conn_id, msg) 365 366 elif isinstance(msg, iec101.ClockSyncMsg): 367 await self._process_clock_sync_msg(conn_id, msg) 368 369 elif isinstance(msg, iec101.TestMsg): 370 await self._process_test_msg(conn_id, msg) 371 372 elif isinstance(msg, iec101.ResetMsg): 373 await self._process_reset_msg(conn_id, msg) 374 375 elif isinstance(msg, iec101.ParameterMsg): 376 await self._process_parameter_msg(conn_id, msg) 377 378 elif isinstance(msg, iec101.ParameterActivationMsg): 379 await self._process_parameter_activation_msg(conn_id, msg) 380 381 else: 382 raise Exception('unsupported message') 383 384 async def _process_command_msg(self, conn_id, msg): 385 if isinstance(msg.cause, iec101.CommandReqCause): 386 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 387 await self._eventer_client.register([event]) 388 389 else: 390 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 391 is_negative_confirm=True) 392 await self._send(conn_id, [res]) 393 394 async def _process_interrogation_msg(self, conn_id, msg): 395 if msg.cause == iec101.CommandReqCause.ACTIVATION: 396 asdu_data_msgs = collections.defaultdict(collections.deque) 397 398 for data_key, data_msg in self._data_msgs.items(): 399 if data_key.data_type == common.DataType.BINARY_COUNTER: 400 continue 401 402 if (msg.asdu_address != self._broadcast_asdu_address and 403 msg.asdu_address != data_key.asdu_address): 404 continue 405 406 asdu_data_msgs[data_key.asdu_address].append(data_msg) 407 408 if msg.asdu_address != self._broadcast_asdu_address: 409 asdu_data_msgs[msg.asdu_address].append(None) 410 411 for asdu_address, data_msgs in asdu_data_msgs.items(): 412 res = msg._replace( 413 asdu_address=asdu_address, 414 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 415 is_negative_confirm=False) 416 await self._send(conn_id, [res]) 417 418 msgs = [ 419 data_msg._replace( 420 is_test=msg.is_test, 421 cause=iec101.DataResCause.INTERROGATED_STATION) 422 for data_msg in data_msgs 423 if data_msg] 424 if msgs: 425 await self._send(conn_id, msgs) 426 427 res = msg._replace( 428 asdu_address=asdu_address, 429 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 430 is_negative_confirm=False) 431 await self._send(conn_id, [res]) 432 433 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 434 res = msg._replace( 435 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 436 is_negative_confirm=True) 437 await self._send(conn_id, [res]) 438 439 else: 440 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 441 is_negative_confirm=True) 442 await self._send(conn_id, [res]) 443 444 async def _process_counter_interrogation_msg(self, conn_id, msg): 445 if msg.cause == iec101.CommandReqCause.ACTIVATION: 446 asdu_data_msgs = collections.defaultdict(collections.deque) 447 448 for data_key, data_msg in self._data_msgs.items(): 449 if data_key.data_type != common.DataType.BINARY_COUNTER: 450 continue 451 452 if (msg.asdu_address != self._broadcast_asdu_address and 453 msg.asdu_address != data_key.asdu_address): 454 continue 455 456 asdu_data_msgs[data_key.asdu_address].append(data_msg) 457 458 if msg.asdu_address != self._broadcast_asdu_address: 459 asdu_data_msgs[msg.asdu_address].append(None) 460 461 for asdu_address, data_msgs in asdu_data_msgs.items(): 462 res = msg._replace( 463 asdu_address=asdu_address, 464 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 465 is_negative_confirm=False) 466 await self._send(conn_id, [res]) 467 468 msgs = [ 469 data_msg._replace( 470 is_test=msg.is_test, 471 cause=iec101.DataResCause.INTERROGATED_COUNTER) 472 for data_msg in data_msgs 473 if data_msg] 474 if msgs: 475 await self._send(conn_id, msgs) 476 477 res = msg._replace( 478 asdu_address=asdu_address, 479 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 480 is_negative_confirm=False) 481 await self._send(conn_id, [res]) 482 483 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 484 res = msg._replace( 485 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 486 is_negative_confirm=True) 487 await self._send(conn_id, [res]) 488 489 else: 490 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 491 is_negative_confirm=True) 492 await self._send(conn_id, [res]) 493 494 async def _process_read_msg(self, conn_id, msg): 495 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 496 await self._send(conn_id, [res]) 497 498 async def _process_clock_sync_msg(self, conn_id, msg): 499 if isinstance(msg.cause, iec101.ClockSyncReqCause): 500 res = msg._replace( 501 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 502 is_negative_confirm=True) 503 await self._send(conn_id, [res]) 504 505 else: 506 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 507 is_negative_confirm=True) 508 await self._send(conn_id, [res]) 509 510 async def _process_test_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_reset_msg(self, conn_id, msg): 515 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 516 await self._send(conn_id, [res]) 517 518 async def _process_parameter_msg(self, conn_id, msg): 519 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 520 await self._send(conn_id, [res]) 521 522 async def _process_parameter_activation_msg(self, conn_id, msg): 523 res = msg._replace( 524 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 525 await self._send(conn_id, [res]) 526 527 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg): 528 sent_cb = (functools.partial(buffer.remove, event_id) 529 if buffer else None) 530 await self._send(conn_id, [data_msg], sent_cb=sent_cb) 531 532 async def _send(self, conn_id, msgs, sent_cb=None): 533 send_queue = self._send_queues.get(conn_id) 534 if send_queue is None: 535 return 536 537 with contextlib.suppress(aio.QueueClosedError): 538 await send_queue.put((msgs, sent_cb)) 539 540 541def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 542 conn_id: int, 543 msg: iec101.CommandMsg 544 ) -> hat.event.common.RegisterEvent: 545 command_type = common.get_command_type(msg.command) 546 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 547 command = common.command_to_json(msg.command) 548 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 549 str(msg.asdu_address), str(msg.io_address)) 550 551 return hat.event.common.RegisterEvent( 552 type=event_type, 553 source_timestamp=None, 554 payload=hat.event.common.EventPayloadJson({ 555 'connection_id': conn_id, 556 'is_test': msg.is_test, 557 'cause': cause, 558 'command': command})) 559 560 561def data_msg_from_event(data_key: common.DataKey, 562 event: hat.event.common.Event 563 ) -> iec101.DataMsg: 564 time = common.time_from_source_timestamp(event.source_timestamp) 565 cause = common.cause_from_json(iec101.DataResCause, 566 event.payload.data['cause']) 567 data = common.data_from_json(data_key.data_type, 568 event.payload.data['data']) 569 570 return iec101.DataMsg(is_test=event.payload.data['is_test'], 571 originator_address=0, 572 asdu_address=data_key.asdu_address, 573 io_address=data_key.io_address, 574 data=data, 575 time=time, 576 cause=cause) 577 578 579def cmd_msg_from_event(cmd_key: common.CommandKey, 580 event: hat.event.common.Event 581 ) -> iec101.CommandMsg: 582 cause = common.cause_from_json(iec101.CommandResCause, 583 event.payload.data['cause']) 584 command = common.command_from_json(cmd_key.cmd_type, 585 event.payload.data['command']) 586 is_negative_confirm = event.payload.data['is_negative_confirm'] 587 588 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 589 originator_address=0, 590 asdu_address=cmd_key.asdu_address, 591 io_address=cmd_key.io_address, 592 command=command, 593 is_negative_confirm=is_negative_confirm, 594 cause=cause) 595 596 597def _get_broadcast_asdu_address(asdu_address_size): 598 if asdu_address_size == iec101.AsduAddressSize.ONE: 599 return 0xFF 600 601 if asdu_address_size == iec101.AsduAddressSize.TWO: 602 return 0xFFFF 603 604 raise ValueError('unsupported asdu address size') 605 606 607def _create_logger_adapter(name): 608 extra = {'meta': {'type': 'Iec101SlaveDevice', 609 'name': name}} 610 611 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://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://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'}}}}}}, '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://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://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://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://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': ['request_id', 'connection_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'connection_id': {'type': 'integer'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'value'], 'properties': {'name': {'type': 'string'}, 'value': {'type': 'integer'}}}}}}}, 'system': {'data': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}}}, 'write': {'type': 'object', 'required': ['request_id', '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']}}}}}}}}, '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']}}}}})
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 try: 275 msgs = await conn.receive() 276 277 except iec101.AsduTypeError as e: 278 self._log.warning("asdu type error: %s", e) 279 continue 280 281 for msg in msgs: 282 try: 283 self._log.debug('received message: %s', msg) 284 await self._process_msg(conn_id, msg) 285 286 except Exception as e: 287 self._log.warning('error processing message: %s', 288 e, exc_info=e) 289 290 except ConnectionError: 291 self._log.debug('connection close') 292 293 except Exception as e: 294 self._log.warning('connection receive loop error: %s', 295 e, exc_info=e) 296 297 finally: 298 conn.close() 299 300 async def _register_connections(self): 301 payload = [{'connection_id': conn_id, 302 'address': conn.info.address} 303 for conn_id, conn in self._conns.items()] 304 305 event = hat.event.common.RegisterEvent( 306 type=(*self._event_type_prefix, 'gateway', 'connections'), 307 source_timestamp=None, 308 payload=hat.event.common.EventPayloadJson(payload)) 309 310 await self._eventer_client.register([event]) 311 312 async def _process_event(self, event): 313 suffix = event.type[len(self._event_type_prefix):] 314 315 if suffix[:2] == ('system', 'data'): 316 data_type_str, asdu_address_str, io_address_str = suffix[2:] 317 data_key = common.DataKey(data_type=common.DataType(data_type_str), 318 asdu_address=int(asdu_address_str), 319 io_address=int(io_address_str)) 320 321 await self._process_data_event(data_key, event) 322 323 elif suffix[:2] == ('system', 'command'): 324 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 325 cmd_key = common.CommandKey( 326 cmd_type=common.CommandType(cmd_type_str), 327 asdu_address=int(asdu_address_str), 328 io_address=int(io_address_str)) 329 330 await self._process_command_event(cmd_key, event) 331 332 else: 333 raise Exception('unsupported event type') 334 335 async def _process_data_event(self, data_key, event): 336 if data_key not in self._data_msgs: 337 raise Exception('data not configured') 338 339 data_msg = data_msg_from_event(data_key, event) 340 self._data_msgs[data_key] = data_msg 341 342 buffer = self._data_buffers.get(data_key) 343 if buffer: 344 buffer.add(event.id, data_msg) 345 346 for conn_id in self._conns.keys(): 347 await self._send_data_msg(conn_id, buffer, event.id, data_msg) 348 349 async def _process_command_event(self, cmd_key, event): 350 cmd_msg = cmd_msg_from_event(cmd_key, event) 351 conn_id = event.payload.data['connection_id'] 352 await self._send(conn_id, [cmd_msg]) 353 354 async def _process_msg(self, conn_id, msg): 355 if isinstance(msg, iec101.CommandMsg): 356 await self._process_command_msg(conn_id, msg) 357 358 elif isinstance(msg, iec101.InterrogationMsg): 359 await self._process_interrogation_msg(conn_id, msg) 360 361 elif isinstance(msg, iec101.CounterInterrogationMsg): 362 await self._process_counter_interrogation_msg(conn_id, msg) 363 364 elif isinstance(msg, iec101.ReadMsg): 365 await self._process_read_msg(conn_id, msg) 366 367 elif isinstance(msg, iec101.ClockSyncMsg): 368 await self._process_clock_sync_msg(conn_id, msg) 369 370 elif isinstance(msg, iec101.TestMsg): 371 await self._process_test_msg(conn_id, msg) 372 373 elif isinstance(msg, iec101.ResetMsg): 374 await self._process_reset_msg(conn_id, msg) 375 376 elif isinstance(msg, iec101.ParameterMsg): 377 await self._process_parameter_msg(conn_id, msg) 378 379 elif isinstance(msg, iec101.ParameterActivationMsg): 380 await self._process_parameter_activation_msg(conn_id, msg) 381 382 else: 383 raise Exception('unsupported message') 384 385 async def _process_command_msg(self, conn_id, msg): 386 if isinstance(msg.cause, iec101.CommandReqCause): 387 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 388 await self._eventer_client.register([event]) 389 390 else: 391 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 392 is_negative_confirm=True) 393 await self._send(conn_id, [res]) 394 395 async def _process_interrogation_msg(self, conn_id, msg): 396 if msg.cause == iec101.CommandReqCause.ACTIVATION: 397 asdu_data_msgs = collections.defaultdict(collections.deque) 398 399 for data_key, data_msg in self._data_msgs.items(): 400 if data_key.data_type == common.DataType.BINARY_COUNTER: 401 continue 402 403 if (msg.asdu_address != self._broadcast_asdu_address and 404 msg.asdu_address != data_key.asdu_address): 405 continue 406 407 asdu_data_msgs[data_key.asdu_address].append(data_msg) 408 409 if msg.asdu_address != self._broadcast_asdu_address: 410 asdu_data_msgs[msg.asdu_address].append(None) 411 412 for asdu_address, data_msgs in asdu_data_msgs.items(): 413 res = msg._replace( 414 asdu_address=asdu_address, 415 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 416 is_negative_confirm=False) 417 await self._send(conn_id, [res]) 418 419 msgs = [ 420 data_msg._replace( 421 is_test=msg.is_test, 422 cause=iec101.DataResCause.INTERROGATED_STATION) 423 for data_msg in data_msgs 424 if data_msg] 425 if msgs: 426 await self._send(conn_id, msgs) 427 428 res = msg._replace( 429 asdu_address=asdu_address, 430 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 431 is_negative_confirm=False) 432 await self._send(conn_id, [res]) 433 434 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 435 res = msg._replace( 436 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 437 is_negative_confirm=True) 438 await self._send(conn_id, [res]) 439 440 else: 441 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 442 is_negative_confirm=True) 443 await self._send(conn_id, [res]) 444 445 async def _process_counter_interrogation_msg(self, conn_id, msg): 446 if msg.cause == iec101.CommandReqCause.ACTIVATION: 447 asdu_data_msgs = collections.defaultdict(collections.deque) 448 449 for data_key, data_msg in self._data_msgs.items(): 450 if data_key.data_type != common.DataType.BINARY_COUNTER: 451 continue 452 453 if (msg.asdu_address != self._broadcast_asdu_address and 454 msg.asdu_address != data_key.asdu_address): 455 continue 456 457 asdu_data_msgs[data_key.asdu_address].append(data_msg) 458 459 if msg.asdu_address != self._broadcast_asdu_address: 460 asdu_data_msgs[msg.asdu_address].append(None) 461 462 for asdu_address, data_msgs in asdu_data_msgs.items(): 463 res = msg._replace( 464 asdu_address=asdu_address, 465 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 466 is_negative_confirm=False) 467 await self._send(conn_id, [res]) 468 469 msgs = [ 470 data_msg._replace( 471 is_test=msg.is_test, 472 cause=iec101.DataResCause.INTERROGATED_COUNTER) 473 for data_msg in data_msgs 474 if data_msg] 475 if msgs: 476 await self._send(conn_id, msgs) 477 478 res = msg._replace( 479 asdu_address=asdu_address, 480 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 481 is_negative_confirm=False) 482 await self._send(conn_id, [res]) 483 484 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 485 res = msg._replace( 486 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 487 is_negative_confirm=True) 488 await self._send(conn_id, [res]) 489 490 else: 491 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 492 is_negative_confirm=True) 493 await self._send(conn_id, [res]) 494 495 async def _process_read_msg(self, conn_id, msg): 496 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 497 await self._send(conn_id, [res]) 498 499 async def _process_clock_sync_msg(self, conn_id, msg): 500 if isinstance(msg.cause, iec101.ClockSyncReqCause): 501 res = msg._replace( 502 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 503 is_negative_confirm=True) 504 await self._send(conn_id, [res]) 505 506 else: 507 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 508 is_negative_confirm=True) 509 await self._send(conn_id, [res]) 510 511 async def _process_test_msg(self, conn_id, msg): 512 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 513 await self._send(conn_id, [res]) 514 515 async def _process_reset_msg(self, conn_id, msg): 516 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 517 await self._send(conn_id, [res]) 518 519 async def _process_parameter_msg(self, conn_id, msg): 520 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 521 await self._send(conn_id, [res]) 522 523 async def _process_parameter_activation_msg(self, conn_id, msg): 524 res = msg._replace( 525 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 526 await self._send(conn_id, [res]) 527 528 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg): 529 sent_cb = (functools.partial(buffer.remove, event_id) 530 if buffer else None) 531 await self._send(conn_id, [data_msg], sent_cb=sent_cb) 532 533 async def _send(self, conn_id, msgs, sent_cb=None): 534 send_queue = self._send_queues.get(conn_id) 535 if send_queue is None: 536 return 537 538 with contextlib.suppress(aio.QueueClosedError): 539 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:
542def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 543 conn_id: int, 544 msg: iec101.CommandMsg 545 ) -> hat.event.common.RegisterEvent: 546 command_type = common.get_command_type(msg.command) 547 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 548 command = common.command_to_json(msg.command) 549 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 550 str(msg.asdu_address), str(msg.io_address)) 551 552 return hat.event.common.RegisterEvent( 553 type=event_type, 554 source_timestamp=None, 555 payload=hat.event.common.EventPayloadJson({ 556 'connection_id': conn_id, 557 'is_test': msg.is_test, 558 'cause': cause, 559 '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:
562def data_msg_from_event(data_key: common.DataKey, 563 event: hat.event.common.Event 564 ) -> iec101.DataMsg: 565 time = common.time_from_source_timestamp(event.source_timestamp) 566 cause = common.cause_from_json(iec101.DataResCause, 567 event.payload.data['cause']) 568 data = common.data_from_json(data_key.data_type, 569 event.payload.data['data']) 570 571 return iec101.DataMsg(is_test=event.payload.data['is_test'], 572 originator_address=0, 573 asdu_address=data_key.asdu_address, 574 io_address=data_key.io_address, 575 data=data, 576 time=time, 577 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:
580def cmd_msg_from_event(cmd_key: common.CommandKey, 581 event: hat.event.common.Event 582 ) -> iec101.CommandMsg: 583 cause = common.cause_from_json(iec101.CommandResCause, 584 event.payload.data['cause']) 585 command = common.command_from_json(cmd_key.cmd_type, 586 event.payload.data['command']) 587 is_negative_confirm = event.payload.data['is_negative_confirm'] 588 589 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 590 originator_address=0, 591 asdu_address=cmd_key.asdu_address, 592 io_address=cmd_key.io_address, 593 command=command, 594 is_negative_confirm=is_negative_confirm, 595 cause=cause)