hat.gateway.devices.iec61850.client

IEC 61850 client device

  1"""IEC 61850 client device"""
  2
  3from collections.abc import Collection
  4import asyncio
  5import collections
  6import datetime
  7import logging
  8import math
  9
 10from hat import aio
 11from hat import json
 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    entry_id_event_types = [
 33        (*event_type_prefix, 'gateway', 'entry_id', i['report_id'])
 34        for i in conf['rcbs'] if i['ref']['type'] == 'BUFFERED']
 35    if entry_id_event_types:
 36        result = await eventer_client.query(
 37            hat.event.common.QueryLatestParams(entry_id_event_types))
 38        rcbs_entry_ids = {
 39            event.type[5]: (bytes.fromhex(event.payload.data)
 40                            if event.payload.data is not None else None)
 41            for event in result.events}
 42    else:
 43        rcbs_entry_ids = {}
 44
 45    device = Iec61850ClientDevice()
 46
 47    device._rcbs_entry_ids = rcbs_entry_ids
 48    device._conf = conf
 49    device._eventer_client = eventer_client
 50    device._event_type_prefix = event_type_prefix
 51    device._conn = None
 52    device._conn_status = None
 53    device._terminations = {}
 54    device._reports_segments = {}
 55
 56    device._data_ref_confs = {(i['ref']['logical_device'],
 57                               i['ref']['logical_node'],
 58                               *i['ref']['names']): i for i in conf['data']}
 59    device._command_name_confs = {i['name']: i for i in conf['commands']}
 60    device._command_name_ctl_nums = {i['name']: 0 for i in conf['commands']}
 61    device._change_name_value_refs = {
 62        i['name']: _value_ref_from_conf(i['ref']) for i in conf['changes']}
 63    device._rcb_type = {rcb_conf['report_id']: rcb_conf['ref']['type']
 64                        for rcb_conf in conf['rcbs']}
 65
 66    device._persist_dyn_datasets = set(_get_persist_dyn_datasets(conf))
 67    device._dyn_datasets_values = {
 68        ds_ref: values
 69        for ds_ref, values in _get_dyn_datasets_values(conf)}
 70
 71    dataset_confs = {_dataset_ref_from_conf(ds_conf['ref']): ds_conf
 72                     for ds_conf in device._conf['datasets']}
 73    device._report_value_refs = collections.defaultdict(collections.deque)
 74    device._values_data = collections.defaultdict(set)
 75    for rcb_conf in device._conf['rcbs']:
 76        report_id = rcb_conf['report_id']
 77        ds_ref = _dataset_ref_from_conf(rcb_conf['dataset'])
 78        for value_conf in dataset_confs[ds_ref]['values']:
 79            value_ref = _value_ref_from_conf(value_conf)
 80            for data_ref, data_conf in device._data_ref_confs.items():
 81                if ('report_ids' in data_conf and
 82                        report_id not in data_conf['report_ids']):
 83                    continue
 84
 85                if not _data_matches_value(data_ref, value_ref):
 86                    continue
 87
 88                device._values_data[value_ref].add(data_ref)
 89                device._report_value_refs[report_id].append(value_ref)
 90
 91    value_types = _value_types_from_conf(conf)
 92    device._value_types = value_types
 93    device._cmd_value_types = {
 94        cmd_ref: value_type
 95        for cmd_ref, value_type in _get_command_value_types(conf, value_types)}
 96    device._data_value_types = {
 97        value_ref: value_type
 98        for value_ref, value_type in _get_data_value_types(conf, value_types)}
 99
