hat.gateway.devices.iec61850.client
IEC 61850 client device
1"""IEC 61850 client device""" 2 3import asyncio 4import collections 5import datetime 6import logging 7import math 8 9from hat import aio 10from hat import json 11from hat import util 12from hat.drivers import iec61850 13from hat.drivers import tcp 14import hat.event.common 15 16from hat.gateway import common 17 18 19mlog: logging.Logger = logging.getLogger(__name__) 20 21 22termination_timeout: int = 100 23 24report_segments_timeout: int = 100 25 26 27async def create(conf: common.DeviceConf, 28 eventer_client: hat.event.eventer.Client, 29 event_type_prefix: common.EventTypePrefix 30 ) -> 'Iec61850ClientDevice': 31 32 value_types = {} 33 value_types_61850 = {} 34 for vt in conf['value_types']: 35 ref = (vt['logical_device'], 36 vt['logical_node'], 37 vt['fc'], 38 vt['name']) 39 value_types_61850[ref] = _vtype_61850_from_vtype_conf(vt['type']) 40 value_types[ref] = _vtype_from_vtype_conf(vt['type']) 41 42 rcb_confs = {_rcb_ref_from_json(i['ref']): i for i in conf['rcbs']} 43 dataset_confs = {_dataset_ref_from_json(ds_conf['ref']): ds_conf 44 for ds_conf in conf['datasets']} 45 value_ref_data_names = collections.defaultdict(collections.deque) 46 data_value_types = {} 47 for data_conf in conf['data']: 48 data_v_ref = _value_ref_from_json(data_conf['value']) 49 data_value_types[data_v_ref] = _value_type_from_ref( 50 value_types, data_v_ref) 51 52 q_ref = (_value_ref_from_json(data_conf['quality']) 53 if data_conf.get('quality') else None) 54 if data_conf.get('quality'): 55 q_type = _value_type_from_ref(value_types, q_ref) 56 if q_type != iec61850.AcsiValueType.QUALITY: 57 raise Exception(f"invalid quality type {q_ref}") 58 59 t_ref = (_value_ref_from_json(data_conf['timestamp']) 60 if data_conf.get('timestamp') else None) 61 if t_ref: 62 t_type = _value_type_from_ref(value_types, t_ref) 63 if t_type != iec61850.AcsiValueType.TIMESTAMP: 64 raise Exception(f"invalid timestamp type {t_ref}") 65 66 seld_ref = (_value_ref_from_json(data_conf['selected']) 67 if data_conf.get('selected') else None) 68 if seld_ref: 69 seld_type = _value_type_from_ref(value_types, seld_ref) 70 if seld_type != iec61850.BasicValueType.BOOLEAN: 71 raise Exception(f"invalid selected type {seld_ref}") 72 73 rcb_conf = rcb_confs[_rcb_ref_from_json(data_conf['rcb'])] 74 ds_ref = _dataset_ref_from_json(rcb_conf['dataset']) 75 ds_conf = dataset_confs[ds_ref] 76 for value_conf in ds_conf['values']: 77 value_ref = _value_ref_from_json(value_conf) 78 if (_refs_match(data_v_ref, value_ref) or 79 (q_ref and _refs_match(q_ref, value_ref)) or 80 (t_ref and _refs_match(t_ref, value_ref)) or 81 (seld_ref and _refs_match(seld_ref, value_ref))): 82 value_ref_data_names[value_ref].append(data_conf['name']) 83 84 dataset_values_ref_type = {} 85 for ds_conf in conf['datasets']: 86 for val_ref_conf in ds_conf['values']: 87 value_ref = _value_ref_from_json(val_ref_conf) 88 value_type = _value_type_from_ref(value_types, value_ref) 89 dataset_values_ref_type[value_ref] = value_type 90 91 command_ref_value_type = {} 92 for cmd_conf in conf['commands']: 93 cmd_ref = iec61850.CommandRef(**cmd_conf['ref']) 94 value_type = _value_type_from_ref(value_types, cmd_ref) 95 command_ref_value_type[cmd_ref] = value_type 96 97 change_ref_value_type = {} 98 for change_conf in conf['changes']: 99 value_ref = _value_ref_from_json(change_conf['ref']) 100 value_type = _value_type_from_ref(value_types, value_ref) 101 change_ref_value_type[value_ref] = value_type 102 103 entry_id_event_types = [ 104 (*event_type_prefix, 'gateway', 'entry_id', 105 _report_id_from_rcb_conf(i)) 106 for i in conf['rcbs'] if i['ref']['type'] == 'BUFFERED'] 107 if entry_id_event_types: 108 result = await eventer_client.query( 109 hat.event.common.QueryLatestParams(entry_id_event_types)) 110 rcbs_entry_ids = { 111 event.type[5]: (bytes.fromhex(event.payload.data) 112 if event.payload.data is not None else None) 113 for event in result.events} 114 else: 115 rcbs_entry_ids = {} 116 117 device = Iec61850ClientDevice() 118 119 device._rcbs_entry_ids = rcbs_entry_ids 120 device._conf = conf 121 device._eventer_client = eventer_client 122 device._event_type_prefix = event_type_prefix 123 device._conn = None 124 device._conn_status = None 125 device._terminations = {} 126 device._reports_segments = {} 127 128 device._value_ref_data_names = value_ref_data_names 129 device._data_value_types = data_value_types 130 device._dataset_values_ref_type = dataset_values_ref_type 131 device._command_ref_value_type = command_ref_value_type 132 device._change_ref_value_type = change_ref_value_type 133 device._data_name_confs = {i['name']: i for i in conf['data']} 134 device._command_name_confs = {i['name']: i for i in conf['commands']} 135 device._command_name_ctl_nums = {i['name']: 0 for i in conf['commands']} 136 device._change_name_value_refs = { 137 i['name']: _value_ref_from_json(i['ref']) for i in conf['changes']} 138 device._rcb_type = { 139 _report_id_from_rcb_conf(rcb_conf): rcb_conf['ref']['type'] 140 for rcb_conf in conf['rcbs']} 141 device._persist_dyn_datasets = set(_get_persist_dyn_datasets(conf)) 142 device._dyn_datasets_values = dict(_get_dyn_datasets_values(conf)) 143 device._report_data_refs = collections.defaultdict(collections.deque) 144 for rcb_conf in device._conf['rcbs']: 145 report_id = _report_id_from_rcb_conf(rcb_conf) 146 ds_ref = _dataset_ref_from_json(rcb_conf['dataset']) 147 for value_conf in dataset_confs[ds_ref]['values']: 148 value_ref = _value_ref_from_json(value_conf) 149 device._report_data_refs[report_id].append(value_ref) 150 151 device._dataset_change_value_types = dict( 152 _get_dataset_change_value_types(conf, value_types_61850)) 153 device._cmd_value_types = dict( 154 _get_cmd_value_types(conf, value_types_61850)) 155 156 device._log = _create_logger_adapter(conf['name']) 157 158 device._async_group = aio.Group() 159 device._async_group.spawn(device._connection_loop) 160 device._loop = asyncio.get_running_loop() 161 162 return device 163 164 165info: common.DeviceInfo = common.DeviceInfo( 166 type="iec61850_client", 167 create=create, 168 json_schema_id="hat-gateway://iec61850.yaml#/$defs/client", 169 json_schema_repo=common.json_schema_repo) 170 171 172class Iec61850ClientDevice(common.Device): 173 174 @property 175 def async_group(self) -> aio.Group: 176 return self._async_group 177 178 async def process_event(self, event: hat.event.common.Event): 179 try: 180 suffix = event.type[len(self._event_type_prefix):] 181 182 if suffix[:2] == ('system', 'command'): 183 cmd_name, = suffix[2:] 184 await self._process_cmd_req(event, cmd_name) 185 186 elif suffix[:2] == ('system', 'change'): 187 val_name, = suffix[2:] 188 await self._process_change_req(event, val_name) 189 190 else: 191 raise Exception('unsupported event type') 192 193 except Exception as e: 194 self._log.warning('error processing event %s: %s', 195 event.type, e, exc_info=e) 196 197 async def _connection_loop(self): 198 199 async def cleanup(): 200 await self._register_status('DISCONNECTED') 201 if self._conn: 202 await self._conn.async_close() 203 204 conn_conf = self._conf['connection'] 205 try: 206 while True: 207 await self._register_status('CONNECTING') 208 try: 209 self._log.debug('connecting to %s:%s', 210 conn_conf['host'], conn_conf['port']) 211 self._conn = await aio.wait_for( 212 iec61850.connect( 213 addr=tcp.Address(conn_conf['host'], 214 conn_conf['port']), 215 data_value_types=self._dataset_change_value_types, 216 cmd_value_types=self._cmd_value_types, 217 report_data_refs=self._report_data_refs, 218 report_cb=self._on_report, 219 termination_cb=self._on_termination, 220 status_delay=conn_conf['status_delay'], 221 status_timeout=conn_conf['status_timeout'], 222 local_tsel=conn_conf.get('local_tsel'), 223 remote_tsel=conn_conf.get('remote_tsel'), 224 local_ssel=conn_conf.get('local_ssel'), 225 remote_ssel=conn_conf.get('remote_ssel'), 226 local_psel=conn_conf.get('local_psel'), 227 remote_psel=conn_conf.get('remote_psel'), 228 local_ap_title=conn_conf.get('local_ap_title'), 229 remote_ap_title=conn_conf.get('remote_ap_title'), 230 local_ae_qualifier=conn_conf.get( 231 'local_ae_qualifier'), 232 remote_ae_qualifier=conn_conf.get( 233 'remote_ae_qualifier'), 234 local_detail_calling=conn_conf.get( 235 'local_detail_calling'), 236 name=self._conf['name']), 237 conn_conf['connect_timeout']) 238 239 except Exception as e: 240 self._log.warning('connnection failed: %s', e, exc_info=e) 241 await self._register_status('DISCONNECTED') 242 await asyncio.sleep(conn_conf['reconnect_delay']) 243 continue 244 245 self._log.debug('connected') 246 await self._register_status('CONNECTED') 247 248 initialized = False 249 try: 250 await self._create_dynamic_datasets() 251 for rcb_conf in self._conf['rcbs']: 252 await self._init_rcb(rcb_conf) 253 initialized = True 254 255 except Exception as e: 256 self._log.warning( 257 'initialization failed: %s, closing connection', 258 e, exc_info=e) 259 self._conn.close() 260 261 await self._conn.wait_closing() 262 await self._register_status('DISCONNECTED') 263 await self._conn.wait_closed() 264 self._conn = None 265 self._terminations = {} 266 self._reports_segments = {} 267 if not initialized: 268 await asyncio.sleep(conn_conf['reconnect_delay']) 269 270 except Exception as e: 271 self._log.error('connection loop error: %s', e, exc_info=e) 272 273 finally: 274 self._log.debug('closing connection loop') 275 self.close() 276 await aio.uncancellable(cleanup()) 277 278 async def _process_cmd_req(self, event, cmd_name): 279 if not self._conn or not self._conn.is_open: 280 raise Exception('no connection') 281 282 if cmd_name not in self._command_name_confs: 283 raise Exception('unexpected command name') 284 285 cmd_conf = self._command_name_confs[cmd_name] 286 cmd_ref = iec61850.CommandRef(**cmd_conf['ref']) 287 action = event.payload.data['action'] 288 evt_session_id = event.payload.data['session_id'] 289 if (action == 'SELECT' and 290 cmd_conf['model'] == 'SBO_WITH_NORMAL_SECURITY'): 291 cmd = None 292 else: 293 ctl_num = self._command_name_ctl_nums[cmd_name] 294 ctl_num = _update_ctl_num(ctl_num, action, cmd_conf['model']) 295 self._command_name_ctl_nums[cmd_name] = ctl_num 296 value_type = self._command_ref_value_type[cmd_ref] 297 if value_type is None: 298 raise Exception('value type undefined') 299 300 cmd = _command_from_event(event, cmd_conf, ctl_num, value_type) 301 302 term_future = None 303 if (action == 'OPERATE' and 304 cmd_conf['model'] in ['DIRECT_WITH_ENHANCED_SECURITY', 305 'SBO_WITH_ENHANCED_SECURITY']): 306 term_future = self._loop.create_future() 307 self._conn.async_group.spawn( 308 self._wait_cmd_term, cmd_name, cmd_ref, cmd, evt_session_id, 309 term_future) 310 311 try: 312 resp = await aio.wait_for( 313 self._send_command(action, cmd_ref, cmd), 314 self._conf['connection']['response_timeout']) 315 316 except (asyncio.TimeoutError, ConnectionError) as e: 317 self._log.warning('send command failed: %s', e, exc_info=e) 318 if term_future and not term_future.done(): 319 term_future.cancel() 320 return 321 322 if resp is not None: 323 if term_future and not term_future.done(): 324 term_future.cancel() 325 326 event = _cmd_resp_to_event( 327 self._event_type_prefix, cmd_name, evt_session_id, action, resp) 328 await self._register_events([event]) 329 330 async def _send_command(self, action, cmd_ref, cmd): 331 if action == 'SELECT': 332 return await self._conn.select(cmd_ref, cmd) 333 334 if action == 'CANCEL': 335 return await self._conn.cancel(cmd_ref, cmd) 336 337 if action == 'OPERATE': 338 return await self._conn.operate(cmd_ref, cmd) 339 340 raise Exception('unsupported action') 341 342 async def _wait_cmd_term(self, cmd_name, cmd_ref, cmd, session_id, future): 343 cmd_session_id = _get_command_session_id(cmd_ref, cmd) 344 self._terminations[cmd_session_id] = future 345 try: 346 term = await aio.wait_for(future, termination_timeout) 347 event = _cmd_resp_to_event( 348 self._event_type_prefix, cmd_name, session_id, 'TERMINATION', 349 term.error) 350 await self._register_events([event]) 351 352 except asyncio.TimeoutError: 353 self._log.warning('command termination timeout') 354 355 finally: 356 del self._terminations[cmd_session_id] 357 358 async def _process_change_req(self, event, value_name): 359 if not self._conn or not self._conn.is_open: 360 raise Exception('no connection') 361 362 if value_name not in self._change_name_value_refs: 363 raise Exception('unexpected change name') 364 365 ref = self._change_name_value_refs[value_name] 366 value_type = self._change_ref_value_type[ref] 367 if value_type is None: 368 raise Exception('value type undefined') 369 370 value = _value_from_json(event.payload.data['value'], value_type) 371 try: 372 resp = await aio.wait_for( 373 self._conn.write_data(ref, value), 374 self._conf['connection']['response_timeout']) 375 376 except asyncio.TimeoutError: 377 self._log.warning('write data response timeout') 378 return 379 380 except ConnectionError as e: 381 self._log.warning('connection error on write data: %s', 382 e, exc_info=e) 383 return 384 385 session_id = event.payload.data['session_id'] 386 event = _write_data_resp_to_event( 387 self._event_type_prefix, value_name, session_id, resp) 388 await self._register_events([event]) 389 390 async def _on_report(self, report): 391 try: 392 events = list(self._events_from_report(report)) 393 394 except Exception as e: 395 self._log.warning('report %s ignored: %s', 396 report.report_id, e, exc_info=e) 397 return 398 399 if not events: 400 return 401 402 await self._register_events(events) 403 404 if self._rcb_type[report.report_id] == 'BUFFERED': 405 self._rcbs_entry_ids[report.report_id] = report.entry_id 406 407 def _events_from_report(self, report): 408 report_id = report.report_id 409 if report_id not in self._report_data_refs: 410 raise Exception(f'unexpected report {report_id}') 411 412 segm_id = (report_id, report.sequence_number) 413 if report.more_segments_follow: 414 if segm_id in self._reports_segments: 415 segment_data, timeout_timer = self._reports_segments[segm_id] 416 timeout_timer.cancel() 417 else: 418 segment_data = collections.deque() 419 420 segment_data.extend(report.data) 421 timeout_timer = self._loop.call_later( 422 report_segments_timeout, self._reports_segments.pop, segm_id) 423 self._reports_segments[segm_id] = (segment_data, timeout_timer) 424 return 425 426 if segm_id in self._reports_segments: 427 report_data, timeout_timer = self._reports_segments.pop(segm_id) 428 timeout_timer.cancel() 429 report_data.extend(report.data) 430 431 else: 432 report_data = report.data 433 434 yield from self._events_from_report_data(report_data, report_id) 435 436 if self._rcb_type[report_id] == 'BUFFERED': 437 yield hat.event.common.RegisterEvent( 438 type=(*self._event_type_prefix, 'gateway', 439 'entry_id', report_id), 440 source_timestamp=None, 441 payload=hat.event.common.EventPayloadJson( 442 report.entry_id.hex() 443 if report.entry_id is not None else None)) 444 445 def _events_from_report_data(self, report_data, report_id): 446 data_values_json = collections.defaultdict(dict) 447 data_reasons = collections.defaultdict(set) 448 for rv in report_data: 449 if rv.ref not in self._value_ref_data_names: 450 continue 451 452 value_type = self._dataset_values_ref_type[rv.ref] 453 if value_type is None: 454 self._log.warning('report data ignored: unknown value type') 455 continue 456 457 value_json = _value_to_json(rv.value, value_type) 458 for data_name in self._value_ref_data_names[rv.ref]: 459 value_path = [rv.ref.logical_device, rv.ref.logical_node, 460 rv.ref.fc, *rv.ref.names] 461 data_values_json[data_name] = json.set_( 462 data_values_json[data_name], value_path, value_json) 463 if rv.reasons: 464 data_reasons[data_name].update( 465 reason.name for reason in rv.reasons) 466 467 for data_name, values_json in data_values_json.items(): 468 payload = {'reasons': list(data_reasons[data_name])} 469 data_conf = self._data_name_confs[data_name] 470 value_path = _conf_ref_to_path(data_conf['value']) 471 value_json = json.get(values_json, value_path) 472 if value_json is not None: 473 value_ref = _value_ref_from_json(data_conf['value']) 474 value_type = self._data_value_types[value_ref] 475 payload['value'] = _value_json_to_event_json( 476 value_json, value_type) 477 478 if 'quality' in data_conf: 479 quality_path = _conf_ref_to_path(data_conf['quality']) 480 quality_json = json.get(values_json, quality_path) 481 if quality_json is not None: 482 payload['quality'] = quality_json 483 484 if 'timestamp' in data_conf: 485 timestamp_path = _conf_ref_to_path(data_conf['timestamp']) 486 timestamp_json = json.get(values_json, timestamp_path) 487 if timestamp_json is not None: 488 payload['timestamp'] = timestamp_json 489 490 if 'selected' in data_conf: 491 selected_path = _conf_ref_to_path(data_conf['selected']) 492 selected_json = json.get(values_json, selected_path) 493 if selected_json is not None: 494 payload['selected'] = selected_json 495 496 yield hat.event.common.RegisterEvent( 497 type=(*self._event_type_prefix, 'gateway', 498 'data', data_name), 499 source_timestamp=None, 500 payload=hat.event.common.EventPayloadJson(payload)) 501 502 def _on_termination(self, termination): 503 cmd_session_id = _get_command_session_id( 504 termination.ref, termination.cmd) 505 if cmd_session_id not in self._terminations: 506 self._log.warning('unexpected termination dropped') 507 return 508 509 term_future = self._terminations[cmd_session_id] 510 if not term_future.done(): 511 self._terminations[cmd_session_id].set_result(termination) 512 513 async def _init_rcb(self, rcb_conf): 514 ref = iec61850.RcbRef( 515 logical_device=rcb_conf['ref']['logical_device'], 516 logical_node=rcb_conf['ref']['logical_node'], 517 type=iec61850.RcbType[rcb_conf['ref']['type']], 518 name=rcb_conf['ref']['name']) 519 self._log.debug('initiating rcb %s', ref) 520 521 get_attrs = collections.deque([iec61850.RcbAttrType.REPORT_ID]) 522 dataset_ref = _dataset_ref_from_json(rcb_conf['dataset']) 523 if dataset_ref not in self._dyn_datasets_values: 524 get_attrs.append(iec61850.RcbAttrType.DATASET) 525 if 'conf_revision' in rcb_conf: 526 get_attrs.append(iec61850.RcbAttrType.CONF_REVISION) 527 528 get_rcb_resp = await self._conn.get_rcb_attrs(ref, get_attrs) 529 _validate_get_rcb_response(get_rcb_resp, rcb_conf) 530 531 if ref.type == iec61850.RcbType.BUFFERED: 532 if 'reservation_time' in rcb_conf: 533 await self._set_rcb( 534 ref, [(iec61850.RcbAttrType.RESERVATION_TIME, 535 rcb_conf['reservation_time'])]) 536 elif ref.type == iec61850.RcbType.UNBUFFERED: 537 await self._set_rcb(ref, [(iec61850.RcbAttrType.RESERVE, True)]) 538 else: 539 raise Exception('unexpected rcb type') 540 541 await self._set_rcb(ref, [(iec61850.RcbAttrType.REPORT_ENABLE, False)]) 542 543 if dataset_ref in self._dyn_datasets_values: 544 await self._set_rcb( 545 ref, [(iec61850.RcbAttrType.DATASET, dataset_ref)], 546 critical=True) 547 548 if ref.type == iec61850.RcbType.BUFFERED: 549 entry_id = self._rcbs_entry_ids.get( 550 _report_id_from_rcb_conf(rcb_conf)) 551 if rcb_conf.get('purge_buffer') or entry_id is None: 552 await self._set_rcb( 553 ref, [(iec61850.RcbAttrType.PURGE_BUFFER, True)]) 554 555 else: 556 try: 557 await self._set_rcb( 558 ref, [(iec61850.RcbAttrType.ENTRY_ID, entry_id)], 559 critical=True) 560 561 except Exception as e: 562 self._log.warning('%s', e, exc_info=e) 563 # try setting entry id to 0 in order to resynchronize 564 await self._set_rcb( 565 ref, [(iec61850.RcbAttrType.ENTRY_ID, b'\x00')]) 566 567 attrs = collections.deque() 568 if 'trigger_options' in rcb_conf: 569 attrs.append((iec61850.RcbAttrType.TRIGGER_OPTIONS, 570 set(iec61850.TriggerCondition[i] 571 for i in rcb_conf['trigger_options']))) 572 if 'optional_fields' in rcb_conf: 573 attrs.append((iec61850.RcbAttrType.OPTIONAL_FIELDS, 574 set(iec61850.OptionalField[i] 575 for i in rcb_conf['optional_fields']))) 576 if 'buffer_time' in rcb_conf: 577 attrs.append((iec61850.RcbAttrType.BUFFER_TIME, 578 rcb_conf['buffer_time'])) 579 if 'integrity_period' in rcb_conf: 580 attrs.append((iec61850.RcbAttrType.INTEGRITY_PERIOD, 581 rcb_conf['integrity_period'])) 582 if attrs: 583 await self._set_rcb(ref, attrs) 584 585 await self._set_rcb( 586 ref, [(iec61850.RcbAttrType.REPORT_ENABLE, True)], critical=True) 587 await self._set_rcb( 588 ref, [(iec61850.RcbAttrType.GI, True)], critical=True) 589 self._log.debug('rcb %s initiated', ref) 590 591 async def _set_rcb(self, ref, attrs, critical=False): 592 try: 593 resp = await self._conn.set_rcb_attrs(ref, attrs) 594 attrs_failed = set((attr, attr_res) 595 for attr, attr_res in resp.items() 596 if isinstance(attr_res, iec61850.ServiceError)) 597 if attrs_failed: 598 raise Exception(f"set attribute errors: {attrs_failed}") 599 600 except Exception as e: 601 if critical: 602 raise Exception(f'set rcb {ref} failed') from e 603 604 else: 605 self._log.warning('set rcb %s failed: %s', ref, e, exc_info=e) 606 607 async def _create_dynamic_datasets(self): 608 existing_ds_refs = set() 609 for ds_ref in self._persist_dyn_datasets: 610 ld = ds_ref.logical_device 611 res = await self._conn.get_persisted_dataset_refs(ld) 612 if isinstance(res, iec61850.ServiceError): 613 raise Exception(f'get datasets for ld {ld} failed: {res}') 614 615 existing_ds_refs.update(res) 616 617 existing_persisted_ds_refs = existing_ds_refs.intersection( 618 self._persist_dyn_datasets) 619 for ds_ref, ds_value_refs in self._dyn_datasets_values.items(): 620 if ds_ref in existing_persisted_ds_refs: 621 res = await self._conn.get_dataset_data_refs(ds_ref) 622 if isinstance(res, iec61850.ServiceError): 623 raise Exception(f'get ds {ds_ref} data refs failed: {res}') 624 else: 625 exist_ds_value_refs = res 626 627 if ds_value_refs == list(exist_ds_value_refs): 628 self._log.debug('dataset %s already exists', ds_ref) 629 continue 630 631 raise Exception('persisted dataset changed') 632 633 res = await self._conn.create_dataset(ds_ref, ds_value_refs) 634 if res is not None: 635 raise Exception(f'create dataset {ds_ref} failed: {res}') 636 637 self._log.debug("dataset %s crated", ds_ref) 638 639 async def _register_events(self, events): 640 try: 641 await self._eventer_client.register(events) 642 643 except ConnectionError: 644 self._log.debug('eventer connection closed') 645 self.close() 646 647 async def _register_status(self, status): 648 if status == self._conn_status: 649 return 650 651 event = hat.event.common.RegisterEvent( 652 type=(*self._event_type_prefix, 'gateway', 'status'), 653 source_timestamp=None, 654 payload=hat.event.common.EventPayloadJson(status)) 655 await self._register_events([event]) 656 self._conn_status = status 657 self._log.debug('registered status %s', status) 658 659 660def _value_ref_from_json(value_ref_conf): 661 return iec61850.DataRef( 662 logical_device=value_ref_conf['logical_device'], 663 logical_node=value_ref_conf['logical_node'], 664 fc=value_ref_conf['fc'], 665 names=tuple(value_ref_conf['names'])) 666 667 668def _dataset_ref_from_json(ds_conf): 669 if isinstance(ds_conf, str): 670 return iec61850.NonPersistedDatasetRef(ds_conf) 671 672 elif isinstance(ds_conf, dict): 673 return iec61850.PersistedDatasetRef(**ds_conf) 674 675 676def _rcb_ref_from_json(rcb_ref_conf): 677 return iec61850.RcbRef( 678 logical_device=rcb_ref_conf['logical_device'], 679 logical_node=rcb_ref_conf['logical_node'], 680 type=iec61850.RcbType[rcb_ref_conf['type']], 681 name=rcb_ref_conf['name']) 682 683 684def _refs_match(ref1, ref2): 685 if ref1.logical_device != ref2.logical_device: 686 return False 687 688 if ref1.logical_node != ref2.logical_node: 689 return False 690 691 if ref1.fc != ref2.fc: 692 return False 693 694 if len(ref1.names) == len(ref2.names): 695 return ref1.names == ref2.names 696 697 if len(ref1.names) < len(ref2.names): 698 names1, names2 = ref1.names, ref2.names 699 else: 700 names1, names2 = ref2.names, ref1.names 701 702 return names2[:len(names1)] == names1 703 704 705def _get_persist_dyn_datasets(conf): 706 for ds_conf in conf['datasets']: 707 if not ds_conf['dynamic']: 708 continue 709 710 ds_ref = _dataset_ref_from_json(ds_conf['ref']) 711 if isinstance(ds_ref, iec61850.PersistedDatasetRef): 712 yield ds_ref 713 714 715def _get_dyn_datasets_values(conf): 716 for ds_conf in conf['datasets']: 717 if not ds_conf['dynamic']: 718 continue 719 720 ds_ref = _dataset_ref_from_json(ds_conf['ref']) 721 yield ds_ref, [_value_ref_from_json(val_conf) 722 for val_conf in ds_conf['values']] 723 724 725def _vtype_61850_from_vtype_conf(vt_conf): 726 if isinstance(vt_conf, str): 727 return _value_type_from_str(vt_conf) 728 729 if vt_conf['type'] == 'ARRAY': 730 return iec61850.ArrayValueType( 731 type=_vtype_61850_from_vtype_conf(vt_conf['element_type']), 732 length=vt_conf['length']) 733 734 if vt_conf['type'] == 'STRUCT': 735 return iec61850.StructValueType( 736 [(el_conf['name'], _vtype_61850_from_vtype_conf(el_conf['type'])) 737 for el_conf in vt_conf['elements']]) 738 739 raise Exception('unsupported value type') 740 741 742def _vtype_from_vtype_conf(vt_conf): 743 if isinstance(vt_conf, str): 744 return _value_type_from_str(vt_conf) 745 746 if vt_conf['type'] == 'ARRAY': 747 return iec61850.ArrayValueType( 748 type=_vtype_from_vtype_conf(vt_conf['element_type']), 749 length=vt_conf['length']) 750 751 if vt_conf['type'] == 'STRUCT': 752 return {el_conf['name']: _vtype_from_vtype_conf(el_conf['type']) 753 for el_conf in vt_conf['elements']} 754 755 raise Exception('unsupported value type') 756 757 758def _value_type_from_str(vt_conf): 759 if vt_conf in ['BOOLEAN', 760 'INTEGER', 761 'UNSIGNED', 762 'FLOAT', 763 'BIT_STRING', 764 'OCTET_STRING', 765 'VISIBLE_STRING', 766 'MMS_STRING']: 767 return iec61850.BasicValueType(vt_conf) 768 769 if vt_conf in ['QUALITY', 770 'TIMESTAMP', 771 'DOUBLE_POINT', 772 'DIRECTION', 773 'SEVERITY', 774 'ANALOGUE', 775 'VECTOR', 776 'STEP_POSITION', 777 'BINARY_CONTROL']: 778 return iec61850.AcsiValueType(vt_conf) 779 780 raise Exception('unsupported value type') 781 782 783def _value_type_from_ref(value_types, ref): 784 left_names = [] 785 if isinstance(ref, iec61850.CommandRef): 786 left_names = ['Oper', 'ctlVal'] 787 key = (ref.logical_device, ref.logical_node, 'CO', ref.name) 788 789 elif isinstance(ref, iec61850.DataRef): 790 left_names = ref.names[1:] 791 key = (ref.logical_device, ref.logical_node, ref.fc, ref.names[0]) 792 793 else: 794 raise Exception('unexpected reference') 795 796 if key not in value_types: 797 return 798 799 value_type = value_types[key] 800 while left_names: 801 name = left_names[0] 802 left_names = left_names[1:] 803 if isinstance(name, str): 804 if isinstance(value_type, iec61850.StructValueType): 805 _, value_type = util.first( 806 value_type.elements, lambda i: i[0] == name) 807 808 elif value_type == iec61850.AcsiValueType.ANALOGUE: 809 if name == 'i': 810 value_type = iec61850.BasicValueType.INTEGER 811 elif name == 'f': 812 value_type = iec61850.BasicValueType.FLOAT 813 else: 814 raise Exception("analogue attribute should be 'i' or 'f'") 815 816 elif value_type == iec61850.AcsiValueType.VECTOR: 817 if name in ('mag', 'ang'): 818 value_type = iec61850.AcsiValueType.ANALOGUE 819 else: 820 raise Exception( 821 "vector attribute should be 'mag' or 'ang'") 822 823 elif value_type == iec61850.AcsiValueType.STEP_POSITION: 824 if name == 'posVal': 825 value_type = iec61850.BasicValueType.INTEGER 826 elif name == 'transInd': 827 value_type = iec61850.BasicValueType.BOOLEAN 828 else: 829 raise Exception("step position attribute should be " 830 "'posVal' or 'transInd'") 831 832 else: 833 value_type = value_type[name] 834 835 if isinstance(name, int): 836 value_type = value_type.type 837 838 return value_type 839 840 841def _get_dataset_change_value_types(conf, value_types): 842 for ds_conf in conf['datasets']: 843 for val_ref_conf in ds_conf['values']: 844 value_ref = _value_ref_from_json(val_ref_conf) 845 value_type = _value_type_from_ref(value_types, value_ref) 846 yield value_ref, value_type 847 848 for change_conf in conf['changes']: 849 value_ref = _value_ref_from_json(change_conf['ref']) 850 value_type = _value_type_from_ref(value_types, value_ref) 851 yield value_ref, value_type 852 853 854def _get_cmd_value_types(conf, value_types): 855 for cmd_conf in conf['commands']: 856 cmd_ref = iec61850.CommandRef(**cmd_conf['ref']) 857 value_type = _value_type_from_ref(value_types, cmd_ref) 858 yield cmd_ref, value_type 859 860 861_epoch_start = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) 862 863 864def _command_from_event(event, cmd_conf, control_number, value_type): 865 if cmd_conf['with_operate_time']: 866 operate_time = iec61850.Timestamp( 867 value=_epoch_start, 868 leap_second=False, 869 clock_failure=False, 870 not_synchronized=False, 871 accuracy=0) 872 else: 873 operate_time = None 874 return iec61850.Command( 875 value=_value_from_json(event.payload.data['value'], value_type), 876 operate_time=operate_time, 877 origin=iec61850.Origin( 878 category=iec61850.OriginCategory[ 879 event.payload.data['origin']['category']], 880 identification=event.payload.data[ 881 'origin']['identification'].encode('utf-8')), 882 control_number=control_number, 883 t=_timestamp_from_event_timestamp(event.timestamp), 884 test=event.payload.data['test'], 885 checks=set(iec61850.Check[i] for i in event.payload.data['checks'])) 886 887 888def _timestamp_from_event_timestamp(timestamp): 889 return iec61850.Timestamp( 890 value=hat.event.common.timestamp_to_datetime(timestamp), 891 leap_second=False, 892 clock_failure=False, 893 not_synchronized=False, 894 accuracy=None) 895 896 897def _cmd_resp_to_event(event_type_prefix, cmd_name, event_session_id, action, 898 resp): 899 success = resp is None 900 payload = {'session_id': event_session_id, 901 'action': action, 902 'success': success} 903 if not success: 904 if resp.service_error is not None: 905 payload['service_error'] = resp.service_error.name 906 if resp.additional_cause is not None: 907 payload['additional_cause'] = resp.additional_cause.name 908 if resp.test_error is not None: 909 payload['test_error'] = resp.test_error.name 910 911 return hat.event.common.RegisterEvent( 912 type=(*event_type_prefix, 'gateway', 'command', cmd_name), 913 source_timestamp=None, 914 payload=hat.event.common.EventPayloadJson(payload)) 915 916 917def _write_data_resp_to_event(event_type_prefix, value_name, session_id, resp): 918 success = resp is None 919 payload = {'session_id': session_id, 920 'success': success} 921 if not success: 922 payload['error'] = resp.name 923 return hat.event.common.RegisterEvent( 924 type=(*event_type_prefix, 'gateway', 'change', value_name), 925 source_timestamp=None, 926 payload=hat.event.common.EventPayloadJson(payload)) 927 928 929def _get_command_session_id(cmd_ref, cmd): 930 return (cmd_ref, cmd.control_number) 931 932 933def _value_from_json(event_value, value_type): 934 if isinstance(value_type, iec61850.BasicValueType): 935 if value_type == iec61850.BasicValueType.OCTET_STRING: 936 return bytes.fromhex(event_value) 937 938 elif value_type == iec61850.BasicValueType.FLOAT: 939 return float(event_value) 940 941 else: 942 return event_value 943 944 if value_type == iec61850.AcsiValueType.QUALITY: 945 return iec61850.Quality( 946 validity=iec61850.QualityValidity[event_value['validity']], 947 details={iec61850.QualityDetail[i] 948 for i in event_value['details']}, 949 source=iec61850.QualitySource[event_value['source']], 950 test=event_value['test'], 951 operator_blocked=event_value['operator_blocked']) 952 953 if value_type == iec61850.AcsiValueType.TIMESTAMP: 954 return iec61850.Timestamp( 955 value=datetime.datetime.fromtimestamp(event_value['value'], 956 datetime.timezone.utc), 957 leap_second=event_value['leap_second'], 958 clock_failure=event_value['clock_failure'], 959 not_synchronized=event_value['not_synchronized'], 960 accuracy=event_value.get('accuracy')) 961 962 if value_type == iec61850.AcsiValueType.DOUBLE_POINT: 963 return iec61850.DoublePoint[event_value] 964 965 if value_type == iec61850.AcsiValueType.DIRECTION: 966 return iec61850.Direction[event_value] 967 968 if value_type == iec61850.AcsiValueType.SEVERITY: 969 return iec61850.Severity[event_value] 970 971 if value_type == iec61850.AcsiValueType.ANALOGUE: 972 return iec61850.Analogue( 973 i=event_value.get('i'), 974 f=(float(event_value['f']) if 'f' in event_value else None)) 975 976 if value_type == iec61850.AcsiValueType.VECTOR: 977 return iec61850.Vector( 978 magnitude=_value_from_json(event_value['magnitude'], 979 iec61850.AcsiValueType.ANALOGUE), 980 angle=(_value_from_json(event_value['angle'], 981 iec61850.AcsiValueType.ANALOGUE) 982 if 'angle' in event_value else None)) 983 984 if value_type == iec61850.AcsiValueType.STEP_POSITION: 985 return iec61850.StepPosition(value=event_value['value'], 986 transient=event_value.get('transient')) 987 988 if value_type == iec61850.AcsiValueType.BINARY_CONTROL: 989 return iec61850.BinaryControl[event_value] 990 991 if isinstance(value_type, iec61850.ArrayValueType): 992 return [_value_from_json(val, value_type.type) 993 for val in event_value] 994 995 if isinstance(value_type, dict): 996 return {k: _value_from_json(v, value_type[k]) 997 for k, v in event_value.items()} 998 999 raise Exception('unsupported value type') 1000 1001 1002def _value_to_json(data_value, value_type): 1003 if isinstance(value_type, iec61850.BasicValueType): 1004 if value_type == iec61850.BasicValueType.OCTET_STRING: 1005 return data_value.hex() 1006 1007 elif value_type == iec61850.BasicValueType.BIT_STRING: 1008 return list(data_value) 1009 1010 elif value_type == iec61850.BasicValueType.FLOAT: 1011 return data_value if math.isfinite(data_value) else str(data_value) 1012 1013 else: 1014 return data_value 1015 1016 if isinstance(value_type, iec61850.AcsiValueType): 1017 if value_type == iec61850.AcsiValueType.QUALITY: 1018 return {'validity': data_value.validity.name, 1019 'details': [i.name for i in data_value.details], 1020 'source': data_value.source.name, 1021 'test': data_value.test, 1022 'operator_blocked': data_value.operator_blocked} 1023 1024 if value_type == iec61850.AcsiValueType.TIMESTAMP: 1025 val = {'value': data_value.value.timestamp(), 1026 'leap_second': data_value.leap_second, 1027 'clock_failure': data_value.clock_failure, 1028 'not_synchronized': data_value.not_synchronized} 1029 if data_value.accuracy is not None: 1030 val['accuracy'] = data_value.accuracy 1031 return val 1032 1033 if value_type in [iec61850.AcsiValueType.DOUBLE_POINT, 1034 iec61850.AcsiValueType.DIRECTION, 1035 iec61850.AcsiValueType.SEVERITY, 1036 iec61850.AcsiValueType.BINARY_CONTROL]: 1037 return data_value.name 1038 1039 if value_type == iec61850.AcsiValueType.ANALOGUE: 1040 val = {} 1041 if data_value.i is not None: 1042 val['i'] = data_value.i 1043 if data_value.f is not None: 1044 val['f'] = (data_value.f if math.isfinite(data_value.f) 1045 else str(data_value.f)) 1046 return val 1047 1048 if value_type == iec61850.AcsiValueType.VECTOR: 1049 val = {'mag': _value_to_json( 1050 data_value.magnitude, iec61850.AcsiValueType.ANALOGUE)} 1051 if data_value.angle is not None: 1052 val['ang'] = _value_to_json( 1053 data_value.angle, iec61850.AcsiValueType.ANALOGUE) 1054 return val 1055 1056 if value_type == iec61850.AcsiValueType.STEP_POSITION: 1057 val = {'posVal': data_value.value} 1058 if data_value.transient is not None: 1059 val['transInd'] = data_value.transient 1060 return val 1061 1062 if isinstance(value_type, iec61850.ArrayValueType): 1063 return [_value_to_json(i, value_type.type) for i in data_value] 1064 1065 if isinstance(value_type, dict): 1066 return { 1067 child_name: _value_to_json(child_value, value_type[child_name]) 1068 for child_name, child_value in data_value.items()} 1069 1070 raise Exception('unsupported value type') 1071 1072 1073def _value_json_to_event_json(data_value, value_type): 1074 if value_type == iec61850.AcsiValueType.VECTOR: 1075 val = {'magnitude': data_value['mag']} 1076 if 'ang' in data_value: 1077 val['angle'] = data_value['ang'] 1078 return val 1079 1080 if value_type == iec61850.AcsiValueType.STEP_POSITION: 1081 val = {'value': data_value['posVal']} 1082 if 'transInd' in data_value: 1083 val['transient'] = data_value['transInd'] 1084 return val 1085 1086 if isinstance(value_type, iec61850.ArrayValueType): 1087 return [_value_json_to_event_json(i, value_type.type) 1088 for i in data_value] 1089 1090 if isinstance(value_type, dict): 1091 return { 1092 child_name: _value_json_to_event_json( 1093 child_value, value_type[child_name]) 1094 for child_name, child_value in data_value.items()} 1095 1096 return data_value 1097 1098 1099def _update_ctl_num(ctl_num, action, cmd_model): 1100 if action == 'SELECT' or ( 1101 action == 'OPERATE' and 1102 cmd_model in ['DIRECT_WITH_NORMAL_SECURITY', 1103 'DIRECT_WITH_ENHANCED_SECURITY']): 1104 return (ctl_num + 1) % 256 1105 1106 return ctl_num 1107 1108 1109def _validate_get_rcb_response(get_rcb_resp, rcb_conf): 1110 for k, v in get_rcb_resp.items(): 1111 if isinstance(v, iec61850.ServiceError): 1112 raise Exception(f"get {k.name} failed: {v}") 1113 1114 if (k == iec61850.RcbAttrType.REPORT_ID and 1115 v != rcb_conf['report_id']): 1116 raise Exception(f"rcb report id {v} different from " 1117 f"configured {rcb_conf['report_id']}") 1118 1119 if (k == iec61850.RcbAttrType.DATASET and 1120 v != _dataset_ref_from_json(rcb_conf['dataset'])): 1121 raise Exception(f"rcb dataset {v} different from " 1122 f"configured {rcb_conf['dataset']}") 1123 1124 if (k == iec61850.RcbAttrType.CONF_REVISION and 1125 v != rcb_conf['conf_revision']): 1126 raise Exception( 1127 f"Conf revision {v} different from " 1128 f"the configuration defined {rcb_conf['conf_revision']}") 1129 1130 1131def _conf_ref_to_path(conf_ref): 1132 return [conf_ref['logical_device'], 1133 conf_ref['logical_node'], 1134 conf_ref['fc'], 1135 *conf_ref['names']] 1136 1137 1138def _report_id_from_rcb_conf(rcb_conf): 1139 return (rcb_conf['report_id'] if rcb_conf['report_id'] else 1140 _report_id_from_rcb_ref(rcb_conf['ref'])) 1141 1142 1143def _report_id_from_rcb_ref(rcb_ref): 1144 return (f"{rcb_ref['logical_device']}/" 1145 f"{rcb_ref['logical_node']}$" 1146 f"{iec61850.RcbType[rcb_ref['type']].value}$" 1147 f"{rcb_ref['name']}") 1148 1149 1150def _create_logger_adapter(name): 1151 extra = {'meta': {'type': 'Iec61850ClientDevice', 1152 'name': name}} 1153 1154 return logging.LoggerAdapter(mlog, extra)
termination_timeout: int =
100
report_segments_timeout: int =
100
async def
create( conf: None | 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]) -> Iec61850ClientDevice:
28async def create(conf: common.DeviceConf, 29 eventer_client: hat.event.eventer.Client, 30 event_type_prefix: common.EventTypePrefix 31 ) -> 'Iec61850ClientDevice': 32 33 value_types = {} 34 value_types_61850 = {} 35 for vt in conf['value_types']: 36 ref = (vt['logical_device'], 37 vt['logical_node'], 38 vt['fc'], 39 vt['name']) 40 value_types_61850[ref] = _vtype_61850_from_vtype_conf(vt['type']) 41 value_types[ref] = _vtype_from_vtype_conf(vt['type']) 42 43 rcb_confs = {_rcb_ref_from_json(i['ref']): i for i in conf['rcbs']} 44 dataset_confs = {_dataset_ref_from_json(ds_conf['ref']): ds_conf 45 for ds_conf in conf['datasets']} 46 value_ref_data_names = collections.defaultdict(collections.deque) 47 data_value_types = {} 48 for data_conf in conf['data']: 49 data_v_ref = _value_ref_from_json(data_conf['value']) 50 data_value_types[data_v_ref] = _value_type_from_ref( 51 value_types, data_v_ref) 52 53 q_ref = (_value_ref_from_json(data_conf['quality']) 54 if data_conf.get('quality') else None) 55 if data_conf.get('quality'): 56 q_type = _value_type_from_ref(value_types, q_ref) 57 if q_type != iec61850.AcsiValueType.QUALITY: 58 raise Exception(f"invalid quality type {q_ref}") 59 60 t_ref = (_value_ref_from_json(data_conf['timestamp']) 61 if data_conf.get('timestamp') else None) 62 if t_ref: 63 t_type = _value_type_from_ref(value_types, t_ref) 64 if t_type != iec61850.AcsiValueType.TIMESTAMP: 65 raise Exception(f"invalid timestamp type {t_ref}") 66 67 seld_ref = (_value_ref_from_json(data_conf['selected']) 68 if data_conf.get('selected') else None) 69 if seld_ref: 70 seld_type = _value_type_from_ref(value_types, seld_ref) 71 if seld_type != iec61850.BasicValueType.BOOLEAN: 72 raise Exception(f"invalid selected type {seld_ref}") 73 74 rcb_conf = rcb_confs[_rcb_ref_from_json(data_conf['rcb'])] 75 ds_ref = _dataset_ref_from_json(rcb_conf['dataset']) 76 ds_conf = dataset_confs[ds_ref] 77 for value_conf in ds_conf['values']: 78 value_ref = _value_ref_from_json(value_conf) 79 if (_refs_match(data_v_ref, value_ref) or 80 (q_ref and _refs_match(q_ref, value_ref)) or 81 (t_ref and _refs_match(t_ref, value_ref)) or 82 (seld_ref and _refs_match(seld_ref, value_ref))): 83 value_ref_data_names[value_ref].append(data_conf['name']) 84 85 dataset_values_ref_type = {} 86 for ds_conf in conf['datasets']: 87 for val_ref_conf in ds_conf['values']: 88 value_ref = _value_ref_from_json(val_ref_conf) 89 value_type = _value_type_from_ref(value_types, value_ref) 90 dataset_values_ref_type[value_ref] = value_type 91 92 command_ref_value_type = {} 93 for cmd_conf in conf['commands']: 94 cmd_ref = iec61850.CommandRef(**cmd_conf['ref']) 95 value_type = _value_type_from_ref(value_types, cmd_ref) 96 command_ref_value_type[cmd_ref] = value_type 97 98 change_ref_value_type = {} 99 for change_conf in conf['changes']: 100 value_ref = _value_ref_from_json(change_conf['ref']) 101 value_type = _value_type_from_ref(value_types, value_ref) 102 change_ref_value_type[value_ref] = value_type 103 104 entry_id_event_types = [ 105 (*event_type_prefix, 'gateway', 'entry_id', 106 _report_id_from_rcb_conf(i)) 107 for i in conf['rcbs'] if i['ref']['type'] == 'BUFFERED'] 108 if entry_id_event_types: 109 result = await eventer_client.query( 110 hat.event.common.QueryLatestParams(entry_id_event_types)) 111 rcbs_entry_ids = { 112 event.type[5]: (bytes.fromhex(event.payload.data) 113 if event.payload.data is not None else None) 114 for event in result.events} 115 else: 116 rcbs_entry_ids = {} 117 118 device = Iec61850ClientDevice() 119 120 device._rcbs_entry_ids = rcbs_entry_ids 121 device._conf = conf 122 device._eventer_client = eventer_client 123 device._event_type_prefix = event_type_prefix 124 device._conn = None 125 device._conn_status = None 126 device._terminations = {} 127 device._reports_segments = {} 128 129 device._value_ref_data_names = value_ref_data_names 130 device._data_value_types = data_value_types 131 device._dataset_values_ref_type = dataset_values_ref_type 132 device._command_ref_value_type = command_ref_value_type 133 device._change_ref_value_type = change_ref_value_type 134 device._data_name_confs = {i['name']: i for i in conf['data']} 135 device._command_name_confs = {i['name']: i for i in conf['commands']} 136 device._command_name_ctl_nums = {i['name']: 0 for i in conf['commands']} 137 device._change_name_value_refs = { 138 i['name']: _value_ref_from_json(i['ref']) for i in conf['changes']} 139 device._rcb_type = { 140 _report_id_from_rcb_conf(rcb_conf): rcb_conf['ref']['type'] 141 for rcb_conf in conf['rcbs']} 142 device._persist_dyn_datasets = set(_get_persist_dyn_datasets(conf)) 143 device._dyn_datasets_values = dict(_get_dyn_datasets_values(conf)) 144 device._report_data_refs = collections.defaultdict(collections.deque) 145 for rcb_conf in device._conf['rcbs']: 146 report_id = _report_id_from_rcb_conf(rcb_conf) 147 ds_ref = _dataset_ref_from_json(rcb_conf['dataset']) 148 for value_conf in dataset_confs[ds_ref]['values']: 149 value_ref = _value_ref_from_json(value_conf) 150 device._report_data_refs[report_id].append(value_ref) 151 152 device._dataset_change_value_types = dict( 153 _get_dataset_change_value_types(conf, value_types_61850)) 154 device._cmd_value_types = dict( 155 _get_cmd_value_types(conf, value_types_61850)) 156 157 device._log = _create_logger_adapter(conf['name']) 158 159 device._async_group = aio.Group() 160 device._async_group.spawn(device._connection_loop) 161 device._loop = asyncio.get_running_loop() 162 163 return device
info: hat.gateway.common.DeviceInfo =
DeviceInfo(type='iec61850_client', create=<function create>, json_schema_id='hat-gateway://iec61850.yaml#/$defs/client', 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://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://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 silent interval\n'}}}]}, 'connect_timeout': {'type': 'number', 'description': 'Maximum number of seconds available to single connection\nattempt\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of read or write\nrequest/response exchange.\n'}, 'request_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive requests\n(minimal duration between response and next request)\n'}, 'request_retry_immediate_count': {'type': 'integer', 'description': 'Number of immediate request retries before remote\ndata is considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delayed_count': {'type': 'integer', 'description': 'Number of delayed request retries before remote data\nis considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive delayed\nrequest retries\n'}}}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['device_id', 'timeout_poll_delay', 'data'], 'properties': {'device_id': {'type': 'integer', 'description': 'Modbus device identifier\n'}, 'timeout_poll_delay': {'type': 'number', 'description': 'Delay (in seconds) after read timeout and\nbefore device polling is resumed\n'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'interval', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'interval': {'type': ['number', 'null'], 'description': 'Polling interval in seconds or\nnull if polling is disabled\n'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}}}}, 'slave': {'type': 'object', 'required': ['name', 'modbus_type', 'transport', 'data'], 'properties': {'name': {'type': 'string'}, 'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'local_host', 'local_port', 'remote_hosts', 'max_connections', 'response_timeout', 'keep_alive_timeout'], 'properties': {'type': {'const': 'TCP'}, 'local_host': {'type': 'string', 'description': 'Local host name\n'}, 'local_port': {'type': 'integer', 'description': 'Local host TCP port\n', 'default': 502}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'response_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of write\nrequest/response exchange.\n'}, 'keep_alive_timeout': {'type': ['number', 'null']}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'response_timeout', 'keep_alive_timeout'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silent interval\n'}, 'response_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of write\nrequest/response exchange.\n'}, 'keep_alive_timeout': {'type': 'number'}}}]}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'device_id', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'device_id': {'type': 'integer'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['DISCONNECTED', 'CONNECTING', 'CONNECTED']}, 'remote_device_status': {'enum': ['DISABLED', 'CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['result'], 'properties': {'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND']}, 'value': {'type': 'integer'}, 'cause': {'enum': ['INTERROGATE', 'CHANGE']}}}, 'write': {'type': 'object', 'required': ['request_id', 'result'], 'properties': {'request_id': {'type': 'string'}, 'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND', 'TIMEOUT']}}}}, 'system': {'enable': {'type': 'boolean'}, 'write': {'type': 'object', 'required': ['request_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'value': {'type': 'integer'}}}}}, 'slave': {'gateway': {'connections': {'type': 'array', 'items': {'oneOf': [{'type': 'object', 'required': ['type', 'connection_id'], 'properties': {'type': {'const': 'SERIAL'}, 'connection_id': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'connection_id', 'local', 'remote'], 'properties': {'type': {'const': 'TCP'}, 'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}]}}, 'write': {'type': 'object', 'required': ['request_id', 'connection_id', 'data'], 'properties': {'request_id': {'type': 'string'}, 'connection_id': {'type': 'integer'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'value'], 'properties': {'name': {'type': 'string'}, 'value': {'type': 'integer'}}}}}}}, 'system': {'data': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}}}, 'write': {'type': 'object', 'required': ['request_id', 'success'], 'properties': {'request_id': {'type': 'string'}, 'success': {'type': 'boolean'}}}}}}}}, '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://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://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', 'with_ack'], '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']}, 'with_ack': {'type': 'boolean'}}}}}}, {'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://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://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://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'rcb', 'value'], 'properties': {'name': {'type': 'string'}, 'rcb': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}, 'commands': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'model', 'ref', 'with_operate_time'], 'properties': {'name': {'type': 'string'}, 'model': {'enum': ['DIRECT_WITH_NORMAL_SECURITY', 'SBO_WITH_NORMAL_SECURITY', 'DIRECT_WITH_ENHANCED_SECURITY', 'SBO_WITH_ENHANCED_SECURITY']}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/command'}, 'with_operate_time': {'type': 'boolean'}}}}, 'changes': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['reasons'], 'properties': {'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}}}, 'command': {'allOf': [{'type': 'object', 'required': ['session_id', 'action'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE', 'TERMINATION']}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'service_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}, 'additional_cause': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/additional_cause'}, 'test_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/test_error'}}}]}]}, 'change': {'allOf': [{'type': 'object', 'required': ['session_id'], 'properties': {'session_id': {'type': 'string'}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}}}]}]}, 'entry_id': {'type': ['string', 'null'], 'description': 'hex encoded bytes'}}, 'system': {'command': {'type': 'object', 'required': ['session_id', 'action', 'value', 'origin', 'test', 'checks'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE']}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'origin': {'type': 'object', 'required': ['category', 'identification'], 'properties': {'category': {'enum': ['BAY_CONTROL', 'STATION_CONTROL', 'REMOTE_CONTROL', 'AUTOMATIC_BAY', 'AUTOMATIC_STATION', 'AUTOMATIC_REMOTE', 'MAINTENANCE', 'PROCESS']}, 'identification': {'type': 'string'}}}, 'test': {'type': 'boolean'}, 'checks': {'type': 'array', 'items': {'enum': ['SYNCHRO', 'INTERLOCK']}}}}, 'change': {'type': 'object', 'requried': ['session_id', 'value'], 'properties': {'session_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}}}}, 'value': {'anyOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/integer'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/unsigned'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/float'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/bit_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/octet_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/visible_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/mms_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/array'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/struct'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/double_point'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/direction'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/severity'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/vector'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/step_position'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/binary_control'}], '$defs': {'boolean': {'type': 'boolean'}, 'integer': {'type': 'integer'}, 'unsigned': {'type': 'integer'}, 'float': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'bit_string': {'type': 'array', 'items': {'type': 'boolean'}}, 'octet_string': {'type': 'string', 'description': 'hex encoded bytes'}, 'visible_string': {'type': 'string'}, 'mms_string': {'type': 'string'}, 'array': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}, 'struct': {'type': 'object', 'patternProperties': {'.+': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}, 'quality': {'type': 'object', 'required': ['validity', 'details', 'source', 'test', 'operator_blocked'], 'properties': {'validity': {'enum': ['GOOD', 'INVALID', 'RESERVED', 'QUESTIONABLE']}, 'details': {'type': 'array', 'items': {'enum': ['OVERFLOW', 'OUT_OF_RANGE', 'BAD_REFERENCE', 'OSCILLATORY', 'FAILURE', 'OLD_DATA', 'INCONSISTENT', 'INACCURATE']}}, 'source': {'enum': ['PROCESS', 'SUBSTITUTED']}, 'test': {'type': 'boolean'}, 'operator_blocked': {'type': 'boolean'}}}, 'timestamp': {'type': 'object', 'required': ['value', 'leap_second', 'clock_failure', 'not_synchronized'], 'properties': {'value': {'type': 'number', 'description': 'seconds since 1970-01-01'}, 'leap_second': {'type': 'boolean'}, 'clock_failure': {'type': 'boolean'}, 'not_synchronized': {'type': 'boolean'}, 'accuracy': {'type': 'integer'}}}, 'double_point': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'BAD']}, 'direction': {'enum': ['UNKNOWN', 'FORWARD', 'BACKWARD', 'BOTH']}, 'severity': {'enum': ['UNKNOWN', 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING']}, 'analogue': {'type': 'object', 'properties': {'i': {'type': 'integer'}, 'f': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}}}, 'vector': {'type': 'object', 'required': ['magnitude'], 'properties': {'magnitude': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, 'angle': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}}}, 'step_position': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}, 'transient': {'type': 'boolean'}}}, 'binary_control': {'enum': ['STOP', 'LOWER', 'HIGHER', 'RESERVED']}}}, 'value_type': {'oneOf': [{'enum': ['BOOLEAN', 'INTEGER', 'UNSIGNED', 'FLOAT', 'BIT_STRING', 'OCTET_STRING', 'VISIBLE_STRING', 'MMS_STRING', 'QUALITY', 'TIMESTAMP', 'DOUBLE_POINT', 'DIRECTION', 'SEVERITY', 'ANALOGUE', 'VECTOR', 'STEP_POSITION', 'BINARY_CONTROL']}, {'type': 'object', 'required': ['type', 'element_type', 'length'], 'properties': {'type': {'const': 'ARRAY'}, 'element_type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}, 'length': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'elements'], 'properties': {'type': {'const': 'STRUCT'}, 'elements': {'type': 'array', 'items': {'type': 'object', 'requried': ['name', 'type'], 'properties': {'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}}}]}, 'refs': {'value': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, 'command': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}, 'rcb': {'type': 'object', 'required': ['logical_device', 'logical_node', 'type', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'type': {'enum': ['BUFFERED', 'UNBUFFERED']}, 'name': {'type': 'string'}}}, 'dataset': {'oneOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/persisted'}], '$defs': {'nonpersisted': {'type': 'string'}, 'persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, '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']}}}]}}}})
173class Iec61850ClientDevice(common.Device): 174 175 @property 176 def async_group(self) -> aio.Group: 177 return self._async_group 178 179 async def process_event(self, event: hat.event.common.Event): 180 try: 181 suffix = event.type[len(self._event_type_prefix):] 182 183 if suffix[:2] == ('system', 'command'): 184 cmd_name, = suffix[2:] 185 await self._process_cmd_req(event, cmd_name) 186 187 elif suffix[:2] == ('system', 'change'): 188 val_name, = suffix[2:] 189 await self._process_change_req(event, val_name) 190 191 else: 192 raise Exception('unsupported event type') 193 194 except Exception as e: 195 self._log.warning('error processing event %s: %s', 196 event.type, e, exc_info=e) 197 198 async def _connection_loop(self): 199 200 async def cleanup(): 201 await self._register_status('DISCONNECTED') 202 if self._conn: 203 await self._conn.async_close() 204 205 conn_conf = self._conf['connection'] 206 try: 207 while True: 208 await self._register_status('CONNECTING') 209 try: 210 self._log.debug('connecting to %s:%s', 211 conn_conf['host'], conn_conf['port']) 212 self._conn = await aio.wait_for( 213 iec61850.connect( 214 addr=tcp.Address(conn_conf['host'], 215 conn_conf['port']), 216 data_value_types=self._dataset_change_value_types, 217 cmd_value_types=self._cmd_value_types, 218 report_data_refs=self._report_data_refs, 219 report_cb=self._on_report, 220 termination_cb=self._on_termination, 221 status_delay=conn_conf['status_delay'], 222 status_timeout=conn_conf['status_timeout'], 223 local_tsel=conn_conf.get('local_tsel'), 224 remote_tsel=conn_conf.get('remote_tsel'), 225 local_ssel=conn_conf.get('local_ssel'), 226 remote_ssel=conn_conf.get('remote_ssel'), 227 local_psel=conn_conf.get('local_psel'), 228 remote_psel=conn_conf.get('remote_psel'), 229 local_ap_title=conn_conf.get('local_ap_title'), 230 remote_ap_title=conn_conf.get('remote_ap_title'), 231 local_ae_qualifier=conn_conf.get( 232 'local_ae_qualifier'), 233 remote_ae_qualifier=conn_conf.get( 234 'remote_ae_qualifier'), 235 local_detail_calling=conn_conf.get( 236 'local_detail_calling'), 237 name=self._conf['name']), 238 conn_conf['connect_timeout']) 239 240 except Exception as e: 241 self._log.warning('connnection failed: %s', e, exc_info=e) 242 await self._register_status('DISCONNECTED') 243 await asyncio.sleep(conn_conf['reconnect_delay']) 244 continue 245 246 self._log.debug('connected') 247 await self._register_status('CONNECTED') 248 249 initialized = False 250 try: 251 await self._create_dynamic_datasets() 252 for rcb_conf in self._conf['rcbs']: 253 await self._init_rcb(rcb_conf) 254 initialized = True 255 256 except Exception as e: 257 self._log.warning( 258 'initialization failed: %s, closing connection', 259 e, exc_info=e) 260 self._conn.close() 261 262 await self._conn.wait_closing() 263 await self._register_status('DISCONNECTED') 264 await self._conn.wait_closed() 265 self._conn = None 266 self._terminations = {} 267 self._reports_segments = {} 268 if not initialized: 269 await asyncio.sleep(conn_conf['reconnect_delay']) 270 271 except Exception as e: 272 self._log.error('connection loop error: %s', e, exc_info=e) 273 274 finally: 275 self._log.debug('closing connection loop') 276 self.close() 277 await aio.uncancellable(cleanup()) 278 279 async def _process_cmd_req(self, event, cmd_name): 280 if not self._conn or not self._conn.is_open: 281 raise Exception('no connection') 282 283 if cmd_name not in self._command_name_confs: 284 raise Exception('unexpected command name') 285 286 cmd_conf = self._command_name_confs[cmd_name] 287 cmd_ref = iec61850.CommandRef(**cmd_conf['ref']) 288 action = event.payload.data['action'] 289 evt_session_id = event.payload.data['session_id'] 290 if (action == 'SELECT' and 291 cmd_conf['model'] == 'SBO_WITH_NORMAL_SECURITY'): 292 cmd = None 293 else: 294 ctl_num = self._command_name_ctl_nums[cmd_name] 295 ctl_num = _update_ctl_num(ctl_num, action, cmd_conf['model']) 296 self._command_name_ctl_nums[cmd_name] = ctl_num 297 value_type = self._command_ref_value_type[cmd_ref] 298 if value_type is None: 299 raise Exception('value type undefined') 300 301 cmd = _command_from_event(event, cmd_conf, ctl_num, value_type) 302 303 term_future = None 304 if (action == 'OPERATE' and 305 cmd_conf['model'] in ['DIRECT_WITH_ENHANCED_SECURITY', 306 'SBO_WITH_ENHANCED_SECURITY']): 307 term_future = self._loop.create_future() 308 self._conn.async_group.spawn( 309 self._wait_cmd_term, cmd_name, cmd_ref, cmd, evt_session_id, 310 term_future) 311 312 try: 313 resp = await aio.wait_for( 314 self._send_command(action, cmd_ref, cmd), 315 self._conf['connection']['response_timeout']) 316 317 except (asyncio.TimeoutError, ConnectionError) as e: 318 self._log.warning('send command failed: %s', e, exc_info=e) 319 if term_future and not term_future.done(): 320 term_future.cancel() 321 return 322 323 if resp is not None: 324 if term_future and not term_future.done(): 325 term_future.cancel() 326 327 event = _cmd_resp_to_event( 328 self._event_type_prefix, cmd_name, evt_session_id, action, resp) 329 await self._register_events([event]) 330 331 async def _send_command(self, action, cmd_ref, cmd): 332 if action == 'SELECT': 333 return await self._conn.select(cmd_ref, cmd) 334 335 if action == 'CANCEL': 336 return await self._conn.cancel(cmd_ref, cmd) 337 338 if action == 'OPERATE': 339 return await self._conn.operate(cmd_ref, cmd) 340 341 raise Exception('unsupported action') 342 343 async def _wait_cmd_term(self, cmd_name, cmd_ref, cmd, session_id, future): 344 cmd_session_id = _get_command_session_id(cmd_ref, cmd) 345 self._terminations[cmd_session_id] = future 346 try: 347 term = await aio.wait_for(future, termination_timeout) 348 event = _cmd_resp_to_event( 349 self._event_type_prefix, cmd_name, session_id, 'TERMINATION', 350 term.error) 351 await self._register_events([event]) 352 353 except asyncio.TimeoutError: 354 self._log.warning('command termination timeout') 355 356 finally: 357 del self._terminations[cmd_session_id] 358 359 async def _process_change_req(self, event, value_name): 360 if not self._conn or not self._conn.is_open: 361 raise Exception('no connection') 362 363 if value_name not in self._change_name_value_refs: 364 raise Exception('unexpected change name') 365 366 ref = self._change_name_value_refs[value_name] 367 value_type = self._change_ref_value_type[ref] 368 if value_type is None: 369 raise Exception('value type undefined') 370 371 value = _value_from_json(event.payload.data['value'], value_type) 372 try: 373 resp = await aio.wait_for( 374 self._conn.write_data(ref, value), 375 self._conf['connection']['response_timeout']) 376 377 except asyncio.TimeoutError: 378 self._log.warning('write data response timeout') 379 return 380 381 except ConnectionError as e: 382 self._log.warning('connection error on write data: %s', 383 e, exc_info=e) 384 return 385 386 session_id = event.payload.data['session_id'] 387 event = _write_data_resp_to_event( 388 self._event_type_prefix, value_name, session_id, resp) 389 await self._register_events([event]) 390 391 async def _on_report(self, report): 392 try: 393 events = list(self._events_from_report(report)) 394 395 except Exception as e: 396 self._log.warning('report %s ignored: %s', 397 report.report_id, e, exc_info=e) 398 return 399 400 if not events: 401 return 402 403 await self._register_events(events) 404 405 if self._rcb_type[report.report_id] == 'BUFFERED': 406 self._rcbs_entry_ids[report.report_id] = report.entry_id 407 408 def _events_from_report(self, report): 409 report_id = report.report_id 410 if report_id not in self._report_data_refs: 411 raise Exception(f'unexpected report {report_id}') 412 413 segm_id = (report_id, report.sequence_number) 414 if report.more_segments_follow: 415 if segm_id in self._reports_segments: 416 segment_data, timeout_timer = self._reports_segments[segm_id] 417 timeout_timer.cancel() 418 else: 419 segment_data = collections.deque() 420 421 segment_data.extend(report.data) 422 timeout_timer = self._loop.call_later( 423 report_segments_timeout, self._reports_segments.pop, segm_id) 424 self._reports_segments[segm_id] = (segment_data, timeout_timer) 425 return 426 427 if segm_id in self._reports_segments: 428 report_data, timeout_timer = self._reports_segments.pop(segm_id) 429 timeout_timer.cancel() 430 report_data.extend(report.data) 431 432 else: 433 report_data = report.data 434 435 yield from self._events_from_report_data(report_data, report_id) 436 437 if self._rcb_type[report_id] == 'BUFFERED': 438 yield hat.event.common.RegisterEvent( 439 type=(*self._event_type_prefix, 'gateway', 440 'entry_id', report_id), 441 source_timestamp=None, 442 payload=hat.event.common.EventPayloadJson( 443 report.entry_id.hex() 444 if report.entry_id is not None else None)) 445 446 def _events_from_report_data(self, report_data, report_id): 447 data_values_json = collections.defaultdict(dict) 448 data_reasons = collections.defaultdict(set) 449 for rv in report_data: 450 if rv.ref not in self._value_ref_data_names: 451 continue 452 453 value_type = self._dataset_values_ref_type[rv.ref] 454 if value_type is None: 455 self._log.warning('report data ignored: unknown value type') 456 continue 457 458 value_json = _value_to_json(rv.value, value_type) 459 for data_name in self._value_ref_data_names[rv.ref]: 460 value_path = [rv.ref.logical_device, rv.ref.logical_node, 461 rv.ref.fc, *rv.ref.names] 462 data_values_json[data_name] = json.set_( 463 data_values_json[data_name], value_path, value_json) 464 if rv.reasons: 465 data_reasons[data_name].update( 466 reason.name for reason in rv.reasons) 467 468 for data_name, values_json in data_values_json.items(): 469 payload = {'reasons': list(data_reasons[data_name])} 470 data_conf = self._data_name_confs[data_name] 471 value_path = _conf_ref_to_path(data_conf['value']) 472 value_json = json.get(values_json, value_path) 473 if value_json is not None: 474 value_ref = _value_ref_from_json(data_conf['value']) 475 value_type = self._data_value_types[value_ref] 476 payload['value'] = _value_json_to_event_json( 477 value_json, value_type) 478 479 if 'quality' in data_conf: 480 quality_path = _conf_ref_to_path(data_conf['quality']) 481 quality_json = json.get(values_json, quality_path) 482 if quality_json is not None: 483 payload['quality'] = quality_json 484 485 if 'timestamp' in data_conf: 486 timestamp_path = _conf_ref_to_path(data_conf['timestamp']) 487 timestamp_json = json.get(values_json, timestamp_path) 488 if timestamp_json is not None: 489 payload['timestamp'] = timestamp_json 490 491 if 'selected' in data_conf: 492 selected_path = _conf_ref_to_path(data_conf['selected']) 493 selected_json = json.get(values_json, selected_path) 494 if selected_json is not None: 495 payload['selected'] = selected_json 496 497 yield hat.event.common.RegisterEvent( 498 type=(*self._event_type_prefix, 'gateway', 499 'data', data_name), 500 source_timestamp=None, 501 payload=hat.event.common.EventPayloadJson(payload)) 502 503 def _on_termination(self, termination): 504 cmd_session_id = _get_command_session_id( 505 termination.ref, termination.cmd) 506 if cmd_session_id not in self._terminations: 507 self._log.warning('unexpected termination dropped') 508 return 509 510 term_future = self._terminations[cmd_session_id] 511 if not term_future.done(): 512 self._terminations[cmd_session_id].set_result(termination) 513 514 async def _init_rcb(self, rcb_conf): 515 ref = iec61850.RcbRef( 516 logical_device=rcb_conf['ref']['logical_device'], 517 logical_node=rcb_conf['ref']['logical_node'], 518 type=iec61850.RcbType[rcb_conf['ref']['type']], 519 name=rcb_conf['ref']['name']) 520 self._log.debug('initiating rcb %s', ref) 521 522 get_attrs = collections.deque([iec61850.RcbAttrType.REPORT_ID]) 523 dataset_ref = _dataset_ref_from_json(rcb_conf['dataset']) 524 if dataset_ref not in self._dyn_datasets_values: 525 get_attrs.append(iec61850.RcbAttrType.DATASET) 526 if 'conf_revision' in rcb_conf: 527 get_attrs.append(iec61850.RcbAttrType.CONF_REVISION) 528 529 get_rcb_resp = await self._conn.get_rcb_attrs(ref, get_attrs) 530 _validate_get_rcb_response(get_rcb_resp, rcb_conf) 531 532 if ref.type == iec61850.RcbType.BUFFERED: 533 if 'reservation_time' in rcb_conf: 534 await self._set_rcb( 535 ref, [(iec61850.RcbAttrType.RESERVATION_TIME, 536 rcb_conf['reservation_time'])]) 537 elif ref.type == iec61850.RcbType.UNBUFFERED: 538 await self._set_rcb(ref, [(iec61850.RcbAttrType.RESERVE, True)]) 539 else: 540 raise Exception('unexpected rcb type') 541 542 await self._set_rcb(ref, [(iec61850.RcbAttrType.REPORT_ENABLE, False)]) 543 544 if dataset_ref in self._dyn_datasets_values: 545 await self._set_rcb( 546 ref, [(iec61850.RcbAttrType.DATASET, dataset_ref)], 547 critical=True) 548 549 if ref.type == iec61850.RcbType.BUFFERED: 550 entry_id = self._rcbs_entry_ids.get( 551 _report_id_from_rcb_conf(rcb_conf)) 552 if rcb_conf.get('purge_buffer') or entry_id is None: 553 await self._set_rcb( 554 ref, [(iec61850.RcbAttrType.PURGE_BUFFER, True)]) 555 556 else: 557 try: 558 await self._set_rcb( 559 ref, [(iec61850.RcbAttrType.ENTRY_ID, entry_id)], 560 critical=True) 561 562 except Exception as e: 563 self._log.warning('%s', e, exc_info=e) 564 # try setting entry id to 0 in order to resynchronize 565 await self._set_rcb( 566 ref, [(iec61850.RcbAttrType.ENTRY_ID, b'\x00')]) 567 568 attrs = collections.deque() 569 if 'trigger_options' in rcb_conf: 570 attrs.append((iec61850.RcbAttrType.TRIGGER_OPTIONS, 571 set(iec61850.TriggerCondition[i] 572 for i in rcb_conf['trigger_options']))) 573 if 'optional_fields' in rcb_conf: 574 attrs.append((iec61850.RcbAttrType.OPTIONAL_FIELDS, 575 set(iec61850.OptionalField[i] 576 for i in rcb_conf['optional_fields']))) 577 if 'buffer_time' in rcb_conf: 578 attrs.append((iec61850.RcbAttrType.BUFFER_TIME, 579 rcb_conf['buffer_time'])) 580 if 'integrity_period' in rcb_conf: 581 attrs.append((iec61850.RcbAttrType.INTEGRITY_PERIOD, 582 rcb_conf['integrity_period'])) 583 if attrs: 584 await self._set_rcb(ref, attrs) 585 586 await self._set_rcb( 587 ref, [(iec61850.RcbAttrType.REPORT_ENABLE, True)], critical=True) 588 await self._set_rcb( 589 ref, [(iec61850.RcbAttrType.GI, True)], critical=True) 590 self._log.debug('rcb %s initiated', ref) 591 592 async def _set_rcb(self, ref, attrs, critical=False): 593 try: 594 resp = await self._conn.set_rcb_attrs(ref, attrs) 595 attrs_failed = set((attr, attr_res) 596 for attr, attr_res in resp.items() 597 if isinstance(attr_res, iec61850.ServiceError)) 598 if attrs_failed: 599 raise Exception(f"set attribute errors: {attrs_failed}") 600 601 except Exception as e: 602 if critical: 603 raise Exception(f'set rcb {ref} failed') from e 604 605 else: 606 self._log.warning('set rcb %s failed: %s', ref, e, exc_info=e) 607 608 async def _create_dynamic_datasets(self): 609 existing_ds_refs = set() 610 for ds_ref in self._persist_dyn_datasets: 611 ld = ds_ref.logical_device 612 res = await self._conn.get_persisted_dataset_refs(ld) 613 if isinstance(res, iec61850.ServiceError): 614 raise Exception(f'get datasets for ld {ld} failed: {res}') 615 616 existing_ds_refs.update(res) 617 618 existing_persisted_ds_refs = existing_ds_refs.intersection( 619 self._persist_dyn_datasets) 620 for ds_ref, ds_value_refs in self._dyn_datasets_values.items(): 621 if ds_ref in existing_persisted_ds_refs: 622 res = await self._conn.get_dataset_data_refs(ds_ref) 623 if isinstance(res, iec61850.ServiceError): 624 raise Exception(f'get ds {ds_ref} data refs failed: {res}') 625 else: 626 exist_ds_value_refs = res 627 628 if ds_value_refs == list(exist_ds_value_refs): 629 self._log.debug('dataset %s already exists', ds_ref) 630 continue 631 632 raise Exception('persisted dataset changed') 633 634 res = await self._conn.create_dataset(ds_ref, ds_value_refs) 635 if res is not None: 636 raise Exception(f'create dataset {ds_ref} failed: {res}') 637 638 self._log.debug("dataset %s crated", ds_ref) 639 640 async def _register_events(self, events): 641 try: 642 await self._eventer_client.register(events) 643 644 except ConnectionError: 645 self._log.debug('eventer connection closed') 646 self.close() 647 648 async def _register_status(self, status): 649 if status == self._conn_status: 650 return 651 652 event = hat.event.common.RegisterEvent( 653 type=(*self._event_type_prefix, 'gateway', 'status'), 654 source_timestamp=None, 655 payload=hat.event.common.EventPayloadJson(status)) 656 await self._register_events([event]) 657 self._conn_status = status 658 self._log.debug('registered status %s', status)
Device interface
async def
process_event(self, event: hat.event.common.common.Event):
179 async def process_event(self, event: hat.event.common.Event): 180 try: 181 suffix = event.type[len(self._event_type_prefix):] 182 183 if suffix[:2] == ('system', 'command'): 184 cmd_name, = suffix[2:] 185 await self._process_cmd_req(event, cmd_name) 186 187 elif suffix[:2] == ('system', 'change'): 188 val_name, = suffix[2:] 189 await self._process_change_req(event, val_name) 190 191 else: 192 raise Exception('unsupported event type') 193 194 except Exception as e: 195 self._log.warning('error processing event %s: %s', 196 event.type, e, exc_info=e)
Process received event
This method can be coroutine or regular function.