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