hat.gateway.devices.iec101.slave

IEC 60870-5-104 slave device

  1"""IEC 60870-5-104 slave device"""
  2
  3from collections.abc import Collection, Iterable
  4import asyncio
  5import collections
  6import contextlib
  7import functools
  8import itertools
  9import logging
 10
 11from hat import aio
 12from hat import json
 13from hat.drivers import iec101
 14from hat.drivers import serial
 15from hat.drivers.iec60870 import link
 16import hat.event.common
 17import hat.event.eventer
 18
 19from hat.gateway.devices.iec101 import common
 20
 21
 22mlog: logging.Logger = logging.getLogger(__name__)
 23
 24
 25async def create(conf: common.DeviceConf,
 26                 eventer_client: hat.event.eventer.Client,
 27                 event_type_prefix: common.EventTypePrefix
 28                 ) -> 'Iec101SlaveDevice':
 29    device = Iec101SlaveDevice()
 30    device._conf = conf
 31    device._eventer_client = eventer_client
 32    device._event_type_prefix = event_type_prefix
 33    device._next_conn_ids = itertools.count(1)
 34    device._conns = {}
 35    device._send_queues = {}
 36    device._buffers = {}
 37    device._data_msgs = {}
 38    device._data_buffers = {}
 39
 40    init_buffers(buffers_conf=conf['buffers'],
 41                 buffers=device._buffers)
 42
 43    await init_data(data_conf=conf['data'],
 44                    data_msgs=device._data_msgs,
 45                    data_buffers=device._data_buffers,
 46                    buffers=device._buffers,
 47                    eventer_client=eventer_client,
 48                    event_type_prefix=event_type_prefix)
 49
 50    if conf['link_type'] == 'BALANCED':
 51        create_link = link.create_balanced_link
 52
 53    elif conf['link_type'] == 'UNBALANCED':
 54        create_link = link.create_slave_link
 55
 56    else:
 57        raise ValueError('unsupported link type')
 58
 59    device._link = await create_link(
 60        port=conf['port'],
 61        address_size=link.AddressSize[conf['device_address_size']],
 62        silent_interval=conf['silent_interval'],
 63        baudrate=conf['baudrate'],
 64        bytesize=serial.ByteSize[conf['bytesize']],
 65        parity=serial.Parity[conf['parity']],
 66        stopbits=serial.StopBits[conf['stopbits']],
 67        xonxoff=conf['flow_control']['xonxoff'],
 68        rtscts=conf['flow_control']['rtscts'],
 69        dsrdtr=conf['flow_control']['dsrdtr'])
 70
 71    try:
 72        for device_conf in conf['devices']:
 73            device.async_group.spawn(device._connection_loop, device_conf)
 74
 75        await device._register_connections()
 76
 77    except BaseException:
 78        await aio.uncancellable(device.async_close())
 79        raise
 80
 81    return device
 82
 83
 84info: common.DeviceInfo = common.DeviceInfo(
 85    type="iec101_slave",
 86    create=create,
 87    json_schema_id="hat-gateway://iec101.yaml#/$defs/slave",
 88    json_schema_repo=common.json_schema_repo)
 89
 90
 91class Buffer:
 92
 93    def __init__(self, size: int):
 94        self._size = size
 95        self._data = collections.OrderedDict()
 96
 97    def add(self,
 98            event_id: hat.event.common.EventId,
 99            data_msg: iec101.DataMsg):
