hat.gateway.devices.iec101.slave

IEC 60870-5-101 slave device

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

Device interface

async_group: hat.aio.group.Group
172    @property
173    def async_group(self) -> aio.Group:
174        return self._link.async_group

Group controlling resource's lifetime.

async def process_event(self, event: hat.event.common.common.Event):
176    async def process_event(self, event: hat.event.common.Event):
177        try:
178            await self._process_event(event)
179
180        except Exception as e:
181            self._log.warning('error processing event: %s', e, exc_info=e)

Process received event

This method can be coroutine or regular function.

def cmd_msg_to_event( event_type_prefix: tuple[str, ...], conn_id: int, msg: hat.drivers.iec101.common.CommandMsg) -> hat.event.common.common.RegisterEvent:
579def cmd_msg_to_event(event_type_prefix: hat.event.common.EventType,
580                     conn_id: int,
581                     msg: iec101.CommandMsg
582                     ) -> hat.event.common.RegisterEvent:
583    command_type = common.get_command_type(msg.command)
584    cause = common.cause_to_json(iec101.CommandReqCause, msg.cause)
585    command = common.command_to_json(msg.command)
586    event_type = (*event_type_prefix, 'gateway', 'command', command_type.value,
587                  str(msg.asdu_address), str(msg.io_address))
588
589    return hat.event.common.RegisterEvent(
590        type=event_type,
591        source_timestamp=None,
592        payload=hat.event.common.EventPayloadJson({
593            'connection_id': conn_id,
594            'is_test': msg.is_test,
595            'cause': cause,
596            '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:
599def data_msg_from_event(data_key: common.DataKey,
600                        event: hat.event.common.Event
601                        ) -> iec101.DataMsg:
602    time = common.time_from_source_timestamp(event.source_timestamp)
603    cause = common.cause_from_json(iec101.DataResCause,
604                                   event.payload.data['cause'])
605    data = common.data_from_json(data_key.data_type,
606                                 event.payload.data['data'])
607
608    return iec101.DataMsg(is_test=event.payload.data['is_test'],
609                          originator_address=0,
610                          asdu_address=data_key.asdu_address,
611                          io_address=data_key.io_address,
612                          data=data,
613                          time=time,
614                          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:
617def cmd_msg_from_event(cmd_key: common.CommandKey,
618                       event: hat.event.common.Event
619                       ) -> iec101.CommandMsg:
620    cause = common.cause_from_json(iec101.CommandResCause,
621                                   event.payload.data['cause'])
622    command = common.command_from_json(cmd_key.cmd_type,
623                                       event.payload.data['command'])
624    is_negative_confirm = event.payload.data['is_negative_confirm']
625
626    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
627                             originator_address=0,
628                             asdu_address=cmd_key.asdu_address,
629                             io_address=cmd_key.io_address,
630                             command=command,
631                             is_negative_confirm=is_negative_confirm,
632                             cause=cause)