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 74 self.async_group.spawn(self._link_loop) 75 self.async_group.spawn(self._send_loop) 76 77 @property 78 def async_group(self) -> aio.Group: 79 return self._async_group 80 81 async def process_events(self, events: Collection[hat.event.common.Event]): 82 for event in events: 83 try: 84 await self._process_event(event) 85 86 except Exception as e: 87 mlog.warning('error processing event: %s', e, exc_info=e) 88 89 async def _link_loop(self): 90 91 async def cleanup(): 92 with contextlib.suppress(ConnectionError): 93 await self._register_status('DISCONNECTED') 94 95 if self._link: 96 await self._link.async_close() 97 98 try: 99 if self._conf['link_type'] == 'BALANCED': 100 create_link = link.create_balanced_link 101 102 elif self._conf['link_type'] == 'UNBALANCED': 103 create_link = link.create_master_link 104 105 else: 106 raise ValueError('unsupported link type') 107 108 while True: 109 await self._register_status('CONNECTING') 110 111 try: 112 self._link = await create_link( 113 port=self._conf['port'], 114 address_size=link.AddressSize[ 115 self._conf['device_address_size']], 116 silent_interval=self._conf['silent_interval'], 117 baudrate=self._conf['baudrate'], 118 bytesize=serial.ByteSize[self._conf['bytesize']], 119 parity=serial.Parity[self._conf['parity']], 120 stopbits=serial.StopBits[self._conf['stopbits']], 121 xonxoff=self._conf['flow_control']['xonxoff'], 122 rtscts=self._conf['flow_control']['rtscts'], 123 dsrdtr=self._conf['flow_control']['dsrdtr']) 124 125 except Exception as e: 126 mlog.warning('link master (endpoint) failed to create: %s', 127 e, exc_info=e) 128 await self._register_status('DISCONNECTED') 129 await asyncio.sleep(self._conf['reconnect_delay']) 130 continue 131 132 await self._register_status('CONNECTED') 133 134 for address, enabled in self._remote_enabled.items(): 135 if enabled: 136 self._enable_remote(address) 137 138 await self._link.wait_closed() 139 await self._register_status('DISCONNECTED') 140 self._link = None 141 142 except Exception as e: 143 mlog.error('create link master error: %s', e, exc_info=e) 144 145 finally: 146 mlog.debug('closing link master loop') 147 self.close() 148 self._conns = {} 149 await aio.uncancellable(cleanup()) 150 151 async def _send_loop(self): 152 while True: 153 msg, address = await self._send_queue.get() 154 155 conn = self._conns.get(address) 156 if not conn or not conn.is_open: 157 mlog.warning('msg %s not sent, connection to %s closed', 158 msg, address) 159 continue 160 161 try: 162 await conn.send([msg]) 163 mlog.debug('msg sent asdu=%s', msg.asdu_address) 164 165 except ConnectionError: 166 mlog.warning('msg %s not sent, connection to %s closed', 167 msg, address) 168 169 async def _connection_loop(self, group, address): 170 171 async def cleanup(): 172 with contextlib.suppress(ConnectionError): 173 await self._register_rmt_status(address, 'DISCONNECTED') 174 175 self._conns.pop(address, None) 176 if conn: 177 await conn.async_close() 178 179 conn = None 180 remote_conf = self._remote_confs[address] 181 182 try: 183 if self._conf['link_type'] == 'BALANCED': 184 conn_args = { 185 'direction': link.Direction[remote_conf['direction']], 186 'addr': address, 187 'response_timeout': remote_conf['response_timeout'], 188 'send_retry_count': remote_conf['send_retry_count'], 189 'status_delay': remote_conf['status_delay']} 190 191 elif self._conf['link_type'] == 'UNBALANCED': 192 conn_args = { 193 'addr': address, 194 'response_timeout': remote_conf['response_timeout'], 195 'send_retry_count': remote_conf['send_retry_count'], 196 'poll_class1_delay': remote_conf['poll_class1_delay'], 197 'poll_class2_delay': remote_conf['poll_class2_delay']} 198 199 else: 200 raise ValueError('unsupported link type') 201 202 while True: 203 await self._register_rmt_status(address, 'CONNECTING') 204 205 try: 206 conn = await self._link.open_connection(**conn_args) 207 208 except Exception as e: 209 mlog.error('connection error to address %s: %s', 210 address, e, exc_info=e) 211 await self._register_rmt_status(address, 'DISCONNECTED') 212 await asyncio.sleep(remote_conf['reconnect_delay']) 213 continue 214 215 await self._register_rmt_status(address, 'CONNECTED') 216 217 conn = iec101.Connection( 218 conn=conn, 219 cause_size=iec101.CauseSize[self._conf['cause_size']], 220 asdu_address_size=iec101.AsduAddressSize[ 221 self._conf['asdu_address_size']], 222 io_address_size=iec101.IoAddressSize[ 223 self._conf['io_address_size']]) 224 self._conns[address] = conn 225 group.spawn(self._receive_loop, conn, address) 226 227 if remote_conf['time_sync_delay'] is not None: 228 group.spawn(self._time_sync_loop, conn, 229 remote_conf['time_sync_delay']) 230 231 await conn.wait_closed() 232 await self._register_rmt_status(address, 'DISCONNECTED') 233 self._conns.pop(address, None) 234 235 except Exception as e: 236 mlog.error('connection loop error: %s', e, exc_info=e) 237 238 finally: 239 mlog.debug('closing remote device %s', address) 240 group.close() 241 await aio.uncancellable(cleanup()) 242 243 async def _receive_loop(self, conn, address): 244 try: 245 while True: 246 try: 247 msgs = await conn.receive() 248 249 except iec101.AsduTypeError as e: 250 mlog.warning("asdu type error: %s", e) 251 continue 252 253 events = collections.deque() 254 for msg in msgs: 255 if isinstance(msg, iec101.ClockSyncMsg): 256 continue 257 258 try: 259 event = _msg_to_event(self._event_type_prefix, address, 260 msg) 261 events.append(event) 262 263 except Exception as e: 264 mlog.warning('message %s ignored due to: %s', 265 msg, e, exc_info=e) 266 267 if not events: 268 continue 269 270 await self._eventer_client.register(events) 271 for e in events: 272 mlog.debug('registered event %s', e) 273 274 except ConnectionError: 275 mlog.debug('connection closed') 276 277 except Exception as e: 278 mlog.error('receive loop error: %s', e, exc_info=e) 279 280 finally: 281 conn.close() 282 283 async def _time_sync_loop(self, conn, delay): 284 try: 285 while True: 286 time_now = datetime.datetime.now(datetime.timezone.utc) 287 time_iec101 = iec101.time_from_datetime(time_now) 288 msg = iec101.ClockSyncMsg( 289 is_test=False, 290 originator_address=0, 291 asdu_address={ 292 'ONE': 0xFF, 293 'TWO': 0xFFFF}[self._conf['asdu_address_size']], 294 time=time_iec101, 295 is_negative_confirm=False, 296 cause=iec101.ClockSyncReqCause.ACTIVATION) 297 await conn.send([msg]) 298 mlog.debug('time sync sent %s', time_iec101) 299 300 await asyncio.sleep(delay) 301 302 except ConnectionError: 303 mlog.debug('connection closed') 304 305 except Exception as e: 306 mlog.error('time sync loop error: %s', e, exc_info=e) 307 308 finally: 309 conn.close() 310 311 async def _process_event(self, event): 312 match_type = functools.partial(hat.event.common.matches_query_type, 313 event.type) 314 315 prefix = (*self._event_type_prefix, 'system', 'remote_device', '?') 316 if not match_type((*prefix, '*')): 317 raise Exception('unexpected event type') 318 319 address = int(event.type[len(prefix) - 1]) 320 suffix = event.type[len(prefix):] 321 322 if match_type((*prefix, 'enable')): 323 self._process_event_enable(address, event) 324 325 elif match_type((*prefix, 'command', '?', '?', '?')): 326 cmd_key = common.CommandKey( 327 cmd_type=common.CommandType(suffix[1]), 328 asdu_address=int(suffix[2]), 329 io_address=int(suffix[3])) 330 msg = _command_from_event(cmd_key, event) 331 332 await self._send_queue.put((msg, address)) 333 mlog.debug('command asdu=%s io=%s prepared for sending', 334 cmd_key.asdu_address, cmd_key.io_address) 335 336 elif match_type((*prefix, 'interrogation', '?')): 337 asdu_address = int(suffix[1]) 338 msg = _interrogation_from_event(asdu_address, event) 339 340 await self._send_queue.put((msg, address)) 341 mlog.debug("interrogation request asdu=%s prepared for sending", 342 asdu_address) 343 344 elif match_type((*prefix, 'counter_interrogation', '?')): 345 asdu_address = int(suffix[1]) 346 msg = _counter_interrogation_from_event(asdu_address, event) 347 348 await self._send_queue.put((msg, address)) 349 mlog.debug("counter interrogation request asdu=%s prepared for " 350 "sending", asdu_address) 351 352 else: 353 raise Exception('unexpected event type') 354 355 def _process_event_enable(self, address, event): 356 if address not in self._remote_enabled: 357 raise Exception('invalid remote device address') 358 359 enable = event.payload.data 360 if not isinstance(enable, bool): 361 raise Exception('invalid enable event payload') 362 363 if address not in self._remote_enabled: 364 mlog.warning('received enable for unexpected remote device') 365 return 366 367 self._remote_enabled[address] = enable 368 369 if not enable: 370 self._disable_remote(address) 371 372 elif not self._link: 373 return 374 375 else: 376 self._enable_remote(address) 377 378 def _enable_remote(self, address): 379 mlog.debug('enabling device %s', address) 380 remote_group = self._remote_groups.get(address) 381 if remote_group and remote_group.is_open: 382 mlog.debug('device %s is already running', address) 383 return 384 385 remote_group = self._async_group.create_subgroup() 386 self._remote_groups[address] = remote_group 387 remote_group.spawn(self._connection_loop, remote_group, address) 388 389 def _disable_remote(self, address): 390 mlog.debug('disabling device %s', address) 391 if address in self._remote_groups: 392 remote_group = self._remote_groups.pop(address) 393 remote_group.close() 394 395 async def _register_status(self, status): 396 event = hat.event.common.RegisterEvent( 397 type=(*self._event_type_prefix, 'gateway', 'status'), 398 source_timestamp=None, 399 payload=hat.event.common.EventPayloadJson(status)) 400 await self._eventer_client.register([event]) 401 mlog.debug('device status -> %s', status) 402 403 async def _register_rmt_status(self, address, status): 404 event = hat.event.common.RegisterEvent( 405 type=(*self._event_type_prefix, 'gateway', 'remote_device', 406 str(address), 'status'), 407 source_timestamp=None, 408 payload=hat.event.common.EventPayloadJson(status)) 409 await self._eventer_client.register([event]) 410 mlog.debug('remote device %s status -> %s', address, status) 411 412 413def _msg_to_event(event_type_prefix, address, msg): 414 if isinstance(msg, iec101.DataMsg): 415 return _data_to_event(event_type_prefix, address, msg) 416 417 if isinstance(msg, iec101.CommandMsg): 418 return _command_to_event(event_type_prefix, address, msg) 419 420 if isinstance(msg, iec101.InterrogationMsg): 421 return _interrogation_to_event(event_type_prefix, address, msg) 422 423 if isinstance(msg, iec101.CounterInterrogationMsg): 424 return _counter_interrogation_to_event(event_type_prefix, address, msg) 425 426 raise Exception('unsupported message type') 427 428 429def _data_to_event(event_type_prefix, address, msg): 430 data_type = common.get_data_type(msg.data) 431 cause = common.cause_to_json(iec101.DataResCause, msg.cause) 432 data = common.data_to_json(msg.data) 433 event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address), 434 'data', data_type.value, str(msg.asdu_address), 435 str(msg.io_address)) 436 source_timestamp = common.time_to_source_timestamp(msg.time) 437 438 return hat.event.common.RegisterEvent( 439 type=event_type, 440 source_timestamp=source_timestamp, 441 payload=hat.event.common.EventPayloadJson({'is_test': msg.is_test, 442 'cause': cause, 443 'data': data})) 444 445 446def _command_to_event(event_type_prefix, address, msg): 447 command_type = common.get_command_type(msg.command) 448 cause = common.cause_to_json(iec101.CommandResCause, msg.cause) 449 command = common.command_to_json(msg.command) 450 event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address), 451 'command', command_type.value, str(msg.asdu_address), 452 str(msg.io_address)) 453 454 return hat.event.common.RegisterEvent( 455 type=event_type, 456 source_timestamp=None, 457 payload=hat.event.common.EventPayloadJson({ 458 'is_test': msg.is_test, 459 'is_negative_confirm': msg.is_negative_confirm, 460 'cause': cause, 461 'command': command})) 462 463 464def _interrogation_to_event(event_type_prefix, address, msg): 465 cause = common.cause_to_json(iec101.CommandResCause, msg.cause) 466 event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address), 467 'interrogation', str(msg.asdu_address)) 468 469 return hat.event.common.RegisterEvent( 470 type=event_type, 471 source_timestamp=None, 472 payload=hat.event.common.EventPayloadJson({ 473 'is_test': msg.is_test, 474 'is_negative_confirm': msg.is_negative_confirm, 475 'request': msg.request, 476 'cause': cause})) 477 478 479def _counter_interrogation_to_event(event_type_prefix, address, msg): 480 cause = common.cause_to_json(iec101.CommandResCause, msg.cause) 481 event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address), 482 'counter_interrogation', str(msg.asdu_address)) 483 484 return hat.event.common.RegisterEvent( 485 type=event_type, 486 source_timestamp=None, 487 payload=hat.event.common.EventPayloadJson({ 488 'is_test': msg.is_test, 489 'is_negative_confirm': msg.is_negative_confirm, 490 'request': msg.request, 491 'freeze': msg.freeze.name, 492 'cause': cause})) 493 494 495def _command_from_event(cmd_key, event): 496 cause = common.cause_from_json(iec101.CommandReqCause, 497 event.payload.data['cause']) 498 command = common.command_from_json(cmd_key.cmd_type, 499 event.payload.data['command']) 500 501 return iec101.CommandMsg(is_test=event.payload.data['is_test'], 502 originator_address=0, 503 asdu_address=cmd_key.asdu_address, 504 io_address=cmd_key.io_address, 505 command=command, 506 is_negative_confirm=False, 507 cause=cause) 508 509 510def _interrogation_from_event(asdu_address, event): 511 cause = common.cause_from_json(iec101.CommandReqCause, 512 event.payload.data['cause']) 513 514 return iec101.InterrogationMsg(is_test=event.payload.data['is_test'], 515 originator_address=0, 516 asdu_address=asdu_address, 517 request=event.payload.data['request'], 518 is_negative_confirm=False, 519 cause=cause) 520 521 522def _counter_interrogation_from_event(asdu_address, event): 523 freeze = iec101.FreezeCode[event.payload.data['freeze']] 524 cause = common.cause_from_json(iec101.CommandReqCause, 525 event.payload.data['cause']) 526 527 return iec101.CounterInterrogationMsg( 528 is_test=event.payload.data['is_test'], 529 originator_address=0, 530 asdu_address=asdu_address, 531 request=event.payload.data['request'], 532 freeze=freeze, 533 is_negative_confirm=False, 534 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]) -> 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': ['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']}}}}})
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 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 mlog.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 126 except Exception as e: 127 mlog.warning('link master (endpoint) failed to create: %s', 128 e, exc_info=e) 129 await self._register_status('DISCONNECTED') 130 await asyncio.sleep(self._conf['reconnect_delay']) 131 continue 132 133 await self._register_status('CONNECTED') 134 135 for address, enabled in self._remote_enabled.items(): 136 if enabled: 137 self._enable_remote(address) 138 139 await self._link.wait_closed() 140 await self._register_status('DISCONNECTED') 141 self._link = None 142 143 except Exception as e: 144 mlog.error('create link master error: %s', e, exc_info=e) 145 146 finally: 147 mlog.debug('closing link master loop') 148 self.close() 149 self._conns = {} 150 await aio.uncancellable(cleanup()) 151 152 async def _send_loop(self): 153 while True: 154 msg, address = await self._send_queue.get() 155 156 conn = self._conns.get(address) 157 if not conn or not conn.is_open: 158 mlog.warning('msg %s not sent, connection to %s closed', 159 msg, address) 160 continue 161 162 try: 163 await conn.send([msg]) 164 mlog.debug('msg sent asdu=%s', msg.asdu_address) 165 166 except ConnectionError: 167 mlog.warning('msg %s not sent, connection to %s closed', 168 msg, address) 169 170 async def _connection_loop(self, group, address): 171 172 async def cleanup(): 173 with contextlib.suppress(ConnectionError): 174 await self._register_rmt_status(address, 'DISCONNECTED') 175 176 self._conns.pop(address, None) 177 if conn: 178 await conn.async_close() 179 180 conn = None 181 remote_conf = self._remote_confs[address] 182 183 try: 184 if self._conf['link_type'] == 'BALANCED': 185 conn_args = { 186 'direction': link.Direction[remote_conf['direction']], 187 'addr': address, 188 'response_timeout': remote_conf['response_timeout'], 189 'send_retry_count': remote_conf['send_retry_count'], 190 'status_delay': remote_conf['status_delay']} 191 192 elif self._conf['link_type'] == 'UNBALANCED': 193 conn_args = { 194 'addr': address, 195 'response_timeout': remote_conf['response_timeout'], 196 'send_retry_count': remote_conf['send_retry_count'], 197 'poll_class1_delay': remote_conf['poll_class1_delay'], 198 'poll_class2_delay': remote_conf['poll_class2_delay']} 199 200 else: 201 raise ValueError('unsupported link type') 202 203 while True: 204 await self._register_rmt_status(address, 'CONNECTING') 205 206 try: 207 conn = await self._link.open_connection(**conn_args) 208 209 except Exception as e: 210 mlog.error('connection error to address %s: %s', 211 address, e, exc_info=e) 212 await self._register_rmt_status(address, 'DISCONNECTED') 213 await asyncio.sleep(remote_conf['reconnect_delay']) 214 continue 215 216 await self._register_rmt_status(address, 'CONNECTED') 217 218 conn = iec101.Connection( 219 conn=conn, 220 cause_size=iec101.CauseSize[self._conf['cause_size']], 221 asdu_address_size=iec101.AsduAddressSize[ 222 self._conf['asdu_address_size']], 223 io_address_size=iec101.IoAddressSize[ 224 self._conf['io_address_size']]) 225 self._conns[address] = conn 226 group.spawn(self._receive_loop, conn, address) 227 228 if remote_conf['time_sync_delay'] is not None: 229 group.spawn(self._time_sync_loop, conn, 230 remote_conf['time_sync_delay']) 231 232 await conn.wait_closed() 233 await self._register_rmt_status(address, 'DISCONNECTED') 234 self._conns.pop(address, None) 235 236 except Exception as e: 237 mlog.error('connection loop error: %s', e, exc_info=e) 238 239 finally: 240 mlog.debug('closing remote device %s', address) 241 group.close() 242 await aio.uncancellable(cleanup()) 243 244 async def _receive_loop(self, conn, address): 245 try: 246 while True: 247 try: 248 msgs = await conn.receive() 249 250 except iec101.AsduTypeError as e: 251 mlog.warning("asdu type error: %s", e) 252 continue 253 254 events = collections.deque() 255 for msg in msgs: 256 if isinstance(msg, iec101.ClockSyncMsg): 257 continue 258 259 try: 260 event = _msg_to_event(self._event_type_prefix, address, 261 msg) 262 events.append(event) 263 264 except Exception as e: 265 mlog.warning('message %s ignored due to: %s', 266 msg, e, exc_info=e) 267 268 if not events: 269 continue 270 271 await self._eventer_client.register(events) 272 for e in events: 273 mlog.debug('registered event %s', e) 274 275 except ConnectionError: 276 mlog.debug('connection closed') 277 278 except Exception as e: 279 mlog.error('receive loop error: %s', e, exc_info=e) 280 281 finally: 282 conn.close() 283 284 async def _time_sync_loop(self, conn, delay): 285 try: 286 while True: 287 time_now = datetime.datetime.now(datetime.timezone.utc) 288 time_iec101 = iec101.time_from_datetime(time_now) 289 msg = iec101.ClockSyncMsg( 290 is_test=False, 291 originator_address=0, 292 asdu_address={ 293 'ONE': 0xFF, 294 'TWO': 0xFFFF}[self._conf['asdu_address_size']], 295 time=time_iec101, 296 is_negative_confirm=False, 297 cause=iec101.ClockSyncReqCause.ACTIVATION) 298 await conn.send([msg]) 299 mlog.debug('time sync sent %s', time_iec101) 300 301 await asyncio.sleep(delay) 302 303 except ConnectionError: 304 mlog.debug('connection closed') 305 306 except Exception as e: 307 mlog.error('time sync loop error: %s', e, exc_info=e) 308 309 finally: 310 conn.close() 311 312 async def _process_event(self, event): 313 match_type = functools.partial(hat.event.common.matches_query_type, 314 event.type) 315 316 prefix = (*self._event_type_prefix, 'system', 'remote_device', '?') 317 if not match_type((*prefix, '*')): 318 raise Exception('unexpected event type') 319 320 address = int(event.type[len(prefix) - 1]) 321 suffix = event.type[len(prefix):] 322 323 if match_type((*prefix, 'enable')): 324 self._process_event_enable(address, event) 325 326 elif match_type((*prefix, 'command', '?', '?', '?')): 327 cmd_key = common.CommandKey( 328 cmd_type=common.CommandType(suffix[1]), 329 asdu_address=int(suffix[2]), 330 io_address=int(suffix[3])) 331 msg = _command_from_event(cmd_key, event) 332 333 await self._send_queue.put((msg, address)) 334 mlog.debug('command asdu=%s io=%s prepared for sending', 335 cmd_key.asdu_address, cmd_key.io_address) 336 337 elif match_type((*prefix, 'interrogation', '?')): 338 asdu_address = int(suffix[1]) 339 msg = _interrogation_from_event(asdu_address, event) 340 341 await self._send_queue.put((msg, address)) 342 mlog.debug("interrogation request asdu=%s prepared for sending", 343 asdu_address) 344 345 elif match_type((*prefix, 'counter_interrogation', '?')): 346 asdu_address = int(suffix[1]) 347 msg = _counter_interrogation_from_event(asdu_address, event) 348 349 await self._send_queue.put((msg, address)) 350 mlog.debug("counter interrogation request asdu=%s prepared for " 351 "sending", asdu_address) 352 353 else: 354 raise Exception('unexpected event type') 355 356 def _process_event_enable(self, address, event): 357 if address not in self._remote_enabled: 358 raise Exception('invalid remote device address') 359 360 enable = event.payload.data 361 if not isinstance(enable, bool): 362 raise Exception('invalid enable event payload') 363 364 if address not in self._remote_enabled: 365 mlog.warning('received enable for unexpected remote device') 366 return 367 368 self._remote_enabled[address] = enable 369 370 if not enable: 371 self._disable_remote(address) 372 373 elif not self._link: 374 return 375 376 else: 377 self._enable_remote(address) 378 379 def _enable_remote(self, address): 380 mlog.debug('enabling device %s', address) 381 remote_group = self._remote_groups.get(address) 382 if remote_group and remote_group.is_open: 383 mlog.debug('device %s is already running', address) 384 return 385 386 remote_group = self._async_group.create_subgroup() 387 self._remote_groups[address] = remote_group 388 remote_group.spawn(self._connection_loop, remote_group, address) 389 390 def _disable_remote(self, address): 391 mlog.debug('disabling device %s', address) 392 if address in self._remote_groups: 393 remote_group = self._remote_groups.pop(address) 394 remote_group.close() 395 396 async def _register_status(self, status): 397 event = hat.event.common.RegisterEvent( 398 type=(*self._event_type_prefix, 'gateway', 'status'), 399 source_timestamp=None, 400 payload=hat.event.common.EventPayloadJson(status)) 401 await self._eventer_client.register([event]) 402 mlog.debug('device status -> %s', status) 403 404 async def _register_rmt_status(self, address, status): 405 event = hat.event.common.RegisterEvent( 406 type=(*self._event_type_prefix, 'gateway', 'remote_device', 407 str(address), 'status'), 408 source_timestamp=None, 409 payload=hat.event.common.EventPayloadJson(status)) 410 await self._eventer_client.register([event]) 411 mlog.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 75 self.async_group.spawn(self._link_loop) 76 self.async_group.spawn(self._send_loop)
async def
process_events(self, events: Collection[hat.event.common.common.Event]):
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 mlog.warning('error processing event: %s', e, exc_info=e)
Process received events
This method can be coroutine or regular function.