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                try:
274                    msgs = await conn.receive()
275
276                except iec101.AsduTypeError as e:
277                    self._log.warning("asdu type error: %s", e)
278                    continue
279
280                for msg in msgs:
281                    try:
282                        self._log.debug('received message: %s', msg)
283                        await self._process_msg(conn_id, msg)
284
285                    except Exception as e:
286                        self._log.warning('error processing message: %s',
287                                          e, exc_info=e)
288
289        except ConnectionError:
290            self._log.debug('connection close')
291
292        except Exception as e:
293            self._log.warning('connection receive loop error: %s',
294                              e, exc_info=e)
295
296        finally:
297            conn.close()
298
299    async def _register_connections(self):
300        payload = [{'connection_id': conn_id,
301                    'address': conn.info.address}
302                   for conn_id, conn in self._conns.items()]
303
304        event = hat.event.common.RegisterEvent(
305            type=(*self._event_type_prefix, 'gateway', 'connections'),
306            source_timestamp=None,
307            payload=hat.event.common.EventPayloadJson(payload))
308
309        await self._eventer_client.register([event])
310
311    async def _process_event(self, event):
312        suffix = event.type[len(self._event_type_prefix):]
313
314        if suffix[:2] == ('system', 'data'):
315            data_type_str, asdu_address_str, io_address_str = suffix[2:]
316            data_key = common.DataKey(data_type=common.DataType(data_type_str),
317                                      asdu_address=int(asdu_address_str),
318                                      io_address=int(io_address_str))
319
320            await self._process_data_event(data_key, event)
321
322        elif suffix[:2] == ('system', 'command'):
323            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
324            cmd_key = common.CommandKey(
325                cmd_type=common.CommandType(cmd_type_str),
326                asdu_address=int(asdu_address_str),
327                io_address=int(io_address_str))
328
329            await self._process_command_event(cmd_key, event)
330
331        else:
332            raise Exception('unsupported event type')
333
334    async def _process_data_event(self, data_key, event):
335        if data_key not in self._data_msgs:
336            raise Exception('data not configured')
337
338        data_msg = data_msg_from_event(data_key, event)
339        self._data_msgs[data_key] = data_msg
340
341        buffer = self._data_buffers.get(data_key)
342        if buffer:
343            buffer.add(event.id, data_msg)
344
345        for conn_id in self._conns.keys():
346            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
347
348    async def _process_command_event(self, cmd_key, event):
349        cmd_msg = cmd_msg_from_event(cmd_key, event)
350        conn_id = event.payload.data['connection_id']
351        await self._send(conn_id, [cmd_msg])
352
353    async def _process_msg(self, conn_id, msg):
354        if isinstance(msg, iec101.CommandMsg):
355            await self._process_command_msg(conn_id, msg)
356
357        elif isinstance(msg, iec101.InterrogationMsg):
358            await self._process_interrogation_msg(conn_id, msg)
359
360        elif isinstance(msg, iec101.CounterInterrogationMsg):
361            await self._process_counter_interrogation_msg(conn_id, msg)
362
363        elif isinstance(msg, iec101.ReadMsg):
364            await self._process_read_msg(conn_id, msg)
365
366        elif isinstance(msg, iec101.ClockSyncMsg):
367            await self._process_clock_sync_msg(conn_id, msg)
368
369        elif isinstance(msg, iec101.TestMsg):
370            await self._process_test_msg(conn_id, msg)
371
372        elif isinstance(msg, iec101.ResetMsg):
373            await self._process_reset_msg(conn_id, msg)
374
375        elif isinstance(msg, iec101.ParameterMsg):
376            await self._process_parameter_msg(conn_id, msg)
377
378        elif isinstance(msg, iec101.ParameterActivationMsg):
379            await self._process_parameter_activation_msg(conn_id, msg)
380
381        else:
382            raise Exception('unsupported message')
383
384    async def _process_command_msg(self, conn_id, msg):
385        if isinstance(msg.cause, iec101.CommandReqCause):
386            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
387            await self._eventer_client.register([event])
388
389        else:
390            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
391                               is_negative_confirm=True)
392            await self._send(conn_id, [res])
393
394    async def _process_interrogation_msg(self, conn_id, msg):
395        if msg.cause == iec101.CommandReqCause.ACTIVATION:
396            asdu_data_msgs = collections.defaultdict(collections.deque)
397
398            for data_key, data_msg in self._data_msgs.items():
399                if data_key.data_type == common.DataType.BINARY_COUNTER:
400                    continue
401
402                if (msg.asdu_address != self._broadcast_asdu_address and
403                        msg.asdu_address != data_key.asdu_address):
404                    continue
405
406                asdu_data_msgs[data_key.asdu_address].append(data_msg)
407
408            if msg.asdu_address != self._broadcast_asdu_address:
409                asdu_data_msgs[msg.asdu_address].append(None)
410
411            for asdu_address, data_msgs in asdu_data_msgs.items():
412                res = msg._replace(
413                    asdu_address=asdu_address,
414                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
415                    is_negative_confirm=False)
416                await self._send(conn_id, [res])
417
418                msgs = [
419                    data_msg._replace(
420                        is_test=msg.is_test,
421                        cause=iec101.DataResCause.INTERROGATED_STATION)
422                    for data_msg in data_msgs
423                    if data_msg]
424                if msgs:
425                    await self._send(conn_id, msgs)
426
427                res = msg._replace(
428                    asdu_address=asdu_address,
429                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
430                    is_negative_confirm=False)
431                await self._send(conn_id, [res])
432
433        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
434            res = msg._replace(
435                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
436                is_negative_confirm=True)
437            await self._send(conn_id, [res])
438
439        else:
440            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
441                               is_negative_confirm=True)
442            await self._send(conn_id, [res])
443
444    async def _process_counter_interrogation_msg(self, conn_id, msg):
445        if msg.cause == iec101.CommandReqCause.ACTIVATION:
446            asdu_data_msgs = collections.defaultdict(collections.deque)
447
448            for data_key, data_msg in self._data_msgs.items():
449                if data_key.data_type != common.DataType.BINARY_COUNTER:
450                    continue
451
452                if (msg.asdu_address != self._broadcast_asdu_address and
453                        msg.asdu_address != data_key.asdu_address):
454                    continue
455
456                asdu_data_msgs[data_key.asdu_address].append(data_msg)
457
458            if msg.asdu_address != self._broadcast_asdu_address:
459                asdu_data_msgs[msg.asdu_address].append(None)
460
461            for asdu_address, data_msgs in asdu_data_msgs.items():
462                res = msg._replace(
463                    asdu_address=asdu_address,
464                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
465                    is_negative_confirm=False)
466                await self._send(conn_id, [res])
467
468                msgs = [
469                    data_msg._replace(
470                        is_test=msg.is_test,
471                        cause=iec101.DataResCause.INTERROGATED_COUNTER)
472                    for data_msg in data_msgs
473                    if data_msg]
474                if msgs:
475                    await self._send(conn_id, msgs)
476
477                res = msg._replace(
478                    asdu_address=asdu_address,
479                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
480                    is_negative_confirm=False)
481                await self._send(conn_id, [res])
482
483        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
484            res = msg._replace(
485                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
486                is_negative_confirm=True)
487            await self._send(conn_id, [res])
488
489        else:
490            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
491                               is_negative_confirm=True)
492            await self._send(conn_id, [res])
493
494    async def _process_read_msg(self, conn_id, msg):
495        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
496        await self._send(conn_id, [res])
497
498    async def _process_clock_sync_msg(self, conn_id, msg):
499        if isinstance(msg.cause, iec101.ClockSyncReqCause):
500            res = msg._replace(
501                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
502                is_negative_confirm=True)
503            await self._send(conn_id, [res])
504
505        else:
506            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
507                               is_negative_confirm=True)
508            await self._send(conn_id, [res])
509
510    async def _process_test_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_reset_msg(self, conn_id, msg):
515        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
516        await self._send(conn_id, [res])
517
518    async def _process_parameter_msg(self, conn_id, msg):
519        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
520        await self._send(conn_id, [res])
521
522    async def _process_parameter_activation_msg(self, conn_id, msg):
523        res = msg._replace(
524            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
525        await self._send(conn_id, [res])
526
527    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
528        sent_cb = (functools.partial(buffer.remove, event_id)
529                   if buffer else None)
530        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
531
532    async def _send(self, conn_id, msgs, sent_cb=None):
533        send_queue = self._send_queues.get(conn_id)
534        if send_queue is None:
535            return
536
537        with contextlib.suppress(aio.QueueClosedError):
538            await send_queue.put((msgs, sent_cb))
539
540
541def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
542                     conn_id: int,
543                     msg: iec101.CommandMsg
544                     ) -> hat.event.common.RegisterEvent:
545    command_type = common.get_command_type(msg.command)
546    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
547    command = common.command_to_json(msg.command)
548    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
549                  str(msg.asdu_address), str(msg.io_address))
550
551    return hat.event.common.RegisterEvent(
552        type=event_type,
553        source_timestamp=None,
554        payload=hat.event.common.EventPayloadJson({
555            'connection_id': conn_id,
556            'is_test': msg.is_test,
557            'cause': cause,
558            'command': command}))
559
560
561def data_msg_from_event(data_key: common.DataKey,
562                        event: hat.event.common.Event
563                        ) -> iec101.DataMsg:
564    time = common.time_from_source_timestamp(event.source_timestamp)
565    cause = common.cause_from_json(iec101.DataResCause,
566                                   event.payload.data['cause'])
567    data = common.data_from_json(data_key.data_type,
568                                 event.payload.data['data'])
569
570    return iec101.DataMsg(is_test=event.payload.data['is_test'],
571                          originator_address=0,
572                          asdu_address=data_key.asdu_address,
573                          io_address=data_key.io_address,
574                          data=data,
575                          time=time,
576                          cause=cause)
577
578
579def cmd_msg_from_event(cmd_key: common.CommandKey,
580                       event: hat.event.common.Event
581                       ) -> iec101.CommandMsg:
582    cause = common.cause_from_json(iec101.CommandResCause,
583                                   event.payload.data['cause'])
584    command = common.command_from_json(cmd_key.cmd_type,
585                                       event.payload.data['command'])
586    is_negative_confirm = event.payload.data['is_negative_confirm']
587
588    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
589                             originator_address=0,
590                             asdu_address=cmd_key.asdu_address,
591                             io_address=cmd_key.io_address,
592                             command=command,
593                             is_negative_confirm=is_negative_confirm,
594                             cause=cause)
595
596
597def _get_broadcast_asdu_address(asdu_address_size):
598    if asdu_address_size == iec101.AsduAddressSize.ONE:
599        return 0xFF
600
601    if asdu_address_size == iec101.AsduAddressSize.TWO:
602        return 0xFFFF
603
604    raise ValueError('unsupported asdu address size')
605
606
607def _create_logger_adapter(name):
608    extra = {'meta': {'type': 'Iec101SlaveDevice',
609                      'name': name}}
610
611    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://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://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'}}}}}}, '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://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://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://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://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': ['request_id', 'connection_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'connection_id': {'type': 'integer'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'value'], 'properties': {'name': {'type': 'string'}, 'value': {'type': 'integer'}}}}}}}, 'system': {'data': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}}}, 'write': {'type': 'object', 'required': ['request_id', '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']}}}}}}}}, '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']}}}}})
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                try:
275                    msgs = await conn.receive()
276
277                except iec101.AsduTypeError as e:
278                    self._log.warning("asdu type error: %s", e)
279                    continue
280
281                for msg in msgs:
282                    try:
283                        self._log.debug('received message: %s', msg)
284                        await self._process_msg(conn_id, msg)
285
286                    except Exception as e:
287                        self._log.warning('error processing message: %s',
288                                          e, exc_info=e)
289
290        except ConnectionError:
291            self._log.debug('connection close')
292
293        except Exception as e:
294            self._log.warning('connection receive loop error: %s',
295                              e, exc_info=e)
296
297        finally:
298            conn.close()
299
300    async def _register_connections(self):
301        payload = [{'connection_id': conn_id,
302                    'address': conn.info.address}
303                   for conn_id, conn in self._conns.items()]
304
305        event = hat.event.common.RegisterEvent(
306            type=(*self._event_type_prefix, 'gateway', 'connections'),
307            source_timestamp=None,
308            payload=hat.event.common.EventPayloadJson(payload))
309
310        await self._eventer_client.register([event])
311
312    async def _process_event(self, event):
313        suffix = event.type[len(self._event_type_prefix):]
314
315        if suffix[:2] == ('system', 'data'):
316            data_type_str, asdu_address_str, io_address_str = suffix[2:]
317            data_key = common.DataKey(data_type=common.DataType(data_type_str),
318                                      asdu_address=int(asdu_address_str),
319                                      io_address=int(io_address_str))
320
321            await self._process_data_event(data_key, event)
322
323        elif suffix[:2] == ('system', 'command'):
324            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
325            cmd_key = common.CommandKey(
326                cmd_type=common.CommandType(cmd_type_str),
327                asdu_address=int(asdu_address_str),
328                io_address=int(io_address_str))
329
330            await self._process_command_event(cmd_key, event)
331
332        else:
333            raise Exception('unsupported event type')
334
335    async def _process_data_event(self, data_key, event):
336        if data_key not in self._data_msgs:
337            raise Exception('data not configured')
338
339        data_msg = data_msg_from_event(data_key, event)
340        self._data_msgs[data_key] = data_msg
341
342        buffer = self._data_buffers.get(data_key)
343        if buffer:
344            buffer.add(event.id, data_msg)
345
346        for conn_id in self._conns.keys():
347            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
348
349    async def _process_command_event(self, cmd_key, event):
350        cmd_msg = cmd_msg_from_event(cmd_key, event)
351        conn_id = event.payload.data['connection_id']
352        await self._send(conn_id, [cmd_msg])
353
354    async def _process_msg(self, conn_id, msg):
355        if isinstance(msg, iec101.CommandMsg):
356            await self._process_command_msg(conn_id, msg)
357
358        elif isinstance(msg, iec101.InterrogationMsg):
359            await self._process_interrogation_msg(conn_id, msg)
360
361        elif isinstance(msg, iec101.CounterInterrogationMsg):
362            await self._process_counter_interrogation_msg(conn_id, msg)
363
364        elif isinstance(msg, iec101.ReadMsg):
365            await self._process_read_msg(conn_id, msg)
366
367        elif isinstance(msg, iec101.ClockSyncMsg):
368            await self._process_clock_sync_msg(conn_id, msg)
369
370        elif isinstance(msg, iec101.TestMsg):
371            await self._process_test_msg(conn_id, msg)
372
373        elif isinstance(msg, iec101.ResetMsg):
374            await self._process_reset_msg(conn_id, msg)
375
376        elif isinstance(msg, iec101.ParameterMsg):
377            await self._process_parameter_msg(conn_id, msg)
378
379        elif isinstance(msg, iec101.ParameterActivationMsg):
380            await self._process_parameter_activation_msg(conn_id, msg)
381
382        else:
383            raise Exception('unsupported message')
384
385    async def _process_command_msg(self, conn_id, msg):
386        if isinstance(msg.cause, iec101.CommandReqCause):
387            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
388            await self._eventer_client.register([event])
389
390        else:
391            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
392                               is_negative_confirm=True)
393            await self._send(conn_id, [res])
394
395    async def _process_interrogation_msg(self, conn_id, msg):
396        if msg.cause == iec101.CommandReqCause.ACTIVATION:
397            asdu_data_msgs = collections.defaultdict(collections.deque)
398
399            for data_key, data_msg in self._data_msgs.items():
400                if data_key.data_type == common.DataType.BINARY_COUNTER:
401                    continue
402
403                if (msg.asdu_address != self._broadcast_asdu_address and
404                        msg.asdu_address != data_key.asdu_address):
405                    continue
406
407                asdu_data_msgs[data_key.asdu_address].append(data_msg)
408
409            if msg.asdu_address != self._broadcast_asdu_address:
410                asdu_data_msgs[msg.asdu_address].append(None)
411
412            for asdu_address, data_msgs in asdu_data_msgs.items():
413                res = msg._replace(
414                    asdu_address=asdu_address,
415                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
416                    is_negative_confirm=False)
417                await self._send(conn_id, [res])
418
419                msgs = [
420                    data_msg._replace(
421                        is_test=msg.is_test,
422                        cause=iec101.DataResCause.INTERROGATED_STATION)
423                    for data_msg in data_msgs
424                    if data_msg]
425                if msgs:
426                    await self._send(conn_id, msgs)
427
428                res = msg._replace(
429                    asdu_address=asdu_address,
430                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
431                    is_negative_confirm=False)
432                await self._send(conn_id, [res])
433
434        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
435            res = msg._replace(
436                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
437                is_negative_confirm=True)
438            await self._send(conn_id, [res])
439
440        else:
441            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
442                               is_negative_confirm=True)
443            await self._send(conn_id, [res])
444
445    async def _process_counter_interrogation_msg(self, conn_id, msg):
446        if msg.cause == iec101.CommandReqCause.ACTIVATION:
447            asdu_data_msgs = collections.defaultdict(collections.deque)
448
449            for data_key, data_msg in self._data_msgs.items():
450                if data_key.data_type != common.DataType.BINARY_COUNTER:
451                    continue
452
453                if (msg.asdu_address != self._broadcast_asdu_address and
454                        msg.asdu_address != data_key.asdu_address):
455                    continue
456
457                asdu_data_msgs[data_key.asdu_address].append(data_msg)
458
459            if msg.asdu_address != self._broadcast_asdu_address:
460                asdu_data_msgs[msg.asdu_address].append(None)
461
462            for asdu_address, data_msgs in asdu_data_msgs.items():
463                res = msg._replace(
464                    asdu_address=asdu_address,
465                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
466                    is_negative_confirm=False)
467                await self._send(conn_id, [res])
468
469                msgs = [
470                    data_msg._replace(
471                        is_test=msg.is_test,
472                        cause=iec101.DataResCause.INTERROGATED_COUNTER)
473                    for data_msg in data_msgs
474                    if data_msg]
475                if msgs:
476                    await self._send(conn_id, msgs)
477
478                res = msg._replace(
479                    asdu_address=asdu_address,
480                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
481                    is_negative_confirm=False)
482                await self._send(conn_id, [res])
483
484        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
485            res = msg._replace(
486                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
487                is_negative_confirm=True)
488            await self._send(conn_id, [res])
489
490        else:
491            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
492                               is_negative_confirm=True)
493            await self._send(conn_id, [res])
494
495    async def _process_read_msg(self, conn_id, msg):
496        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
497        await self._send(conn_id, [res])
498
499    async def _process_clock_sync_msg(self, conn_id, msg):
500        if isinstance(msg.cause, iec101.ClockSyncReqCause):
501            res = msg._replace(
502                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
503                is_negative_confirm=True)
504            await self._send(conn_id, [res])
505
506        else:
507            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
508                               is_negative_confirm=True)
509            await self._send(conn_id, [res])
510
511    async def _process_test_msg(self, conn_id, msg):
512        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
513        await self._send(conn_id, [res])
514
515    async def _process_reset_msg(self, conn_id, msg):
516        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
517        await self._send(conn_id, [res])
518
519    async def _process_parameter_msg(self, conn_id, msg):
520        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
521        await self._send(conn_id, [res])
522
523    async def _process_parameter_activation_msg(self, conn_id, msg):
524        res = msg._replace(
525            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
526        await self._send(conn_id, [res])
527
528    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
529        sent_cb = (functools.partial(buffer.remove, event_id)
530                   if buffer else None)
531        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
532
533    async def _send(self, conn_id, msgs, sent_cb=None):
534        send_queue = self._send_queues.get(conn_id)
535        if send_queue is None:
536            return
537
538        with contextlib.suppress(aio.QueueClosedError):
539            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:
542def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
543                     conn_id: int,
544                     msg: iec101.CommandMsg
545                     ) -> hat.event.common.RegisterEvent:
546    command_type = common.get_command_type(msg.command)
547    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
548    command = common.command_to_json(msg.command)
549    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
550                  str(msg.asdu_address), str(msg.io_address))
551
552    return hat.event.common.RegisterEvent(
553        type=event_type,
554        source_timestamp=None,
555        payload=hat.event.common.EventPayloadJson({
556            'connection_id': conn_id,
557            'is_test': msg.is_test,
558            'cause': cause,
559            '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:
562def data_msg_from_event(data_key: common.DataKey,
563                        event: hat.event.common.Event
564                        ) -> iec101.DataMsg:
565    time = common.time_from_source_timestamp(event.source_timestamp)
566    cause = common.cause_from_json(iec101.DataResCause,
567                                   event.payload.data['cause'])
568    data = common.data_from_json(data_key.data_type,
569                                 event.payload.data['data'])
570
571    return iec101.DataMsg(is_test=event.payload.data['is_test'],
572                          originator_address=0,
573                          asdu_address=data_key.asdu_address,
574                          io_address=data_key.io_address,
575                          data=data,
576                          time=time,
577                          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:
580def cmd_msg_from_event(cmd_key: common.CommandKey,
581                       event: hat.event.common.Event
582                       ) -> iec101.CommandMsg:
583    cause = common.cause_from_json(iec101.CommandResCause,
584                                   event.payload.data['cause'])
585    command = common.command_from_json(cmd_key.cmd_type,
586                                       event.payload.data['command'])
587    is_negative_confirm = event.payload.data['is_negative_confirm']
588
589    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
590                             originator_address=0,
591                             asdu_address=cmd_key.asdu_address,
592                             io_address=cmd_key.io_address,
593                             command=command,
594                             is_negative_confirm=is_negative_confirm,
595                             cause=cause)