hat.gateway.devices.iec101.slave
IEC 60870-5-104 slave device
1"""IEC 60870-5-104 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._log = _create_logger_adapter(conf['name']) 40 41 init_buffers(buffers_conf=conf['buffers'], 42 buffers=device._buffers) 43 44 await init_data(log=device._log, 45 data_conf=conf['data'], 46 data_msgs=device._data_msgs, 47 data_buffers=device._data_buffers, 48 buffers=device._buffers, 49 eventer_client=eventer_client, 50 event_type_prefix=event_type_prefix) 51 52 if conf['link_type'] == 'BALANCED': 53 create_link = link.create_balanced_link 54 55 elif conf['link_type'] == 'UNBALANCED': 56 create_link = link.create_slave_link 57 58 else: 59 raise ValueError('unsupported link type') 60 61 device._link = await create_link( 62 port=conf['port'], 63 address_size=link.AddressSize[conf['device_address_size']], 64 silent_interval=conf['silent_interval'], 65 baudrate=conf['baudrate'], 66 bytesize=serial.ByteSize[conf['bytesize']], 67 parity=serial.Parity[conf['parity']], 68 stopbits=serial.StopBits[conf['stopbits']], 69 xonxoff=conf['flow_control']['xonxoff'], 70 rtscts=conf['flow_control']['rtscts'], 71 dsrdtr=conf['flow_control']['dsrdtr'], 72 name=conf['name']) 73 74 try: 75 for device_conf in conf['devices']: 76 device.async_group.spawn(device._connection_loop, device_conf) 77 78 await device._register_connections() 79 80 except BaseException: 81 await aio.uncancellable(device.async_close()) 82 raise 83 84 return device 85 86 87info: common.DeviceInfo = common.DeviceInfo( 88 type="iec101_slave", 89 create=create, 90 json_schema_id="hat-gateway://iec101.yaml#/$defs/slave", 91 json_schema_repo=common.json_schema_repo) 92 93 94class Buffer: 95 96 def __init__(self, size: int): 97 self._size = size 98 self._data = collections.OrderedDict() 99 100 def add(self, 101 event_id: hat.event.common.EventId, 102 data_msg: iec101.DataMsg): 103 self._data[event_id] = data_msg 104 while len(self._data) > self._size: 105 self._data.popitem(last=False) 106 107 def remove(self, event_id: hat.event.common.EventId): 108 self._data.pop(event_id, None) 109 110 def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId, # NOQA 111 iec101.DataMsg]]: 112 return self._data.items() 113 114 115def init_buffers(buffers_conf: json.Data, 116 buffers: dict[str, Buffer]): 117 for buffer_conf in buffers_conf: 118 buffers[buffer_conf['name']] = Buffer(buffer_conf['size']) 119 120 121async def init_data(log: logging.Logger, 122 data_conf: json.Data, 123 data_msgs: dict[common.DataKey, iec101.DataMsg], 124 data_buffers: dict[common.DataKey, Buffer], 125 buffers: dict[str, Buffer], 126 eventer_client: hat.event.eventer.Client, 127 event_type_prefix: common.EventTypePrefix): 128 for data in data_conf: 129 data_key = common.DataKey(data_type=common.DataType[data['data_type']], 130 asdu_address=data['asdu_address'], 131 io_address=data['io_address']) 132 data_msgs[data_key] = None 133 if data['buffer']: 134 data_buffers[data_key] = buffers[data['buffer']] 135 136 event_types = [(*event_type_prefix, 'system', 'data', '*')] 137 params = hat.event.common.QueryLatestParams(event_types) 138 result = await eventer_client.query(params) 139 140 for event in result.events: 141 try: 142 data_type_str, asdu_address_str, io_address_str = \ 143 event.type[len(event_type_prefix)+2:] 144 data_key = common.DataKey(data_type=common.DataType(data_type_str), 145 asdu_address=int(asdu_address_str), 146 io_address=int(io_address_str)) 147 if data_key not in data_msgs: 148 raise Exception(f'data {data_key} not configured') 149 150 data_msgs[data_key] = data_msg_from_event(data_key, event) 151 152 except Exception as e: 153 log.debug('skipping initial data: %s', e, exc_info=e) 154 155 156class Iec101SlaveDevice(common.Device): 157 158 @property 159 def async_group(self) -> aio.Group: 160 return self._link.async_group 161 162 async def process_events(self, events: Collection[hat.event.common.Event]): 163 for event in events: 164 try: 165 await self._process_event(event) 166 167 except Exception as e: 168 self._log.warning('error processing event: %s', e, exc_info=e) 169 170 async def _connection_loop(self, device_conf): 171 conn = None 172 173 try: 174 if self._conf['link_type'] == 'BALANCED': 175 conn_args = { 176 'direction': link.Direction[device_conf['direction']], 177 'addr': device_conf['address'], 178 'response_timeout': device_conf['response_timeout'], 179 'send_retry_count': device_conf['send_retry_count'], 180 'status_delay': device_conf['status_delay'], 181 'name': self._conf['name']} 182 183 elif self._conf['link_type'] == 'UNBALANCED': 184 conn_args = { 185 'addr': device_conf['address'], 186 'keep_alive_timeout': device_conf['keep_alive_timeout'], 187 'name': self._conf['name']} 188 189 else: 190 raise ValueError('unsupported link type') 191 192 while True: 193 try: 194 conn = await self._link.open_connection(**conn_args) 195 196 except Exception as e: 197 self._log.error('connection error for address %s: %s', 198 device_conf['address'], e, exc_info=e) 199 await asyncio.sleep(device_conf['reconnect_delay']) 200 continue 201 202 conn = iec101.Connection( 203 conn=conn, 204 cause_size=iec101.CauseSize[self._conf['cause_size']], 205 asdu_address_size=iec101.AsduAddressSize[ 206 self._conf['asdu_address_size']], 207 io_address_size=iec101.IoAddressSize[ 208 self._conf['io_address_size']]) 209 210 conn_id = next(self._next_conn_ids) 211 self._conns[conn_id] = conn 212 213 send_queue = aio.Queue(1024) 214 self._send_queues[conn_id] = send_queue 215 216 try: 217 conn.async_group.spawn(self._connection_send_loop, conn, 218 send_queue) 219 conn.async_group.spawn(self._connection_receive_loop, conn, 220 conn_id) 221 222 await self._register_connections() 223 224 with contextlib.suppress(Exception): 225 for buffer in self._buffers.values(): 226 for event_id, data_msg in buffer.get_event_id_data_msgs(): # NOQA 227 await self._send_data_msg(conn_id, buffer, 228 event_id, data_msg) 229 230 await conn.wait_closed() 231 232 finally: 233 send_queue.close() 234 235 self._conns.pop(conn_id, None) 236 self._send_queues.pop(conn_id, None) 237 238 with contextlib.suppress(Exception): 239 await aio.uncancellable(self._register_connections()) 240 241 await conn.async_close() 242 243 except Exception as e: 244 self._log.warning('connection loop error: %s', e, exc_info=e) 245 246 finally: 247 self._log.debug('closing connection') 248 self.close() 249 250 if conn: 251 await aio.uncancellable(conn.async_close()) 252 253 async def _connection_send_loop(self, conn, send_queue): 254 try: 255 while True: 256 msgs, sent_cb = await send_queue.get() 257 await conn.send(msgs, sent_cb=sent_cb) 258 259 except ConnectionError: 260 self._log.debug('connection close') 261 262 except Exception as e: 263 self._log.warning('connection send loop error: %s', e, exc_info=e) 264 265 finally: 266 conn.close() 267 268 async def _connection_receive_loop(self, conn, conn_id): 269 try: 270 while True: 271 msgs = await conn.receive() 272 273 for msg in msgs: 274 try: 275 self._log.debug('received message: %s', msg) 276 await self._process_msg(conn_id, msg) 277 278 except Exception as e: 279 self._log.warning('error processing message: %s', 280 e, exc_info=e) 281 282 except ConnectionError: 283 self._log.debug('connection close') 284 285 except Exception as e: 286 self._log.warning('connection receive loop error: %s', 287 e, exc_info=e) 288 289 finally: 290 conn.close() 291 292 async def _register_connections(self): 293 payload = [{'connection_id': conn_id, 294 'address': conn.info.address} 295 for conn_id, conn in self._conns.items()] 296 297 event = hat.event.common.RegisterEvent( 298 type=(*self._event_type_prefix, 'gateway', 'connections'), 299 source_timestamp=None, 300 payload=hat.event.common.EventPayloadJson(payload)) 301 302 await self._eventer_client.register([event]) 303 304 async def _process_event(self, event): 305 suffix = event.type[len(self._event_type_prefix):] 306 307 if suffix[:2] == ('system', 'data'): 308 data_type_str, asdu_address_str, io_address_str = suffix[2:] 309 data_key = common.DataKey(data_type=common.DataType(data_type_str), 310 asdu_address=int(asdu_address_str), 311 io_address=int(io_address_str)) 312 313 await self._process_data_event(data_key, event) 314 315 elif suffix[:2] == ('system', 'command'): 316 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 317 cmd_key = common.CommandKey( 318 cmd_type=common.CommandType(cmd_type_str), 319 asdu_address=int(asdu_address_str), 320 io_address=int(io_address_str)) 321 322 await self._process_command_event(cmd_key, event) 323 324 else: 325 raise Exception('unsupported event type') 326 327 async def _process_data_event(self, data_key, event): 328 if data_key not in self._data_msgs: 329 raise Exception('data not configured') 330 331 data_msg = data_msg_from_event(data_key, event) 332 self._data_msgs[data_key] = data_msg 333 334 buffer = self._data_buffers.get(data_key) 335 if buffer: 336 buffer.add(event.id, data_msg) 337 338 for conn_id in self._conns.keys(): 339 await self._send_data_msg(conn_id, buffer, event.id, data_msg) 340 341 async def _process_command_event(self, cmd_key, event): 342 cmd_msg = cmd_msg_from_event(cmd_key, event) 343 conn_id = event.payload.data['connection_id'] 344 await self._send(conn_id, [cmd_msg]) 345 346 async def _process_msg(self, conn_id, msg): 347 if isinstance(msg, iec101.CommandMsg): 348 await self._process_command_msg(conn_id, msg) 349 350 elif isinstance(msg, iec101.InterrogationMsg): 351 await self._process_interrogation_msg(conn_id, msg) 352 353 elif isinstance(msg, iec101.CounterInterrogationMsg): 354 await self._process_counter_interrogation_msg(conn_id, msg) 355 356 elif isinstance(msg, iec101.ReadMsg): 357 await self._process_read_msg(conn_id, msg) 358 359 elif isinstance(msg, iec101.ClockSyncMsg): 360 await self._process_clock_sync_msg(conn_id, msg) 361 362 elif isinstance(msg, iec101.TestMsg): 363 await self._process_test_msg(conn_id, msg) 364 365 elif isinstance(msg, iec101.ResetMsg): 366 await self._process_reset_msg(conn_id, msg) 367 368 elif isinstance(msg, iec101.ParameterMsg): 369 await self._process_parameter_msg(conn_id, msg) 370 371 elif isinstance(msg, iec101.ParameterActivationMsg): 372 await self._process_parameter_activation_msg(conn_id, msg) 373 374 else: 375 raise Exception('unsupported message') 376 377 async def _process_command_msg(self, conn_id, msg): 378 if isinstance(msg.cause, iec101.CommandReqCause): 379 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 380 await self._eventer_client.register([event]) 381 382 else: 383 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 384 is_negative_confirm=True) 385 await self._send(conn_id, [res]) 386 387 async def _process_interrogation_msg(self, conn_id, msg): 388 if msg.cause == iec101.CommandReqCause.ACTIVATION: 389 res = msg._replace( 390 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 391 is_negative_confirm=False) 392 await self._send(conn_id, [res]) 393 394 data_msgs = [ 395 data_msg._replace( 396 is_test=msg.is_test, 397 cause=iec101.DataResCause.INTERROGATED_STATION) 398 for data_msg in self._data_msgs.values() 399 if (data_msg and 400 (msg.asdu_address == 0xFFFF or 401 msg.asdu_address == data_msg.asdu_address) and 402 not isinstance(data_msg.data, iec101.BinaryCounterData))] 403 await self._send(conn_id, data_msgs) 404 405 res = msg._replace( 406 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 407 is_negative_confirm=False) 408 await self._send(conn_id, [res]) 409 410 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 411 res = msg._replace( 412 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 413 is_negative_confirm=True) 414 await self._send(conn_id, [res]) 415 416 else: 417 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 418 is_negative_confirm=True) 419 await self._send(conn_id, [res]) 420 421 async def _process_counter_interrogation_msg(self, conn_id, msg): 422 if msg.cause == iec101.CommandReqCause.ACTIVATION: 423 res = msg._replace( 424 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 425 is_negative_confirm=False) 426 await self._send(conn_id, [res]) 427 428 data_msgs = [ 429 data_msg._replace( 430 is_test=msg.is_test, 431 cause=iec101.DataResCause.INTERROGATED_COUNTER) 432 for data_msg in self._data_msgs.values() 433 if (data_msg and 434 (msg.asdu_address == 0xFFFF or 435 msg.asdu_address == data_msg.asdu_address) and 436 isinstance(data_msg.data, iec101.BinaryCounterData))] 437 await self._send(conn_id, data_msgs) 438 439 res = msg._replace( 440 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 441 is_negative_confirm=False) 442 await self._send(conn_id, [res]) 443 444 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 445 res = msg._replace( 446 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 447 is_negative_confirm=True) 448 await self._send(conn_id, [res]) 449 450 else: 451 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 452 is_negative_confirm=True) 453 await self._send(conn_id, [res]) 454 455 async def _process_read_msg(self, conn_id, msg): 456 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 457 await self._send(conn_id, [res]) 458 459 async def _process_clock_sync_msg(self, conn_id, msg): 460 if isinstance(msg.cause, iec101.ClockSyncReqCause): 461 res = msg._replace( 462 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 463 is_negative_confirm=True) 464 await self._send(conn_id, [res]) 465 466 else: 467 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 468 is_negative_confirm=True) 469 await self._send(conn_id, [res]) 470 471 async def _process_test_msg(self, conn_id, msg): 472 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 473 await self._send(conn_id, [res]) 474 475 async def _process_reset_msg(self, conn_id, msg): 476 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 477 await self._send(conn_id, [res]) 478 479 async def _process_parameter_msg(self, conn_id, msg): 480 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 481 await self._send(conn_id, [res]) 482 483 async def _process_parameter_activation_msg(self, conn_id, msg): 484 res = msg._replace( 485 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 486 await self._send(conn_id, [res]) 487 488 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg): 489 sent_cb = (functools.partial(buffer.remove, event_id) 490 if buffer else None) 491 await self._send(conn_id, [data_msg], sent_cb=sent_cb) 492 493 async def _send(self, conn_id, msgs, sent_cb=None): 494 send_queue = self._send_queues.get(conn_id) 495 if send_queue is None: 496 return 497 498 with contextlib.suppress(aio.QueueClosedError): 499 await send_queue.put((msgs, sent_cb)) 500 501 502def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 503 conn_id: int, 504 msg: iec101.CommandMsg 505 ) -> hat.event.common.RegisterEvent: 506 command_type = common.get_command_type(msg.command) 507 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 508 command = common.command_to_json(msg.command) 509 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 510 str(msg.asdu_address), str(msg.io_address)) 511 512 return hat.event.common.RegisterEvent( 513 type=event_type, 514 source_timestamp=None, 515 payload=hat.event.common.EventPayloadJson({ 516 'connection_id': conn_id, 517 'is_test': msg.is_test, 518 'cause': cause, 519 'command': command})) 520 521 522def data_msg_from_event(data_key: common.DataKey, 523 event: hat.event.common.Event 524 ) -> iec101.DataMsg: 525 time = common.time_from_source_timestamp(event.source_timestamp) 526 cause = common.cause_from_json(iec101.DataResCause, 527 event.payload.data['cause']) 528 data = common.data_from_json(data_key.data_type, 529 event.payload.data['data']) 530 531 return iec101.DataMsg(is_test=event.payload.data['is_test'], 532 originator_address=0, 533 asdu_address=data_key.asdu_address, 534 io_address=data_key.io_address, 535 data=data, 536 time=time, 537 cause=cause) 538 539 540def cmd_msg_from_event(cmd_key: common.CommandKey, 541 event: hat.event.common.Event 542 ) -> iec101.CommandMsg: 543 cause = common.cause_from_json(iec101.CommandResCause, 544 event.payload.data['cause']) 545 command = common.command_from_json(cmd_key.cmd_type, 546 event.payload.data['command']) 547 is_negative_confirm = event.payload.data['is_negative_confirm'] 548 549 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 550 originator_address=0, 551 asdu_address=cmd_key.asdu_address, 552 io_address=cmd_key.io_address, 553 command=command, 554 is_negative_confirm=is_negative_confirm, 555 cause=cause) 556 557 558def _create_logger_adapter(name): 559 extra = {'meta': {'type': 'Iec101SlaveDevice', 560 'name': name}} 561 562 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._log = _create_logger_adapter(conf['name']) 41 42 init_buffers(buffers_conf=conf['buffers'], 43 buffers=device._buffers) 44 45 await init_data(log=device._log, 46 data_conf=conf['data'], 47 data_msgs=device._data_msgs, 48 data_buffers=device._data_buffers, 49 buffers=device._buffers, 50 eventer_client=eventer_client, 51 event_type_prefix=event_type_prefix) 52 53 if conf['link_type'] == 'BALANCED': 54 create_link = link.create_balanced_link 55 56 elif conf['link_type'] == 'UNBALANCED': 57 create_link = link.create_slave_link 58 59 else: 60 raise ValueError('unsupported link type') 61 62 device._link = await create_link( 63 port=conf['port'], 64 address_size=link.AddressSize[conf['device_address_size']], 65 silent_interval=conf['silent_interval'], 66 baudrate=conf['baudrate'], 67 bytesize=serial.ByteSize[conf['bytesize']], 68 parity=serial.Parity[conf['parity']], 69 stopbits=serial.StopBits[conf['stopbits']], 70 xonxoff=conf['flow_control']['xonxoff'], 71 rtscts=conf['flow_control']['rtscts'], 72 dsrdtr=conf['flow_control']['dsrdtr'], 73 name=conf['name']) 74 75 try: 76 for device_conf in conf['devices']: 77 device.async_group.spawn(device._connection_loop, device_conf) 78 79 await device._register_connections() 80 81 except BaseException: 82 await aio.uncancellable(device.async_close()) 83 raise 84 85 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://smpp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://smpp.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'remote_address', 'ssl', 'system_id', 'password', 'enquire_link_delay', 'enquire_link_timeout', 'connect_timeout', 'reconnect_delay', 'short_message', 'priority', 'data_coding', 'message_encoding', 'message_timeout'], 'properties': {'name': {'type': 'string'}, 'remote_address': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'ssl': {'type': 'boolean'}, 'system_id': {'type': 'string'}, 'password': {'type': 'string'}, 'enquire_link_delay': {'type': ['null', 'number']}, 'enquire_link_timeout': {'type': 'number'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'short_message': {'type': 'boolean'}, 'priority': {'enum': ['BULK', 'NORMAL', 'URGENT', 'VERY_URGENT']}, 'data_coding': {'enum': ['DEFAULT', 'ASCII', 'UNSPECIFIED_1', 'LATIN_1', 'UNSPECIFIED_2', 'JIS', 'CYRLLIC', 'LATIN_HEBREW', 'UCS2', 'PICTOGRAM', 'MUSIC', 'EXTENDED_KANJI', 'KS']}, 'message_encoding': {'type': 'string'}, 'message_timeout': {'type': 'number'}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}}, 'system': {'message': {'type': 'object', 'required': ['address', 'message'], 'properties': {'address': {'type': 'string'}, 'message': {'type': 'string'}}}}}}}}, 'hat-gateway://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'report_id', 'value'], 'properties': {'name': {'type': 'string'}, 'report_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}, 'commands': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'model', 'ref', 'with_operate_time'], 'properties': {'name': {'type': 'string'}, 'model': {'enum': ['DIRECT_WITH_NORMAL_SECURITY', 'SBO_WITH_NORMAL_SECURITY', 'DIRECT_WITH_ENHANCED_SECURITY', 'SBO_WITH_ENHANCED_SECURITY']}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/command'}, 'with_operate_time': {'type': 'boolean'}}}}, 'changes': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['reasons'], 'properties': {'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}}}, 'command': {'allOf': [{'type': 'object', 'required': ['session_id', 'action'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE', 'TERMINATION']}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'service_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}, 'additional_cause': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/additional_cause'}, 'test_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/test_error'}}}]}]}, 'change': {'allOf': [{'type': 'object', 'required': ['session_id'], 'properties': {'session_id': {'type': 'string'}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}}}]}]}, 'entry_id': {'type': ['string', 'null'], 'description': 'hex encoded bytes'}}, 'system': {'command': {'type': 'object', 'required': ['session_id', 'action', 'value', 'origin', 'test', 'checks'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE']}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'origin': {'type': 'object', 'required': ['category', 'identification'], 'properties': {'category': {'enum': ['BAY_CONTROL', 'STATION_CONTROL', 'REMOTE_CONTROL', 'AUTOMATIC_BAY', 'AUTOMATIC_STATION', 'AUTOMATIC_REMOTE', 'MAINTENANCE', 'PROCESS']}, 'identification': {'type': 'string'}}}, 'test': {'type': 'boolean'}, 'checks': {'type': 'array', 'items': {'enum': ['SYNCHRO', 'INTERLOCK']}}}}, 'change': {'type': 'object', 'requried': ['session_id', 'value'], 'properties': {'session_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}}}}, 'value': {'anyOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/integer'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/unsigned'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/float'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/bit_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/octet_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/visible_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/mms_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/array'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/struct'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/double_point'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/direction'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/severity'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/vector'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/step_position'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/binary_control'}], '$defs': {'boolean': {'type': 'boolean'}, 'integer': {'type': 'integer'}, 'unsigned': {'type': 'integer'}, 'float': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'bit_string': {'type': 'array', 'items': {'type': 'boolean'}}, 'octet_string': {'type': 'string', 'description': 'hex encoded bytes'}, 'visible_string': {'type': 'string'}, 'mms_string': {'type': 'string'}, 'array': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}, 'struct': {'type': 'object', 'patternProperties': {'.+': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}, 'quality': {'type': 'object', 'required': ['validity', 'details', 'source', 'test', 'operator_blocked'], 'properties': {'validity': {'enum': ['GOOD', 'INVALID', 'RESERVED', 'QUESTIONABLE']}, 'details': {'type': 'array', 'items': {'enum': ['OVERFLOW', 'OUT_OF_RANGE', 'BAD_REFERENCE', 'OSCILLATORY', 'FAILURE', 'OLD_DATA', 'INCONSISTENT', 'INACCURATE']}}, 'source': {'enum': ['PROCESS', 'SUBSTITUTED']}, 'test': {'type': 'boolean'}, 'operator_blocked': {'type': 'boolean'}}}, 'timestamp': {'type': 'object', 'required': ['value', 'leap_second', 'clock_failure', 'not_synchronized'], 'properties': {'value': {'type': 'number', 'description': 'seconds since 1970-01-01'}, 'leap_second': {'type': 'boolean'}, 'clock_failure': {'type': 'boolean'}, 'not_synchronized': {'type': 'boolean'}, 'accuracy': {'type': 'integer'}}}, 'double_point': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'BAD']}, 'direction': {'enum': ['UNKNOWN', 'FORWARD', 'BACKWARD', 'BOTH']}, 'severity': {'enum': ['UNKNOWN', 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING']}, 'analogue': {'type': 'object', 'properties': {'i': {'type': 'integer'}, 'f': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}}}, 'vector': {'type': 'object', 'required': ['magnitude'], 'properties': {'magnitude': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, 'angle': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}}}, 'step_position': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}, 'transient': {'type': 'boolean'}}}, 'binary_control': {'enum': ['STOP', 'LOWER', 'HIGHER', 'RESERVED']}}}, 'value_type': {'oneOf': [{'enum': ['BOOLEAN', 'INTEGER', 'UNSIGNED', 'FLOAT', 'BIT_STRING', 'OCTET_STRING', 'VISIBLE_STRING', 'MMS_STRING', 'QUALITY', 'TIMESTAMP', 'DOUBLE_POINT', 'DIRECTION', 'SEVERITY', 'ANALOGUE', 'VECTOR', 'STEP_POSITION', 'BINARY_CONTROL']}, {'type': 'object', 'required': ['type', 'element_type', 'length'], 'properties': {'type': {'const': 'ARRAY'}, 'element_type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}, 'length': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'elements'], 'properties': {'type': {'const': 'STRUCT'}, 'elements': {'type': 'array', 'items': {'type': 'object', 'requried': ['name', 'type'], 'properties': {'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}}}]}, 'refs': {'value': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, 'command': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}, 'rcb': {'type': 'object', 'required': ['logical_device', 'logical_node', 'type', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'type': {'enum': ['BUFFERED', 'UNBUFFERED']}, 'name': {'type': 'string'}}}, 'dataset': {'oneOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/persisted'}], '$defs': {'nonpersisted': {'type': 'string'}, 'persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, 'hat-gateway://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://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://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://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://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'}}}}}}}}}, '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'}}}}}}}}})
class
Buffer:
95class Buffer: 96 97 def __init__(self, size: int): 98 self._size = size 99 self._data = collections.OrderedDict() 100 101 def add(self, 102 event_id: hat.event.common.EventId, 103 data_msg: iec101.DataMsg): 104 self._data[event_id] = data_msg 105 while len(self._data) > self._size: 106 self._data.popitem(last=False) 107 108 def remove(self, event_id: hat.event.common.EventId): 109 self._data.pop(event_id, None) 110 111 def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId, # NOQA 112 iec101.DataMsg]]: 113 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]):
122async def init_data(log: logging.Logger, 123 data_conf: json.Data, 124 data_msgs: dict[common.DataKey, iec101.DataMsg], 125 data_buffers: dict[common.DataKey, Buffer], 126 buffers: dict[str, Buffer], 127 eventer_client: hat.event.eventer.Client, 128 event_type_prefix: common.EventTypePrefix): 129 for data in data_conf: 130 data_key = common.DataKey(data_type=common.DataType[data['data_type']], 131 asdu_address=data['asdu_address'], 132 io_address=data['io_address']) 133 data_msgs[data_key] = None 134 if data['buffer']: 135 data_buffers[data_key] = buffers[data['buffer']] 136 137 event_types = [(*event_type_prefix, 'system', 'data', '*')] 138 params = hat.event.common.QueryLatestParams(event_types) 139 result = await eventer_client.query(params) 140 141 for event in result.events: 142 try: 143 data_type_str, asdu_address_str, io_address_str = \ 144 event.type[len(event_type_prefix)+2:] 145 data_key = common.DataKey(data_type=common.DataType(data_type_str), 146 asdu_address=int(asdu_address_str), 147 io_address=int(io_address_str)) 148 if data_key not in data_msgs: 149 raise Exception(f'data {data_key} not configured') 150 151 data_msgs[data_key] = data_msg_from_event(data_key, event) 152 153 except Exception as e: 154 log.debug('skipping initial data: %s', e, exc_info=e)
157class Iec101SlaveDevice(common.Device): 158 159 @property 160 def async_group(self) -> aio.Group: 161 return self._link.async_group 162 163 async def process_events(self, events: Collection[hat.event.common.Event]): 164 for event in events: 165 try: 166 await self._process_event(event) 167 168 except Exception as e: 169 self._log.warning('error processing event: %s', e, exc_info=e) 170 171 async def _connection_loop(self, device_conf): 172 conn = None 173 174 try: 175 if self._conf['link_type'] == 'BALANCED': 176 conn_args = { 177 'direction': link.Direction[device_conf['direction']], 178 'addr': device_conf['address'], 179 'response_timeout': device_conf['response_timeout'], 180 'send_retry_count': device_conf['send_retry_count'], 181 'status_delay': device_conf['status_delay'], 182 'name': self._conf['name']} 183 184 elif self._conf['link_type'] == 'UNBALANCED': 185 conn_args = { 186 'addr': device_conf['address'], 187 'keep_alive_timeout': device_conf['keep_alive_timeout'], 188 'name': self._conf['name']} 189 190 else: 191 raise ValueError('unsupported link type') 192 193 while True: 194 try: 195 conn = await self._link.open_connection(**conn_args) 196 197 except Exception as e: 198 self._log.error('connection error for address %s: %s', 199 device_conf['address'], e, exc_info=e) 200 await asyncio.sleep(device_conf['reconnect_delay']) 201 continue 202 203 conn = iec101.Connection( 204 conn=conn, 205 cause_size=iec101.CauseSize[self._conf['cause_size']], 206 asdu_address_size=iec101.AsduAddressSize[ 207 self._conf['asdu_address_size']], 208 io_address_size=iec101.IoAddressSize[ 209 self._conf['io_address_size']]) 210 211 conn_id = next(self._next_conn_ids) 212 self._conns[conn_id] = conn 213 214 send_queue = aio.Queue(1024) 215 self._send_queues[conn_id] = send_queue 216 217 try: 218 conn.async_group.spawn(self._connection_send_loop, conn, 219 send_queue) 220 conn.async_group.spawn(self._connection_receive_loop, conn, 221 conn_id) 222 223 await self._register_connections() 224 225 with contextlib.suppress(Exception): 226 for buffer in self._buffers.values(): 227 for event_id, data_msg in buffer.get_event_id_data_msgs(): # NOQA 228 await self._send_data_msg(conn_id, buffer, 229 event_id, data_msg) 230 231 await conn.wait_closed() 232 233 finally: 234 send_queue.close() 235 236 self._conns.pop(conn_id, None) 237 self._send_queues.pop(conn_id, None) 238 239 with contextlib.suppress(Exception): 240 await aio.uncancellable(self._register_connections()) 241 242 await conn.async_close() 243 244 except Exception as e: 245 self._log.warning('connection loop error: %s', e, exc_info=e) 246 247 finally: 248 self._log.debug('closing connection') 249 self.close() 250 251 if conn: 252 await aio.uncancellable(conn.async_close()) 253 254 async def _connection_send_loop(self, conn, send_queue): 255 try: 256 while True: 257 msgs, sent_cb = await send_queue.get() 258 await conn.send(msgs, sent_cb=sent_cb) 259 260 except ConnectionError: 261 self._log.debug('connection close') 262 263 except Exception as e: 264 self._log.warning('connection send loop error: %s', e, exc_info=e) 265 266 finally: 267 conn.close() 268 269 async def _connection_receive_loop(self, conn, conn_id): 270 try: 271 while True: 272 msgs = await conn.receive() 273 274 for msg in msgs: 275 try: 276 self._log.debug('received message: %s', msg) 277 await self._process_msg(conn_id, msg) 278 279 except Exception as e: 280 self._log.warning('error processing message: %s', 281 e, exc_info=e) 282 283 except ConnectionError: 284 self._log.debug('connection close') 285 286 except Exception as e: 287 self._log.warning('connection receive loop error: %s', 288 e, exc_info=e) 289 290 finally: 291 conn.close() 292 293 async def _register_connections(self): 294 payload = [{'connection_id': conn_id, 295 'address': conn.info.address} 296 for conn_id, conn in self._conns.items()] 297 298 event = hat.event.common.RegisterEvent( 299 type=(*self._event_type_prefix, 'gateway', 'connections'), 300 source_timestamp=None, 301 payload=hat.event.common.EventPayloadJson(payload)) 302 303 await self._eventer_client.register([event]) 304 305 async def _process_event(self, event): 306 suffix = event.type[len(self._event_type_prefix):] 307 308 if suffix[:2] == ('system', 'data'): 309 data_type_str, asdu_address_str, io_address_str = suffix[2:] 310 data_key = common.DataKey(data_type=common.DataType(data_type_str), 311 asdu_address=int(asdu_address_str), 312 io_address=int(io_address_str)) 313 314 await self._process_data_event(data_key, event) 315 316 elif suffix[:2] == ('system', 'command'): 317 cmd_type_str, asdu_address_str, io_address_str = suffix[2:] 318 cmd_key = common.CommandKey( 319 cmd_type=common.CommandType(cmd_type_str), 320 asdu_address=int(asdu_address_str), 321 io_address=int(io_address_str)) 322 323 await self._process_command_event(cmd_key, event) 324 325 else: 326 raise Exception('unsupported event type') 327 328 async def _process_data_event(self, data_key, event): 329 if data_key not in self._data_msgs: 330 raise Exception('data not configured') 331 332 data_msg = data_msg_from_event(data_key, event) 333 self._data_msgs[data_key] = data_msg 334 335 buffer = self._data_buffers.get(data_key) 336 if buffer: 337 buffer.add(event.id, data_msg) 338 339 for conn_id in self._conns.keys(): 340 await self._send_data_msg(conn_id, buffer, event.id, data_msg) 341 342 async def _process_command_event(self, cmd_key, event): 343 cmd_msg = cmd_msg_from_event(cmd_key, event) 344 conn_id = event.payload.data['connection_id'] 345 await self._send(conn_id, [cmd_msg]) 346 347 async def _process_msg(self, conn_id, msg): 348 if isinstance(msg, iec101.CommandMsg): 349 await self._process_command_msg(conn_id, msg) 350 351 elif isinstance(msg, iec101.InterrogationMsg): 352 await self._process_interrogation_msg(conn_id, msg) 353 354 elif isinstance(msg, iec101.CounterInterrogationMsg): 355 await self._process_counter_interrogation_msg(conn_id, msg) 356 357 elif isinstance(msg, iec101.ReadMsg): 358 await self._process_read_msg(conn_id, msg) 359 360 elif isinstance(msg, iec101.ClockSyncMsg): 361 await self._process_clock_sync_msg(conn_id, msg) 362 363 elif isinstance(msg, iec101.TestMsg): 364 await self._process_test_msg(conn_id, msg) 365 366 elif isinstance(msg, iec101.ResetMsg): 367 await self._process_reset_msg(conn_id, msg) 368 369 elif isinstance(msg, iec101.ParameterMsg): 370 await self._process_parameter_msg(conn_id, msg) 371 372 elif isinstance(msg, iec101.ParameterActivationMsg): 373 await self._process_parameter_activation_msg(conn_id, msg) 374 375 else: 376 raise Exception('unsupported message') 377 378 async def _process_command_msg(self, conn_id, msg): 379 if isinstance(msg.cause, iec101.CommandReqCause): 380 event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg) 381 await self._eventer_client.register([event]) 382 383 else: 384 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 385 is_negative_confirm=True) 386 await self._send(conn_id, [res]) 387 388 async def _process_interrogation_msg(self, conn_id, msg): 389 if msg.cause == iec101.CommandReqCause.ACTIVATION: 390 res = msg._replace( 391 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 392 is_negative_confirm=False) 393 await self._send(conn_id, [res]) 394 395 data_msgs = [ 396 data_msg._replace( 397 is_test=msg.is_test, 398 cause=iec101.DataResCause.INTERROGATED_STATION) 399 for data_msg in self._data_msgs.values() 400 if (data_msg and 401 (msg.asdu_address == 0xFFFF or 402 msg.asdu_address == data_msg.asdu_address) and 403 not isinstance(data_msg.data, iec101.BinaryCounterData))] 404 await self._send(conn_id, data_msgs) 405 406 res = msg._replace( 407 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 408 is_negative_confirm=False) 409 await self._send(conn_id, [res]) 410 411 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 412 res = msg._replace( 413 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 414 is_negative_confirm=True) 415 await self._send(conn_id, [res]) 416 417 else: 418 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 419 is_negative_confirm=True) 420 await self._send(conn_id, [res]) 421 422 async def _process_counter_interrogation_msg(self, conn_id, msg): 423 if msg.cause == iec101.CommandReqCause.ACTIVATION: 424 res = msg._replace( 425 cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION, 426 is_negative_confirm=False) 427 await self._send(conn_id, [res]) 428 429 data_msgs = [ 430 data_msg._replace( 431 is_test=msg.is_test, 432 cause=iec101.DataResCause.INTERROGATED_COUNTER) 433 for data_msg in self._data_msgs.values() 434 if (data_msg and 435 (msg.asdu_address == 0xFFFF or 436 msg.asdu_address == data_msg.asdu_address) and 437 isinstance(data_msg.data, iec101.BinaryCounterData))] 438 await self._send(conn_id, data_msgs) 439 440 res = msg._replace( 441 cause=iec101.CommandResCause.ACTIVATION_TERMINATION, 442 is_negative_confirm=False) 443 await self._send(conn_id, [res]) 444 445 elif msg.cause == iec101.CommandReqCause.DEACTIVATION: 446 res = msg._replace( 447 cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION, 448 is_negative_confirm=True) 449 await self._send(conn_id, [res]) 450 451 else: 452 res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE, 453 is_negative_confirm=True) 454 await self._send(conn_id, [res]) 455 456 async def _process_read_msg(self, conn_id, msg): 457 res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE) 458 await self._send(conn_id, [res]) 459 460 async def _process_clock_sync_msg(self, conn_id, msg): 461 if isinstance(msg.cause, iec101.ClockSyncReqCause): 462 res = msg._replace( 463 cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION, 464 is_negative_confirm=True) 465 await self._send(conn_id, [res]) 466 467 else: 468 res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE, 469 is_negative_confirm=True) 470 await self._send(conn_id, [res]) 471 472 async def _process_test_msg(self, conn_id, msg): 473 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 474 await self._send(conn_id, [res]) 475 476 async def _process_reset_msg(self, conn_id, msg): 477 res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE) 478 await self._send(conn_id, [res]) 479 480 async def _process_parameter_msg(self, conn_id, msg): 481 res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE) 482 await self._send(conn_id, [res]) 483 484 async def _process_parameter_activation_msg(self, conn_id, msg): 485 res = msg._replace( 486 cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE) 487 await self._send(conn_id, [res]) 488 489 async def _send_data_msg(self, conn_id, buffer, event_id, data_msg): 490 sent_cb = (functools.partial(buffer.remove, event_id) 491 if buffer else None) 492 await self._send(conn_id, [data_msg], sent_cb=sent_cb) 493 494 async def _send(self, conn_id, msgs, sent_cb=None): 495 send_queue = self._send_queues.get(conn_id) 496 if send_queue is None: 497 return 498 499 with contextlib.suppress(aio.QueueClosedError): 500 await send_queue.put((msgs, sent_cb))
Device interface
async def
process_events(self, events: Collection[hat.event.common.common.Event]):
163 async def process_events(self, events: Collection[hat.event.common.Event]): 164 for event in events: 165 try: 166 await self._process_event(event) 167 168 except Exception as e: 169 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:
503def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType, 504 conn_id: int, 505 msg: iec101.CommandMsg 506 ) -> hat.event.common.RegisterEvent: 507 command_type = common.get_command_type(msg.command) 508 cause = common.cause_to_json(iec101.CommandReqCause, msg.cause) 509 command = common.command_to_json(msg.command) 510 event_type = (*event_type_prefix, 'gateway', 'command', command_type.value, 511 str(msg.asdu_address), str(msg.io_address)) 512 513 return hat.event.common.RegisterEvent( 514 type=event_type, 515 source_timestamp=None, 516 payload=hat.event.common.EventPayloadJson({ 517 'connection_id': conn_id, 518 'is_test': msg.is_test, 519 'cause': cause, 520 '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:
523def data_msg_from_event(data_key: common.DataKey, 524 event: hat.event.common.Event 525 ) -> iec101.DataMsg: 526 time = common.time_from_source_timestamp(event.source_timestamp) 527 cause = common.cause_from_json(iec101.DataResCause, 528 event.payload.data['cause']) 529 data = common.data_from_json(data_key.data_type, 530 event.payload.data['data']) 531 532 return iec101.DataMsg(is_test=event.payload.data['is_test'], 533 originator_address=0, 534 asdu_address=data_key.asdu_address, 535 io_address=data_key.io_address, 536 data=data, 537 time=time, 538 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:
541def cmd_msg_from_event(cmd_key: common.CommandKey, 542 event: hat.event.common.Event 543 ) -> iec101.CommandMsg: 544 cause = common.cause_from_json(iec101.CommandResCause, 545 event.payload.data['cause']) 546 command = common.command_from_json(cmd_key.cmd_type, 547 event.payload.data['command']) 548 is_negative_confirm = event.payload.data['is_negative_confirm'] 549 550 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 551 originator_address=0, 552 asdu_address=cmd_key.asdu_address, 553 io_address=cmd_key.io_address, 554 command=command, 555 is_negative_confirm=is_negative_confirm, 556 cause=cause)