hat.gateway.devices.iec101.master

IEC 60870-5-101 master device

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

Device interface

Iec101MasterDevice( conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str], send_queue_size: int = 1024)
57    def __init__(self,
58                 conf: common.DeviceConf,
59                 eventer_client: hat.event.eventer.Client,
60                 event_type_prefix: common.EventTypePrefix,
61                 send_queue_size: int = 1024):
62        self._conf = conf
63        self._event_type_prefix = event_type_prefix
64        self._eventer_client = eventer_client
65        self._link = None
66        self._conns = {}
67        self._send_queue = aio.Queue(send_queue_size)
68        self._async_group = aio.Group()
69        self._remote_enabled = {i['address']: False
70                                for i in conf['remote_devices']}
71        self._remote_confs = {i['address']: i
72                              for i in conf['remote_devices']}
73        self._remote_groups = {}
74
75        self.async_group.spawn(self._link_loop)
76        self.async_group.spawn(self._send_loop)
async_group: hat.aio.group.Group
78    @property
79    def async_group(self) -> aio.Group:
80        return self._async_group

Group controlling resource's lifetime.

async def process_events(self, events: Collection[hat.event.common.common.Event]):
82    async def process_events(self, events: Collection[hat.event.common.Event]):
83        for event in events:
84            try:
85                await self._process_event(event)
86
87            except Exception as e:
88                mlog.warning('error processing event: %s', e, exc_info=e)

Process received events

This method can be coroutine or regular function.