hat.gateway.devices.iec101.slave

IEC 60870-5-104 slave device

  1"""IEC 60870-5-104 slave device"""
  2
  3from collections.abc import Collection, Iterable
  4import asyncio
  5import collections
  6import contextlib
  7import functools
  8import itertools
  9import logging
 10
 11from hat import aio
 12from hat import json
 13from hat.drivers import iec101
 14from hat.drivers import serial
 15from hat.drivers.iec60870 import link
 16import hat.event.common
 17import hat.event.eventer
 18
 19from hat.gateway.devices.iec101 import common
 20
 21
 22mlog: logging.Logger = logging.getLogger(__name__)
 23
 24
 25async def create(conf: common.DeviceConf,
 26                 eventer_client: hat.event.eventer.Client,
 27                 event_type_prefix: common.EventTypePrefix
 28                 ) -> 'Iec101SlaveDevice':
 29    device = Iec101SlaveDevice()
 30    device._conf = conf
 31    device._eventer_client = eventer_client
 32    device._event_type_prefix = event_type_prefix
 33    device._next_conn_ids = itertools.count(1)
 34    device._conns = {}
 35    device._send_queues = {}
 36    device._buffers = {}
 37    device._data_msgs = {}
 38    device._data_buffers = {}
 39    device._log = _create_logger_adapter(conf['name'])
 40
 41    init_buffers(buffers_conf=conf['buffers'],
 42                 buffers=device._buffers)
 43
 44    await init_data(log=device._log,
 45                    data_conf=conf['data'],
 46                    data_msgs=device._data_msgs,
 47                    data_buffers=device._data_buffers,
 48                    buffers=device._buffers,
 49                    eventer_client=eventer_client,
 50                    event_type_prefix=event_type_prefix)
 51
 52    if conf['link_type'] == 'BALANCED':
 53        create_link = link.create_balanced_link
 54
 55    elif conf['link_type'] == 'UNBALANCED':
 56        create_link = link.create_slave_link
 57
 58    else:
 59        raise ValueError('unsupported link type')
 60
 61    device._link = await create_link(
 62        port=conf['port'],
 63        address_size=link.AddressSize[conf['device_address_size']],
 64        silent_interval=conf['silent_interval'],
 65        baudrate=conf['baudrate'],
 66        bytesize=serial.ByteSize[conf['bytesize']],
 67        parity=serial.Parity[conf['parity']],
 68        stopbits=serial.StopBits[conf['stopbits']],
 69        xonxoff=conf['flow_control']['xonxoff'],
 70        rtscts=conf['flow_control']['rtscts'],
 71        dsrdtr=conf['flow_control']['dsrdtr'],
 72        name=conf['name'])
 73
 74    try:
 75        for device_conf in conf['devices']:
 76            device.async_group.spawn(device._connection_loop, device_conf)
 77
 78        await device._register_connections()
 79
 80    except BaseException:
 81        await aio.uncancellable(device.async_close())
 82        raise
 83
 84    return device
 85
 86
 87info: common.DeviceInfo = common.DeviceInfo(
 88    type="iec101_slave",
 89    create=create,
 90    json_schema_id="hat-gateway://iec101.yaml#/$defs/slave",
 91    json_schema_repo=common.json_schema_repo)
 92
 93
 94class Buffer:
 95
 96    def __init__(self, size: int):
 97        self._size = size
 98        self._data = collections.OrderedDict()
 99