100    device._async_group = aio.Group()
101    device._async_group.spawn(device._connection_loop)
102    device._loop = asyncio.get_running_loop()
103
104    return device
105
106
107info: common.DeviceInfo = common.DeviceInfo(
108    type="iec61850_client",
109    create=create,
110    json_schema_id="hat-gateway://iec61850.yaml#/$defs/client",
111    json_schema_repo=common.json_schema_repo)
112
113
114class Iec61850ClientDevice(common.Device):
115
116    @property
117    def async_group(self) -> aio.Group:
118        return self._async_group
119
120    async def process_events(self, events: Collection[hat.event.common.Event]):
121        try:
122            for event in events:
123                suffix = event.type[len(self._event_type_prefix):]
124
125                if suffix[:2] == ('system', 'command'):
126                    cmd_name, = suffix[2:]
127                    await self._process_cmd_req(event, cmd_name)
128
129                elif suffix[:2] == ('system', 'change'):
130                    val_name, = suffix[2:]
131                    await self._process_change_req(event, val_name)
132
133                else:
134                    raise Exception('unsupported event type')
135
136        except Exception as e:
137            mlog.warning('error processing event: %s', e, exc_info=e)
138
139    async def _connection_loop(self):
140
141        async def cleanup():
142            await self._register_status('DISCONNECTED')
143            if self._conn:
144                await self._conn.async_close()
145
146        conn_conf = self._conf['connection']
147        try:
148            while True:
149                await self._register_status('CONNECTING')
150                try:
151                    mlog.debug('connecting to %s:%s',
152                               conn_conf['host'], conn_conf['port'])
153                    self._conn = await aio.wait_for(
154                        iec61850.connect(
155                            addr=tcp.Address(conn_conf['host'],
156                                             conn_conf['port']),
157                            data_value_types=self._data_value_types,
158                            cmd_value_types=self._cmd_value_types,
159                            report_data_refs=self._report_value_refs,
160                            report_cb=self._on_report,
161                            termination_cb=self._on_termination,
162                            status_delay=conn_conf['status_delay'],
163                            status_timeout=conn_conf['status_timeout'],
164                            local_tsel=conn_conf.get('local_tsel'),
165                            remote_tsel=conn_conf.get('remote_tsel'),
166                            local_ssel=conn_conf.get('local_ssel'),
167                            remote_ssel=conn_conf.get('remote_ssel'),
168                            local_psel=conn_conf.get('local_psel'),
169                            remote_psel=conn_conf.get('remote_psel'),
170                            local_ap_title=conn_conf.get('local_ap_title'),
171                            remote_ap_title=conn_conf.get('remote_ap_title'),
172                            local_ae_qualifier=conn_conf.get(
173                                'local_ae_qualifier'),
174                            remote_ae_qualifier=conn_conf.get(
175                                'remote_ae_qualifier'),
176                            local_detail_calling=conn_conf.get(
177                                'local_detail_calling')),
178                        conn_conf['connect_timeout'])
179
180                except Exception as e:
181                    mlog.warning('connnection failed: %s', e, exc_info=e)
182                    await self._register_status('DISCONNECTED')
183                    await asyncio.sleep(conn_conf['reconnect_delay'])
184                    continue
185
186                mlog.debug('connected')
187                await self._register_status('CONNECTED')
188
189                initialized = False
190                try:
191                    await self._create_dynamic_datasets()
192                    for rcb_conf in self._conf['rcbs']:
193                        await self._init_rcb(rcb_conf)
194                    initialized = True
195
196                except Exception as e:
197                    mlog.warning(
198                        'initialization failed: %s, closing connection',
199                        e, exc_info=e)
200                    self._conn.close()
201
202                await self._conn.wait_closing()
203                await self._register_status('DISCONNECTED')
204                await self._conn.wait_closed()
205                self._conn = None
206                self._terminations = {}
207                self._reports_segments = {}
208                if not initialized:
209                    await asyncio.sleep(conn_conf['reconnect_delay'])
210
211        except Exception as e:
212            mlog.error('connection loop error: %s', e, exc_info=e)
213
214        finally:
215            mlog.debug('closing connection loop')
216            self.close()
217            await aio.uncancellable(cleanup())
218
219    async def _process_cmd_req(self, event, cmd_name):
220        if not self._conn or not self._conn.is_open:
221            mlog.warning('command %s ignored: no connection', cmd_name)
222            return
223
224        if cmd_name not in self._command_name_confs:
225            raise Exception('unexpected command name')
226
227        cmd_conf = self._command_name_confs[cmd_name]
228        cmd_ref = iec61850.CommandRef(**cmd_conf['ref'])
229        action = event.payload.data['action']
230        evt_session_id = event.payload.data['session_id']
231        if (action == 'SELECT' and
232                cmd_conf['model'] == 'SBO_WITH_NORMAL_SECURITY'):
233            cmd = None
234        else:
235            ctl_num = self._command_name_ctl_nums[cmd_name]
236            ctl_num = _update_ctl_num(ctl_num, action, cmd_conf['model'])
237            self._command_name_ctl_nums[cmd_name] = ctl_num
238            value_type = _value_type_from_ref(self._value_types, cmd_ref)
239            cmd = _command_from_event(event, cmd_conf, ctl_num, value_type)
240
241        term_future = None
242        if (action == 'OPERATE' and
243                cmd_conf['model'] in ['DIRECT_WITH_ENHANCED_SECURITY',
244                                      'SBO_WITH_ENHANCED_SECURITY']):
245            term_future = self._loop.create_future()
246            self._conn.async_group.spawn(
247                self._wait_cmd_term, cmd_name, cmd_ref, cmd, evt_session_id,
248                term_future)
249
250        try:
251            resp = await aio.wait_for(
252                self._send_command(action, cmd_ref, cmd),
253                self._conf['connection']['response_timeout'])
254
255        except (asyncio.TimeoutError, ConnectionError) as e:
256            mlog.warning('send command failed: %s', e, exc_info=e)
257            if term_future and not term_future.done():
258                term_future.cancel()
259            return
260
261        if resp is not None:
262            if term_future and not term_future.done():
263                term_future.cancel()
264
265        event = _cmd_resp_to_event(
266            self._event_type_prefix, cmd_name, evt_session_id, action, resp)
267        await self._register_events([event])
268
269    async def _send_command(self, action, cmd_ref, cmd):
270        if action == 'SELECT':
271            return await self._conn.select(cmd_ref, cmd)
272
273        if action == 'CANCEL':
274            return await self._conn.cancel(cmd_ref, cmd)
275
276        if action == 'OPERATE':
277            return await self._conn.operate(cmd_ref, cmd)
278
279        raise Exception('unsupported action')
280
281    async def _wait_cmd_term(self, cmd_name, cmd_ref, cmd, session_id, future):
282        cmd_session_id = _get_command_session_id(cmd_ref, cmd)
283        self._terminations[cmd_session_id] = future
284        try:
285            term = await aio.wait_for(future, termination_timeout)
286            event = _cmd_resp_to_event(
287                self._event_type_prefix, cmd_name, session_id, 'TERMINATION',
288                term.error)
289            await self._register_events([event])
290
291        except asyncio.TimeoutError:
292            mlog.warning('command termination timeout')
293
294        finally:
295            del self._terminations[cmd_session_id]
296
297    async def _process_change_req(self, event, value_name):
298        if not self._conn or not self._conn.is_open:
299            mlog.warning('change event %s ignored: no connection', value_name)
300            return
301
302        if value_name not in self._change_name_value_refs:
303            raise Exception('unexpected command name')
304
305        ref = self._change_name_value_refs[value_name]
306        value_type = _value_type_from_ref(self._value_types, ref)
307        if value_type is None:
308            raise Exception('value type undefined')
309
310        value = _value_from_json(event.payload.data['value'], value_type)
311        try:
312            resp = await aio.wait_for(
313                self._conn.write_data(ref, value),
314                self._conf['connection']['response_timeout'])
315
316        except asyncio.TimeoutError:
317            mlog.warning('write data response timeout')
318            return
319
320        except ConnectionError as e:
321            mlog.warning('connection error on write data: %s', e, exc_info=e)
322            return
323
324        session_id = event.payload.data['session_id']
325        event = _write_data_resp_to_event(
326            self._event_type_prefix, value_name, session_id, resp)
327        await self._register_events([event])
328
329    async def _on_report(self, report):
330        report_id = report.report_id
331        if report_id not in self._report_value_refs:
332            mlog.warning('unexpected report dropped')
333            return
334
335        segm_id = (report_id, report.sequence_number)
336        if report.more_segments_follow:
337            if segm_id in self._reports_segments:
338                segment_data, timeout_timer = self._reports_segments[segm_id]
339                timeout_timer.cancel()
340            else:
341                segment_data = collections.deque()
342
343            segment_data.extend(report.data)
344            timeout_timer = self._loop.call_later(
345                report_segments_timeout, self._reports_segments.pop, segm_id)
346            self._reports_segments[segm_id] = (segment_data, timeout_timer)
347            return
348
349        if segm_id in self._reports_segments:
350            report_data, timeout_timer = self._reports_segments.pop(segm_id)
351            timeout_timer.cancel()
352            report_data.extend(report.data)
353
354        else:
355            report_data = report.data
356
357        events = collections.deque()
358        events.extend(self._report_data_to_events(report_data, report_id))
359
360        if self._rcb_type[report_id] == 'BUFFERED':
361            events.append(hat.event.common.RegisterEvent(
362                type=(*self._event_type_prefix, 'gateway',
363                      'entry_id', report_id),
364                source_timestamp=None,
365                payload=hat.event.common.EventPayloadJson(
366                    report.entry_id.hex()
367                    if report.entry_id is not None else None)))
368
369        await self._register_events(events)
370        if self._rcb_type[report_id] == 'BUFFERED':
371            self._rcbs_entry_ids[report_id] = report.entry_id
372
373    def _report_data_to_events(self, report_data, report_id):
374        report_data_values_types = collections.defaultdict(collections.deque)
375        for rv in report_data:
376            data_refs = self._values_data.get(rv.ref)
377            value_type = _value_type_from_ref(self._value_types, rv.ref)
378            for data_ref in data_refs:
379                report_data_values_types[data_ref].append((rv, value_type))
380
381        for data_ref, report_values_types in report_data_values_types.items():
382            data_conf = self._data_ref_confs[data_ref]
383            if ('report_ids' in data_conf and
384                    report_id not in data_conf['report_ids']):
385                continue
386
387            try:
388                yield _report_values_to_event(
389                    self._event_type_prefix, report_values_types,
390                    data_conf['name'], data_ref)
391
392            except Exception as e:
393                mlog.warning('report values dropped: %s', e, exc_info=e)
394                continue
395
396    def _on_termination(self, termination):
397        cmd_session_id = _get_command_session_id(
398            termination.ref, termination.cmd)
399        if cmd_session_id not in self._terminations:
400            mlog.warning('unexpected termination dropped')
401            return
402
403        term_future = self._terminations[cmd_session_id]
404        if not term_future.done():
405            self._terminations[cmd_session_id].set_result(termination)
406
407    async def _init_rcb(self, rcb_conf):
408        ref = iec61850.RcbRef(
409            logical_device=rcb_conf['ref']['logical_device'],
410            logical_node=rcb_conf['ref']['logical_node'],
411            type=iec61850.RcbType[rcb_conf['ref']['type']],
412            name=rcb_conf['ref']['name'])
413        mlog.debug('initiating rcb %s', ref)
414
415        get_attrs = collections.deque([iec61850.RcbAttrType.REPORT_ID])
416        dataset_ref = _dataset_ref_from_conf(rcb_conf['dataset'])
417        if dataset_ref not in self._dyn_datasets_values:
418            get_attrs.append(iec61850.RcbAttrType.DATASET)
419        if 'conf_revision' in rcb_conf:
420            get_attrs.append(iec61850.RcbAttrType.CONF_REVISION)
421
422        get_rcb_resp = await self._conn.get_rcb_attrs(ref, get_attrs)
423        _validate_get_rcb_response(get_rcb_resp, rcb_conf)
424
425        await self._set_rcb(ref, [(iec61850.RcbAttrType.REPORT_ENABLE, False)])
426
427        if dataset_ref in self._dyn_datasets_values:
428            await self._set_rcb(
429                ref, [(iec61850.RcbAttrType.DATASET, dataset_ref)],
430                critical=True)
431
432        if ref.type == iec61850.RcbType.BUFFERED:
433            if 'reservation_time' in rcb_conf:
434                await self._set_rcb(
435                    ref, [(iec61850.RcbAttrType.RESERVATION_TIME,
436                           rcb_conf['reservation_time'])])
437
438            entry_id = self._rcbs_entry_ids.get(rcb_conf['report_id'])
439            if rcb_conf.get('purge_buffer') or entry_id is None:
440                await self._set_rcb(
441                    ref, [(iec61850.RcbAttrType.PURGE_BUFFER, True)])
442
443            else:
444                try:
445                    await self._set_rcb(
446                        ref, [(iec61850.RcbAttrType.ENTRY_ID, entry_id)],
447                        critical=True)
448
449                except Exception as e:
450                    mlog.warning('%s', e, exc_info=e)
451                    # try setting entry id to 0 in order to resynchronize
452                    await self._set_rcb(
453                        ref, [(iec61850.RcbAttrType.ENTRY_ID, b'\x00')])
454
455        elif ref.type == iec61850.RcbType.UNBUFFERED:
456            await self._set_rcb(ref, [(iec61850.RcbAttrType.RESERVE, True)])
457
458        attrs = collections.deque()
459        if 'trigger_options' in rcb_conf:
460            attrs.append((iec61850.RcbAttrType.TRIGGER_OPTIONS,
461                          set(iec61850.TriggerCondition[i]
462                              for i in rcb_conf['trigger_options'])))
463        if 'optional_fields' in rcb_conf:
464            attrs.append((iec61850.RcbAttrType.OPTIONAL_FIELDS,
465                          set(iec61850.OptionalField[i]
466                              for i in rcb_conf['optional_fields'])))
467        if 'buffer_time' in rcb_conf:
468            attrs.append((iec61850.RcbAttrType.BUFFER_TIME,
469                          rcb_conf['buffer_time']))
470        if 'integrity_period' in rcb_conf:
471            attrs.append((iec61850.RcbAttrType.INTEGRITY_PERIOD,
472                          rcb_conf['integrity_period']))
473        if attrs:
474            await self._set_rcb(ref, attrs)
475
476        await self._set_rcb(
477            ref, [(iec61850.RcbAttrType.REPORT_ENABLE, True)], critical=True)
478        await self._set_rcb(
479            ref, [(iec61850.RcbAttrType.GI, True)], critical=True)
480        mlog.debug('rcb %s initiated', ref)
481
482    async def _set_rcb(self, ref, attrs, critical=False):
483        try:
484            resp = await self._conn.set_rcb_attrs(ref, attrs)
485            attrs_failed = set((attr, attr_res)
486                               for attr, attr_res in resp.items()
487                               if isinstance(attr_res, iec61850.ServiceError))
488            if attrs_failed:
489                raise Exception(f"set attribute errors: {attrs_failed}")
490
491        except Exception as e:
492            if critical:
493                raise Exception(f'set rcb {ref} failed') from e
494
495            else:
496                mlog.warning('set rcb %s failed: %s', ref, e, exc_info=e)
497
498    async def _create_dynamic_datasets(self):
499        existing_ds_refs = set()
500        for ds_ref in self._persist_dyn_datasets:
501            ld = ds_ref.logical_device
502            res = await self._conn.get_persisted_dataset_refs(ld)
503            if isinstance(res, iec61850.ServiceError):
504                raise Exception(f'get datasets for ld {ld} failed: {res}')
505
506            existing_ds_refs.update(res)
507
508        existing_persisted_ds_refs = existing_ds_refs.intersection(
509            self._persist_dyn_datasets)
510        for ds_ref, ds_value_refs in self._dyn_datasets_values.items():
511            if ds_ref in existing_persisted_ds_refs:
512                res = await self._conn.get_dataset_data_refs(ds_ref)
513                if isinstance(res, iec61850.ServiceError):
514                    raise Exception(f'get ds {ds_ref} data refs failed: {res}')
515                else:
516                    exist_ds_value_refs = res
517
518                if ds_value_refs == list(exist_ds_value_refs):
519                    mlog.debug('dataset %s already exists', ds_ref)
520                    continue
521
522                mlog.debug("dataset %s exists, but different", ds_ref)
523                res = await self._conn.delete_dataset(ds_ref)
524                if res is not None:
525                    raise Exception(f'delete dataset {ds_ref} failed: {res}')
526                mlog.debug("dataset %s deleted", ds_ref)
527
528            res = await self._conn.create_dataset(ds_ref, ds_value_refs)
529            if res is not None:
530                raise Exception(f'create dataset {ds_ref} failed: {res}')
531
532            mlog.debug("dataset %s crated", ds_ref)
533
534    async def _register_events(self, events):
535        try:
536            await self._eventer_client.register(events)
537
538        except ConnectionError:
539            self.close()
540
541    async def _register_status(self, status):
542        if status == self._conn_status:
543            return
544
545        event = hat.event.common.RegisterEvent(
546            type=(*self._event_type_prefix, 'gateway', 'status'),
547            source_timestamp=None,
548            payload=hat.event.common.EventPayloadJson(status))
549        await self._register_events([event])
550        self._conn_status = status
551        mlog.debug('registered status %s', status)
552
553
554def _value_ref_from_conf(value_ref_conf):
555    return iec61850.DataRef(
556        logical_device=value_ref_conf['logical_device'],
557        logical_node=value_ref_conf['logical_node'],
558        fc=value_ref_conf['fc'],
559        names=tuple(value_ref_conf['names']))
560
561
562def _dataset_ref_from_conf(ds_conf):
563    if isinstance(ds_conf, str):
564        return iec61850.NonPersistedDatasetRef(ds_conf)
565
566    elif isinstance(ds_conf, dict):
567        return iec61850.PersistedDatasetRef(**ds_conf)
568
569
570def _data_matches_value(data_path, value_ref):
571    value_path = (value_ref.logical_device,
572                  value_ref.logical_node,
573                  *value_ref.names)
574    if len(data_path) == len(value_path):
575        return data_path == value_path
576
577    if len(data_path) > len(value_path):
578        return data_path[:len(value_path)] == value_path
579
580    return value_path[:len(data_path)] == data_path
581
582
583def _get_persist_dyn_datasets(conf):
584    for ds_conf in conf['datasets']:
585        if not ds_conf['dynamic']:
586            continue
587
588        ds_ref = _dataset_ref_from_conf(ds_conf['ref'])
589        if isinstance(ds_ref, iec61850.PersistedDatasetRef):
590            yield ds_ref
591
592
593def _get_dyn_datasets_values(conf):
594    for ds_conf in conf['datasets']:
595        if not ds_conf['dynamic']:
596            continue
597
598        ds_ref = _dataset_ref_from_conf(ds_conf['ref'])
599        yield ds_ref, [_value_ref_from_conf(val_conf)
600                       for val_conf in ds_conf['values']]
601
602
603def _get_data_value_types(conf, value_types):
604    for ds_conf in conf['datasets']:
605        for val_ref_conf in ds_conf['values']:
606            value_ref = _value_ref_from_conf(val_ref_conf)
607            value_type = _value_type_to_iec61850(
608                _value_type_from_ref(value_types, value_ref))
609            yield value_ref, value_type
610
611    for change_conf in conf['changes']:
612        value_ref = _value_ref_from_conf(change_conf['ref'])
613        value_type = _value_type_to_iec61850(
614            _value_type_from_ref(value_types, value_ref))
615        yield value_ref, value_type
616
617
618def _get_command_value_types(conf, value_types):
619    for cmd_conf in conf['commands']:
620        cmd_ref = iec61850.CommandRef(**cmd_conf['ref'])
621        value_type = _value_type_to_iec61850(
622            _value_type_from_ref(value_types, cmd_ref))
623        yield cmd_ref, value_type
624
625
626def _value_types_from_conf(conf):
627    value_types = {}
628    for vt in conf['value_types']:
629        ref = (vt['logical_device'],
630               vt['logical_node'],
631               vt['name'])
632        value_type = _value_type_from_vt_conf(vt['type'])
633        if ref in value_types:
634            value_types[ref] = _merge_types(
635                value_type, value_types[ref], set(ref))
636        else:
637            value_types[ref] = value_type
638
639    return value_types
640
641
642def _value_type_from_vt_conf(vt_conf):
643    if isinstance(vt_conf, str):
644        if vt_conf in ['BOOLEAN',
645                       'INTEGER',
646                       'UNSIGNED',
647                       'FLOAT',
648                       'BIT_STRING',
649                       'OCTET_STRING',
650                       'VISIBLE_STRING',
651                       'MMS_STRING']:
652            return iec61850.BasicValueType(vt_conf)
653
654        if vt_conf in ['QUALITY',
655                       'TIMESTAMP',
656                       'DOUBLE_POINT',
657                       'DIRECTION',
658                       'SEVERITY',
659                       'ANALOGUE',
660                       'VECTOR',
661                       'STEP_POSITION',
662                       'BINARY_CONTROL']:
663            return iec61850.AcsiValueType(vt_conf)
664
665        raise Exception('unsupported value type')
666
667    if vt_conf['type'] == 'ARRAY':
668        element_type = _value_type_from_vt_conf(vt_conf['element_type'])
669        return iec61850.ArrayValueType(type=element_type,
670                                       length=vt_conf['length'])
671
672    if vt_conf['type'] == 'STRUCT':
673        return {el_conf['name']: _value_type_from_vt_conf(el_conf['type'])
674                for el_conf in vt_conf['elements']}
675
676    raise Exception('unsupported value type')
677
678
679def _merge_types(type1, type2, ref):
680    if isinstance(type1, dict) and isinstance(type2, dict):
681        return {**type1,
682                **{k: (subtype if k not in type1
683                       else _merge_types(type1[k], subtype, ref.union(k)))
684                   for k, subtype in type2.items()}}
685
686    mlog.warning('value types conflict on reference %s: %s and %s',
687                 ref, type1, type2)
688    return type2
689
690
691def _value_type_from_ref(value_types, ref):
692    left_names = []
693    if isinstance(ref, iec61850.DataRef):
694        left_names = ref.names[1:]
695        ref = (ref.logical_device, ref.logical_node, ref.names[0])
696
697    if ref not in value_types:
698        return
699
700    value_type = value_types[ref]
701    while left_names:
702        name = left_names[0]
703        left_names = left_names[1:]
704        if isinstance(name, str):
705            value_type = value_type[name]
706
707        if isinstance(name, int):
708            value_type = value_type.type
709
710    return value_type
711
712
713def _value_type_to_iec61850(val_type):
714    if (isinstance(val_type, iec61850.BasicValueType) or
715            isinstance(val_type, iec61850.AcsiValueType)):
716        return val_type
717
718    if isinstance(val_type, iec61850.ArrayValueType):
719        return iec61850.ArrayValueType(
720            type=_value_type_to_iec61850(val_type.type),
721            length=val_type.length)
722
723    if isinstance(val_type, dict):
724        return iec61850.StructValueType(
725            [(k, _value_type_to_iec61850(v)) for k, v in val_type.items()])
726
727
728_epoch_start = datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
729
730
731def _command_from_event(event, cmd_conf, control_number, value_type):
732    if cmd_conf['with_operate_time']:
733        operate_time = iec61850.Timestamp(
734            value=_epoch_start,
735            leap_second=False,
736            clock_failure=False,
737            not_synchronized=False,
738            accuracy=0)
739    else:
740        operate_time = None
741    return iec61850.Command(
742        value=_value_from_json(event.payload.data['value'], value_type),
743        operate_time=operate_time,
744        origin=iec61850.Origin(
745            category=iec61850.OriginCategory[
746                event.payload.data['origin']['category']],
747            identification=event.payload.data[
748                'origin']['identification'].encode('utf-8')),
749        control_number=control_number,
750        t=_timestamp_from_event_timestamp(event.timestamp),
751        test=event.payload.data['test'],
752        checks=set(iec61850.Check[i] for i in event.payload.data['checks']))
753
754
755def _timestamp_from_event_timestamp(timestamp):
756    return iec61850.Timestamp(
757        value=hat.event.common.timestamp_to_datetime(timestamp),
758        leap_second=False,
759        clock_failure=False,
760        not_synchronized=False,
761        accuracy=None)
762
763
764def _cmd_resp_to_event(event_type_prefix, cmd_name, event_session_id, action,
765                       resp):
766    success = resp is None
767    payload = {'session_id': event_session_id,
768               'action': action,
769               'success': success}
770    if not success:
771        if resp.service_error is not None:
772            payload['service_error'] = resp.service_error.name
773        if resp.additional_cause is not None:
774            payload['additional_cause'] = resp.additional_cause.name
775        if resp.test_error is not None:
776            payload['test_error'] = resp.test_error.name
777
778    return hat.event.common.RegisterEvent(
779        type=(*event_type_prefix, 'gateway', 'command', cmd_name),
780        source_timestamp=None,
781        payload=hat.event.common.EventPayloadJson(payload))
782
783
784def _write_data_resp_to_event(event_type_prefix, value_name, session_id, resp):
785    success = resp is None
786    payload = {'session_id': session_id,
787               'success': success}
788    if not success:
789        payload['error'] = resp.name
790    return hat.event.common.RegisterEvent(
791        type=(*event_type_prefix, 'gateway', 'change', value_name),
792        source_timestamp=None,
793        payload=hat.event.common.EventPayloadJson(payload))
794
795
796def _get_command_session_id(cmd_ref, cmd):
797    return (cmd_ref, cmd.control_number)
798
799
800def _report_values_to_event(event_type_prefix, report_values_types, data_name,
801                            data_ref):
802    reasons = list(set(reason.name for rv, _ in report_values_types
803                       for reason in rv.reasons or []))
804    data = _event_data_from_report(report_values_types, data_ref)
805    payload = {'data': data,
806               'reasons': reasons}
807    return hat.event.common.RegisterEvent(
808        type=(*event_type_prefix, 'gateway', 'data', data_name),
809        source_timestamp=None,
810        payload=hat.event.common.EventPayloadJson(payload))
811
812
813def _event_data_from_report(report_values_types, data_ref):
814    values_json = {}
815    for rv, value_type in report_values_types:
816        val_json = _value_to_json(rv.value, value_type)
817        val_path = [rv.ref.logical_device, rv.ref.logical_node, *rv.ref.names]
818        values_json = json.set_(values_json, val_path, val_json)
819    return json.get(values_json, list(data_ref))
820
821
822def _value_from_json(event_value, value_type):
823    if isinstance(value_type, iec61850.BasicValueType):
824        if value_type == iec61850.BasicValueType.OCTET_STRING:
825            return bytes.fromhex(event_value)
826
827        elif value_type == iec61850.BasicValueType.FLOAT:
828            return float(event_value)
829
830        else:
831            return event_value
832
833    if value_type == iec61850.AcsiValueType.QUALITY:
834        return iec61850.Quality(
835            validity=iec61850.QualityValidity[event_value['validity']],
836            details={iec61850.QualityDetail[i]
837                     for i in event_value['details']},
838            source=iec61850.QualitySource[event_value['source']],
839            test=event_value['test'],
840            operator_blocked=event_value['operator_blocked'])
841
842    if value_type == iec61850.AcsiValueType.TIMESTAMP:
843        return iec61850.Timestamp(
844            value=datetime.datetime.fromtimestamp(event_value['value'],
845                                                  datetime.timezone.utc),
846            leap_second=event_value['leap_second'],
847            clock_failure=event_value['clock_failure'],
848            not_synchronized=event_value['not_synchronized'],
849            accuracy=event_value.get('accuracy'))
850
851    if value_type == iec61850.AcsiValueType.DOUBLE_POINT:
852        return iec61850.DoublePoint[event_value]
853
854    if value_type == iec61850.AcsiValueType.DIRECTION:
855        return iec61850.Direction[event_value]
856
857    if value_type == iec61850.AcsiValueType.SEVERITY:
858        return iec61850.Severity[event_value]
859
860    if value_type == iec61850.AcsiValueType.ANALOGUE:
861        return iec61850.Analogue(
862            i=event_value.get('i'),
863            f=(float(event_value['f']) if 'f' in event_value else None))
864
865    if value_type == iec61850.AcsiValueType.VECTOR:
866        return iec61850.Vector(
867            magnitude=_value_from_json(event_value['magnitude'],
868                                       iec61850.AcsiValueType.ANALOGUE),
869            angle=(_value_from_json(event_value['angle'],
870                                    iec61850.AcsiValueType.ANALOGUE)
871                   if 'angle' in event_value else None))
872
873    if value_type == iec61850.AcsiValueType.STEP_POSITION:
874        return iec61850.StepPosition(value=event_value['value'],
875                                     transient=event_value.get('transient'))
876
877    if value_type == iec61850.AcsiValueType.BINARY_CONTROL:
878        return iec61850.BinaryControl[event_value]
879
880    if isinstance(value_type, iec61850.ArrayValueType):
881        return [_value_from_json(val, value_type.type) for val in event_value]
882
883    if isinstance(value_type, dict):
884        return {k: _value_from_json(v, value_type[k])
885                for k, v in event_value.items()}
886
887    raise Exception('unsupported value type')
888
889
890def _value_to_json(data_value, value_type):
891    if isinstance(value_type, iec61850.BasicValueType):
892        if value_type == iec61850.BasicValueType.OCTET_STRING:
893            return data_value.hex()
894
895        elif value_type == iec61850.BasicValueType.BIT_STRING:
896            return list(data_value)
897
898        elif value_type == iec61850.BasicValueType.FLOAT:
899            return data_value if math.isfinite(data_value) else str(data_value)
900
901        else:
902            return data_value
903
904    if isinstance(value_type, iec61850.AcsiValueType):
905        if value_type == iec61850.AcsiValueType.QUALITY:
906            return {'validity': data_value.validity.name,
907                    'details': [i.name for i in data_value.details],
908                    'source': data_value.source.name,
909                    'test': data_value.test,
910                    'operator_blocked': data_value.operator_blocked}
911
912        if value_type == iec61850.AcsiValueType.TIMESTAMP:
913            val = {'value': data_value.value.timestamp(),
914                   'leap_second': data_value.leap_second,
915                   'clock_failure': data_value.clock_failure,
916                   'not_synchronized': data_value.not_synchronized}
917            if data_value.accuracy is not None:
918                val['accuracy'] = data_value.accuracy
919            return val
920
921        if value_type in [iec61850.AcsiValueType.DOUBLE_POINT,
922                          iec61850.AcsiValueType.DIRECTION,
923                          iec61850.AcsiValueType.SEVERITY,
924                          iec61850.AcsiValueType.BINARY_CONTROL]:
925            return data_value.name
926
927        if value_type == iec61850.AcsiValueType.ANALOGUE:
928            val = {}
929            if data_value.i is not None:
930                val['i'] = data_value.i
931            if data_value.f is not None:
932                val['f'] = (data_value.f if math.isfinite(data_value.f)
933                            else str(data_value.f))
934            return val
935
936        if value_type == iec61850.AcsiValueType.VECTOR:
937            val = {'magnitude': _value_to_json(
938                        data_value.magnitude, iec61850.AcsiValueType.ANALOGUE)}
939            if data_value.angle is not None:
940                val['angle'] = _value_to_json(
941                    data_value.angle, iec61850.AcsiValueType.ANALOGUE)
942            return val
943
944        if value_type == iec61850.AcsiValueType.STEP_POSITION:
945            val = {'value': data_value.value}
946            if data_value.transient is not None:
947                val['transient'] = data_value.transient
948            return val
949
950    if isinstance(value_type, iec61850.ArrayValueType):
951        return [_value_to_json(i, value_type.type) for i in data_value]
952
953    if isinstance(value_type, dict):
954        return {
955            child_name: _value_to_json(child_value, value_type[child_name])
956            for child_name, child_value in data_value.items()}
957
958    raise Exception('unsupported value type')
959
960
961def _update_ctl_num(ctl_num, action, cmd_model):
962    if action == 'SELECT' or (
963        action == 'OPERATE' and
964        cmd_model in ['DIRECT_WITH_NORMAL_SECURITY',
965                      'DIRECT_WITH_ENHANCED_SECURITY']):
966        return (ctl_num + 1) % 256
967
968    return ctl_num
969
970
971def _validate_get_rcb_response(get_rcb_resp, rcb_conf):
972    for k, v in get_rcb_resp.items():
973        if isinstance(v, iec61850.ServiceError):
974            raise Exception(f"get {k.name} failed: {v}")
975
976        if (k == iec61850.RcbAttrType.REPORT_ID and
977                v != rcb_conf['report_id']):
978            raise Exception(f"rcb report id {v} different from "
979                            f"configured {rcb_conf['report_id']}")
980
981        if (k == iec61850.RcbAttrType.DATASET and
982                v != _dataset_ref_from_conf(rcb_conf['dataset'])):
983            raise Exception(f"rcb dataset {v} different from "
984                            f"configured {rcb_conf['report_id']}")
985
986        if (k == iec61850.RcbAttrType.CONF_REVISION and
987                v != rcb_conf['conf_revision']):
988            raise Exception(
989                f"Conf revision {v} different from "
990                f"the configuration defined {rcb_conf['conf_revision']}")
mlog: logging.Logger = <Logger hat.gateway.devices.iec61850.client (WARNING)>
termination_timeout: int = 100
report_segments_timeout: int = 100
async def create( conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]) -> Iec61850ClientDevice:
 28async def create(conf: common.DeviceConf,
 29                 eventer_client: hat.event.eventer.Client,
 30                 event_type_prefix: common.EventTypePrefix
 31                 ) -> 'Iec61850ClientDevice':
 32
 33    entry_id_event_types = [
 34        (*event_type_prefix, 'gateway', 'entry_id', i['report_id'])
 35        for i in conf['rcbs'] if i['ref']['type'] == 'BUFFERED']
 36    if entry_id_event_types:
 37        result = await eventer_client.query(
 38            hat.event.common.QueryLatestParams(entry_id_event_types))
 39        rcbs_entry_ids = {
 40            event.type[5]: (bytes.fromhex(event.payload.data)
 41                            if event.payload.data is not None else None)
 42            for event in result.events}
 43    else:
 44        rcbs_entry_ids = {}
 45
 46    device = Iec61850ClientDevice()
 47
 48    device._rcbs_entry_ids = rcbs_entry_ids
 49    device._conf = conf
 50    device._eventer_client = eventer_client
 51    device._event_type_prefix = event_type_prefix
 52    device._conn = None
 53    device._conn_status = None
 54    device._terminations = {}
 55    device._reports_segments = {}
 56
 57    device._data_ref_confs = {(i['ref']['logical_device'],
 58                               i['ref']['logical_node'],
 59                               *i['ref']['names']): i for i in conf['data']}
 60    device._command_name_confs = {i['name']: i for i in conf['commands']}
 61    device._command_name_ctl_nums = {i['name']: 0 for i in conf['commands']}
 62    device._change_name_value_refs = {
 63        i['name']: _value_ref_from_conf(i['ref']) for i in conf['changes']}
 64    device._rcb_type = {rcb_conf['report_id']: rcb_conf['ref']['type']
 65                        for rcb_conf in conf['rcbs']}
 66
 67    device._persist_dyn_datasets = set(_get_persist_dyn_datasets(conf))
 68    device._dyn_datasets_values = {
 69        ds_ref: values
 70        for ds_ref, values in _get_dyn_datasets_values(conf)}
 71
 72    dataset_confs = {_dataset_ref_from_conf(ds_conf['ref']): ds_conf
 73                     for ds_conf in device._conf['datasets']}
 74    device._report_value_refs = collections.defaultdict(collections.deque)
 75    device._values_data = collections.defaultdict(set)
 76    for rcb_conf in device._conf['rcbs']:
 77        report_id = rcb_conf['report_id']
 78        ds_ref = _dataset_ref_from_conf(rcb_conf['dataset'])
 79        for value_conf in dataset_confs[ds_ref]['values']:
 80            value_ref = _value_ref_from_conf(value_conf)
 81            for data_ref, data_conf in device._data_ref_confs.items():
 82                if ('report_ids' in data_conf and
 83                        report_id not in data_conf['report_ids']):
 84                    continue
 85
 86                if not _data_matches_value(data_ref, value_ref):
 87                    continue
 88
 89                device._values_data[value_ref].add(data_ref)
 90                device._report_value_refs[report_id].append(value_ref)
 91
 92    value_types = _value_types_from_conf(conf)
 93    device._value_types = value_types
 94    device._cmd_value_types = {
 95        cmd_ref: value_type
 96        for cmd_ref, value_type in _get_command_value_types(conf, value_types)}
 97    device._data_value_types = {
 98        value_ref: value_type
 99        for value_ref, value_type in _get_data_value_types(conf, value_types)}
