hat.gateway.devices.iec101.slave

IEC 60870-5-101 slave device

  1"""IEC 60870-5-101 slave device"""
  2
  3from collections.abc import 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_event(self, event: hat.event.common.Event):
165        try:
166            await self._process_event(event)
167
168        except Exception as e:
169            self._log.warning('error processing event: %s', e, exc_info=e)
170
171    async def _connection_loop(self, device_conf):
172        conn = None
173
174        try:
175            if self._conf['link_type'] == 'BALANCED':
176                conn_args = {
177                    'direction': link.Direction[device_conf['direction']],
178                    'addr': device_conf['address'],
179                    'response_timeout': device_conf['response_timeout'],
180                    'send_retry_count': device_conf['send_retry_count'],
181                    'status_delay': device_conf['status_delay'],
182                    'name': self._conf['name']}
183
184            elif self._conf['link_type'] == 'UNBALANCED':
185                conn_args = {
186                    'addr': device_conf['address'],
187                    'keep_alive_timeout': device_conf['keep_alive_timeout'],
188                    'name': self._conf['name']}
189
190            else:
191                raise ValueError('unsupported link type')
192
193            while True:
194                try:
195                    conn = await self._link.open_connection(**conn_args)
196
197                except Exception as e:
198                    self._log.error('connection error for address %s: %s',
199                                    device_conf['address'], e, exc_info=e)
200                    await asyncio.sleep(device_conf['reconnect_delay'])
201                    continue
202
203                conn = iec101.Connection(
204                    conn=conn,
205                    cause_size=iec101.CauseSize[self._conf['cause_size']],
206                    asdu_address_size=iec101.AsduAddressSize[
207                        self._conf['asdu_address_size']],
208                    io_address_size=iec101.IoAddressSize[
209                        self._conf['io_address_size']])
210
211                conn_id = next(self._next_conn_ids)
212                self._conns[conn_id] = conn
213
214                send_queue = aio.Queue(1024)
215                self._send_queues[conn_id] = send_queue
216
217                try:
218                    conn.async_group.spawn(self._connection_send_loop, conn,
219                                           send_queue)
220                    conn.async_group.spawn(self._connection_receive_loop, conn,
221                                           conn_id)
222
223                    await self._register_connections()
224
225                    with contextlib.suppress(Exception):
226                        for buffer in self._buffers.values():
227                            for event_id, data_msg in buffer.get_event_id_data_msgs():  # NOQA
228                                await self._send_data_msg(conn_id, buffer,
229                                                          event_id, data_msg)
230
231                    await conn.wait_closed()
232
233                finally:
234                    send_queue.close()
235
236                    self._conns.pop(conn_id, None)
237                    self._send_queues.pop(conn_id, None)
238
239                    with contextlib.suppress(Exception):
240                        await aio.uncancellable(self._register_connections())
241
242                await conn.async_close()
243
244        except Exception as e:
245            self._log.warning('connection loop error: %s', e, exc_info=e)
246
247        finally:
248            self._log.debug('closing connection')
249            self.close()
250
251            if conn:
252                await aio.uncancellable(conn.async_close())
253
254    async def _connection_send_loop(self, conn, send_queue):
255        try:
256            while True:
257                msgs, sent_cb = await send_queue.get()
258                await conn.send(msgs, sent_cb=sent_cb)
259
260        except ConnectionError:
261            self._log.debug('connection close')
262
263        except Exception as e:
264            self._log.warning('connection send loop error: %s', e, exc_info=e)
265
266        finally:
267            conn.close()
268
269    async def _connection_receive_loop(self, conn, conn_id):
270        try:
271            while True:
272                try:
273                    msgs = await conn.receive()
274
275                except iec101.AsduTypeError as e:
276                    self._log.warning("asdu type error: %s", e)
277                    continue
278
279                for msg in msgs:
280                    try:
281                        self._log.debug('received message: %s', msg)
282                        await self._process_msg(conn_id, msg)
283
284                    except Exception as e:
285                        self._log.warning('error processing message: %s',
286                                          e, exc_info=e)
287
288        except ConnectionError:
289            self._log.debug('connection close')
290
291        except Exception as e:
292            self._log.warning('connection receive loop error: %s',
293                              e, exc_info=e)
294
295        finally:
296            conn.close()
297
298    async def _register_connections(self):
299        payload = [{'connection_id': conn_id,
300                    'address': conn.info.address}
301                   for conn_id, conn in self._conns.items()]
302
303        event = hat.event.common.RegisterEvent(
304            type=(*self._event_type_prefix, 'gateway', 'connections'),
305            source_timestamp=None,
306            payload=hat.event.common.EventPayloadJson(payload))
307
308        await self._eventer_client.register([event])
309
310    async def _process_event(self, event):
311        suffix = event.type[len(self._event_type_prefix):]
312
313        if suffix[:2] == ('system', 'data'):
314            data_type_str, asdu_address_str, io_address_str = suffix[2:]
315            data_key = common.DataKey(data_type=common.DataType(data_type_str),
316                                      asdu_address=int(asdu_address_str),
317                                      io_address=int(io_address_str))
318
319            await self._process_data_event(data_key, event)
320
321        elif suffix[:2] == ('system', 'command'):
322            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
323            cmd_key = common.CommandKey(
324                cmd_type=common.CommandType(cmd_type_str),
325                asdu_address=int(asdu_address_str),
326                io_address=int(io_address_str))
327
328            await self._process_command_event(cmd_key, event)
329
330        else:
331            raise Exception('unsupported event type')
332
333    async def _process_data_event(self, data_key, event):
334        if data_key not in self._data_msgs:
335            raise Exception('data not configured')
336
337        data_msg = data_msg_from_event(data_key, event)
338        self._data_msgs[data_key] = data_msg
339
340        buffer = self._data_buffers.get(data_key)
341        if buffer:
342            buffer.add(event.id, data_msg)
343
344        for conn_id in self._conns.keys():
345            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
346
347    async def _process_command_event(self, cmd_key, event):
348        cmd_msg = cmd_msg_from_event(cmd_key, event)
349        conn_id = event.payload.data['connection_id']
350        await self._send(conn_id, [cmd_msg])
351
352    async def _process_msg(self, conn_id, msg):
353        if isinstance(msg, iec101.CommandMsg):
354            await self._process_command_msg(conn_id, msg)
355
356        elif isinstance(msg, iec101.InterrogationMsg):
357            await self._process_interrogation_msg(conn_id, msg)
358
359        elif isinstance(msg, iec101.CounterInterrogationMsg):
360            await self._process_counter_interrogation_msg(conn_id, msg)
361
362        elif isinstance(msg, iec101.ReadMsg):
363            await self._process_read_msg(conn_id, msg)
364
365        elif isinstance(msg, iec101.ClockSyncMsg):
366            await self._process_clock_sync_msg(conn_id, msg)
367
368        elif isinstance(msg, iec101.TestMsg):
369            await self._process_test_msg(conn_id, msg)
370
371        elif isinstance(msg, iec101.ResetMsg):
372            await self._process_reset_msg(conn_id, msg)
373
374        elif isinstance(msg, iec101.ParameterMsg):
375            await self._process_parameter_msg(conn_id, msg)
376
377        elif isinstance(msg, iec101.ParameterActivationMsg):
378            await self._process_parameter_activation_msg(conn_id, msg)
379
380        else:
381            raise Exception('unsupported message')
382
383    async def _process_command_msg(self, conn_id, msg):
384        if isinstance(msg.cause, iec101.CommandReqCause):
385            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
386            await self._eventer_client.register([event])
387
388        else:
389            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
390                               is_negative_confirm=True)
391            await self._send(conn_id, [res])
392
393    async def _process_interrogation_msg(self, conn_id, msg):
394        if msg.cause == iec101.CommandReqCause.ACTIVATION:
395            asdu_data_msgs = collections.defaultdict(collections.deque)
396
397            for data_key, data_msg in self._data_msgs.items():
398                if data_key.data_type == common.DataType.BINARY_COUNTER:
399                    continue
400
401                if (msg.asdu_address != self._broadcast_asdu_address and
402                        msg.asdu_address != data_key.asdu_address):
403                    continue
404
405                asdu_data_msgs[data_key.asdu_address].append(data_msg)
406
407            if msg.asdu_address != self._broadcast_asdu_address:
408                asdu_data_msgs[msg.asdu_address].append(None)
409
410            for asdu_address, data_msgs in asdu_data_msgs.items():
411                res = msg._replace(
412                    asdu_address=asdu_address,
413                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
414                    is_negative_confirm=False)
415                await self._send(conn_id, [res])
416
417                msgs = [
418                    data_msg._replace(
419                        is_test=msg.is_test,
420                        cause=iec101.DataResCause.INTERROGATED_STATION)
421                    for data_msg in data_msgs
422                    if data_msg]
423                if msgs:
424                    await self._send(conn_id, msgs)
425
426                res = msg._replace(
427                    asdu_address=asdu_address,
428                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
429                    is_negative_confirm=False)
430                await self._send(conn_id, [res])
431
432        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
433            res = msg._replace(
434                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
435                is_negative_confirm=True)
436            await self._send(conn_id, [res])
437
438        else:
439            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
440                               is_negative_confirm=True)
441            await self._send(conn_id, [res])
442
443    async def _process_counter_interrogation_msg(self, conn_id, msg):
444        if msg.cause == iec101.CommandReqCause.ACTIVATION:
445            asdu_data_msgs = collections.defaultdict(collections.deque)
446
447            for data_key, data_msg in self._data_msgs.items():
448                if data_key.data_type != common.DataType.BINARY_COUNTER:
449                    continue
450
451                if (msg.asdu_address != self._broadcast_asdu_address and
452                        msg.asdu_address != data_key.asdu_address):
453                    continue
454
455                asdu_data_msgs[data_key.asdu_address].append(data_msg)
456
457            if msg.asdu_address != self._broadcast_asdu_address:
458                asdu_data_msgs[msg.asdu_address].append(None)
459
460            for asdu_address, data_msgs in asdu_data_msgs.items():
461                res = msg._replace(
462                    asdu_address=asdu_address,
463                    cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
464                    is_negative_confirm=False)
465                await self._send(conn_id, [res])
466
467                msgs = [
468                    data_msg._replace(
469                        is_test=msg.is_test,
470                        cause=iec101.DataResCause.INTERROGATED_COUNTER)
471                    for data_msg in data_msgs
472                    if data_msg]
473                if msgs:
474                    await self._send(conn_id, msgs)
475
476                res = msg._replace(
477                    asdu_address=asdu_address,
478                    cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
479                    is_negative_confirm=False)
480                await self._send(conn_id, [res])
481
482        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
483            res = msg._replace(
484                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
485                is_negative_confirm=True)
486            await self._send(conn_id, [res])
487
488        else:
489            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
490                               is_negative_confirm=True)
491            await self._send(conn_id, [res])
492
493    async def _process_read_msg(self, conn_id, msg):
494        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
495        await self._send(conn_id, [res])
496
497    async def _process_clock_sync_msg(self, conn_id, msg):
498        if isinstance(msg.cause, iec101.ClockSyncReqCause):
499            res = msg._replace(
500                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
501                is_negative_confirm=True)
502            await self._send(conn_id, [res])
503
504        else:
505            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
506                               is_negative_confirm=True)
507            await self._send(conn_id, [res])
508
509    async def _process_test_msg(self, conn_id, msg):
510        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
511        await self._send(conn_id, [res])
512
513    async def _process_reset_msg(self, conn_id, msg):
514        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
515        await self._send(conn_id, [res])
516
517    async def _process_parameter_msg(self, conn_id, msg):
518        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
519        await self._send(conn_id, [res])
520
521    async def _process_parameter_activation_msg(self, conn_id, msg):
522        res = msg._replace(
523            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
524        await self._send(conn_id, [res])
525
526    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
527        sent_cb = (functools.partial(buffer.remove, event_id)
528                   if buffer else None)
529        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
530
531    async def _send(self, conn_id, msgs, sent_cb=None):
532        send_queue = self._send_queues.get(conn_id)
533        if send_queue is None:
534            return
535
536        with contextlib.suppress(aio.QueueClosedError):
537            await send_queue.put((msgs, sent_cb))
538
539
540def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
541                     conn_id: int,
542                     msg: iec101.CommandMsg
543                     ) -> hat.event.common.RegisterEvent:
544    command_type = common.get_command_type(msg.command)
545    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
546    command = common.command_to_json(msg.command)
547    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
548                  str(msg.asdu_address), str(msg.io_address))
549
550    return hat.event.common.RegisterEvent(
551        type=event_type,
552        source_timestamp=None,
553        payload=hat.event.common.EventPayloadJson({
554            'connection_id': conn_id,
555            'is_test': msg.is_test,
556            'cause': cause,
557            'command': command}))
558
559
560def data_msg_from_event(data_key: common.DataKey,
561                        event: hat.event.common.Event
562                        ) -> iec101.DataMsg:
563    time = common.time_from_source_timestamp(event.source_timestamp)
564    cause = common.cause_from_json(iec101.DataResCause,
565                                   event.payload.data['cause'])
566    data = common.data_from_json(data_key.data_type,
567                                 event.payload.data['data'])
568
569    return iec101.DataMsg(is_test=event.payload.data['is_test'],
570                          originator_address=0,
571                          asdu_address=data_key.asdu_address,
572                          io_address=data_key.io_address,
573                          data=data,
574                          time=time,
575                          cause=cause)
576
577
578def cmd_msg_from_event(cmd_key: common.CommandKey,
579                       event: hat.event.common.Event
580                       ) -> iec101.CommandMsg:
581    cause = common.cause_from_json(iec101.CommandResCause,
582                                   event.payload.data['cause'])
583    command = common.command_from_json(cmd_key.cmd_type,
584                                       event.payload.data['command'])
585    is_negative_confirm = event.payload.data['is_negative_confirm']
586
587    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
588                             originator_address=0,
589                             asdu_address=cmd_key.asdu_address,
590                             io_address=cmd_key.io_address,
591                             command=command,
592                             is_negative_confirm=is_negative_confirm,
593                             cause=cause)
594
595
596def _get_broadcast_asdu_address(asdu_address_size):
597    if asdu_address_size == iec101.AsduAddressSize.ONE:
598        return 0xFF
599
600    if asdu_address_size == iec101.AsduAddressSize.TWO:
601        return 0xFFFF
602
603    raise ValueError('unsupported asdu address size')
604
605
606def _create_logger_adapter(name):
607    extra = {'meta': {'type': 'Iec101SlaveDevice',
608                      'name': name}}
609
610    return logging.LoggerAdapter(mlog, extra)
mlog: logging.Logger = <Logger hat.gateway.devices.iec101.slave (WARNING)>
async def create( conf: None | 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://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 silent 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', 'response_timeout', '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']}, 'response_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of write\nrequest/response exchange.\n'}, 'keep_alive_timeout': {'type': ['number', 'null']}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'response_timeout', '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 silent interval\n'}, 'response_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of write\nrequest/response exchange.\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', 'data'], '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', 'success'], 'properties': {'request_id': {'type': 'string'}, 'success': {'type': 'boolean'}}}}}}}}, '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://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://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://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://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://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://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: None | 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: None | 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_event(self, event: hat.event.common.Event):
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))

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_event(self, event: hat.event.common.common.Event):
165    async def process_event(self, event: hat.event.common.Event):
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)

Process received event

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:
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}))
def data_msg_from_event( data_key: hat.gateway.devices.iec101.common.DataKey, event: hat.event.common.common.Event) -> hat.drivers.iec101.common.DataMsg:
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)
def cmd_msg_from_event( cmd_key: hat.gateway.devices.iec101.common.CommandKey, event: hat.event.common.common.Event) -> hat.drivers.iec101.common.CommandMsg:
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)