100    def add(self,
101            event_id: hat.event.common.EventId,
102            data_msg: iec101.DataMsg):
103        self._data[event_id] = data_msg
104        while len(self._data) > self._size:
105            self._data.popitem(last=False)
106
107    def remove(self, event_id: hat.event.common.EventId):
108        self._data.pop(event_id, None)
109
110    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
111                                                       iec101.DataMsg]]:
112        return self._data.items()
113
114
115def init_buffers(buffers_conf: json.Data,
116                 buffers: dict[str, Buffer]):
117    for buffer_conf in buffers_conf:
118        buffers[buffer_conf['name']] = Buffer(buffer_conf['size'])
119
120
121async def init_data(log: logging.Logger,
122                    data_conf: json.Data,
123                    data_msgs: dict[common.DataKey, iec101.DataMsg],
124                    data_buffers: dict[common.DataKey, Buffer],
125                    buffers: dict[str, Buffer],
126                    eventer_client: hat.event.eventer.Client,
127                    event_type_prefix: common.EventTypePrefix):
128    for data in data_conf:
129        data_key = common.DataKey(data_type=common.DataType[data['data_type']],
130                                  asdu_address=data['asdu_address'],
131                                  io_address=data['io_address'])
132        data_msgs[data_key] = None
133        if data['buffer']:
134            data_buffers[data_key] = buffers[data['buffer']]
135
136    event_types = [(*event_type_prefix, 'system', 'data', '*')]
137    params = hat.event.common.QueryLatestParams(event_types)
138    result = await eventer_client.query(params)
139
140    for event in result.events:
141        try:
142            data_type_str, asdu_address_str, io_address_str = \
143                event.type[len(event_type_prefix)+2:]
144            data_key = common.DataKey(data_type=common.DataType(data_type_str),
145                                      asdu_address=int(asdu_address_str),
146                                      io_address=int(io_address_str))
147            if data_key not in data_msgs:
148                raise Exception(f'data {data_key} not configured')
149
150            data_msgs[data_key] = data_msg_from_event(data_key, event)
151
152        except Exception as e:
153            log.debug('skipping initial data: %s', e, exc_info=e)
154
155
156class Iec101SlaveDevice(common.Device):
157
158    @property
159    def async_group(self) -> aio.Group:
160        return self._link.async_group
161
162    async def process_events(self, events: Collection[hat.event.common.Event]):
163        for event in events:
164            try:
165                await self._process_event(event)
166
167            except Exception as e:
168                self._log.warning('error processing event: %s', e, exc_info=e)
169
170    async def _connection_loop(self, device_conf):
171        conn = None
172
173        try:
174            if self._conf['link_type'] == 'BALANCED':
175                conn_args = {
176                    'direction': link.Direction[device_conf['direction']],
177                    'addr': device_conf['address'],
178                    'response_timeout': device_conf['response_timeout'],
179                    'send_retry_count': device_conf['send_retry_count'],
180                    'status_delay': device_conf['status_delay'],
181                    'name': self._conf['name']}
182
183            elif self._conf['link_type'] == 'UNBALANCED':
184                conn_args = {
185                    'addr': device_conf['address'],
186                    'keep_alive_timeout': device_conf['keep_alive_timeout'],
187                    'name': self._conf['name']}
188
189            else:
190                raise ValueError('unsupported link type')
191
192            while True:
193                try:
194                    conn = await self._link.open_connection(**conn_args)
195
196                except Exception as e:
197                    self._log.error('connection error for address %s: %s',
198                                    device_conf['address'], e, exc_info=e)
199                    await asyncio.sleep(device_conf['reconnect_delay'])
200                    continue
201
202                conn = iec101.Connection(
203                    conn=conn,
204                    cause_size=iec101.CauseSize[self._conf['cause_size']],
205                    asdu_address_size=iec101.AsduAddressSize[
206                        self._conf['asdu_address_size']],
207                    io_address_size=iec101.IoAddressSize[
208                        self._conf['io_address_size']])
209
210                conn_id = next(self._next_conn_ids)
211                self._conns[conn_id] = conn
212
213                send_queue = aio.Queue(1024)
214                self._send_queues[conn_id] = send_queue
215
216                try:
217                    conn.async_group.spawn(self._connection_send_loop, conn,
218                                           send_queue)
219                    conn.async_group.spawn(self._connection_receive_loop, conn,
220                                           conn_id)
221
222                    await self._register_connections()
223
224                    with contextlib.suppress(Exception):
225                        for buffer in self._buffers.values():
226                            for event_id, data_msg in buffer.get_event_id_data_msgs():  # NOQA
227                                await self._send_data_msg(conn_id, buffer,
228                                                          event_id, data_msg)
229
230                    await conn.wait_closed()
231
232                finally:
233                    send_queue.close()
234
235                    self._conns.pop(conn_id, None)
236                    self._send_queues.pop(conn_id, None)
237
238                    with contextlib.suppress(Exception):
239                        await aio.uncancellable(self._register_connections())
240
241                await conn.async_close()
242
243        except Exception as e:
244            self._log.warning('connection loop error: %s', e, exc_info=e)
245
246        finally:
247            self._log.debug('closing connection')
248            self.close()
249
250            if conn:
251                await aio.uncancellable(conn.async_close())
252
253    async def _connection_send_loop(self, conn, send_queue):
254        try:
255            while True:
256                msgs, sent_cb = await send_queue.get()
257                await conn.send(msgs, sent_cb=sent_cb)
258
259        except ConnectionError:
260            self._log.debug('connection close')
261
262        except Exception as e:
263            self._log.warning('connection send loop error: %s', e, exc_info=e)
264
265        finally:
266            conn.close()
267
268    async def _connection_receive_loop(self, conn, conn_id):
269        try:
270            while True:
271                msgs = await conn.receive()
272
273                for msg in msgs:
274                    try:
275                        self._log.debug('received message: %s', msg)
276                        await self._process_msg(conn_id, msg)
277
278                    except Exception as e:
279                        self._log.warning('error processing message: %s',
280                                          e, exc_info=e)
281
282        except ConnectionError:
283            self._log.debug('connection close')
284
285        except Exception as e:
286            self._log.warning('connection receive loop error: %s',
287                              e, exc_info=e)
288
289        finally:
290            conn.close()
291
292    async def _register_connections(self):
293        payload = [{'connection_id': conn_id,
294                    'address': conn.info.address}
295                   for conn_id, conn in self._conns.items()]
296
297        event = hat.event.common.RegisterEvent(
298            type=(*self._event_type_prefix, 'gateway', 'connections'),
299            source_timestamp=None,
300            payload=hat.event.common.EventPayloadJson(payload))
301
302        await self._eventer_client.register([event])
303
304    async def _process_event(self, event):
305        suffix = event.type[len(self._event_type_prefix):]
306
307        if suffix[:2] == ('system', 'data'):
308            data_type_str, asdu_address_str, io_address_str = suffix[2:]
309            data_key = common.DataKey(data_type=common.DataType(data_type_str),
310                                      asdu_address=int(asdu_address_str),
311                                      io_address=int(io_address_str))
312
313            await self._process_data_event(data_key, event)
314
315        elif suffix[:2] == ('system', 'command'):
316            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
317            cmd_key = common.CommandKey(
318                cmd_type=common.CommandType(cmd_type_str),
319                asdu_address=int(asdu_address_str),
320                io_address=int(io_address_str))
321
322            await self._process_command_event(cmd_key, event)
323
324        else:
325            raise Exception('unsupported event type')
326
327    async def _process_data_event(self, data_key, event):
328        if data_key not in self._data_msgs:
329            raise Exception('data not configured')
330
331        data_msg = data_msg_from_event(data_key, event)
332        self._data_msgs[data_key] = data_msg
333
334        buffer = self._data_buffers.get(data_key)
335        if buffer:
336            buffer.add(event.id, data_msg)
337
338        for conn_id in self._conns.keys():
339            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
340
341    async def _process_command_event(self, cmd_key, event):
342        cmd_msg = cmd_msg_from_event(cmd_key, event)
343        conn_id = event.payload.data['connection_id']
344        await self._send(conn_id, [cmd_msg])
345
346    async def _process_msg(self, conn_id, msg):
347        if isinstance(msg, iec101.CommandMsg):
348            await self._process_command_msg(conn_id, msg)
349
350        elif isinstance(msg, iec101.InterrogationMsg):
351            await self._process_interrogation_msg(conn_id, msg)
352
353        elif isinstance(msg, iec101.CounterInterrogationMsg):
354            await self._process_counter_interrogation_msg(conn_id, msg)
355
356        elif isinstance(msg, iec101.ReadMsg):
357            await self._process_read_msg(conn_id, msg)
358
359        elif isinstance(msg, iec101.ClockSyncMsg):
360            await self._process_clock_sync_msg(conn_id, msg)
361
362        elif isinstance(msg, iec101.TestMsg):
363            await self._process_test_msg(conn_id, msg)
364
365        elif isinstance(msg, iec101.ResetMsg):
366            await self._process_reset_msg(conn_id, msg)
367
368        elif isinstance(msg, iec101.ParameterMsg):
369            await self._process_parameter_msg(conn_id, msg)
370
371        elif isinstance(msg, iec101.ParameterActivationMsg):
372            await self._process_parameter_activation_msg(conn_id, msg)
373
374        else:
375            raise Exception('unsupported message')
376
377    async def _process_command_msg(self, conn_id, msg):
378        if isinstance(msg.cause, iec101.CommandReqCause):
379            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
380            await self._eventer_client.register([event])
381
382        else:
383            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
384                               is_negative_confirm=True)
385            await self._send(conn_id, [res])
386
387    async def _process_interrogation_msg(self, conn_id, msg):
388        if msg.cause == iec101.CommandReqCause.ACTIVATION:
389            res = msg._replace(
390                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
391                is_negative_confirm=False)
392            await self._send(conn_id, [res])
393
394            data_msgs = [
395                data_msg._replace(
396                    is_test=msg.is_test,
397                    cause=iec101.DataResCause.INTERROGATED_STATION)
398                for data_msg in self._data_msgs.values()
399                if (data_msg and
400                    (msg.asdu_address == 0xFFFF or
401                     msg.asdu_address == data_msg.asdu_address) and
402                    not isinstance(data_msg.data, iec101.BinaryCounterData))]
403            await self._send(conn_id, data_msgs)
404
405            res = msg._replace(
406                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
407                is_negative_confirm=False)
408            await self._send(conn_id, [res])
409
410        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
411            res = msg._replace(
412                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
413                is_negative_confirm=True)
414            await self._send(conn_id, [res])
415
416        else:
417            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
418                               is_negative_confirm=True)
419            await self._send(conn_id, [res])
420
421    async def _process_counter_interrogation_msg(self, conn_id, msg):
422        if msg.cause == iec101.CommandReqCause.ACTIVATION:
423            res = msg._replace(
424                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
425                is_negative_confirm=False)
426            await self._send(conn_id, [res])
427
428            data_msgs = [
429                data_msg._replace(
430                    is_test=msg.is_test,
431                    cause=iec101.DataResCause.INTERROGATED_COUNTER)
432                for data_msg in self._data_msgs.values()
433                if (data_msg and
434                    (msg.asdu_address == 0xFFFF or
435                     msg.asdu_address == data_msg.asdu_address) and
436                    isinstance(data_msg.data, iec101.BinaryCounterData))]
437            await self._send(conn_id, data_msgs)
438
439            res = msg._replace(
440                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
441                is_negative_confirm=False)
442            await self._send(conn_id, [res])
443
444        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
445            res = msg._replace(
446                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
447                is_negative_confirm=True)
448            await self._send(conn_id, [res])
449
450        else:
451            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
452                               is_negative_confirm=True)
453            await self._send(conn_id, [res])
454
455    async def _process_read_msg(self, conn_id, msg):
456        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
457        await self._send(conn_id, [res])
458
459    async def _process_clock_sync_msg(self, conn_id, msg):
460        if isinstance(msg.cause, iec101.ClockSyncReqCause):
461            res = msg._replace(
462                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
463                is_negative_confirm=True)
464            await self._send(conn_id, [res])
465
466        else:
467            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
468                               is_negative_confirm=True)
469            await self._send(conn_id, [res])
470
471    async def _process_test_msg(self, conn_id, msg):
472        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
473        await self._send(conn_id, [res])
474
475    async def _process_reset_msg(self, conn_id, msg):
476        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
477        await self._send(conn_id, [res])
478
479    async def _process_parameter_msg(self, conn_id, msg):
480        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
481        await self._send(conn_id, [res])
482
483    async def _process_parameter_activation_msg(self, conn_id, msg):
484        res = msg._replace(
485            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
486        await self._send(conn_id, [res])
487
488    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
489        sent_cb = (functools.partial(buffer.remove, event_id)
490                   if buffer else None)
491        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
492
493    async def _send(self, conn_id, msgs, sent_cb=None):
494        send_queue = self._send_queues.get(conn_id)
495        if send_queue is None:
496            return
497
498        with contextlib.suppress(aio.QueueClosedError):
499            await send_queue.put((msgs, sent_cb))
500
501
502def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
503                     conn_id: int,
504                     msg: iec101.CommandMsg
505                     ) -> hat.event.common.RegisterEvent:
506    command_type = common.get_command_type(msg.command)
507    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
508    command = common.command_to_json(msg.command)
509    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
510                  str(msg.asdu_address), str(msg.io_address))
511
512    return hat.event.common.RegisterEvent(
513        type=event_type,
514        source_timestamp=None,
515        payload=hat.event.common.EventPayloadJson({
516            'connection_id': conn_id,
517            'is_test': msg.is_test,
518            'cause': cause,
519            'command': command}))
520
521
522def data_msg_from_event(data_key: common.DataKey,
523                        event: hat.event.common.Event
524                        ) -> iec101.DataMsg:
525    time = common.time_from_source_timestamp(event.source_timestamp)
526    cause = common.cause_from_json(iec101.DataResCause,
527                                   event.payload.data['cause'])
528    data = common.data_from_json(data_key.data_type,
529                                 event.payload.data['data'])
530
531    return iec101.DataMsg(is_test=event.payload.data['is_test'],
532                          originator_address=0,
533                          asdu_address=data_key.asdu_address,
534                          io_address=data_key.io_address,
535                          data=data,
536                          time=time,
537                          cause=cause)
538
539
540def cmd_msg_from_event(cmd_key: common.CommandKey,
541                       event: hat.event.common.Event
542                       ) -> iec101.CommandMsg:
543    cause = common.cause_from_json(iec101.CommandResCause,
544                                   event.payload.data['cause'])
545    command = common.command_from_json(cmd_key.cmd_type,
546                                       event.payload.data['command'])
547    is_negative_confirm = event.payload.data['is_negative_confirm']
548
549    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
550                             originator_address=0,
551                             asdu_address=cmd_key.asdu_address,
552                             io_address=cmd_key.io_address,
553                             command=command,
554                             is_negative_confirm=is_negative_confirm,
555                             cause=cause)
556
557
558def _create_logger_adapter(name):
559    extra = {'meta': {'type': 'Iec101SlaveDevice',
560                      'name': name}}
561
562    return logging.LoggerAdapter(mlog, extra)
mlog: logging.Logger = <Logger hat.gateway.devices.iec101.slave (WARNING)>
async def create( conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]) -> Iec101SlaveDevice:
26async def create(conf: common.DeviceConf,
27                 eventer_client: hat.event.eventer.Client,
28                 event_type_prefix: common.EventTypePrefix
29                 ) -> 'Iec101SlaveDevice':
30    device = Iec101SlaveDevice()
31    device._conf = conf
32    device._eventer_client = eventer_client
33    device._event_type_prefix = event_type_prefix
34    device._next_conn_ids = itertools.count(1)
35    device._conns = {}
36    device._send_queues = {}
37    device._buffers = {}
38    device._data_msgs = {}
39    device._data_buffers = {}
40    device._log = _create_logger_adapter(conf['name'])
41
42    init_buffers(buffers_conf=conf['buffers'],
43                 buffers=device._buffers)
44
45    await init_data(log=device._log,
46                    data_conf=conf['data'],
47                    data_msgs=device._data_msgs,
48                    data_buffers=device._data_buffers,
49                    buffers=device._buffers,
50                    eventer_client=eventer_client,
51                    event_type_prefix=event_type_prefix)
52
53    if conf['link_type'] == 'BALANCED':
54        create_link = link.create_balanced_link
55
56    elif conf['link_type'] == 'UNBALANCED':
57        create_link = link.create_slave_link
58
59    else:
60        raise ValueError('unsupported link type')
61
62    device._link = await create_link(
63        port=conf['port'],
64        address_size=link.AddressSize[conf['device_address_size']],
65        silent_interval=conf['silent_interval'],
66        baudrate=conf['baudrate'],
67        bytesize=serial.ByteSize[conf['bytesize']],
68        parity=serial.Parity[conf['parity']],
69        stopbits=serial.StopBits[conf['stopbits']],
70        xonxoff=conf['flow_control']['xonxoff'],
71        rtscts=conf['flow_control']['rtscts'],
72        dsrdtr=conf['flow_control']['dsrdtr'],
73        name=conf['name'])
74
75    try:
76        for device_conf in conf['devices']:
77            device.async_group.spawn(device._connection_loop, device_conf)
78
79        await device._register_connections()
80
81    except BaseException:
82        await aio.uncancellable(device.async_close())
83        raise
84
85    return device
info: hat.gateway.common.DeviceInfo = DeviceInfo(type='iec101_slave', create=<function create>, json_schema_id='hat-gateway://iec101.yaml#/$defs/slave', json_schema_repo={'hat-json://path.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-json://path.yaml', 'title': 'JSON Path', 'oneOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'array', 'items': {'$ref': 'hat-json://path.yaml'}}]}, 'hat-json://logging.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-json://logging.yaml', 'title': 'Logging', 'description': 'Logging configuration', 'type': 'object', 'required': ['version'], 'properties': {'version': {'title': 'Version', 'type': 'integer', 'default': 1}, 'formatters': {'title': 'Formatters', 'type': 'object', 'patternProperties': {'.+': {'title': 'Formatter', 'type': 'object', 'properties': {'format': {'title': 'Format', 'type': 'string', 'default': None}, 'datefmt': {'title': 'Date format', 'type': 'string', 'default': None}}}}}, 'filters': {'title': 'Filters', 'type': 'object', 'patternProperties': {'.+': {'title': 'Filter', 'type': 'object', 'properties': {'name': {'title': 'Logger name', 'type': 'string', 'default': ''}}}}}, 'handlers': {'title': 'Handlers', 'type': 'object', 'patternProperties': {'.+': {'title': 'Handler', 'type': 'object', 'description': 'Additional properties are passed as keyword arguments to\nconstructor\n', 'required': ['class'], 'properties': {'class': {'title': 'Class', 'type': 'string'}, 'level': {'title': 'Level', 'type': 'string'}, 'formatter': {'title': 'Formatter', 'type': 'string'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}}}}}, 'loggers': {'title': 'Loggers', 'type': 'object', 'patternProperties': {'.+': {'title': 'Logger', 'type': 'object', 'properties': {'level': {'title': 'Level', 'type': 'string'}, 'propagate': {'title': 'Propagate', 'type': 'boolean'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}, 'handlers': {'title': 'Handlers', 'type': 'array', 'items': {'title': 'Handler id', 'type': 'string'}}}}}}, 'root': {'title': 'Root logger', 'type': 'object', 'properties': {'level': {'title': 'Level', 'type': 'string'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}, 'handlers': {'title': 'Handlers', 'type': 'array', 'items': {'title': 'Handler id', 'type': 'string'}}}}, 'incremental': {'title': 'Incremental configuration', 'type': 'boolean', 'default': False}, 'disable_existing_loggers': {'title': 'Disable existing loggers', 'type': 'boolean', 'default': True}}}, 'hat-gateway://smpp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://smpp.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'remote_address', 'ssl', 'system_id', 'password', 'enquire_link_delay', 'enquire_link_timeout', 'connect_timeout', 'reconnect_delay', 'short_message', 'priority', 'data_coding', 'message_encoding', 'message_timeout'], 'properties': {'name': {'type': 'string'}, 'remote_address': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'ssl': {'type': 'boolean'}, 'system_id': {'type': 'string'}, 'password': {'type': 'string'}, 'enquire_link_delay': {'type': ['null', 'number']}, 'enquire_link_timeout': {'type': 'number'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'short_message': {'type': 'boolean'}, 'priority': {'enum': ['BULK', 'NORMAL', 'URGENT', 'VERY_URGENT']}, 'data_coding': {'enum': ['DEFAULT', 'ASCII', 'UNSPECIFIED_1', 'LATIN_1', 'UNSPECIFIED_2', 'JIS', 'CYRLLIC', 'LATIN_HEBREW', 'UCS2', 'PICTOGRAM', 'MUSIC', 'EXTENDED_KANJI', 'KS']}, 'message_encoding': {'type': 'string'}, 'message_timeout': {'type': 'number'}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}}, 'system': {'message': {'type': 'object', 'required': ['address', 'message'], 'properties': {'address': {'type': 'string'}, 'message': {'type': 'string'}}}}}}}}, 'hat-gateway://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'report_id', 'value'], 'properties': {'name': {'type': 'string'}, 'report_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}, 'commands': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'model', 'ref', 'with_operate_time'], 'properties': {'name': {'type': 'string'}, 'model': {'enum': ['DIRECT_WITH_NORMAL_SECURITY', 'SBO_WITH_NORMAL_SECURITY', 'DIRECT_WITH_ENHANCED_SECURITY', 'SBO_WITH_ENHANCED_SECURITY']}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/command'}, 'with_operate_time': {'type': 'boolean'}}}}, 'changes': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['reasons'], 'properties': {'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}}}, 'command': {'allOf': [{'type': 'object', 'required': ['session_id', 'action'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE', 'TERMINATION']}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'service_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}, 'additional_cause': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/additional_cause'}, 'test_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/test_error'}}}]}]}, 'change': {'allOf': [{'type': 'object', 'required': ['session_id'], 'properties': {'session_id': {'type': 'string'}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}}}]}]}, 'entry_id': {'type': ['string', 'null'], 'description': 'hex encoded bytes'}}, 'system': {'command': {'type': 'object', 'required': ['session_id', 'action', 'value', 'origin', 'test', 'checks'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE']}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'origin': {'type': 'object', 'required': ['category', 'identification'], 'properties': {'category': {'enum': ['BAY_CONTROL', 'STATION_CONTROL', 'REMOTE_CONTROL', 'AUTOMATIC_BAY', 'AUTOMATIC_STATION', 'AUTOMATIC_REMOTE', 'MAINTENANCE', 'PROCESS']}, 'identification': {'type': 'string'}}}, 'test': {'type': 'boolean'}, 'checks': {'type': 'array', 'items': {'enum': ['SYNCHRO', 'INTERLOCK']}}}}, 'change': {'type': 'object', 'requried': ['session_id', 'value'], 'properties': {'session_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}}}}, 'value': {'anyOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/integer'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/unsigned'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/float'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/bit_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/octet_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/visible_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/mms_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/array'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/struct'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/double_point'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/direction'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/severity'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/vector'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/step_position'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/binary_control'}], '$defs': {'boolean': {'type': 'boolean'}, 'integer': {'type': 'integer'}, 'unsigned': {'type': 'integer'}, 'float': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'bit_string': {'type': 'array', 'items': {'type': 'boolean'}}, 'octet_string': {'type': 'string', 'description': 'hex encoded bytes'}, 'visible_string': {'type': 'string'}, 'mms_string': {'type': 'string'}, 'array': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}, 'struct': {'type': 'object', 'patternProperties': {'.+': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}, 'quality': {'type': 'object', 'required': ['validity', 'details', 'source', 'test', 'operator_blocked'], 'properties': {'validity': {'enum': ['GOOD', 'INVALID', 'RESERVED', 'QUESTIONABLE']}, 'details': {'type': 'array', 'items': {'enum': ['OVERFLOW', 'OUT_OF_RANGE', 'BAD_REFERENCE', 'OSCILLATORY', 'FAILURE', 'OLD_DATA', 'INCONSISTENT', 'INACCURATE']}}, 'source': {'enum': ['PROCESS', 'SUBSTITUTED']}, 'test': {'type': 'boolean'}, 'operator_blocked': {'type': 'boolean'}}}, 'timestamp': {'type': 'object', 'required': ['value', 'leap_second', 'clock_failure', 'not_synchronized'], 'properties': {'value': {'type': 'number', 'description': 'seconds since 1970-01-01'}, 'leap_second': {'type': 'boolean'}, 'clock_failure': {'type': 'boolean'}, 'not_synchronized': {'type': 'boolean'}, 'accuracy': {'type': 'integer'}}}, 'double_point': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'BAD']}, 'direction': {'enum': ['UNKNOWN', 'FORWARD', 'BACKWARD', 'BOTH']}, 'severity': {'enum': ['UNKNOWN', 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING']}, 'analogue': {'type': 'object', 'properties': {'i': {'type': 'integer'}, 'f': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}}}, 'vector': {'type': 'object', 'required': ['magnitude'], 'properties': {'magnitude': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, 'angle': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}}}, 'step_position': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}, 'transient': {'type': 'boolean'}}}, 'binary_control': {'enum': ['STOP', 'LOWER', 'HIGHER', 'RESERVED']}}}, 'value_type': {'oneOf': [{'enum': ['BOOLEAN', 'INTEGER', 'UNSIGNED', 'FLOAT', 'BIT_STRING', 'OCTET_STRING', 'VISIBLE_STRING', 'MMS_STRING', 'QUALITY', 'TIMESTAMP', 'DOUBLE_POINT', 'DIRECTION', 'SEVERITY', 'ANALOGUE', 'VECTOR', 'STEP_POSITION', 'BINARY_CONTROL']}, {'type': 'object', 'required': ['type', 'element_type', 'length'], 'properties': {'type': {'const': 'ARRAY'}, 'element_type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}, 'length': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'elements'], 'properties': {'type': {'const': 'STRUCT'}, 'elements': {'type': 'array', 'items': {'type': 'object', 'requried': ['name', 'type'], 'properties': {'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}}}]}, 'refs': {'value': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, 'command': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}, 'rcb': {'type': 'object', 'required': ['logical_device', 'logical_node', 'type', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'type': {'enum': ['BUFFERED', 'UNBUFFERED']}, 'name': {'type': 'string'}}}, 'dataset': {'oneOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/persisted'}], '$defs': {'nonpersisted': {'type': 'string'}, 'persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, 'hat-gateway://iec103.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec103.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'reconnect_delay', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['cause', 'value'], 'properties': {'cause': {'oneOf': [{'enum': ['SPONTANEOUS', 'CYCLIC', 'TEST_MODE', 'GENERAL_INTERROGATION', 'LOCAL_OPERATION', 'REMOTE_OPERATION']}, {'type': 'integer', 'description': 'other cause in range [0, 255]\n'}]}, 'value': {'oneOf': [{'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}, {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/measurand'}]}}}, 'command': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'enable': {'type': 'boolean'}, 'command': {'type': 'object', 'required': ['session_id', 'value'], 'properties': {'value': {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}}}}}}, 'values': {'double': {'enum': ['TRANSIENT', 'OFF', 'ON', 'ERROR']}, 'measurand': {'type': 'object', 'required': ['overflow', 'invalid', 'value'], 'properties': {'overflow': {'type': 'boolean'}, 'invalid': {'type': 'boolean'}, 'value': {'type': 'number'}}}}}}, 'hat-gateway://main.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://main.yaml', 'title': 'Gateway', 'description': "Gateway's configuration", 'type': 'object', 'required': ['name', 'event_server', 'devices'], 'properties': {'type': {'const': 'gateway', 'description': 'configuration type identification'}, 'version': {'type': 'string', 'description': 'component version'}, 'log': {'$ref': 'hat-json://logging.yaml'}, 'name': {'type': 'string', 'description': 'component name'}, 'event_server': {'allOf': [{'type': 'object', 'properties': {'require_operational': {'type': 'boolean'}}}, {'oneOf': [{'type': 'object', 'required': ['monitor_component'], 'properties': {'monitor_component': {'type': 'object', 'required': ['host', 'port', 'gateway_group', 'event_server_group'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23010}, 'gateway_group': {'type': 'string'}, 'event_server_group': {'type': 'string'}}}}}, {'type': 'object', 'required': ['eventer_server'], 'properties': {'eventer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23012}}}}}]}]}, 'devices': {'type': 'array', 'items': {'$ref': 'hat-gateway://main.yaml#/$defs/device'}}, 'adminer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23016}}}}, '$defs': {'device': {'type': 'object', 'description': 'structure of device configuration depends on device type\n', 'required': ['module', 'name'], 'properties': {'module': {'type': 'string', 'description': 'full python module name that implements device\n'}, 'name': {'type': 'string'}}}}}, 'hat-gateway://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://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://ping.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://ping.yaml', '$defs': {'device': {'type': 'object', 'required': ['name', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'host', 'ping_delay', 'ping_timeout', 'retry_count', 'retry_delay'], 'properties': {'name': {'type': 'string'}, 'host': {'type': 'string'}, 'ping_delay': {'type': 'number'}, 'ping_timeout': {'type': 'number'}, 'retry_count': {'type': 'number'}, 'retry_delay': {'type': 'number'}}}}}}, 'events': {'status': {'enum': ['AVAILABLE', 'NOT_AVAILABLE']}}}}, 'hat-gateway://modbus.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://modbus.yaml', 'title': 'Modbus devices', '$defs': {'master': {'type': 'object', 'title': 'Modbus master', 'required': ['name', 'connection', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['modbus_type', 'transport', 'connect_timeout', 'connect_delay', 'request_timeout', 'request_delay', 'request_retry_immediate_count', 'request_retry_delayed_count', 'request_retry_delay'], 'properties': {'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'host', 'port'], 'properties': {'type': {'const': 'TCP'}, 'host': {'type': 'string', 'description': 'Remote host name\n'}, 'port': {'type': 'integer', 'description': 'Remote host TCP port\n', 'default': 502}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silet interval\n'}}}]}, 'connect_timeout': {'type': 'number', 'description': 'Maximum number of seconds available to single connection\nattempt\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of read or write\nrequest/response exchange.\n'}, 'request_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive requests\n(minimal duration between response and next request)\n'}, 'request_retry_immediate_count': {'type': 'integer', 'description': 'Number of immediate request retries before remote\ndata is considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delayed_count': {'type': 'integer', 'description': 'Number of delayed request retries before remote data\nis considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive delayed\nrequest retries\n'}}}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['device_id', 'timeout_poll_delay', 'data'], 'properties': {'device_id': {'type': 'integer', 'description': 'Modbus device identifier\n'}, 'timeout_poll_delay': {'type': 'number', 'description': 'Delay (in seconds) after read timeout and\nbefore device polling is resumed\n'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'interval', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'interval': {'type': ['number', 'null'], 'description': 'Polling interval in seconds or\nnull if polling is disabled\n'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}}}}, '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'}}}}}}}}})
class Buffer:
 95class Buffer:
 96
 97    def __init__(self, size: int):
 98        self._size = size
 99        self._data = collections.OrderedDict()
100
101    def add(self,
102            event_id: hat.event.common.EventId,
103            data_msg: iec101.DataMsg):
104        self._data[event_id] = data_msg
105        while len(self._data) > self._size:
106            self._data.popitem(last=False)
107
108    def remove(self, event_id: hat.event.common.EventId):
109        self._data.pop(event_id, None)
110
111    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
112                                                       iec101.DataMsg]]:
113        return self._data.items()
Buffer(size: int)
97    def __init__(self, size: int):
98        self._size = size
99        self._data = collections.OrderedDict()
def add( self, event_id: hat.event.common.common.EventId, data_msg: hat.drivers.iec101.common.DataMsg):
101    def add(self,
102            event_id: hat.event.common.EventId,
103            data_msg: iec101.DataMsg):
104        self._data[event_id] = data_msg
105        while len(self._data) > self._size:
106            self._data.popitem(last=False)
def remove(self, event_id: hat.event.common.common.EventId):
108    def remove(self, event_id: hat.event.common.EventId):
109        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]]:
111    def get_event_id_data_msgs(self) -> Iterable[tuple[hat.event.common.EventId,  # NOQA
112                                                       iec101.DataMsg]]:
113        return self._data.items()
def init_buffers( buffers_conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], buffers: dict[str, Buffer]):
116def init_buffers(buffers_conf: json.Data,
117                 buffers: dict[str, Buffer]):
118    for buffer_conf in buffers_conf:
119        buffers[buffer_conf['name']] = Buffer(buffer_conf['size'])
async def init_data( log: logging.Logger, data_conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], data_msgs: dict[hat.gateway.devices.iec101.common.DataKey, hat.drivers.iec101.common.DataMsg], data_buffers: dict[hat.gateway.devices.iec101.common.DataKey, Buffer], buffers: dict[str, Buffer], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]):
122async def init_data(log: logging.Logger,
123                    data_conf: json.Data,
124                    data_msgs: dict[common.DataKey, iec101.DataMsg],
125                    data_buffers: dict[common.DataKey, Buffer],
126                    buffers: dict[str, Buffer],
127                    eventer_client: hat.event.eventer.Client,
128                    event_type_prefix: common.EventTypePrefix):
129    for data in data_conf:
130        data_key = common.DataKey(data_type=common.DataType[data['data_type']],
131                                  asdu_address=data['asdu_address'],
132                                  io_address=data['io_address'])
133        data_msgs[data_key] = None
134        if data['buffer']:
135            data_buffers[data_key] = buffers[data['buffer']]
136
137    event_types = [(*event_type_prefix, 'system', 'data', '*')]
138    params = hat.event.common.QueryLatestParams(event_types)
139    result = await eventer_client.query(params)
140
141    for event in result.events:
142        try:
143            data_type_str, asdu_address_str, io_address_str = \
144                event.type[len(event_type_prefix)+2:]
145            data_key = common.DataKey(data_type=common.DataType(data_type_str),
146                                      asdu_address=int(asdu_address_str),
147                                      io_address=int(io_address_str))
148            if data_key not in data_msgs:
149                raise Exception(f'data {data_key} not configured')
150
151            data_msgs[data_key] = data_msg_from_event(data_key, event)
152
153        except Exception as e:
154            log.debug('skipping initial data: %s', e, exc_info=e)
class Iec101SlaveDevice(hat.gateway.common.Device):
157class Iec101SlaveDevice(common.Device):
158
159    @property
160    def async_group(self) -> aio.Group:
161        return self._link.async_group
162
163    async def process_events(self, events: Collection[hat.event.common.Event]):
164        for event in events:
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                msgs = await conn.receive()
273
274                for msg in msgs:
275                    try:
276                        self._log.debug('received message: %s', msg)
277                        await self._process_msg(conn_id, msg)
278
279                    except Exception as e:
280                        self._log.warning('error processing message: %s',
281                                          e, exc_info=e)
282
283        except ConnectionError:
284            self._log.debug('connection close')
285
286        except Exception as e:
287            self._log.warning('connection receive loop error: %s',
288                              e, exc_info=e)
289
290        finally:
291            conn.close()
292
293    async def _register_connections(self):
294        payload = [{'connection_id': conn_id,
295                    'address': conn.info.address}
296                   for conn_id, conn in self._conns.items()]
297
298        event = hat.event.common.RegisterEvent(
299            type=(*self._event_type_prefix, 'gateway', 'connections'),
300            source_timestamp=None,
301            payload=hat.event.common.EventPayloadJson(payload))
302
303        await self._eventer_client.register([event])
304
305    async def _process_event(self, event):
306        suffix = event.type[len(self._event_type_prefix):]
307
308        if suffix[:2] == ('system', 'data'):
309            data_type_str, asdu_address_str, io_address_str = suffix[2:]
310            data_key = common.DataKey(data_type=common.DataType(data_type_str),
311                                      asdu_address=int(asdu_address_str),
312                                      io_address=int(io_address_str))
313
314            await self._process_data_event(data_key, event)
315
316        elif suffix[:2] == ('system', 'command'):
317            cmd_type_str, asdu_address_str, io_address_str = suffix[2:]
318            cmd_key = common.CommandKey(
319                cmd_type=common.CommandType(cmd_type_str),
320                asdu_address=int(asdu_address_str),
321                io_address=int(io_address_str))
322
323            await self._process_command_event(cmd_key, event)
324
325        else:
326            raise Exception('unsupported event type')
327
328    async def _process_data_event(self, data_key, event):
329        if data_key not in self._data_msgs:
330            raise Exception('data not configured')
331
332        data_msg = data_msg_from_event(data_key, event)
333        self._data_msgs[data_key] = data_msg
334
335        buffer = self._data_buffers.get(data_key)
336        if buffer:
337            buffer.add(event.id, data_msg)
338
339        for conn_id in self._conns.keys():
340            await self._send_data_msg(conn_id, buffer, event.id, data_msg)
341
342    async def _process_command_event(self, cmd_key, event):
343        cmd_msg = cmd_msg_from_event(cmd_key, event)
344        conn_id = event.payload.data['connection_id']
345        await self._send(conn_id, [cmd_msg])
346
347    async def _process_msg(self, conn_id, msg):
348        if isinstance(msg, iec101.CommandMsg):
349            await self._process_command_msg(conn_id, msg)
350
351        elif isinstance(msg, iec101.InterrogationMsg):
352            await self._process_interrogation_msg(conn_id, msg)
353
354        elif isinstance(msg, iec101.CounterInterrogationMsg):
355            await self._process_counter_interrogation_msg(conn_id, msg)
356
357        elif isinstance(msg, iec101.ReadMsg):
358            await self._process_read_msg(conn_id, msg)
359
360        elif isinstance(msg, iec101.ClockSyncMsg):
361            await self._process_clock_sync_msg(conn_id, msg)
362
363        elif isinstance(msg, iec101.TestMsg):
364            await self._process_test_msg(conn_id, msg)
365
366        elif isinstance(msg, iec101.ResetMsg):
367            await self._process_reset_msg(conn_id, msg)
368
369        elif isinstance(msg, iec101.ParameterMsg):
370            await self._process_parameter_msg(conn_id, msg)
371
372        elif isinstance(msg, iec101.ParameterActivationMsg):
373            await self._process_parameter_activation_msg(conn_id, msg)
374
375        else:
376            raise Exception('unsupported message')
377
378    async def _process_command_msg(self, conn_id, msg):
379        if isinstance(msg.cause, iec101.CommandReqCause):
380            event = cmd_msg_to_event(self._event_type_prefix, conn_id, msg)
381            await self._eventer_client.register([event])
382
383        else:
384            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
385                               is_negative_confirm=True)
386            await self._send(conn_id, [res])
387
388    async def _process_interrogation_msg(self, conn_id, msg):
389        if msg.cause == iec101.CommandReqCause.ACTIVATION:
390            res = msg._replace(
391                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
392                is_negative_confirm=False)
393            await self._send(conn_id, [res])
394
395            data_msgs = [
396                data_msg._replace(
397                    is_test=msg.is_test,
398                    cause=iec101.DataResCause.INTERROGATED_STATION)
399                for data_msg in self._data_msgs.values()
400                if (data_msg and
401                    (msg.asdu_address == 0xFFFF or
402                     msg.asdu_address == data_msg.asdu_address) and
403                    not isinstance(data_msg.data, iec101.BinaryCounterData))]
404            await self._send(conn_id, data_msgs)
405
406            res = msg._replace(
407                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
408                is_negative_confirm=False)
409            await self._send(conn_id, [res])
410
411        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
412            res = msg._replace(
413                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
414                is_negative_confirm=True)
415            await self._send(conn_id, [res])
416
417        else:
418            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
419                               is_negative_confirm=True)
420            await self._send(conn_id, [res])
421
422    async def _process_counter_interrogation_msg(self, conn_id, msg):
423        if msg.cause == iec101.CommandReqCause.ACTIVATION:
424            res = msg._replace(
425                cause=iec101.CommandResCause.ACTIVATION_CONFIRMATION,
426                is_negative_confirm=False)
427            await self._send(conn_id, [res])
428
429            data_msgs = [
430                data_msg._replace(
431                    is_test=msg.is_test,
432                    cause=iec101.DataResCause.INTERROGATED_COUNTER)
433                for data_msg in self._data_msgs.values()
434                if (data_msg and
435                    (msg.asdu_address == 0xFFFF or
436                     msg.asdu_address == data_msg.asdu_address) and
437                    isinstance(data_msg.data, iec101.BinaryCounterData))]
438            await self._send(conn_id, data_msgs)
439
440            res = msg._replace(
441                cause=iec101.CommandResCause.ACTIVATION_TERMINATION,
442                is_negative_confirm=False)
443            await self._send(conn_id, [res])
444
445        elif msg.cause == iec101.CommandReqCause.DEACTIVATION:
446            res = msg._replace(
447                cause=iec101.CommandResCause.DEACTIVATION_CONFIRMATION,
448                is_negative_confirm=True)
449            await self._send(conn_id, [res])
450
451        else:
452            res = msg._replace(cause=iec101.CommandResCause.UNKNOWN_CAUSE,
453                               is_negative_confirm=True)
454            await self._send(conn_id, [res])
455
456    async def _process_read_msg(self, conn_id, msg):
457        res = msg._replace(cause=iec101.ReadResCause.UNKNOWN_TYPE)
458        await self._send(conn_id, [res])
459
460    async def _process_clock_sync_msg(self, conn_id, msg):
461        if isinstance(msg.cause, iec101.ClockSyncReqCause):
462            res = msg._replace(
463                cause=iec101.ClockSyncResCause.ACTIVATION_CONFIRMATION,
464                is_negative_confirm=True)
465            await self._send(conn_id, [res])
466
467        else:
468            res = msg._replace(cause=iec101.ClockSyncResCause.UNKNOWN_CAUSE,
469                               is_negative_confirm=True)
470            await self._send(conn_id, [res])
471
472    async def _process_test_msg(self, conn_id, msg):
473        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
474        await self._send(conn_id, [res])
475
476    async def _process_reset_msg(self, conn_id, msg):
477        res = msg._replace(cause=iec101.ActivationResCause.UNKNOWN_TYPE)
478        await self._send(conn_id, [res])
479
480    async def _process_parameter_msg(self, conn_id, msg):
481        res = msg._replace(cause=iec101.ParameterResCause.UNKNOWN_TYPE)
482        await self._send(conn_id, [res])
483
484    async def _process_parameter_activation_msg(self, conn_id, msg):
485        res = msg._replace(
486            cause=iec101.ParameterActivationResCause.UNKNOWN_TYPE)
487        await self._send(conn_id, [res])
488
489    async def _send_data_msg(self, conn_id, buffer, event_id, data_msg):
490        sent_cb = (functools.partial(buffer.remove, event_id)
491                   if buffer else None)
492        await self._send(conn_id, [data_msg], sent_cb=sent_cb)
493
494    async def _send(self, conn_id, msgs, sent_cb=None):
495        send_queue = self._send_queues.get(conn_id)
496        if send_queue is None:
497            return
498
499        with contextlib.suppress(aio.QueueClosedError):
500            await send_queue.put((msgs, sent_cb))