100
101    device._async_group = aio.Group()
102    device._async_group.spawn(device._connection_loop)
103    device._loop = asyncio.get_running_loop()
104
105    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://iec101.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec101.yaml', '$defs': {'master': {'type': 'object', 'required': ['port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'device_address_size', 'cause_size', 'asdu_address_size', 'io_address_size', 'reconnect_delay', 'remote_devices'], 'properties': {'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, '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']}}}}}}, 'slave': {'type': 'object', 'required': ['port', 'addresses', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'device_address_size', 'keep_alive_timeout', 'cause_size', 'asdu_address_size', 'io_address_size', 'buffers', 'data'], 'properties': {'port': {'type': 'string'}, 'addresses': {'type': 'array', 'items': {'type': 'integer'}}, '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'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'keep_alive_timeout': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}}}}}}, '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://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/data'}, 'report_ids': {'type': 'array', 'items': {'type': 'string'}}}}}, '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': ['data', 'reasons'], 'properties': {'data': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}}}, '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': {'data': {'type': 'object', 'required': ['logical_device', 'logical_node', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, '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_nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset_persisted'}]}, 'dataset_nonpersisted': {'type': 'string'}, 'dataset_persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, 'hat-gateway://iec104.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec104.yaml', '$defs': {'master': {'type': 'object', 'required': ['remote_addresses', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'reconnect_delay', 'time_sync_delay', 'security'], 'properties': {'remote_addresses': {'type': 'array', 'items': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}}}, 'slave': {'type': 'object', 'required': ['local_host', 'local_port', 'remote_hosts', 'max_connections', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'security', 'buffers', 'data'], 'properties': {'local_host': {'type': 'string'}, 'local_port': {'type': 'integer'}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}}}}}}, 'events': {'master': {'gateway': {'status': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/status'}, 'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/counter_interrogation'}}, 'system': {'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/counter_interrogation'}}}, 'slave': {'gateway': {'connections': {'$ref': 'hat-gateway://iec104.yaml#/$defs/messages/connections'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/gateway/command'}}, 'system': {'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/command'}}}}, 'messages': {'connections': {'type': 'array', 'items': {'type': 'object', 'required': ['connection_id', 'local', 'remote'], 'properties': {'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}}}, 'security': {'type': 'object', 'required': ['cert_path', 'key_path', 'verify_cert', 'ca_path'], 'properties': {'cert_path': {'type': 'string'}, 'key_path': {'type': ['null', 'string']}, 'verify_cert': {'type': 'boolean'}, 'ca_path': {'type': ['null', 'string']}, 'strict_mode': {'type': 'boolean'}, 'renegotiate_delay': {'type': ['null', 'number']}}}}}, 'hat-gateway://snmp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://snmp.yaml', '$defs': {'manager': {'allOf': [{'oneOf': [{'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v1'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v2c'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v3'}]}, {'type': 'object', 'required': ['remote_host', 'remote_port', 'connect_delay', 'request_timeout', 'request_retry_count', 'request_retry_delay', 'polling_delay', 'polling_oids', 'string_hex_oids'], 'properties': {'remote_host': {'type': 'string', 'description': 'Remote hostname or IP address\n'}, 'remote_port': {'type': 'integer', 'description': 'Remote UDP port\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of request/response\nexchange\n'}, 'request_retry_count': {'type': 'integer', 'description': 'Number of request retries before remote data is\nconsidered unavailable\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive request\nretries\n'}, 'polling_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive polling\ncycles\n'}, 'polling_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID read during polling cycle formated as integers\nseparated by '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value formated as\nintegers separated by '.'\n"}}}}]}, 'trap_listener': {'type': 'object', 'required': ['local_host', 'local_port', 'users', 'remote_devices'], 'properties': {'local_host': {'type': 'string', 'description': 'Local listening hostname or IP address\n'}, 'local_port': {'type': 'integer', 'description': 'Local listening UDP port\n'}, 'users': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'authentication', 'privacy'], 'properties': {'name': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'remote_devices': {'type': 'array', 'items': {'allOf': [{'oneOf': [{'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'enum': ['V1', 'V2C']}, 'community': {'type': ['null', 'string']}}}, {'type': 'object', 'required': ['version', 'context'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal\ndigits\n'}, 'name': {'type': 'string'}}}]}}}]}, {'type': 'object', 'required': ['name', 'oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'remote device name\n'}, 'oids': {'type': 'array', 'items': {'type': 'string', 'description': "data OID formated as integers separated\nby '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value\nformated as integers separated by '.'\n"}}}}]}}}}, 'managers': {'v1': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V1'}, 'community': {'type': 'string'}}}, 'v2c': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V2C'}, 'community': {'type': 'string'}}}, 'v3': {'type': 'object', 'required': ['version', 'context', 'user', 'authentication', 'privacy'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal digits\n'}, 'name': {'type': 'string'}}}]}, 'user': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'events': {'manager': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['session_id', 'cause', 'data'], 'properties': {'session_id': {'oneOf': [{'type': 'null', 'description': 'In case of INTERROGATE or CHANGE cause\n'}, {'description': 'In case of REQUESTED cause\n'}]}, 'cause': ['INTERROGATE', 'CHANGE', 'REQUESTED'], 'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}, 'write': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'read': {'type': 'object', 'required': ['session_id']}, 'write': {'type': 'object', 'required': ['session_id', 'data'], 'properties': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}}, 'trap_listener': {'gateway': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}, 'data': {'oneOf': [{'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['INTEGER', 'UNSIGNED', 'COUNTER', 'BIG_COUNTER', 'TIME_TICKS']}, 'value': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['STRING', 'STRING_HEX', 'OBJECT_ID', 'IP_ADDRESS', 'ARBITRARY']}, 'value': {'type': 'string'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'const': 'ERROR'}, 'value': {'enum': ['TOO_BIG', 'NO_SUCH_NAME', 'BAD_VALUE', 'READ_ONLY', 'GEN_ERR', 'NO_ACCESS', 'WRONG_TYPE', 'WRONG_LENGTH', 'WRONG_ENCODING', 'WRONG_VALUE', 'NO_CREATION', 'INCONSISTENT_VALUE', 'RESOURCE_UNAVAILABLE', 'COMMIT_FAILED', 'UNDO_FAILED', 'AUTHORIZATION_ERROR', 'NOT_WRITABLE', 'INCONSISTENT_NAME', 'EMPTY', 'UNSPECIFIED', 'NO_SUCH_OBJECT', 'NO_SUCH_INSTANCE', 'END_OF_MIB_VIEW', 'NOT_IN_TIME_WINDOWS', 'UNKNOWN_USER_NAMES', 'UNKNOWN_ENGINE_IDS', 'WRONG_DIGESTS', 'DECRYPTION_ERRORS']}}}]}}}, 'hat-gateway://iec103.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec103.yaml', '$defs': {'master': {'type': 'object', 'required': ['port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'reconnect_delay', 'remote_devices'], 'properties': {'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['cause', 'value'], 'properties': {'cause': {'oneOf': [{'enum': ['SPONTANEOUS', 'CYCLIC', 'TEST_MODE', 'GENERAL_INTERROGATION', 'LOCAL_OPERATION', 'REMOTE_OPERATION']}, {'type': 'integer', 'description': 'other cause in range [0, 255]\n'}]}, 'value': {'oneOf': [{'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}, {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/measurand'}]}}}, 'command': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'enable': {'type': 'boolean'}, 'command': {'type': 'object', 'required': ['session_id', 'value'], 'properties': {'value': {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}}}}}}, 'values': {'double': {'enum': ['TRANSIENT', 'OFF', 'ON', 'ERROR']}, 'measurand': {'type': 'object', 'required': ['overflow', 'invalid', 'value'], 'properties': {'overflow': {'type': 'boolean'}, 'invalid': {'type': 'boolean'}, 'value': {'type': 'number'}}}}}}, 'hat-gateway://smpp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://smpp.yaml', '$defs': {'client': {'type': 'object', 'required': ['remote_address', 'ssl', 'system_id', 'password', 'enquire_link_delay', 'enquire_link_timeout', 'connect_timeout', 'reconnect_delay', 'short_message', 'priority', 'data_coding', 'message_encoding', 'message_timeout'], 'properties': {'remote_address': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'ssl': {'type': 'boolean'}, 'system_id': {'type': 'string'}, 'password': {'type': 'string'}, 'enquire_link_delay': {'type': ['null', 'number']}, 'enquire_link_timeout': {'type': 'number'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'short_message': {'type': 'boolean'}, 'priority': {'enum': ['BULK', 'NORMAL', 'URGENT', 'VERY_URGENT']}, 'data_coding': {'enum': ['DEFAULT', 'ASCII', 'UNSPECIFIED_1', 'LATIN_1', 'UNSPECIFIED_2', 'JIS', 'CYRLLIC', 'LATIN_HEBREW', 'UCS2', 'PICTOGRAM', 'MUSIC', 'EXTENDED_KANJI', 'KS']}, 'message_encoding': {'type': 'string'}, 'message_timeout': {'type': 'number'}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}}, 'system': {'message': {'type': 'object', 'required': ['address', 'message'], 'properties': {'address': {'type': 'string'}, 'message': {'type': 'string'}}}}}}}}, 'hat-gateway://main.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://main.yaml', 'title': 'Gateway', 'description': "Gateway's configuration", 'type': 'object', 'required': ['name', 'event_server', 'devices'], 'properties': {'type': {'const': 'gateway', 'description': 'configuration type identification'}, 'version': {'type': 'string', 'description': 'component version'}, 'log': {'$ref': 'hat-json://logging.yaml'}, 'name': {'type': 'string', 'description': 'component name'}, 'event_server': {'allOf': [{'type': 'object', 'properties': {'require_operational': {'type': 'boolean'}}}, {'oneOf': [{'type': 'object', 'required': ['monitor_component'], 'properties': {'monitor_component': {'type': 'object', 'required': ['host', 'port', 'gateway_group', 'event_server_group'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23010}, 'gateway_group': {'type': 'string'}, 'event_server_group': {'type': 'string'}}}}}, {'type': 'object', 'required': ['eventer_server'], 'properties': {'eventer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23012}}}}}]}]}, 'devices': {'type': 'array', 'items': {'$ref': 'hat-gateway://main.yaml#/$defs/device'}}, 'adminer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23016}}}}, '$defs': {'device': {'type': 'object', 'description': 'structure of device configuration depends on device type\n', 'required': ['module', 'name'], 'properties': {'module': {'type': 'string', 'description': 'full python module name that implements device\n'}, 'name': {'type': 'string'}}}}}, 'hat-gateway://ping.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://ping.yaml', '$defs': {'device': {'type': 'object', 'required': ['remote_devices'], 'properties': {'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'host', 'ping_delay', 'ping_timeout', 'retry_count', 'retry_delay'], 'properties': {'name': {'type': 'string'}, 'host': {'type': 'string'}, 'ping_delay': {'type': 'number'}, 'ping_timeout': {'type': 'number'}, 'retry_count': {'type': 'number'}, 'retry_delay': {'type': 'number'}}}}}}, 'events': {'status': {'enum': ['AVAILABLE', 'NOT_AVAILABLE']}}}}, 'hat-gateway://modbus.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://modbus.yaml', 'title': 'Modbus devices', '$defs': {'master': {'type': 'object', 'title': 'Modbus master', 'required': ['connection', 'remote_devices'], 'properties': {'connection': {'type': 'object', 'required': ['modbus_type', 'transport', 'connect_timeout', 'connect_delay', 'request_timeout', 'request_delay', 'request_retry_immediate_count', 'request_retry_delayed_count', 'request_retry_delay'], 'properties': {'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'host', 'port'], 'properties': {'type': {'const': 'TCP'}, 'host': {'type': 'string', 'description': 'Remote host name\n'}, 'port': {'type': 'integer', 'description': 'Remote host TCP port\n', 'default': 502}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silet interval\n'}}}]}, 'connect_timeout': {'type': 'number', 'description': 'Maximum number of seconds available to single connection\nattempt\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of read or write\nrequest/response exchange.\n'}, 'request_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive requests\n(minimal duration between response and next request)\n'}, 'request_retry_immediate_count': {'type': 'integer', 'description': 'Number of immediate request retries before remote\ndata is considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delayed_count': {'type': 'integer', 'description': 'Number of delayed request retries before remote data\nis considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive delayed\nrequest retries\n'}}}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['device_id', 'timeout_poll_delay', 'data'], 'properties': {'device_id': {'type': 'integer', 'description': 'Modbus device identifier\n'}, 'timeout_poll_delay': {'type': 'number', 'description': 'Delay (in seconds) after read timeout and\nbefore device polling is resumed\n'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'interval', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'interval': {'type': ['number', 'null'], 'description': 'Polling interval in seconds or\nnull if polling is disabled\n'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['DISCONNECTED', 'CONNECTING', 'CONNECTED']}, 'remote_device_status': {'enum': ['DISABLED', 'CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['result'], 'properties': {'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND']}, 'value': {'type': 'integer'}, 'cause': {'enum': ['INTERROGATE', 'CHANGE']}}}, 'write': {'type': 'object', 'required': ['request_id', 'result'], 'properties': {'request_id': {'type': 'string'}, 'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND', 'TIMEOUT']}}}}, 'system': {'enable': {'type': 'boolean'}, 'write': {'type': 'object', 'required': ['request_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'value': {'type': 'integer'}}}}}}}}})
class Iec61850ClientDevice(hat.gateway.common.Device):
115class Iec61850ClientDevice(common.Device):
116
117    @property
118    def async_group(self) -> aio.Group:
119        return self._async_group
120
121    async def process_events(self, events: Collection[hat.event.common.Event]):
122        try:
123            for event in events:
124                suffix = event.type[len(self._event_type_prefix):]
125
126                if suffix[:2] == ('system', 'command'):
127                    cmd_name, = suffix[2:]
128                    await self._process_cmd_req(event, cmd_name)
129
130                elif suffix[:2] == ('system', 'change'):
131                    val_name, = suffix[2:]
132                    await self._process_change_req(event, val_name)
133
134                else:
135                    raise Exception('unsupported event type')
136
137        except Exception as e:
138            mlog.warning('error processing event: %s', e, exc_info=e)
139
140    async def _connection_loop(self):
141
142        async def cleanup():
143            await self._register_status('DISCONNECTED')
144            if self._conn:
145                await self._conn.async_close()
146
147        conn_conf = self._conf['connection']
148        try:
149            while True:
150                await self._register_status('CONNECTING')
151                try:
152                    mlog.debug('connecting to %s:%s',
153                               conn_conf['host'], conn_conf['port'])
154                    self._conn = await aio.wait_for(
155                        iec61850.connect(
156                            addr=tcp.Address(conn_conf['host'],
157                                             conn_conf['port']),
158                            data_value_types=self._data_value_types,
159                            cmd_value_types=self._cmd_value_types,
160                            report_data_refs=self._report_value_refs,
161                            report_cb=self._on_report,
162                            termination_cb=self._on_termination,
163                            status_delay=conn_conf['status_delay'],
164                            status_timeout=conn_conf['status_timeout'],
165                            local_tsel=conn_conf.get('local_tsel'),
166                            remote_tsel=conn_conf.get('remote_tsel'),
167                            local_ssel=conn_conf.get('local_ssel'),
168                            remote_ssel=conn_conf.get('remote_ssel'),
169                            local_psel=conn_conf.get('local_psel'),
170                            remote_psel=conn_conf.get('remote_psel'),
171                            local_ap_title=conn_conf.get('local_ap_title'),
172                            remote_ap_title=conn_conf.get('remote_ap_title'),
173                            local_ae_qualifier=conn_conf.get(
174                                'local_ae_qualifier'),
175                            remote_ae_qualifier=conn_conf.get(
176                                'remote_ae_qualifier'),
177                            local_detail_calling=conn_conf.get(
178                                'local_detail_calling')),
179                        conn_conf['connect_timeout'])
180
181                except Exception as e:
182                    mlog.warning('connnection failed: %s', e, exc_info=e)
183                    await self._register_status('DISCONNECTED')
184                    await asyncio.sleep(conn_conf['reconnect_delay'])
185                    continue
186
187                mlog.debug('connected')
188                await self._register_status('CONNECTED')
189
190                initialized = False
191                try:
192                    await self._create_dynamic_datasets()
193                    for rcb_conf in self._conf['rcbs']:
194                        await self._init_rcb(rcb_conf)
195                    initialized = True
196
197                except Exception as e:
198                    mlog.warning(
199                        'initialization failed: %s, closing connection',
200                        e, exc_info=e)
201                    self._conn.close()
202
203                await self._conn.wait_closing()
204                await self._register_status('DISCONNECTED')
205                await self._conn.wait_closed()
206                self._conn = None
207                self._terminations = {}
208                self._reports_segments = {}
209                if not initialized:
210                    await asyncio.sleep(conn_conf['reconnect_delay'])
211
212        except Exception as e:
213            mlog.error('connection loop error: %s', e, exc_info=e)
214
215        finally:
216            mlog.debug('closing connection loop')
217            self.close()
218            await aio.uncancellable(cleanup())
219
220    async def _process_cmd_req(self, event, cmd_name):
221        if not self._conn or not self._conn.is_open:
222            mlog.warning('command %s ignored: no connection', cmd_name)
223            return
224
225        if cmd_name not in self._command_name_confs:
226            raise Exception('unexpected command name')
227
228        cmd_conf = self._command_name_confs[cmd_name]
229        cmd_ref = iec61850.CommandRef(**cmd_conf['ref'])
230        action = event.payload.data['action']
231        evt_session_id = event.payload.data['session_id']
232        if (action == 'SELECT' and
233                cmd_conf['model'] == 'SBO_WITH_NORMAL_SECURITY'):
234            cmd = None
235        else:
236            ctl_num = self._command_name_ctl_nums[cmd_name]
237            ctl_num = _update_ctl_num(ctl_num, action, cmd_conf['model'])
238            self._command_name_ctl_nums[cmd_name] = ctl_num
239            value_type = _value_type_from_ref(self._value_types, cmd_ref)
240            cmd = _command_from_event(event, cmd_conf, ctl_num, value_type)
241
242        term_future = None
243        if (action == 'OPERATE' and
244                cmd_conf['model'] in ['DIRECT_WITH_ENHANCED_SECURITY',
245                                      'SBO_WITH_ENHANCED_SECURITY']):
246            term_future = self._loop.create_future()
247            self._conn.async_group.spawn(
248                self._wait_cmd_term, cmd_name, cmd_ref, cmd, evt_session_id,
249                term_future)
250
251        try:
252            resp = await aio.wait_for(
253                self._send_command(action, cmd_ref, cmd),
254                self._conf['connection']['response_timeout'])
255
256        except (asyncio.TimeoutError, ConnectionError) as e:
257            mlog.warning('send command failed: %s', e, exc_info=e)
258            if term_future and not term_future.done():
259                term_future.cancel()
260            return
261
262        if resp is not None:
263            if term_future and not term_future.done():
264                term_future.cancel()
265
266        event = _cmd_resp_to_event(
267            self._event_type_prefix, cmd_name, evt_session_id, action, resp)
268        await self._register_events([event])
269
270    async def _send_command(self, action, cmd_ref, cmd):
271        if action == 'SELECT':
272            return await self._conn.select(cmd_ref, cmd)
273
274        if action == 'CANCEL':
275            return await self._conn.cancel(cmd_ref, cmd)
276
277        if action == 'OPERATE':
278            return await self._conn.operate(cmd_ref, cmd)
279
280        raise Exception('unsupported action')
281
282    async def _wait_cmd_term(self, cmd_name, cmd_ref, cmd, session_id, future):
283        cmd_session_id = _get_command_session_id(cmd_ref, cmd)
284        self._terminations[cmd_session_id] = future
285        try:
286            term = await aio.wait_for(future, termination_timeout)
287            event = _cmd_resp_to_event(
288                self._event_type_prefix, cmd_name, session_id, 'TERMINATION',
289                term.error)
290            await self._register_events([event])
291
292        except asyncio.TimeoutError:
293            mlog.warning('command termination timeout')
294
295        finally:
296            del self._terminations[cmd_session_id]
297
298    async def _process_change_req(self, event, value_name):
299        if not self._conn or not self._conn.is_open:
300            mlog.warning('change event %s ignored: no connection', value_name)
301            return
302
303        if value_name not in self._change_name_value_refs:
304            raise Exception('unexpected command name')
305
306        ref = self._change_name_value_refs[value_name]
307        value_type = _value_type_from_ref(self._value_types, ref)
308        if value_type is None:
309            raise Exception('value type undefined')
310
311        value = _value_from_json(event.payload.data['value'], value_type)
312        try:
313            resp = await aio.wait_for(
314                self._conn.write_data(ref, value),
315                self._conf['connection']['response_timeout'])
316
317        except asyncio.TimeoutError:
318            mlog.warning('write data response timeout')
319            return
320
321        except ConnectionError as e:
322            mlog.warning('connection error on write data: %s', e, exc_info=e)
323            return
324
325        session_id = event.payload.data['session_id']
326        event = _write_data_resp_to_event(
327            self._event_type_prefix, value_name, session_id, resp)
328        await self._register_events([event])
329
330    async def _on_report(self, report):
331        report_id = report.report_id
332        if report_id not in self._report_value_refs:
333            mlog.warning('unexpected report dropped')
334            return
335
336        segm_id = (report_id, report.sequence_number)
337        if report.more_segments_follow:
338            if segm_id in self._reports_segments:
339                segment_data, timeout_timer = self._reports_segments[segm_id]
340                timeout_timer.cancel()
341            else:
342                segment_data = collections.deque()
343
344            segment_data.extend(report.data)
345            timeout_timer = self._loop.call_later(
346                report_segments_timeout, self._reports_segments.pop, segm_id)
347            self._reports_segments[segm_id] = (segment_data, timeout_timer)
348            return
349
350        if segm_id in self._reports_segments:
351            report_data, timeout_timer = self._reports_segments.pop(segm_id)
352            timeout_timer.cancel()
353            report_data.extend(report.data)
354
355        else:
356            report_data = report.data
357
358        events = collections.deque()
359        events.extend(self._report_data_to_events(report_data, report_id))
360
361        if self._rcb_type[report_id] == 'BUFFERED':
362            events.append(hat.event.common.RegisterEvent(
363                type=(*self._event_type_prefix, 'gateway',
364                      'entry_id', report_id),
365                source_timestamp=None,
366                payload=hat.event.common.EventPayloadJson(
367                    report.entry_id.hex()
368                    if report.entry_id is not None else None)))
369
370        await self._register_events(events)
371        if self._rcb_type[report_id] == 'BUFFERED':
372            self._rcbs_entry_ids[report_id] = report.entry_id
373
374    def _report_data_to_events(self, report_data, report_id):
375        report_data_values_types = collections.defaultdict(collections.deque)
376        for rv in report_data:
377            data_refs = self._values_data.get(rv.ref)
378            value_type = _value_type_from_ref(self._value_types, rv.ref)
379            for data_ref in data_refs:
380                report_data_values_types[data_ref].append((rv, value_type))
381
382        for data_ref, report_values_types in report_data_values_types.items():
383            data_conf = self._data_ref_confs[data_ref]
384            if ('report_ids' in data_conf and
385                    report_id not in data_conf['report_ids']):
386                continue
387
388            try:
389                yield _report_values_to_event(
390                    self._event_type_prefix, report_values_types,
391                    data_conf['name'], data_ref)
392
393            except Exception as e:
394                mlog.warning('report values dropped: %s', e, exc_info=e)
395                continue
396
397    def _on_termination(self, termination):
398        cmd_session_id = _get_command_session_id(
399            termination.ref, termination.cmd)
400        if cmd_session_id not in self._terminations:
401            mlog.warning('unexpected termination dropped')
402            return
403
404        term_future = self._terminations[cmd_session_id]
405        if not term_future.done():
406            self._terminations[cmd_session_id].set_result(termination)
407
408    async def _init_rcb(self, rcb_conf):
409        ref = iec61850.RcbRef(
410            logical_device=rcb_conf['ref']['logical_device'],
411            logical_node=rcb_conf['ref']['logical_node'],
412            type=iec61850.RcbType[rcb_conf['ref']['type']],
413            name=rcb_conf['ref']['name'])
414        mlog.debug('initiating rcb %s', ref)
415
416        get_attrs = collections.deque([iec61850.RcbAttrType.REPORT_ID])
417        dataset_ref = _dataset_ref_from_conf(rcb_conf['dataset'])
418        if dataset_ref not in self._dyn_datasets_values:
419            get_attrs.append(iec61850.RcbAttrType.DATASET)
420        if 'conf_revision' in rcb_conf:
421            get_attrs.append(iec61850.RcbAttrType.CONF_REVISION)
422
423        get_rcb_resp = await self._conn.get_rcb_attrs(ref, get_attrs)
424        _validate_get_rcb_response(get_rcb_resp, rcb_conf)
425
426        await self._set_rcb(ref, [(iec61850.RcbAttrType.REPORT_ENABLE, False)])
427
428        if dataset_ref in self._dyn_datasets_values:
429            await self._set_rcb(
430                ref, [(iec61850.RcbAttrType.DATASET, dataset_ref)],
431                critical=True)
432
433        if ref.type == iec61850.RcbType.BUFFERED:
434            if 'reservation_time' in rcb_conf:
435                await self._set_rcb(
436                    ref, [(iec61850.RcbAttrType.RESERVATION_TIME,
437                           rcb_conf['reservation_time'])])
438
439            entry_id = self._rcbs_entry_ids.get(rcb_conf['report_id'])
440            if rcb_conf.get('purge_buffer') or entry_id is None:
441                await self._set_rcb(
442                    ref, [(iec61850.RcbAttrType.PURGE_BUFFER, True)])
443
444            else:
445                try:
446                    await self._set_rcb(
447                        ref, [(iec61850.RcbAttrType.ENTRY_ID, entry_id)],
448                        critical=True)
449
450                except Exception as e:
451                    mlog.warning('%s', e, exc_info=e)
452                    # try setting entry id to 0 in order to resynchronize
453                    await self._set_rcb(
454                        ref, [(iec61850.RcbAttrType.ENTRY_ID, b'\x00')])
455
456        elif ref.type == iec61850.RcbType.UNBUFFERED:
457            await self._set_rcb(ref, [(iec61850.RcbAttrType.RESERVE, True)])
458
459        attrs = collections.deque()
460        if 'trigger_options' in rcb_conf:
461            attrs.append((iec61850.RcbAttrType.TRIGGER_OPTIONS,
462                          set(iec61850.TriggerCondition[i]
463                              for i in rcb_conf['trigger_options'])))
464        if 'optional_fields' in rcb_conf:
465            attrs.append((iec61850.RcbAttrType.OPTIONAL_FIELDS,
466                          set(iec61850.OptionalField[i]
467                              for i in rcb_conf['optional_fields'])))
468        if 'buffer_time' in rcb_conf:
469            attrs.append((iec61850.RcbAttrType.BUFFER_TIME,
470                          rcb_conf['buffer_time']))
471        if 'integrity_period' in rcb_conf:
472            attrs.append((iec61850.RcbAttrType.INTEGRITY_PERIOD,
473                          rcb_conf['integrity_period']))
474        if attrs:
475            await self._set_rcb(ref, attrs)
476
477        await self._set_rcb(
478            ref, [(iec61850.RcbAttrType.REPORT_ENABLE, True)], critical=True)
479        await self._set_rcb(
480            ref, [(iec61850.RcbAttrType.GI, True)], critical=True)
481        mlog.debug('rcb %s initiated', ref)
482
483    async def _set_rcb(self, ref, attrs, critical=False):
484        try:
485            resp = await self._conn.set_rcb_attrs(ref, attrs)
486            attrs_failed = set((attr, attr_res)
487                               for attr, attr_res in resp.items()
488                               if isinstance(attr_res, iec61850.ServiceError))
489            if attrs_failed:
490                raise Exception(f"set attribute errors: {attrs_failed}")
491
492        except Exception as e:
493            if critical:
494                raise Exception(f'set rcb {ref} failed') from e
495
496            else:
497                mlog.warning('set rcb %s failed: %s', ref, e, exc_info=e)
498
499    async def _create_dynamic_datasets(self):
500        existing_ds_refs = set()
501        for ds_ref in self._persist_dyn_datasets:
502            ld = ds_ref.logical_device
503            res = await self._conn.get_persisted_dataset_refs(ld)
504            if isinstance(res, iec61850.ServiceError):
505                raise Exception(f'get datasets for ld {ld} failed: {res}')
506
507            existing_ds_refs.update(res)
508
509        existing_persisted_ds_refs = existing_ds_refs.intersection(
510            self._persist_dyn_datasets)
511        for ds_ref, ds_value_refs in self._dyn_datasets_values.items():
512            if ds_ref in existing_persisted_ds_refs:
513                res = await self._conn.get_dataset_data_refs(ds_ref)
514                if isinstance(res, iec61850.ServiceError):
515                    raise Exception(f'get ds {ds_ref} data refs failed: {res}')
516                else:
517                    exist_ds_value_refs = res
518
519                if ds_value_refs == list(exist_ds_value_refs):
520                    mlog.debug('dataset %s already exists', ds_ref)
521                    continue
522
523                mlog.debug("dataset %s exists, but different", ds_ref)
524                res = await self._conn.delete_dataset(ds_ref)
525                if res is not None:
526                    raise Exception(f'delete dataset {ds_ref} failed: {res}')
527                mlog.debug("dataset %s deleted", ds_ref)
528
529            res = await self._conn.create_dataset(ds_ref, ds_value_refs)
530            if res is not None:
531                raise Exception(f'create dataset {ds_ref} failed: {res}')
532
533            mlog.debug("dataset %s crated", ds_ref)
534
535    async def _register_events(self, events):
536        try:
537            await self._eventer_client.register(events)
538
539        except ConnectionError:
540            self.close()
541
542    async def _register_status(self, status):
543        if status == self._conn_status:
544            return
545
546        event = hat.event.common.RegisterEvent(
547            type=(*self._event_type_prefix, 'gateway', 'status'),
548            source_timestamp=None,
549            payload=hat.event.common.EventPayloadJson(status))
550        await self._register_events([event])
551        self._conn_status = status
552        mlog.debug('registered status %s', status)

Device interface

async_group: hat.aio.group.Group
117    @property
118    def async_group(self) -> aio.Group:
119        return self._async_group

Group controlling resource's lifetime.

async def process_events(self, events: Collection[hat.event.common.common.Event]):
121    async def process_events(self, events: Collection[hat.event.common.Event]):
122        try:
123            for event in events:
124                suffix = event.type[len(self._event_type_prefix):]
125
126                if suffix[:2] == ('system', 'command'):
127                    cmd_name, = suffix[2:]
128                    await self._process_cmd_req(event, cmd_name)
129
130                elif suffix[:2] == ('system', 'change'):
131                    val_name, = suffix[2:]
132                    await self._process_change_req(event, val_name)
133
134                else:
135                    raise Exception('unsupported event type')
136
137        except Exception as e:
138            mlog.warning('error processing event: %s', e, exc_info=e)

Process received events

This method can be coroutine or regular function.