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)
mlog: logging.Logger = <Logger hat.gateway.devices.iec61850.client (WARNING)>
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']}}}]}}}})
class Iec61850ClientDevice(hat.gateway.common.Device):
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_group: hat.aio.group.Group
175    @property
176    def async_group(self) -> aio.Group:
177        return self._async_group

Group controlling resource's lifetime.

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.