Device interface

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

Group controlling resource's lifetime.

async def process_events(self, events: Collection[hat.event.common.common.Event]):
163    async def process_events(self, events: Collection[hat.event.common.Event]):
164        for event in events:
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)

Process received events

This method can be coroutine or regular function.

def cmd_msg_to_event( event_type_prefix: tuple[str, ...], conn_id: int, msg: hat.drivers.iec101.common.CommandMsg) -> hat.event.common.common.RegisterEvent:
503def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
504                     conn_id: int,
505                     msg: iec101.CommandMsg
506                     ) -> hat.event.common.RegisterEvent:
507    command_type = common.get_command_type(msg.command)
508    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
509    command = common.command_to_json(msg.command)
510    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
511                  str(msg.asdu_address), str(msg.io_address))
512
513    return hat.event.common.RegisterEvent(
514        type=event_type,
515        source_timestamp=None,
516        payload=hat.event.common.EventPayloadJson({
517            'connection_id': conn_id,
518            'is_test': msg.is_test,
519            'cause': cause,
520            '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:
523def data_msg_from_event(data_key: common.DataKey,
524                        event: hat.event.common.Event
525                        ) -> iec101.DataMsg:
526    time = common.time_from_source_timestamp(event.source_timestamp)
527    cause = common.cause_from_json(iec101.DataResCause,
528                                   event.payload.data['cause'])
529    data = common.data_from_json(data_key.data_type,
530                                 event.payload.data['data'])
531
532    return iec101.DataMsg(is_test=event.payload.data['is_test'],
533                          originator_address=0,
534                          asdu_address=data_key.asdu_address,
535                          io_address=data_key.io_address,
536                          data=data,
537                          time=time,
538                          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:
541def cmd_msg_from_event(cmd_key: common.CommandKey,
542                       event: hat.event.common.Event
543                       ) -> iec101.CommandMsg:
544    cause = common.cause_from_json(iec101.CommandResCause,
545                                   event.payload.data['cause'])
546    command = common.command_from_json(cmd_key.cmd_type,
547                                       event.payload.data['command'])
548    is_negative_confirm = event.payload.data['is_negative_confirm']
549
550    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
551                             originator_address=0,
552                             asdu_address=cmd_key.asdu_address,
553                             io_address=cmd_key.io_address,
554                             command=command,
555                             is_negative_confirm=is_negative_confirm,
556                             cause=cause)