100        self._data[event_id] = data_msg
101        while len(self._data) > self._size:
102            self._data.popitem(last=False)
103
104    def remove(self, event_id: hat.event.common.EventId):
105        self._data.pop(event_id, None)
106
107    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
108                                                       iec101.DataMsg]]:
109        return self._data.items()
110
111
112def init_buffers(buffers_conf: json.Data,
113                 buffers: dict[str, Buffer]):
114    for buffer_conf in buffers_conf:
115        buffers[buffer_conf['name']] = Buffer(buffer_conf['size'])
116
117
118async def init_data(data_conf: json.Data,
119                    data_msgs: dict[common.DataKey, iec101.DataMsg],
120                    data_buffers: dict[common.DataKey, Buffer],
121                    buffers: dict[str, Buffer],
122                    eventer_client: hat.event.eventer.Client,
123                    event_type_prefix: common.EventTypePrefix):
124    for data in data_conf:
125        data_key = common.DataKey(data_type=common.DataType[data['data_type']],
126                                  asdu_address=data['asdu_address'],
127                                  io_address=data['io_address'])
128        data_msgs[data_key] = None
129        if data['buffer']:
130            data_buffers[data_key] = buffers[data['buffer']]
131
132    event_types = [(*event_type_prefix, 'system', 'data', '*')]
133    params = hat.event.common.QueryLatestParams(event_types)
134    result = await eventer_client.query(params)
135
136    for event in result.events:
137        try:
138            data_type_str, asdu_address_str, io_address_str = \
139                event.type[len(event_type_prefix)+2:]
140            data_key = common.DataKey(data_type=common.DataType(data_type_str),
141                                      asdu_address=int(asdu_address_str),
142                                      io_address=int(io_address_str))
143            if data_key not in data_msgs:
144                raise Exception(f'data {data_key} not configured')
145
146            data_msgs[data_key] = data_msg_from_event(data_key, event)
147
148        except Exception as e:
149            mlog.debug('skipping initial data: %s', e, exc_info=e)
150
151
152class Iec101SlaveDevice(common.Device):
153
154    @property
155    def async_group(self) -> aio.Group:
156        return self._link.async_group
157
158    async def process_events(self, events: Collection[hat.event.common.Event]):
159        for event in events:
160            try:
161                await self._process_event(event)
162
163            except Exception as e:
164                mlog.warning('error processing event: %s', e, exc_info=e)
165
166    async def _connection_loop(self, device_conf):
167        conn = None
168
169        try:
170            if self._conf['link_type'] == 'BALANCED':
171                conn_args = {
172                    'direction': link.Direction[device_conf['direction']],
173                    'addr': device_conf['address'],
174                    'response_timeout': device_conf['response_timeout'],
175                    'send_retry_count': device_conf['send_retry_count'],
176                    'status_delay': device_conf['status_delay']}
177
178            elif self._conf['link_type'] == 'UNBALANCED':
179                conn_args = {
180                    'addr': device_conf['address'],
181                    'keep_alive_timeout': device_conf['keep_alive_timeout']}
182
183            else:
184                raise ValueError('unsupported link type')
185
186            while True:
187                try:
188                    conn = await self._link.open_connection(**conn_args)
189
190                except Exception as e:
191                    mlog.error('connection error for address %s: %s',
192                               device_conf['address'], e, exc_info=e)
193                    await asyncio.sleep(device_conf['reconnect_delay'])
194                    continue
195
196                conn = iec101.Connection(
197                    conn=conn,
198                    cause_size=iec101.CauseSize[self._conf['cause_size']],
199                    asdu_address_size=iec101.AsduAddressSize[
200                        self._conf['asdu_address_size']],
201                    io_address_size=iec101.IoAddressSize[
202                        self._conf['io_address_size']])
203
204                conn_id = next(self._next_conn_ids)
205                self._conns[conn_id] = conn
206
207                send_queue = aio.Queue(1024)
208                self._send_queues[conn_id] = send_queue
209
210                try:
211                    conn.async_group.spawn(self._connection_send_loop, conn,
212                                           send_queue)
213                    conn.async_group.spawn(self._connection_receive_loop, conn,
214                                           conn_id)
215
216                    await self._register_connections()
217
218                    with contextlib.suppress(Exception):
219                        for buffer in self._buffers.values():
220                            for event_id, data_msg in buffer.get_event_id_data_msgs():  # NOQA
221                                await self._send_data_msg(conn_id, buffer,
222                                                          event_id, data_msg)
223
224                    await conn.wait_closed()
225
226                finally:
227                    send_queue.close()
228
229                    self._conns.pop(conn_id, None)
230                    self._send_queues.pop(conn_id, None)
231
232                    with contextlib.suppress(Exception):
233                        await aio.uncancellable(self._register_connections())
234
235                await conn.async_close()
236
237        except Exception as e:
238            mlog.warning('connection loop error: %s', e, exc_info=e)
239
240        finally:
241            mlog.debug('closing connection')
242            self.close()
243
244            if conn:
245                await aio.uncancellable(conn.async_close())
246
247    async def _connection_send_loop(self, conn, send_queue):
248        try:
249            while True:
250                msgs, sent_cb = await send_queue.get()
251                await conn.send(msgs, sent_cb=sent_cb)
252
253        except ConnectionError:
254            mlog.debug('connection close')
255
256        except Exception as e:
257            mlog.warning('connection send loop error: %s', e, exc_info=e)
258
259        finally:
260            conn.close()
261
262    async def _connection_receive_loop(self, conn, conn_id):
263        try:
264            while True:
265                msgs = await conn.receive()
266
267                for msg in msgs:
268                    try:
269                        mlog.debug('received message: %s', msg)
270                        await self._process_msg(conn_id, msg)
271
272                    except Exception as e:
273                        mlog.warning('error processing message: %s',
274                                     e, exc_info=e)
275
276        except ConnectionError:
277            mlog.debug('connection close')
278
279        except Exception as e:
280            mlog.warning('connection receive loop error: %s', e, exc_info=e)
281
282        finally:
283            conn.close()
284
285    async def _register_connections(self):
286        payload = [{'connection_id': conn_id,
287                    'address': conn.address}
288                   for conn_id, conn in self._conns.items()]
289
290        event = hat.event.common.RegisterEvent(
291            type=(*self._event_type_prefix, 'gateway', 'connections'),
292            source_timestamp=None,
293            payload=hat.event.common.EventPayloadJson(payload))
294
295        await self._eventer_client.register([event])
296
297    async def _process_event(self, event):
298        suffix = event.type[len(self._event_type_prefix):]
299
300        if suffix[:2] == ('system', 'data'):
301            data_type_str, asdu_address_str, io_address_str = suffix[2:]
302            data_key = common.DataKey(data_type=common.DataType(data_type_str),
303                                      asdu_address=int(asdu_address_str),
304                                      io_address=int(io_address_str))
305
306            await self._process_data_event(data_key, event)
307
308        elif suffix[:2] == ('system', 'command'):
309            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
310            cmd_key = common.CommandKey(
311                cmd_type=common.CommandType(cmd_type_str),
312                asdu_address=int(asdu_address_str),
313                io_address=int(io_address_str))
314
315            await self._process_command_event(cmd_key, event)
316
317        else:
318            raise Exception('unsupported event type')
319
320    async def _process_data_event(self, data_key, event):
321        if data_key not in self._data_msgs:
322            raise Exception('data not configured')
323
324        data_msg = data_msg_from_event(data_key, event)
325        self._data_msgs[data_key] = data_msg
326
327        buffer = self._data_buffers.get(data_key)
328        if buffer:
329            buffer.add(event.id, data_msg)
330
331        for conn_id in self._conns.keys():
332            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
333
334    async def _process_command_event(self, cmd_key, event):
335        cmd_msg = cmd_msg_from_event(cmd_key, event)
336        conn_id = event.payload.data['connection_id']
337        await self._send(conn_id, [cmd_msg])
338
339    async def _process_msg(self, conn_id, msg):
340        if isinstance(msg, iec101.CommandMsg):
341            await self._process_command_msg(conn_id, msg)
342
343        elif isinstance(msg, iec101.InterrogationMsg):
344            await self._process_interrogation_msg(conn_id, msg)
345
346        elif isinstance(msg, iec101.CounterInterrogationMsg):
347            await self._process_counter_interrogation_msg(conn_id, msg)
348
349        elif isinstance(msg, iec101.ReadMsg):
350            await self._process_read_msg(conn_id, msg)
351
352        elif isinstance(msg, iec101.ClockSyncMsg):
353            await self._process_clock_sync_msg(conn_id, msg)
354
355        elif isinstance(msg, iec101.TestMsg):
356            await self._process_test_msg(conn_id, msg)
357
358        elif isinstance(msg, iec101.ResetMsg):
359            await self._process_reset_msg(conn_id, msg)
360
361        elif isinstance(msg, iec101.ParameterMsg):
362            await self._process_parameter_msg(conn_id, msg)
363
364        elif isinstance(msg, iec101.ParameterActivationMsg):
365            await self._process_parameter_activation_msg(conn_id, msg)
366
367        else:
368            raise Exception('unsupported message')
369
370    async def _process_command_msg(self, conn_id, msg):
371        if isinstance(msg.cause, iec101.CommandReqCause):
372            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
373            await self._eventer_client.register([event])
374
375        else:
376            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
377                               is_negative_confirm=True)
378            await self._send(conn_id, [res])
379
380    async def _process_interrogation_msg(self, conn_id, msg):
381        if msg.cause == iec101.CommandReqCause.ACTIVATION:
382            res = msg._replace(
383                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
384                is_negative_confirm=False)
385            await self._send(conn_id, [res])
386
387            data_msgs = [
388                data_msg._replace(
389                    is_test=msg.is_test,
390                    cause=iec101.DataResCause.INTERROGATED_STATION)
391                for data_msg in self._data_msgs.values()
392                if (data_msg and
393                    (msg.asdu_address == 0xFFFF or
394                     msg.asdu_address == data_msg.asdu_address) and
395                    not isinstance(data_msg.data, iec101.BinaryCounterData))]
396            await self._send(conn_id, data_msgs)
397
398            res = msg._replace(
399                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
400                is_negative_confirm=False)
401            await self._send(conn_id, [res])
402
403        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
404            res = msg._replace(
405                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
406                is_negative_confirm=True)
407            await self._send(conn_id, [res])
408
409        else:
410            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
411                               is_negative_confirm=True)
412            await self._send(conn_id, [res])
413
414    async def _process_counter_interrogation_msg(self, conn_id, msg):
415        if msg.cause == iec101.CommandReqCause.ACTIVATION:
416            res = msg._replace(
417                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
418                is_negative_confirm=False)
419            await self._send(conn_id, [res])
420
421            data_msgs = [
422                data_msg._replace(
423                    is_test=msg.is_test,
424                    cause=iec101.DataResCause.INTERROGATED_COUNTER)
425                for data_msg in self._data_msgs.values()
426                if (data_msg and
427                    (msg.asdu_address == 0xFFFF or
428                     msg.asdu_address == data_msg.asdu_address) and
429                    isinstance(data_msg.data, iec101.BinaryCounterData))]
430            await self._send(conn_id, data_msgs)
431
432            res = msg._replace(
433                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
434                is_negative_confirm=False)
435            await self._send(conn_id, [res])
436
437        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
438            res = msg._replace(
439                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
440                is_negative_confirm=True)
441            await self._send(conn_id, [res])
442
443        else:
444            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
445                               is_negative_confirm=True)
446            await self._send(conn_id, [res])
447
448    async def _process_read_msg(self, conn_id, msg):
449        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
450        await self._send(conn_id, [res])
451
452    async def _process_clock_sync_msg(self, conn_id, msg):
453        if isinstance(msg.cause, iec101.ClockSyncReqCause):
454            res = msg._replace(
455                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
456                is_negative_confirm=True)
457            await self._send(conn_id, [res])
458
459        else:
460            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
461                               is_negative_confirm=True)
462            await self._send(conn_id, [res])
463
464    async def _process_test_msg(self, conn_id, msg):
465        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
466        await self._send(conn_id, [res])
467
468    async def _process_reset_msg(self, conn_id, msg):
469        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
470        await self._send(conn_id, [res])
471
472    async def _process_parameter_msg(self, conn_id, msg):
473        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
474        await self._send(conn_id, [res])
475
476    async def _process_parameter_activation_msg(self, conn_id, msg):
477        res = msg._replace(
478            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
479        await self._send(conn_id, [res])
480
481    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
482        sent_cb = (functools.partial(buffer.remove, event_id)
483                   if buffer else None)
484        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
485
486    async def _send(self, conn_id, msgs, sent_cb=None):
487        send_queue = self._send_queues.get(conn_id)
488        if send_queue is None:
489            return
490
491        with contextlib.suppress(aio.QueueClosedError):
492            await send_queue.put((msgs, sent_cb))
493
494
495def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
496                     conn_id: int,
497                     msg: iec101.CommandMsg
498                     ) -> hat.event.common.RegisterEvent:
499    command_type = common.get_command_type(msg.command)
500    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
501    command = common.command_to_json(msg.command)
502    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
503                  str(msg.asdu_address), str(msg.io_address))
504
505    return hat.event.common.RegisterEvent(
506        type=event_type,
507        source_timestamp=None,
508        payload=hat.event.common.EventPayloadJson({
509            'connection_id': conn_id,
510            'is_test': msg.is_test,
511            'cause': cause,
512            'command': command}))
513
514
515def data_msg_from_event(data_key: common.DataKey,
516                        event: hat.event.common.Event
517                        ) -> iec101.DataMsg:
518    time = common.time_from_source_timestamp(event.source_timestamp)
519    cause = common.cause_from_json(iec101.DataResCause,
520                                   event.payload.data['cause'])
521    data = common.data_from_json(data_key.data_type,
522                                 event.payload.data['data'])
523
524    return iec101.DataMsg(is_test=event.payload.data['is_test'],
525                          originator_address=0,
526                          asdu_address=data_key.asdu_address,
527                          io_address=data_key.io_address,
528                          data=data,
529                          time=time,
530                          cause=cause)
531
532
533def cmd_msg_from_event(cmd_key: common.CommandKey,
534                       event: hat.event.common.Event
535                       ) -> iec101.CommandMsg:
536    cause = common.cause_from_json(iec101.CommandResCause,
537                                   event.payload.data['cause'])
538    command = common.command_from_json(cmd_key.cmd_type,
539                                       event.payload.data['command'])
540    is_negative_confirm = event.payload.data['is_negative_confirm']
541
542    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
543                             originator_address=0,
544                             asdu_address=cmd_key.asdu_address,
545                             io_address=cmd_key.io_address,
546                             command=command,
547                             is_negative_confirm=is_negative_confirm,
548                             cause=cause)
mlog: logging.Logger = <Logger hat.gateway.devices.iec101.slave (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]) -> Iec101SlaveDevice:
26async def create(conf: common.DeviceConf,
27                 eventer_client: hat.event.eventer.Client,
28                 event_type_prefix: common.EventTypePrefix
29                 ) -> 'Iec101SlaveDevice':
30    device = Iec101SlaveDevice()
31    device._conf = conf
32    device._eventer_client = eventer_client
33    device._event_type_prefix = event_type_prefix
34    device._next_conn_ids = itertools.count(1)
35    device._conns = {}
36    device._send_queues = {}
37    device._buffers = {}
38    device._data_msgs = {}
39    device._data_buffers = {}
40
41    init_buffers(buffers_conf=conf['buffers'],
42                 buffers=device._buffers)
43
44    await init_data(data_conf=conf['data'],
45                    data_msgs=device._data_msgs,
46                    data_buffers=device._data_buffers,
47                    buffers=device._buffers,
48                    eventer_client=eventer_client,
49                    event_type_prefix=event_type_prefix)
50
51    if conf['link_type'] == 'BALANCED':
52        create_link = link.create_balanced_link
53
54    elif conf['link_type'] == 'UNBALANCED':
55        create_link = link.create_slave_link
56
57    else:
58        raise ValueError('unsupported link type')
59
60    device._link = await create_link(
61        port=conf['port'],
62        address_size=link.AddressSize[conf['device_address_size']],
63        silent_interval=conf['silent_interval'],
64        baudrate=conf['baudrate'],
65        bytesize=serial.ByteSize[conf['bytesize']],
66        parity=serial.Parity[conf['parity']],
67        stopbits=serial.StopBits[conf['stopbits']],
68        xonxoff=conf['flow_control']['xonxoff'],
69        rtscts=conf['flow_control']['rtscts'],
70        dsrdtr=conf['flow_control']['dsrdtr'])
71
72    try:
73        for device_conf in conf['devices']:
74            device.async_group.spawn(device._connection_loop, device_conf)
75
76        await device._register_connections()
77
78    except BaseException:
79        await aio.uncancellable(device.async_close())
80        raise
81
82    return device
info: hat.gateway.common.DeviceInfo = DeviceInfo(type='iec101_slave', create=<function create>, json_schema_id='hat-gateway://iec101.yaml#/$defs/slave', 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 Buffer:
 92class Buffer:
 93
 94    def __init__(self, size: int):
 95        self._size = size
 96        self._data = collections.OrderedDict()
 97
 98    def add(self,
 99            event_id: hat.event.common.EventId,
100            data_msg: iec101.DataMsg):
101        self._data[event_id] = data_msg
102        while len(self._data) > self._size:
103            self._data.popitem(last=False)
104
105    def remove(self, event_id: hat.event.common.EventId):
106        self._data.pop(event_id, None)
107
108    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
109                                                       iec101.DataMsg]]:
110        return self._data.items()
Buffer(size: int)
94    def __init__(self, size: int):
95        self._size = size
96        self._data = collections.OrderedDict()
def add( self, event_id: hat.event.common.common.EventId, data_msg: hat.drivers.iec101.common.DataMsg):
 98    def add(self,
 99            event_id: hat.event.common.EventId,
100            data_msg: iec101.DataMsg):
101        self._data[event_id] = data_msg
102        while len(self._data) > self._size:
103            self._data.popitem(last=False)
def remove(self, event_id: hat.event.common.common.EventId):
105    def remove(self, event_id: hat.event.common.EventId):
106        self._data.pop(event_id, None)
def get_event_id_data_msgs( self) -> Iterable[tuple[hat.event.common.common.EventId, hat.drivers.iec101.common.DataMsg]]:
108    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
109                                                       iec101.DataMsg]]:
110        return self._data.items()
def init_buffers( buffers_conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], buffers: dict[str, Buffer]):
113def init_buffers(buffers_conf: json.Data,
114                 buffers: dict[str, Buffer]):
115    for buffer_conf in buffers_conf:
116        buffers[buffer_conf['name']] = Buffer(buffer_conf['size'])
async def init_data( data_conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], data_msgs: dict[hat.gateway.devices.iec101.common.DataKey, hat.drivers.iec101.common.DataMsg], data_buffers: dict[hat.gateway.devices.iec101.common.DataKey, Buffer], buffers: dict[str, Buffer], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]):
119async def init_data(data_conf: json.Data,
120                    data_msgs: dict[common.DataKey, iec101.DataMsg],
121                    data_buffers: dict[common.DataKey, Buffer],
122                    buffers: dict[str, Buffer],
123                    eventer_client: hat.event.eventer.Client,
124                    event_type_prefix: common.EventTypePrefix):
125    for data in data_conf:
126        data_key = common.DataKey(data_type=common.DataType[data['data_type']],
127                                  asdu_address=data['asdu_address'],
128                                  io_address=data['io_address'])
129        data_msgs[data_key] = None
130        if data['buffer']:
131            data_buffers[data_key] = buffers[data['buffer']]
132
133    event_types = [(*event_type_prefix, 'system', 'data', '*')]
134    params = hat.event.common.QueryLatestParams(event_types)
135    result = await eventer_client.query(params)
136
137    for event in result.events:
138        try:
139            data_type_str, asdu_address_str, io_address_str = \
140                event.type[len(event_type_prefix)+2:]
141            data_key = common.DataKey(data_type=common.DataType(data_type_str),
142                                      asdu_address=int(asdu_address_str),
143                                      io_address=int(io_address_str))
144            if data_key not in data_msgs:
145                raise Exception(f'data {data_key} not configured')
146
147            data_msgs[data_key] = data_msg_from_event(data_key, event)
148
149        except Exception as e:
150            mlog.debug('skipping initial data: %s', e, exc_info=e)
class Iec101SlaveDevice(hat.gateway.common.Device):
153class Iec101SlaveDevice(common.Device):
154
155    @property
156    def async_group(self) -> aio.Group:
157        return self._link.async_group
158
159    async def process_events(self, events: Collection[hat.event.common.Event]):
160        for event in events:
161            try:
162                await self._process_event(event)
163
164            except Exception as e:
165                mlog.warning('error processing event: %s', e, exc_info=e)
166
167    async def _connection_loop(self, device_conf):
168        conn = None
169
170        try:
171            if self._conf['link_type'] == 'BALANCED':
172                conn_args = {
173                    'direction': link.Direction[device_conf['direction']],
174                    'addr': device_conf['address'],
175                    'response_timeout': device_conf['response_timeout'],
176                    'send_retry_count': device_conf['send_retry_count'],
177                    'status_delay': device_conf['status_delay']}
178
179            elif self._conf['link_type'] == 'UNBALANCED':
180                conn_args = {
181                    'addr': device_conf['address'],
182                    'keep_alive_timeout': device_conf['keep_alive_timeout']}
183
184            else:
185                raise ValueError('unsupported link type')
186
187            while True:
188                try:
189                    conn = await self._link.open_connection(**conn_args)
190
191                except Exception as e:
192                    mlog.error('connection error for address %s: %s',
193                               device_conf['address'], e, exc_info=e)
194                    await asyncio.sleep(device_conf['reconnect_delay'])
195                    continue
196
197                conn = iec101.Connection(
198                    conn=conn,
199                    cause_size=iec101.CauseSize[self._conf['cause_size']],
200                    asdu_address_size=iec101.AsduAddressSize[
201                        self._conf['asdu_address_size']],
202                    io_address_size=iec101.IoAddressSize[
203                        self._conf['io_address_size']])
204
205                conn_id = next(self._next_conn_ids)
206                self._conns[conn_id] = conn
207
208                send_queue = aio.Queue(1024)
209                self._send_queues[conn_id] = send_queue
210
211                try:
212                    conn.async_group.spawn(self._connection_send_loop, conn,
213                                           send_queue)
214                    conn.async_group.spawn(self._connection_receive_loop, conn,
215                                           conn_id)
216
217                    await self._register_connections()
218
219                    with contextlib.suppress(Exception):
220                        for buffer in self._buffers.values():
221                            for event_id, data_msg in buffer.get_event_id_data_msgs():  # NOQA
222                                await self._send_data_msg(conn_id, buffer,
223                                                          event_id, data_msg)
224
225                    await conn.wait_closed()
226
227                finally:
228                    send_queue.close()
229
230                    self._conns.pop(conn_id, None)
231                    self._send_queues.pop(conn_id, None)
232
233                    with contextlib.suppress(Exception):
234                        await aio.uncancellable(self._register_connections())
235
236                await conn.async_close()
237
238        except Exception as e:
239            mlog.warning('connection loop error: %s', e, exc_info=e)
240
241        finally:
242            mlog.debug('closing connection')
243            self.close()
244
245            if conn:
246                await aio.uncancellable(conn.async_close())
247
248    async def _connection_send_loop(self, conn, send_queue):
249        try:
250            while True:
251                msgs, sent_cb = await send_queue.get()
252                await conn.send(msgs, sent_cb=sent_cb)
253
254        except ConnectionError:
255            mlog.debug('connection close')
256
257        except Exception as e:
258            mlog.warning('connection send loop error: %s', e, exc_info=e)
259
260        finally:
261            conn.close()
262
263    async def _connection_receive_loop(self, conn, conn_id):
264        try:
265            while True:
266                msgs = await conn.receive()
267
268                for msg in msgs:
269                    try:
270                        mlog.debug('received message: %s', msg)
271                        await self._process_msg(conn_id, msg)
272
273                    except Exception as e:
274                        mlog.warning('error processing message: %s',
275                                     e, exc_info=e)
276
277        except ConnectionError:
278            mlog.debug('connection close')
279
280        except Exception as e:
281            mlog.warning('connection receive loop error: %s', e, exc_info=e)
282
283        finally:
284            conn.close()
285
286    async def _register_connections(self):
287        payload = [{'connection_id': conn_id,
288                    'address': conn.address}
289                   for conn_id, conn in self._conns.items()]
290
291        event = hat.event.common.RegisterEvent(
292            type=(*self._event_type_prefix, 'gateway', 'connections'),
293            source_timestamp=None,
294            payload=hat.event.common.EventPayloadJson(payload))
295
296        await self._eventer_client.register([event])
297
298    async def _process_event(self, event):
299        suffix = event.type[len(self._event_type_prefix):]
300
301        if suffix[:2] == ('system', 'data'):
302            data_type_str, asdu_address_str, io_address_str = suffix[2:]
303            data_key = common.DataKey(data_type=common.DataType(data_type_str),
304                                      asdu_address=int(asdu_address_str),
305                                      io_address=int(io_address_str))
306
307            await self._process_data_event(data_key, event)
308
309        elif suffix[:2] == ('system', 'command'):
310            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
311            cmd_key = common.CommandKey(
312                cmd_type=common.CommandType(cmd_type_str),
313                asdu_address=int(asdu_address_str),
314                io_address=int(io_address_str))
315
316            await self._process_command_event(cmd_key, event)
317
318        else:
319            raise Exception('unsupported event type')
320
321    async def _process_data_event(self, data_key, event):
322        if data_key not in self._data_msgs:
323            raise Exception('data not configured')
324
325        data_msg = data_msg_from_event(data_key, event)
326        self._data_msgs[data_key] = data_msg
327
328        buffer = self._data_buffers.get(data_key)
329        if buffer:
330            buffer.add(event.id, data_msg)
331
332        for conn_id in self._conns.keys():
333            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
334
335    async def _process_command_event(self, cmd_key, event):
336        cmd_msg = cmd_msg_from_event(cmd_key, event)
337        conn_id = event.payload.data['connection_id']
338        await self._send(conn_id, [cmd_msg])
339
340    async def _process_msg(self, conn_id, msg):
341        if isinstance(msg, iec101.CommandMsg):
342            await self._process_command_msg(conn_id, msg)
343
344        elif isinstance(msg, iec101.InterrogationMsg):
345            await self._process_interrogation_msg(conn_id, msg)
346
347        elif isinstance(msg, iec101.CounterInterrogationMsg):
348            await self._process_counter_interrogation_msg(conn_id, msg)
349
350        elif isinstance(msg, iec101.ReadMsg):
351            await self._process_read_msg(conn_id, msg)
352
353        elif isinstance(msg, iec101.ClockSyncMsg):
354            await self._process_clock_sync_msg(conn_id, msg)
355
356        elif isinstance(msg, iec101.TestMsg):
357            await self._process_test_msg(conn_id, msg)
358
359        elif isinstance(msg, iec101.ResetMsg):
360            await self._process_reset_msg(conn_id, msg)
361
362        elif isinstance(msg, iec101.ParameterMsg):
363            await self._process_parameter_msg(conn_id, msg)
364
365        elif isinstance(msg, iec101.ParameterActivationMsg):
366            await self._process_parameter_activation_msg(conn_id, msg)
367
368        else:
369            raise Exception('unsupported message')
370
371    async def _process_command_msg(self, conn_id, msg):
372        if isinstance(msg.cause, iec101.CommandReqCause):
373            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
374            await self._eventer_client.register([event])
375
376        else:
377            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
378                               is_negative_confirm=True)
379            await self._send(conn_id, [res])
380
381    async def _process_interrogation_msg(self, conn_id, msg):
382        if msg.cause == iec101.CommandReqCause.ACTIVATION:
383            res = msg._replace(
384                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
385                is_negative_confirm=False)
386            await self._send(conn_id, [res])
387
388            data_msgs = [
389                data_msg._replace(
390                    is_test=msg.is_test,
391                    cause=iec101.DataResCause.INTERROGATED_STATION)
392                for data_msg in self._data_msgs.values()
393                if (data_msg and
394                    (msg.asdu_address == 0xFFFF or
395                     msg.asdu_address == data_msg.asdu_address) and
396                    not isinstance(data_msg.data, iec101.BinaryCounterData))]
397            await self._send(conn_id, data_msgs)
398
399            res = msg._replace(
400                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
401                is_negative_confirm=False)
402            await self._send(conn_id, [res])
403
404        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
405            res = msg._replace(
406                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
407                is_negative_confirm=True)
408            await self._send(conn_id, [res])
409
410        else:
411            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
412                               is_negative_confirm=True)
413            await self._send(conn_id, [res])
414
415    async def _process_counter_interrogation_msg(self, conn_id, msg):
416        if msg.cause == iec101.CommandReqCause.ACTIVATION:
417            res = msg._replace(
418                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
419                is_negative_confirm=False)
420            await self._send(conn_id, [res])
421
422            data_msgs = [
423                data_msg._replace(
424                    is_test=msg.is_test,
425                    cause=iec101.DataResCause.INTERROGATED_COUNTER)
426                for data_msg in self._data_msgs.values()
427                if (data_msg and
428                    (msg.asdu_address == 0xFFFF or
429                     msg.asdu_address == data_msg.asdu_address) and
430                    isinstance(data_msg.data, iec101.BinaryCounterData))]
431            await self._send(conn_id, data_msgs)
432
433            res = msg._replace(
434                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
435                is_negative_confirm=False)
436            await self._send(conn_id, [res])
437
438        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
439            res = msg._replace(
440                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
441                is_negative_confirm=True)
442            await self._send(conn_id, [res])
443
444        else:
445            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
446                               is_negative_confirm=True)
447            await self._send(conn_id, [res])
448
449    async def _process_read_msg(self, conn_id, msg):
450        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
451        await self._send(conn_id, [res])
452
453    async def _process_clock_sync_msg(self, conn_id, msg):
454        if isinstance(msg.cause, iec101.ClockSyncReqCause):
455            res = msg._replace(
456                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
457                is_negative_confirm=True)
458            await self._send(conn_id, [res])
459
460        else:
461            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
462                               is_negative_confirm=True)
463            await self._send(conn_id, [res])
464
465    async def _process_test_msg(self, conn_id, msg):
466        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
467        await self._send(conn_id, [res])
468
469    async def _process_reset_msg(self, conn_id, msg):
470        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
471        await self._send(conn_id, [res])
472
473    async def _process_parameter_msg(self, conn_id, msg):
474        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
475        await self._send(conn_id, [res])
476
477    async def _process_parameter_activation_msg(self, conn_id, msg):
478        res = msg._replace(
479            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
480        await self._send(conn_id, [res])
481
482    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
483        sent_cb = (functools.partial(buffer.remove, event_id)
484                   if buffer else None)
485        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
486
487    async def _send(self, conn_id, msgs, sent_cb=None):
488        send_queue = self._send_queues.get(conn_id)
489        if send_queue is None:
490            return
491
492        with contextlib.suppress(aio.QueueClosedError):
493            await send_queue.put((msgs, sent_cb))

Device interface

async_group: hat.aio.group.Group
155    @property
156    def async_group(self) -> aio.Group:
157        return self._link.async_group

Group controlling resource's lifetime.

async def process_events(self, events: Collection[hat.event.common.common.Event]):
159    async def process_events(self, events: Collection[hat.event.common.Event]):
160        for event in events:
161            try:
162                await self._process_event(event)
163
164            except Exception as e:
165                mlog.warning('error processing event: %s', e, exc_info=e)

Process received events

This method can be coroutine or regular function.

def cmd_msg_to_event( event_type_prefix: tuple[str, ...], conn_id: int, msg: hat.drivers.iec101.common.CommandMsg) -> hat.event.common.common.RegisterEvent:
496def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
497                     conn_id: int,
498                     msg: iec101.CommandMsg
499                     ) -> hat.event.common.RegisterEvent:
500    command_type = common.get_command_type(msg.command)
501    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
502    command = common.command_to_json(msg.command)
503    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
504                  str(msg.asdu_address), str(msg.io_address))
505
506    return hat.event.common.RegisterEvent(
507        type=event_type,
508        source_timestamp=None,
509        payload=hat.event.common.EventPayloadJson({
510            'connection_id': conn_id,
511            'is_test': msg.is_test,
512            'cause': cause,
513            'command': command}))
def data_msg_from_event( data_key: hat.gateway.devices.iec101.common.DataKey, event: hat.event.common.common.Event) -> hat.drivers.iec101.common.DataMsg:
516def data_msg_from_event(data_key: common.DataKey,
517                        event: hat.event.common.Event
518                        ) -> iec101.DataMsg:
519    time = common.time_from_source_timestamp(event.source_timestamp)
520    cause = common.cause_from_json(iec101.DataResCause,
521                                   event.payload.data['cause'])
522    data = common.data_from_json(data_key.data_type,
523                                 event.payload.data['data'])
524
525    return iec101.DataMsg(is_test=event.payload.data['is_test'],
526                          originator_address=0,
527                          asdu_address=data_key.asdu_address,
528                          io_address=data_key.io_address,
529                          data=data,
530                          time=time,
531                          cause=cause)
def cmd_msg_from_event( cmd_key: hat.gateway.devices.iec101.common.CommandKey, event: hat.event.common.common.Event) -> hat.drivers.iec101.common.CommandMsg:
534def cmd_msg_from_event(cmd_key: common.CommandKey,
535                       event: hat.event.common.Event
536                       ) -> iec101.CommandMsg:
537    cause = common.cause_from_json(iec101.CommandResCause,
538                                   event.payload.data['cause'])
539    command = common.command_from_json(cmd_key.cmd_type,
540                                       event.payload.data['command'])
541    is_negative_confirm = event.payload.data['is_negative_confirm']
542
543    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
544                             originator_address=0,
545                             asdu_address=cmd_key.asdu_address,
546                             io_address=cmd_key.io_address,
547                             command=command,
548                             is_negative_confirm=is_negative_confirm,
549                             cause=cause)