hat.gateway.devices.iec101.slave

IEC 60870-5-101 slave device

  1"""IEC 60870-5-101 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    device._broadcast_asdu_address = _get_broadcast_asdu_address(
 40        iec101.AsduAddressSize[conf['asdu_address_size']])
 41    device._log = _create_logger_adapter(conf['name'])
 42
 43    init_buffers(buffers_conf=conf['buffers'],
 44                 buffers=device._buffers)
 45
 46    await init_data(log=device._log,
 47                    data_conf=conf['data'],
 48                    data_msgs=device._data_msgs,
 49                    data_buffers=device._data_buffers,
 50                    buffers=device._buffers,
 51                    eventer_client=eventer_client,
 52                    event_type_prefix=event_type_prefix)
 53
 54    if conf['link_type'] == 'BALANCED':
 55        create_link = link.create_balanced_link
 56
 57    elif conf['link_type'] == 'UNBALANCED':
 58        create_link = link.create_slave_link
 59
 60    else:
 61        raise ValueError('unsupported link type')
 62
 63    device._link = await create_link(
 64        port=conf['port'],
 65        address_size=link.AddressSize[conf['device_address_size']],
 66        silent_interval=conf['silent_interval'],
 67        baudrate=conf['baudrate'],
 68        bytesize=serial.ByteSize[conf['bytesize']],
 69        parity=serial.Parity[conf['parity']],
 70        stopbits=serial.StopBits[conf['stopbits']],
 71        xonxoff=conf['flow_control']['xonxoff'],
 72        rtscts=conf['flow_control']['rtscts'],
 73        dsrdtr=conf['flow_control']['dsrdtr'],
 74        name=conf['name'])
 75
 76    try:
 77        for device_conf in conf['devices']:
 78            device.async_group.spawn(device._connection_loop, device_conf)
 79
 80        await device._register_connections()
 81
 82    except BaseException:
 83        await aio.uncancellable(device.async_close())
 84        raise
 85
 86    return device
 87
 88
 89info: common.DeviceInfo = common.DeviceInfo(
 90    type="iec101_slave",
 91    create=create,
 92    json_schema_id="hat-gateway://iec101.yaml#/$defs/slave",
 93    json_schema_repo=common.json_schema_repo)
 94
 95
 96class Buffer:
 97
 98    def __init__(self, size: int):
 99        self._size = size
100        self._data = collections.OrderedDict()
101
102    def add(self,
103            event_id: hat.event.common.EventId,
104            data_msg: iec101.DataMsg):
105        self._data[event_id] = data_msg
106        while len(self._data) > self._size:
107            self._data.popitem(last=False)
108
109    def remove(self, event_id: hat.event.common.EventId):
110        self._data.pop(event_id, None)
111
112    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
113                                                       iec101.DataMsg]]:
114        return self._data.items()
115
116
117def init_buffers(buffers_conf: json.Data,
118                 buffers: dict[str, Buffer]):
119    for buffer_conf in buffers_conf:
120        buffers[buffer_conf['name']] = Buffer(buffer_conf['size'])
121
122
123async def init_data(log: logging.Logger,
124                    data_conf: json.Data,
125                    data_msgs: dict[common.DataKey, iec101.DataMsg],
126                    data_buffers: dict[common.DataKey, Buffer],
127                    buffers: dict[str, Buffer],
128                    eventer_client: hat.event.eventer.Client,
129                    event_type_prefix: common.EventTypePrefix):
130    for data in data_conf:
131        data_key = common.DataKey(data_type=common.DataType[data['data_type']],
132                                  asdu_address=data['asdu_address'],
133                                  io_address=data['io_address'])
134        data_msgs[data_key] = None
135        if data['buffer']:
136            data_buffers[data_key] = buffers[data['buffer']]
137
138    event_types = [(*event_type_prefix, 'system', 'data', '*')]
139    params = hat.event.common.QueryLatestParams(event_types)
140    result = await eventer_client.query(params)
141
142    for event in result.events:
143        try:
144            data_type_str, asdu_address_str, io_address_str = \
145                event.type[len(event_type_prefix)+2:]
146            data_key = common.DataKey(data_type=common.DataType(data_type_str),
147                                      asdu_address=int(asdu_address_str),
148                                      io_address=int(io_address_str))
149            if data_key not in data_msgs:
150                raise Exception(f'data {data_key} not configured')
151
152            data_msgs[data_key] = data_msg_from_event(data_key, event)
153
154        except Exception as e:
155            log.debug('skipping initial data: %s', e, exc_info=e)
156
157
158class Iec101SlaveDevice(common.Device):
159
160    @property
161    def async_group(self) -> aio.Group:
162        return self._link.async_group
163
164    async def process_events(self, events: Collection[hat.event.common.Event]):
165        for event in events:
166            try:
167                await self._process_event(event)
168
169            except Exception as e:
170                self._log.warning('error processing event: %s', e, exc_info=e)
171
172    async def _connection_loop(self, device_conf):
173        conn = None
174
175        try:
176            if self._conf['link_type'] == 'BALANCED':
177                conn_args = {
178                    'direction': link.Direction[device_conf['direction']],
179                    'addr': device_conf['address'],
180                    'response_timeout': device_conf['response_timeout'],
181                    'send_retry_count': device_conf['send_retry_count'],
182                    'status_delay': device_conf['status_delay'],
183                    'name': self._conf['name']}
184
185            elif self._conf['link_type'] == 'UNBALANCED':
186                conn_args = {
187                    'addr': device_conf['address'],
188                    'keep_alive_timeout': device_conf['keep_alive_timeout'],
189                    'name': self._conf['name']}
190
191            else:
192                raise ValueError('unsupported link type')
193
194            while True:
195                try:
196                    conn = await self._link.open_connection(**conn_args)
197
198                except Exception as e:
199                    self._log.error('connection error for address %s: %s',
200                                    device_conf['address'], e, exc_info=e)
201                    await asyncio.sleep(device_conf['reconnect_delay'])
202                    continue
203
204                conn = iec101.Connection(
205                    conn=conn,
206                    cause_size=iec101.CauseSize[self._conf['cause_size']],
207                    asdu_address_size=iec101.AsduAddressSize[
208                        self._conf['asdu_address_size']],
209                    io_address_size=iec101.IoAddressSize[
210                        self._conf['io_address_size']])
211
212                conn_id = next(self._next_conn_ids)
213                self._conns[conn_id] = conn
214
215                send_queue = aio.Queue(1024)
216                self._send_queues[conn_id] = send_queue
217
218                try:
219                    conn.async_group.spawn(self._connection_send_loop, conn,
220                                           send_queue)
221                    conn.async_group.spawn(self._connection_receive_loop, conn,
222                                           conn_id)
223
224                    await self._register_connections()
225
226                    with contextlib.suppress(Exception):
227                        for buffer in self._buffers.values():
228                            for event_id, data_msg in buffer.get_event_id_data_msgs():  # NOQA
229                                await self._send_data_msg(conn_id, buffer,
230                                                          event_id, data_msg)
231
232                    await conn.wait_closed()
233
234                finally:
235                    send_queue.close()
236
237                    self._conns.pop(conn_id, None)
238                    self._send_queues.pop(conn_id, None)
239
240                    with contextlib.suppress(Exception):
241                        await aio.uncancellable(self._register_connections())
242
243                await conn.async_close()
244
245        except Exception as e:
246            self._log.warning('connection loop error: %s', e, exc_info=e)
247
248        finally:
249            self._log.debug('closing connection')
250            self.close()
251
252            if conn:
253                await aio.uncancellable(conn.async_close())
254
255    async def _connection_send_loop(self, conn, send_queue):
256        try:
257            while True:
258                msgs, sent_cb = await send_queue.get()
259                await conn.send(msgs, sent_cb=sent_cb)
260
261        except ConnectionError:
262            self._log.debug('connection close')
263
264        except Exception as e:
265            self._log.warning('connection send loop error: %s', e, exc_info=e)
266
267        finally:
268            conn.close()
269
270    async def _connection_receive_loop(self, conn, conn_id):
271        try:
272            while True:
273                msgs = await conn.receive()
274
275                for msg in msgs:
276                    try:
277                        self._log.debug('received message: %s', msg)
278                        await self._process_msg(conn_id, msg)
279
280                    except Exception as e:
281                        self._log.warning('error processing message: %s',
282                                          e, exc_info=e)
283
284        except ConnectionError:
285            self._log.debug('connection close')
286
287        except Exception as e:
288            self._log.warning('connection receive loop error: %s',
289                              e, exc_info=e)
290
291        finally:
292            conn.close()
293
294    async def _register_connections(self):
295        payload = [{'connection_id': conn_id,
296                    'address': conn.info.address}
297                   for conn_id, conn in self._conns.items()]
298
299        event = hat.event.common.RegisterEvent(
300            type=(*self._event_type_prefix, 'gateway', 'connections'),
301            source_timestamp=None,
302            payload=hat.event.common.EventPayloadJson(payload))
303
304        await self._eventer_client.register([event])
305
306    async def _process_event(self, event):
307        suffix = event.type[len(self._event_type_prefix):]
308
309        if suffix[:2] == ('system', 'data'):
310            data_type_str, asdu_address_str, io_address_str = suffix[2:]
311            data_key = common.DataKey(data_type=common.DataType(data_type_str),
312                                      asdu_address=int(asdu_address_str),
313                                      io_address=int(io_address_str))
314
315            await self._process_data_event(data_key, event)
316
317        elif suffix[:2] == ('system', 'command'):
318            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
319            cmd_key = common.CommandKey(
320                cmd_type=common.CommandType(cmd_type_str),
321                asdu_address=int(asdu_address_str),
322                io_address=int(io_address_str))
323
324            await self._process_command_event(cmd_key, event)
325
326        else:
327            raise Exception('unsupported event type')
328
329    async def _process_data_event(self, data_key, event):
330        if data_key not in self._data_msgs:
331            raise Exception('data not configured')
332
333        data_msg = data_msg_from_event(data_key, event)
334        self._data_msgs[data_key] = data_msg
335
336        buffer = self._data_buffers.get(data_key)
337        if buffer:
338            buffer.add(event.id, data_msg)
339
340        for conn_id in self._conns.keys():
341            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
342
343    async def _process_command_event(self, cmd_key, event):
344        cmd_msg = cmd_msg_from_event(cmd_key, event)
345        conn_id = event.payload.data['connection_id']
346        await self._send(conn_id, [cmd_msg])
347
348    async def _process_msg(self, conn_id, msg):
349        if isinstance(msg, iec101.CommandMsg):
350            await self._process_command_msg(conn_id, msg)
351
352        elif isinstance(msg, iec101.InterrogationMsg):
353            await self._process_interrogation_msg(conn_id, msg)
354
355        elif isinstance(msg, iec101.CounterInterrogationMsg):
356            await self._process_counter_interrogation_msg(conn_id, msg)
357
358        elif isinstance(msg, iec101.ReadMsg):
359            await self._process_read_msg(conn_id, msg)
360
361        elif isinstance(msg, iec101.ClockSyncMsg):
362            await self._process_clock_sync_msg(conn_id, msg)
363
364        elif isinstance(msg, iec101.TestMsg):
365            await self._process_test_msg(conn_id, msg)
366
367        elif isinstance(msg, iec101.ResetMsg):
368            await self._process_reset_msg(conn_id, msg)
369
370        elif isinstance(msg, iec101.ParameterMsg):
371            await self._process_parameter_msg(conn_id, msg)
372
373        elif isinstance(msg, iec101.ParameterActivationMsg):
374            await self._process_parameter_activation_msg(conn_id, msg)
375
376        else:
377            raise Exception('unsupported message')
378
379    async def _process_command_msg(self, conn_id, msg):
380        if isinstance(msg.cause, iec101.CommandReqCause):
381            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
382            await self._eventer_client.register([event])
383
384        else:
385            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
386                               is_negative_confirm=True)
387            await self._send(conn_id, [res])
388
389    async def _process_interrogation_msg(self, conn_id, msg):
390        if msg.cause == iec101.CommandReqCause.ACTIVATION:
391            asdu_data_msgs = collections.defaultdict(collections.deque)
392
393            for data_key, data_msg in self._data_msgs.items():
394                if data_key.data_type == common.DataType.BINARY_COUNTER:
395                    continue
396
397                if (msg.asdu_address != self._broadcast_asdu_address and
398                        msg.asdu_address != data_key.asdu_address):
399                    continue
400
401                asdu_data_msgs[data_key.asdu_address].append(data_msg)
402
403            if msg.asdu_address != self._broadcast_asdu_address:
404                asdu_data_msgs[msg.asdu_address].append(None)
405
406            for asdu_address, data_msgs in asdu_data_msgs.items():
407                res = msg._replace(
408                    asdu_address=asdu_address,
409                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
410                    is_negative_confirm=False)
411                await self._send(conn_id, [res])
412
413                msgs = [
414                    data_msg._replace(
415                        is_test=msg.is_test,
416                        cause=iec101.DataResCause.INTERROGATED_STATION)
417                    for data_msg in data_msgs
418                    if data_msg]
419                if msgs:
420                    await self._send(conn_id, msgs)
421
422                res = msg._replace(
423                    asdu_address=asdu_address,
424                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
425                    is_negative_confirm=False)
426                await self._send(conn_id, [res])
427
428        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
429            res = msg._replace(
430                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
431                is_negative_confirm=True)
432            await self._send(conn_id, [res])
433
434        else:
435            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
436                               is_negative_confirm=True)
437            await self._send(conn_id, [res])
438
439    async def _process_counter_interrogation_msg(self, conn_id, msg):
440        if msg.cause == iec101.CommandReqCause.ACTIVATION:
441            asdu_data_msgs = collections.defaultdict(collections.deque)
442
443            for data_key, data_msg in self._data_msgs.items():
444                if data_key.data_type != common.DataType.BINARY_COUNTER:
445                    continue
446
447                if (msg.asdu_address != self._broadcast_asdu_address and
448                        msg.asdu_address != data_key.asdu_address):
449                    continue
450
451                asdu_data_msgs[data_key.asdu_address].append(data_msg)
452
453            if msg.asdu_address != self._broadcast_asdu_address:
454                asdu_data_msgs[msg.asdu_address].append(None)
455
456            for asdu_address, data_msgs in asdu_data_msgs.items():
457                res = msg._replace(
458                    asdu_address=asdu_address,
459                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
460                    is_negative_confirm=False)
461                await self._send(conn_id, [res])
462
463                msgs = [
464                    data_msg._replace(
465                        is_test=msg.is_test,
466                        cause=iec101.DataResCause.INTERROGATED_COUNTER)
467                    for data_msg in data_msgs
468                    if data_msg]
469                if msgs:
470                    await self._send(conn_id, msgs)
471
472                res = msg._replace(
473                    asdu_address=asdu_address,
474                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
475                    is_negative_confirm=False)
476                await self._send(conn_id, [res])
477
478        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
479            res = msg._replace(
480                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
481                is_negative_confirm=True)
482            await self._send(conn_id, [res])
483
484        else:
485            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
486                               is_negative_confirm=True)
487            await self._send(conn_id, [res])
488
489    async def _process_read_msg(self, conn_id, msg):
490        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
491        await self._send(conn_id, [res])
492
493    async def _process_clock_sync_msg(self, conn_id, msg):
494        if isinstance(msg.cause, iec101.ClockSyncReqCause):
495            res = msg._replace(
496                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
497                is_negative_confirm=True)
498            await self._send(conn_id, [res])
499
500        else:
501            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
502                               is_negative_confirm=True)
503            await self._send(conn_id, [res])
504
505    async def _process_test_msg(self, conn_id, msg):
506        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
507        await self._send(conn_id, [res])
508
509    async def _process_reset_msg(self, conn_id, msg):
510        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
511        await self._send(conn_id, [res])
512
513    async def _process_parameter_msg(self, conn_id, msg):
514        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
515        await self._send(conn_id, [res])
516
517    async def _process_parameter_activation_msg(self, conn_id, msg):
518        res = msg._replace(
519            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
520        await self._send(conn_id, [res])
521
522    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
523        sent_cb = (functools.partial(buffer.remove, event_id)
524                   if buffer else None)
525        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
526
527    async def _send(self, conn_id, msgs, sent_cb=None):
528        send_queue = self._send_queues.get(conn_id)
529        if send_queue is None:
530            return
531
532        with contextlib.suppress(aio.QueueClosedError):
533            await send_queue.put((msgs, sent_cb))
534
535
536def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
537                     conn_id: int,
538                     msg: iec101.CommandMsg
539                     ) -> hat.event.common.RegisterEvent:
540    command_type = common.get_command_type(msg.command)
541    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
542    command = common.command_to_json(msg.command)
543    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
544                  str(msg.asdu_address), str(msg.io_address))
545
546    return hat.event.common.RegisterEvent(
547        type=event_type,
548        source_timestamp=None,
549        payload=hat.event.common.EventPayloadJson({
550            'connection_id': conn_id,
551            'is_test': msg.is_test,
552            'cause': cause,
553            'command': command}))
554
555
556def data_msg_from_event(data_key: common.DataKey,
557                        event: hat.event.common.Event
558                        ) -> iec101.DataMsg:
559    time = common.time_from_source_timestamp(event.source_timestamp)
560    cause = common.cause_from_json(iec101.DataResCause,
561                                   event.payload.data['cause'])
562    data = common.data_from_json(data_key.data_type,
563                                 event.payload.data['data'])
564
565    return iec101.DataMsg(is_test=event.payload.data['is_test'],
566                          originator_address=0,
567                          asdu_address=data_key.asdu_address,
568                          io_address=data_key.io_address,
569                          data=data,
570                          time=time,
571                          cause=cause)
572
573
574def cmd_msg_from_event(cmd_key: common.CommandKey,
575                       event: hat.event.common.Event
576                       ) -> iec101.CommandMsg:
577    cause = common.cause_from_json(iec101.CommandResCause,
578                                   event.payload.data['cause'])
579    command = common.command_from_json(cmd_key.cmd_type,
580                                       event.payload.data['command'])
581    is_negative_confirm = event.payload.data['is_negative_confirm']
582
583    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
584                             originator_address=0,
585                             asdu_address=cmd_key.asdu_address,
586                             io_address=cmd_key.io_address,
587                             command=command,
588                             is_negative_confirm=is_negative_confirm,
589                             cause=cause)
590
591
592def _get_broadcast_asdu_address(asdu_address_size):
593    if asdu_address_size == iec101.AsduAddressSize.ONE:
594        return 0xFF
595
596    if asdu_address_size == iec101.AsduAddressSize.TWO:
597        return 0xFFFF
598
599    raise ValueError('unsupported asdu address size')
600
601
602def _create_logger_adapter(name):
603    extra = {'meta': {'type': 'Iec101SlaveDevice',
604                      'name': name}}
605
606    return logging.LoggerAdapter(mlog, extra)
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    device._broadcast_asdu_address = _get_broadcast_asdu_address(
41        iec101.AsduAddressSize[conf['asdu_address_size']])
42    device._log = _create_logger_adapter(conf['name'])
43
44    init_buffers(buffers_conf=conf['buffers'],
45                 buffers=device._buffers)
46
47    await init_data(log=device._log,
48                    data_conf=conf['data'],
49                    data_msgs=device._data_msgs,
50                    data_buffers=device._data_buffers,
51                    buffers=device._buffers,
52                    eventer_client=eventer_client,
53                    event_type_prefix=event_type_prefix)
54
55    if conf['link_type'] == 'BALANCED':
56        create_link = link.create_balanced_link
57
58    elif conf['link_type'] == 'UNBALANCED':
59        create_link = link.create_slave_link
60
61    else:
62        raise ValueError('unsupported link type')
63
64    device._link = await create_link(
65        port=conf['port'],
66        address_size=link.AddressSize[conf['device_address_size']],
67        silent_interval=conf['silent_interval'],
68        baudrate=conf['baudrate'],
69        bytesize=serial.ByteSize[conf['bytesize']],
70        parity=serial.Parity[conf['parity']],
71        stopbits=serial.StopBits[conf['stopbits']],
72        xonxoff=conf['flow_control']['xonxoff'],
73        rtscts=conf['flow_control']['rtscts'],
74        dsrdtr=conf['flow_control']['dsrdtr'],
75        name=conf['name'])
76
77    try:
78        for device_conf in conf['devices']:
79            device.async_group.spawn(device._connection_loop, device_conf)
80
81        await device._register_connections()
82
83    except BaseException:
84        await aio.uncancellable(device.async_close())
85        raise
86
87    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://iec104.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec104.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'remote_addresses', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'reconnect_delay', 'time_sync_delay', 'security'], 'properties': {'name': {'type': 'string'}, 'remote_addresses': {'type': 'array', 'items': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}}}, 'slave': {'type': 'object', 'required': ['local_host', 'local_port', 'remote_hosts', 'max_connections', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'security', 'buffers', 'data'], 'properties': {'local_host': {'type': 'string'}, 'local_port': {'type': 'integer'}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}}}}}}, 'events': {'master': {'gateway': {'status': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/status'}, 'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/counter_interrogation'}}, 'system': {'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/counter_interrogation'}}}, 'slave': {'gateway': {'connections': {'$ref': 'hat-gateway://iec104.yaml#/$defs/messages/connections'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/gateway/command'}}, 'system': {'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/command'}}}}, 'messages': {'connections': {'type': 'array', 'items': {'type': 'object', 'required': ['connection_id', 'local', 'remote'], 'properties': {'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}}}, 'security': {'type': 'object', 'required': ['cert_path', 'key_path', 'verify_cert', 'ca_path'], 'properties': {'cert_path': {'type': 'string'}, 'key_path': {'type': ['null', 'string']}, 'verify_cert': {'type': 'boolean'}, 'ca_path': {'type': ['null', 'string']}, 'strict_mode': {'type': 'boolean'}, 'renegotiate_delay': {'type': ['null', 'number']}}}}}, 'hat-gateway://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://modbus.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://modbus.yaml', 'title': 'Modbus devices', '$defs': {'master': {'type': 'object', 'title': 'Modbus master', 'required': ['name', 'connection', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['modbus_type', 'transport', 'connect_timeout', 'connect_delay', 'request_timeout', 'request_delay', 'request_retry_immediate_count', 'request_retry_delayed_count', 'request_retry_delay'], 'properties': {'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'host', 'port'], 'properties': {'type': {'const': 'TCP'}, 'host': {'type': 'string', 'description': 'Remote host name\n'}, 'port': {'type': 'integer', 'description': 'Remote host TCP port\n', 'default': 502}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication 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'}}}}}}}}}, 'slave': {'type': 'object', 'required': ['name', 'modbus_type', 'transport', 'data'], 'properties': {'name': {'type': 'string'}, 'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'local_host', 'local_port', 'remote_hosts', 'max_connections', 'keep_alive_timeout'], 'properties': {'type': {'const': 'TCP'}, 'local_host': {'type': 'string', 'description': 'Local host name\n'}, 'local_port': {'type': 'integer', 'description': 'Local host TCP port\n', 'default': 502}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'keep_alive_timeout': {'type': ['number', 'null']}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'keep_alive_timeout'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silet interval\n'}, 'keep_alive_timeout': {'type': 'number'}}}]}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'device_id', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'device_id': {'type': 'integer'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['DISCONNECTED', 'CONNECTING', 'CONNECTED']}, 'remote_device_status': {'enum': ['DISABLED', 'CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['result'], 'properties': {'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND']}, 'value': {'type': 'integer'}, 'cause': {'enum': ['INTERROGATE', 'CHANGE']}}}, 'write': {'type': 'object', 'required': ['request_id', 'result'], 'properties': {'request_id': {'type': 'string'}, 'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND', 'TIMEOUT']}}}}, 'system': {'enable': {'type': 'boolean'}, 'write': {'type': 'object', 'required': ['request_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'value': {'type': 'integer'}}}}}, 'slave': {'gateway': {'connections': {'type': 'array', 'items': {'oneOf': [{'type': 'object', 'required': ['type', 'connection_id'], 'properties': {'type': {'const': 'SERIAL'}, 'connection_id': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'connection_id', 'local', 'remote'], 'properties': {'type': {'const': 'TCP'}, 'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}]}}, 'write': {'type': 'object', 'required': ['connection_id', 'value'], 'properties': {'connection': {'type': 'integer'}, 'value': {'type': 'integer'}}}}, 'system': {'data': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}}}}}}}}, 'hat-gateway://ping.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://ping.yaml', '$defs': {'device': {'type': 'object', 'required': ['name', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'host', 'ping_delay', 'ping_timeout', 'retry_count', 'retry_delay'], 'properties': {'name': {'type': 'string'}, 'host': {'type': 'string'}, 'ping_delay': {'type': 'number'}, 'ping_timeout': {'type': 'number'}, 'retry_count': {'type': 'number'}, 'retry_delay': {'type': 'number'}}}}}}, 'events': {'status': {'enum': ['AVAILABLE', 'NOT_AVAILABLE']}}}}, 'hat-gateway://snmp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://snmp.yaml', '$defs': {'manager': {'allOf': [{'oneOf': [{'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v1'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v2c'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v3'}]}, {'type': 'object', 'required': ['name', 'remote_host', 'remote_port', 'connect_delay', 'request_timeout', 'request_retry_count', 'request_retry_delay', 'polling_delay', 'polling_oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'Device name\n'}, 'remote_host': {'type': 'string', 'description': 'Remote hostname or IP address\n'}, 'remote_port': {'type': 'integer', 'description': 'Remote UDP port\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of request/response\nexchange\n'}, 'request_retry_count': {'type': 'integer', 'description': 'Number of request retries before remote data is\nconsidered unavailable\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive request\nretries\n'}, 'polling_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive polling\ncycles\n'}, 'polling_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID read during polling cycle formated as integers\nseparated by '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value formated as\nintegers separated by '.'\n"}}}}]}, 'trap_listener': {'type': 'object', 'required': ['name', 'local_host', 'local_port', 'users', 'remote_devices'], 'properties': {'name': {'type': 'string', 'description': 'Device name\n'}, 'local_host': {'type': 'string', 'description': 'Local listening hostname or IP address\n'}, 'local_port': {'type': 'integer', 'description': 'Local listening UDP port\n'}, 'users': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'authentication', 'privacy'], 'properties': {'name': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'remote_devices': {'type': 'array', 'items': {'allOf': [{'oneOf': [{'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'enum': ['V1', 'V2C']}, 'community': {'type': ['null', 'string']}}}, {'type': 'object', 'required': ['version', 'context'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal\ndigits\n'}, 'name': {'type': 'string'}}}]}}}]}, {'type': 'object', 'required': ['name', 'oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'remote device name\n'}, 'oids': {'type': 'array', 'items': {'type': 'string', 'description': "data OID formated as integers separated\nby '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value\nformated as integers separated by '.'\n"}}}}]}}}}, 'managers': {'v1': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V1'}, 'community': {'type': 'string'}}}, 'v2c': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V2C'}, 'community': {'type': 'string'}}}, 'v3': {'type': 'object', 'required': ['version', 'context', 'user', 'authentication', 'privacy'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal digits\n'}, 'name': {'type': 'string'}}}]}, 'user': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'events': {'manager': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['session_id', 'cause', 'data'], 'properties': {'session_id': {'oneOf': [{'type': 'null', 'description': 'In case of INTERROGATE or CHANGE cause\n'}, {'description': 'In case of REQUESTED cause\n'}]}, 'cause': ['INTERROGATE', 'CHANGE', 'REQUESTED'], 'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}, 'write': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'read': {'type': 'object', 'required': ['session_id']}, 'write': {'type': 'object', 'required': ['session_id', 'data'], 'properties': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}}, 'trap_listener': {'gateway': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}, 'data': {'oneOf': [{'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['INTEGER', 'UNSIGNED', 'COUNTER', 'BIG_COUNTER', 'TIME_TICKS']}, 'value': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['STRING', 'STRING_HEX', 'OBJECT_ID', 'IP_ADDRESS', 'ARBITRARY']}, 'value': {'type': 'string'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'const': 'ERROR'}, 'value': {'enum': ['TOO_BIG', 'NO_SUCH_NAME', 'BAD_VALUE', 'READ_ONLY', 'GEN_ERR', 'NO_ACCESS', 'WRONG_TYPE', 'WRONG_LENGTH', 'WRONG_ENCODING', 'WRONG_VALUE', 'NO_CREATION', 'INCONSISTENT_VALUE', 'RESOURCE_UNAVAILABLE', 'COMMIT_FAILED', 'UNDO_FAILED', 'AUTHORIZATION_ERROR', 'NOT_WRITABLE', 'INCONSISTENT_NAME', 'EMPTY', 'UNSPECIFIED', 'NO_SUCH_OBJECT', 'NO_SUCH_INSTANCE', 'END_OF_MIB_VIEW', 'NOT_IN_TIME_WINDOWS', 'UNKNOWN_USER_NAMES', 'UNKNOWN_ENGINE_IDS', 'WRONG_DIGESTS', 'DECRYPTION_ERRORS']}}}]}}}, 'hat-gateway://smpp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://smpp.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'remote_address', 'ssl', 'system_id', 'password', 'enquire_link_delay', 'enquire_link_timeout', 'connect_timeout', 'reconnect_delay', 'short_message', 'priority', 'data_coding', 'message_encoding', 'message_timeout'], 'properties': {'name': {'type': 'string'}, 'remote_address': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'ssl': {'type': 'boolean'}, 'system_id': {'type': 'string'}, 'password': {'type': 'string'}, 'enquire_link_delay': {'type': ['null', 'number']}, 'enquire_link_timeout': {'type': 'number'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'short_message': {'type': 'boolean'}, 'priority': {'enum': ['BULK', 'NORMAL', 'URGENT', 'VERY_URGENT']}, 'data_coding': {'enum': ['DEFAULT', 'ASCII', 'UNSPECIFIED_1', 'LATIN_1', 'UNSPECIFIED_2', 'JIS', 'CYRLLIC', 'LATIN_HEBREW', 'UCS2', 'PICTOGRAM', 'MUSIC', 'EXTENDED_KANJI', 'KS']}, 'message_encoding': {'type': 'string'}, 'message_timeout': {'type': 'number'}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}}, 'system': {'message': {'type': 'object', 'required': ['address', 'message'], 'properties': {'address': {'type': 'string'}, 'message': {'type': 'string'}}}}}}}}, 'hat-gateway://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'rcb', 'value'], 'properties': {'name': {'type': 'string'}, 'rcb': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}, 'commands': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'model', 'ref', 'with_operate_time'], 'properties': {'name': {'type': 'string'}, 'model': {'enum': ['DIRECT_WITH_NORMAL_SECURITY', 'SBO_WITH_NORMAL_SECURITY', 'DIRECT_WITH_ENHANCED_SECURITY', 'SBO_WITH_ENHANCED_SECURITY']}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/command'}, 'with_operate_time': {'type': 'boolean'}}}}, 'changes': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['reasons'], 'properties': {'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}}}, 'command': {'allOf': [{'type': 'object', 'required': ['session_id', 'action'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE', 'TERMINATION']}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'service_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}, 'additional_cause': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/additional_cause'}, 'test_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/test_error'}}}]}]}, 'change': {'allOf': [{'type': 'object', 'required': ['session_id'], 'properties': {'session_id': {'type': 'string'}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}}}]}]}, 'entry_id': {'type': ['string', 'null'], 'description': 'hex encoded bytes'}}, 'system': {'command': {'type': 'object', 'required': ['session_id', 'action', 'value', 'origin', 'test', 'checks'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE']}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'origin': {'type': 'object', 'required': ['category', 'identification'], 'properties': {'category': {'enum': ['BAY_CONTROL', 'STATION_CONTROL', 'REMOTE_CONTROL', 'AUTOMATIC_BAY', 'AUTOMATIC_STATION', 'AUTOMATIC_REMOTE', 'MAINTENANCE', 'PROCESS']}, 'identification': {'type': 'string'}}}, 'test': {'type': 'boolean'}, 'checks': {'type': 'array', 'items': {'enum': ['SYNCHRO', 'INTERLOCK']}}}}, 'change': {'type': 'object', 'requried': ['session_id', 'value'], 'properties': {'session_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}}}}, 'value': {'anyOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/integer'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/unsigned'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/float'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/bit_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/octet_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/visible_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/mms_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/array'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/struct'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/double_point'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/direction'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/severity'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/vector'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/step_position'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/binary_control'}], '$defs': {'boolean': {'type': 'boolean'}, 'integer': {'type': 'integer'}, 'unsigned': {'type': 'integer'}, 'float': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'bit_string': {'type': 'array', 'items': {'type': 'boolean'}}, 'octet_string': {'type': 'string', 'description': 'hex encoded bytes'}, 'visible_string': {'type': 'string'}, 'mms_string': {'type': 'string'}, 'array': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}, 'struct': {'type': 'object', 'patternProperties': {'.+': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}, 'quality': {'type': 'object', 'required': ['validity', 'details', 'source', 'test', 'operator_blocked'], 'properties': {'validity': {'enum': ['GOOD', 'INVALID', 'RESERVED', 'QUESTIONABLE']}, 'details': {'type': 'array', 'items': {'enum': ['OVERFLOW', 'OUT_OF_RANGE', 'BAD_REFERENCE', 'OSCILLATORY', 'FAILURE', 'OLD_DATA', 'INCONSISTENT', 'INACCURATE']}}, 'source': {'enum': ['PROCESS', 'SUBSTITUTED']}, 'test': {'type': 'boolean'}, 'operator_blocked': {'type': 'boolean'}}}, 'timestamp': {'type': 'object', 'required': ['value', 'leap_second', 'clock_failure', 'not_synchronized'], 'properties': {'value': {'type': 'number', 'description': 'seconds since 1970-01-01'}, 'leap_second': {'type': 'boolean'}, 'clock_failure': {'type': 'boolean'}, 'not_synchronized': {'type': 'boolean'}, 'accuracy': {'type': 'integer'}}}, 'double_point': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'BAD']}, 'direction': {'enum': ['UNKNOWN', 'FORWARD', 'BACKWARD', 'BOTH']}, 'severity': {'enum': ['UNKNOWN', 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING']}, 'analogue': {'type': 'object', 'properties': {'i': {'type': 'integer'}, 'f': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}}}, 'vector': {'type': 'object', 'required': ['magnitude'], 'properties': {'magnitude': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, 'angle': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}}}, 'step_position': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}, 'transient': {'type': 'boolean'}}}, 'binary_control': {'enum': ['STOP', 'LOWER', 'HIGHER', 'RESERVED']}}}, 'value_type': {'oneOf': [{'enum': ['BOOLEAN', 'INTEGER', 'UNSIGNED', 'FLOAT', 'BIT_STRING', 'OCTET_STRING', 'VISIBLE_STRING', 'MMS_STRING', 'QUALITY', 'TIMESTAMP', 'DOUBLE_POINT', 'DIRECTION', 'SEVERITY', 'ANALOGUE', 'VECTOR', 'STEP_POSITION', 'BINARY_CONTROL']}, {'type': 'object', 'required': ['type', 'element_type', 'length'], 'properties': {'type': {'const': 'ARRAY'}, 'element_type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}, 'length': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'elements'], 'properties': {'type': {'const': 'STRUCT'}, 'elements': {'type': 'array', 'items': {'type': 'object', 'requried': ['name', 'type'], 'properties': {'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}}}]}, 'refs': {'value': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, 'command': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}, 'rcb': {'type': 'object', 'required': ['logical_device', 'logical_node', 'type', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'type': {'enum': ['BUFFERED', 'UNBUFFERED']}, 'name': {'type': 'string'}}}, 'dataset': {'oneOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/persisted'}], '$defs': {'nonpersisted': {'type': 'string'}, 'persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, 'hat-gateway://iec101.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec101.yaml', '$defs': {'master': {'allOf': [{'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'cause_size', 'asdu_address_size', 'io_address_size', 'reconnect_delay'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'reconnect_delay': {'type': 'number'}}}, {'oneOf': [{'type': 'object', 'required': ['link_type', 'device_address_size', 'remote_devices'], 'properties': {'link_type': {'const': 'BALANCED'}, 'device_address_size': {'enum': ['ZERO', 'ONE', 'TWO']}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['direction', 'address', 'response_timeout', 'send_retry_count', 'status_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'direction': {'enum': ['A_TO_B', 'B_TO_A']}, 'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'status_delay': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, {'type': 'object', 'required': ['link_type', 'device_address_size', 'remote_devices'], 'properties': {'link_type': {'const': 'UNBALANCED'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}]}]}, 'slave': {'allOf': [{'type': 'object', 'required': ['port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'cause_size', 'asdu_address_size', 'io_address_size', 'buffers', 'data'], 'properties': {'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], '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://iec103.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec103.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'reconnect_delay', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['cause', 'value'], 'properties': {'cause': {'oneOf': [{'enum': ['SPONTANEOUS', 'CYCLIC', 'TEST_MODE', 'GENERAL_INTERROGATION', 'LOCAL_OPERATION', 'REMOTE_OPERATION']}, {'type': 'integer', 'description': 'other cause in range [0, 255]\n'}]}, 'value': {'oneOf': [{'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}, {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/measurand'}]}}}, 'command': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'enable': {'type': 'boolean'}, 'command': {'type': 'object', 'required': ['session_id', 'value'], 'properties': {'value': {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}}}}}}, 'values': {'double': {'enum': ['TRANSIENT', 'OFF', 'ON', 'ERROR']}, 'measurand': {'type': 'object', 'required': ['overflow', 'invalid', 'value'], 'properties': {'overflow': {'type': 'boolean'}, 'invalid': {'type': 'boolean'}, 'value': {'type': 'number'}}}}}}})
class Buffer:
 97class Buffer:
 98
 99    def __init__(self, size: int):
100        self._size = size
101        self._data = collections.OrderedDict()
102
103    def add(self,
104            event_id: hat.event.common.EventId,
105            data_msg: iec101.DataMsg):
106        self._data[event_id] = data_msg
107        while len(self._data) > self._size:
108            self._data.popitem(last=False)
109
110    def remove(self, event_id: hat.event.common.EventId):
111        self._data.pop(event_id, None)
112
113    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
114                                                       iec101.DataMsg]]:
115        return self._data.items()
Buffer(size: int)
 99    def __init__(self, size: int):
100        self._size = size
101        self._data = collections.OrderedDict()
def add( self, event_id: hat.event.common.common.EventId, data_msg: hat.drivers.iec101.common.DataMsg):
103    def add(self,
104            event_id: hat.event.common.EventId,
105            data_msg: iec101.DataMsg):
106        self._data[event_id] = data_msg
107        while len(self._data) > self._size:
108            self._data.popitem(last=False)
def remove(self, event_id: hat.event.common.common.EventId):
110    def remove(self, event_id: hat.event.common.EventId):
111        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]]:
113    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
114                                                       iec101.DataMsg]]:
115        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]):
118def init_buffers(buffers_conf: json.Data,
119                 buffers: dict[str, Buffer]):
120    for buffer_conf in buffers_conf:
121        buffers[buffer_conf['name']] = Buffer(buffer_conf['size'])
async def init_data( log: logging.Logger, 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]):
124async def init_data(log: logging.Logger,
125                    data_conf: json.Data,
126                    data_msgs: dict[common.DataKey, iec101.DataMsg],
127                    data_buffers: dict[common.DataKey, Buffer],
128                    buffers: dict[str, Buffer],
129                    eventer_client: hat.event.eventer.Client,
130                    event_type_prefix: common.EventTypePrefix):
131    for data in data_conf:
132        data_key = common.DataKey(data_type=common.DataType[data['data_type']],
133                                  asdu_address=data['asdu_address'],
134                                  io_address=data['io_address'])
135        data_msgs[data_key] = None
136        if data['buffer']:
137            data_buffers[data_key] = buffers[data['buffer']]
138
139    event_types = [(*event_type_prefix, 'system', 'data', '*')]
140    params = hat.event.common.QueryLatestParams(event_types)
141    result = await eventer_client.query(params)
142
143    for event in result.events:
144        try:
145            data_type_str, asdu_address_str, io_address_str = \
146                event.type[len(event_type_prefix)+2:]
147            data_key = common.DataKey(data_type=common.DataType(data_type_str),
148                                      asdu_address=int(asdu_address_str),
149                                      io_address=int(io_address_str))
150            if data_key not in data_msgs:
151                raise Exception(f'data {data_key} not configured')
152
153            data_msgs[data_key] = data_msg_from_event(data_key, event)
154
155        except Exception as e:
156            log.debug('skipping initial data: %s', e, exc_info=e)
class Iec101SlaveDevice(hat.gateway.common.Device):
159class Iec101SlaveDevice(common.Device):
160
161    @property
162    def async_group(self) -> aio.Group:
163        return self._link.async_group
164
165    async def process_events(self, events: Collection[hat.event.common.Event]):
166        for event in events:
167            try:
168                await self._process_event(event)
169
170            except Exception as e:
171                self._log.warning('error processing event: %s', e, exc_info=e)
172
173    async def _connection_loop(self, device_conf):
174        conn = None
175
176        try:
177            if self._conf['link_type'] == 'BALANCED':
178                conn_args = {
179                    'direction': link.Direction[device_conf['direction']],
180                    'addr': device_conf['address'],
181                    'response_timeout': device_conf['response_timeout'],
182                    'send_retry_count': device_conf['send_retry_count'],
183                    'status_delay': device_conf['status_delay'],
184                    'name': self._conf['name']}
185
186            elif self._conf['link_type'] == 'UNBALANCED':
187                conn_args = {
188                    'addr': device_conf['address'],
189                    'keep_alive_timeout': device_conf['keep_alive_timeout'],
190                    'name': self._conf['name']}
191
192            else:
193                raise ValueError('unsupported link type')
194
195            while True:
196                try:
197                    conn = await self._link.open_connection(**conn_args)
198
199                except Exception as e:
200                    self._log.error('connection error for address %s: %s',
201                                    device_conf['address'], e, exc_info=e)
202                    await asyncio.sleep(device_conf['reconnect_delay'])
203                    continue
204
205                conn = iec101.Connection(
206                    conn=conn,
207                    cause_size=iec101.CauseSize[self._conf['cause_size']],
208                    asdu_address_size=iec101.AsduAddressSize[
209                        self._conf['asdu_address_size']],
210                    io_address_size=iec101.IoAddressSize[
211                        self._conf['io_address_size']])
212
213                conn_id = next(self._next_conn_ids)
214                self._conns[conn_id] = conn
215
216                send_queue = aio.Queue(1024)
217                self._send_queues[conn_id] = send_queue
218
219                try:
220                    conn.async_group.spawn(self._connection_send_loop, conn,
221                                           send_queue)
222                    conn.async_group.spawn(self._connection_receive_loop, conn,
223                                           conn_id)
224
225                    await self._register_connections()
226
227                    with contextlib.suppress(Exception):
228                        for buffer in self._buffers.values():
229                            for event_id, data_msg in buffer.get_event_id_data_msgs():  # NOQA
230                                await self._send_data_msg(conn_id, buffer,
231                                                          event_id, data_msg)
232
233                    await conn.wait_closed()
234
235                finally:
236                    send_queue.close()
237
238                    self._conns.pop(conn_id, None)
239                    self._send_queues.pop(conn_id, None)
240
241                    with contextlib.suppress(Exception):
242                        await aio.uncancellable(self._register_connections())
243
244                await conn.async_close()
245
246        except Exception as e:
247            self._log.warning('connection loop error: %s', e, exc_info=e)
248
249        finally:
250            self._log.debug('closing connection')
251            self.close()
252
253            if conn:
254                await aio.uncancellable(conn.async_close())
255
256    async def _connection_send_loop(self, conn, send_queue):
257        try:
258            while True:
259                msgs, sent_cb = await send_queue.get()
260                await conn.send(msgs, sent_cb=sent_cb)
261
262        except ConnectionError:
263            self._log.debug('connection close')
264
265        except Exception as e:
266            self._log.warning('connection send loop error: %s', e, exc_info=e)
267
268        finally:
269            conn.close()
270
271    async def _connection_receive_loop(self, conn, conn_id):
272        try:
273            while True:
274                msgs = await conn.receive()
275
276                for msg in msgs:
277                    try:
278                        self._log.debug('received message: %s', msg)
279                        await self._process_msg(conn_id, msg)
280
281                    except Exception as e:
282                        self._log.warning('error processing message: %s',
283                                          e, exc_info=e)
284
285        except ConnectionError:
286            self._log.debug('connection close')
287
288        except Exception as e:
289            self._log.warning('connection receive loop error: %s',
290                              e, exc_info=e)
291
292        finally:
293            conn.close()
294
295    async def _register_connections(self):
296        payload = [{'connection_id': conn_id,
297                    'address': conn.info.address}
298                   for conn_id, conn in self._conns.items()]
299
300        event = hat.event.common.RegisterEvent(
301            type=(*self._event_type_prefix, 'gateway', 'connections'),
302            source_timestamp=None,
303            payload=hat.event.common.EventPayloadJson(payload))
304
305        await self._eventer_client.register([event])
306
307    async def _process_event(self, event):
308        suffix = event.type[len(self._event_type_prefix):]
309
310        if suffix[:2] == ('system', 'data'):
311            data_type_str, asdu_address_str, io_address_str = suffix[2:]
312            data_key = common.DataKey(data_type=common.DataType(data_type_str),
313                                      asdu_address=int(asdu_address_str),
314                                      io_address=int(io_address_str))
315
316            await self._process_data_event(data_key, event)
317
318        elif suffix[:2] == ('system', 'command'):
319            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
320            cmd_key = common.CommandKey(
321                cmd_type=common.CommandType(cmd_type_str),
322                asdu_address=int(asdu_address_str),
323                io_address=int(io_address_str))
324
325            await self._process_command_event(cmd_key, event)
326
327        else:
328            raise Exception('unsupported event type')
329
330    async def _process_data_event(self, data_key, event):
331        if data_key not in self._data_msgs:
332            raise Exception('data not configured')
333
334        data_msg = data_msg_from_event(data_key, event)
335        self._data_msgs[data_key] = data_msg
336
337        buffer = self._data_buffers.get(data_key)
338        if buffer:
339            buffer.add(event.id, data_msg)
340
341        for conn_id in self._conns.keys():
342            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
343
344    async def _process_command_event(self, cmd_key, event):
345        cmd_msg = cmd_msg_from_event(cmd_key, event)
346        conn_id = event.payload.data['connection_id']
347        await self._send(conn_id, [cmd_msg])
348
349    async def _process_msg(self, conn_id, msg):
350        if isinstance(msg, iec101.CommandMsg):
351            await self._process_command_msg(conn_id, msg)
352
353        elif isinstance(msg, iec101.InterrogationMsg):
354            await self._process_interrogation_msg(conn_id, msg)
355
356        elif isinstance(msg, iec101.CounterInterrogationMsg):
357            await self._process_counter_interrogation_msg(conn_id, msg)
358
359        elif isinstance(msg, iec101.ReadMsg):
360            await self._process_read_msg(conn_id, msg)
361
362        elif isinstance(msg, iec101.ClockSyncMsg):
363            await self._process_clock_sync_msg(conn_id, msg)
364
365        elif isinstance(msg, iec101.TestMsg):
366            await self._process_test_msg(conn_id, msg)
367
368        elif isinstance(msg, iec101.ResetMsg):
369            await self._process_reset_msg(conn_id, msg)
370
371        elif isinstance(msg, iec101.ParameterMsg):
372            await self._process_parameter_msg(conn_id, msg)
373
374        elif isinstance(msg, iec101.ParameterActivationMsg):
375            await self._process_parameter_activation_msg(conn_id, msg)
376
377        else:
378            raise Exception('unsupported message')
379
380    async def _process_command_msg(self, conn_id, msg):
381        if isinstance(msg.cause, iec101.CommandReqCause):
382            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
383            await self._eventer_client.register([event])
384
385        else:
386            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
387                               is_negative_confirm=True)
388            await self._send(conn_id, [res])
389
390    async def _process_interrogation_msg(self, conn_id, msg):
391        if msg.cause == iec101.CommandReqCause.ACTIVATION:
392            asdu_data_msgs = collections.defaultdict(collections.deque)
393
394            for data_key, data_msg in self._data_msgs.items():
395                if data_key.data_type == common.DataType.BINARY_COUNTER:
396                    continue
397
398                if (msg.asdu_address != self._broadcast_asdu_address and
399                        msg.asdu_address != data_key.asdu_address):
400                    continue
401
402                asdu_data_msgs[data_key.asdu_address].append(data_msg)
403
404            if msg.asdu_address != self._broadcast_asdu_address:
405                asdu_data_msgs[msg.asdu_address].append(None)
406
407            for asdu_address, data_msgs in asdu_data_msgs.items():
408                res = msg._replace(
409                    asdu_address=asdu_address,
410                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
411                    is_negative_confirm=False)
412                await self._send(conn_id, [res])
413
414                msgs = [
415                    data_msg._replace(
416                        is_test=msg.is_test,
417                        cause=iec101.DataResCause.INTERROGATED_STATION)
418                    for data_msg in data_msgs
419                    if data_msg]
420                if msgs:
421                    await self._send(conn_id, msgs)
422
423                res = msg._replace(
424                    asdu_address=asdu_address,
425                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
426                    is_negative_confirm=False)
427                await self._send(conn_id, [res])
428
429        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
430            res = msg._replace(
431                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
432                is_negative_confirm=True)
433            await self._send(conn_id, [res])
434
435        else:
436            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
437                               is_negative_confirm=True)
438            await self._send(conn_id, [res])
439
440    async def _process_counter_interrogation_msg(self, conn_id, msg):
441        if msg.cause == iec101.CommandReqCause.ACTIVATION:
442            asdu_data_msgs = collections.defaultdict(collections.deque)
443
444            for data_key, data_msg in self._data_msgs.items():
445                if data_key.data_type != common.DataType.BINARY_COUNTER:
446                    continue
447
448                if (msg.asdu_address != self._broadcast_asdu_address and
449                        msg.asdu_address != data_key.asdu_address):
450                    continue
451
452                asdu_data_msgs[data_key.asdu_address].append(data_msg)
453
454            if msg.asdu_address != self._broadcast_asdu_address:
455                asdu_data_msgs[msg.asdu_address].append(None)
456
457            for asdu_address, data_msgs in asdu_data_msgs.items():
458                res = msg._replace(
459                    asdu_address=asdu_address,
460                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
461                    is_negative_confirm=False)
462                await self._send(conn_id, [res])
463
464                msgs = [
465                    data_msg._replace(
466                        is_test=msg.is_test,
467                        cause=iec101.DataResCause.INTERROGATED_COUNTER)
468                    for data_msg in data_msgs
469                    if data_msg]
470                if msgs:
471                    await self._send(conn_id, msgs)
472
473                res = msg._replace(
474                    asdu_address=asdu_address,
475                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
476                    is_negative_confirm=False)
477                await self._send(conn_id, [res])
478
479        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
480            res = msg._replace(
481                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
482                is_negative_confirm=True)
483            await self._send(conn_id, [res])
484
485        else:
486            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
487                               is_negative_confirm=True)
488            await self._send(conn_id, [res])
489
490    async def _process_read_msg(self, conn_id, msg):
491        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
492        await self._send(conn_id, [res])
493
494    async def _process_clock_sync_msg(self, conn_id, msg):
495        if isinstance(msg.cause, iec101.ClockSyncReqCause):
496            res = msg._replace(
497                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
498                is_negative_confirm=True)
499            await self._send(conn_id, [res])
500
501        else:
502            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
503                               is_negative_confirm=True)
504            await self._send(conn_id, [res])
505
506    async def _process_test_msg(self, conn_id, msg):
507        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
508        await self._send(conn_id, [res])
509
510    async def _process_reset_msg(self, conn_id, msg):
511        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
512        await self._send(conn_id, [res])
513
514    async def _process_parameter_msg(self, conn_id, msg):
515        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
516        await self._send(conn_id, [res])
517
518    async def _process_parameter_activation_msg(self, conn_id, msg):
519        res = msg._replace(
520            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
521        await self._send(conn_id, [res])
522
523    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
524        sent_cb = (functools.partial(buffer.remove, event_id)
525                   if buffer else None)
526        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
527
528    async def _send(self, conn_id, msgs, sent_cb=None):
529        send_queue = self._send_queues.get(conn_id)
530        if send_queue is None:
531            return
532
533        with contextlib.suppress(aio.QueueClosedError):
534            await send_queue.put((msgs, sent_cb))

Device interface

async_group: hat.aio.group.Group
161    @property
162    def async_group(self) -> aio.Group:
163        return self._link.async_group

Group controlling resource's lifetime.

async def process_events(self, events: Collection[hat.event.common.common.Event]):
165    async def process_events(self, events: Collection[hat.event.common.Event]):
166        for event in events:
167            try:
168                await self._process_event(event)
169
170            except Exception as e:
171                self._log.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:
537def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
538                     conn_id: int,
539                     msg: iec101.CommandMsg
540                     ) -> hat.event.common.RegisterEvent:
541    command_type = common.get_command_type(msg.command)
542    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
543    command = common.command_to_json(msg.command)
544    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
545                  str(msg.asdu_address), str(msg.io_address))
546
547    return hat.event.common.RegisterEvent(
548        type=event_type,
549        source_timestamp=None,
550        payload=hat.event.common.EventPayloadJson({
551            'connection_id': conn_id,
552            'is_test': msg.is_test,
553            'cause': cause,
554            '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:
557def data_msg_from_event(data_key: common.DataKey,
558                        event: hat.event.common.Event
559                        ) -> iec101.DataMsg:
560    time = common.time_from_source_timestamp(event.source_timestamp)
561    cause = common.cause_from_json(iec101.DataResCause,
562                                   event.payload.data['cause'])
563    data = common.data_from_json(data_key.data_type,
564                                 event.payload.data['data'])
565
566    return iec101.DataMsg(is_test=event.payload.data['is_test'],
567                          originator_address=0,
568                          asdu_address=data_key.asdu_address,
569                          io_address=data_key.io_address,
570                          data=data,
571                          time=time,
572                          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:
575def cmd_msg_from_event(cmd_key: common.CommandKey,
576                       event: hat.event.common.Event
577                       ) -> iec101.CommandMsg:
578    cause = common.cause_from_json(iec101.CommandResCause,
579                                   event.payload.data['cause'])
580    command = common.command_from_json(cmd_key.cmd_type,
581                                       event.payload.data['command'])
582    is_negative_confirm = event.payload.data['is_negative_confirm']
583
584    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
585                             originator_address=0,
586                             asdu_address=cmd_key.asdu_address,
587                             io_address=cmd_key.io_address,
588                             command=command,
589                             is_negative_confirm=is_negative_confirm,
590                             cause=cause)