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