hat.gateway.devices.iec101.master

IEC 60870-5-101 master device

  1"""IEC 60870-5-101 master device"""
  2
  3from collections.abc import Collection
  4import asyncio
  5import collections
  6import contextlib
  7import datetime
  8import functools
  9import logging
 10
 11from hat import aio
 12from hat.drivers import iec101
 13from hat.drivers import serial
 14from hat.drivers.iec60870 import link
 15import hat.event.common
 16import hat.event.eventer
 17
 18from hat.gateway.devices.iec101 import common
 19
 20
 21mlog: logging.Logger = logging.getLogger(__name__)
 22
 23
 24async def create(conf: common.DeviceConf,
 25                 eventer_client: hat.event.eventer.Client,
 26                 event_type_prefix: common.EventTypePrefix
 27                 ) -> 'Iec101MasterDevice':
 28    event_types = [(*event_type_prefix, 'system', 'remote_device',
 29                    str(i['address']), 'enable')
 30                   for i in conf['remote_devices']]
 31    params = hat.event.common.QueryLatestParams(event_types)
 32    result = await eventer_client.query(params)
 33
 34    device = Iec101MasterDevice(conf=conf,
 35                                eventer_client=eventer_client,
 36                                event_type_prefix=event_type_prefix)
 37    try:
 38        await device.process_events(result.events)
 39
 40    except BaseException:
 41        await aio.uncancellable(device.async_close())
 42        raise
 43
 44    return device
 45
 46
 47info: common.DeviceInfo = common.DeviceInfo(
 48    type="iec101_master",
 49    create=create,
 50    json_schema_id="hat-gateway://iec101.yaml#/$defs/master",
 51    json_schema_repo=common.json_schema_repo)
 52
 53
 54class Iec101MasterDevice(common.Device):
 55
 56    def __init__(self,
 57                 conf: common.DeviceConf,
 58                 eventer_client: hat.event.eventer.Client,
 59                 event_type_prefix: common.EventTypePrefix,
 60                 send_queue_size: int = 1024):
 61        self._conf = conf
 62        self._event_type_prefix = event_type_prefix
 63        self._eventer_client = eventer_client
 64        self._link = None
 65        self._conns = {}
 66        self._send_queue = aio.Queue(send_queue_size)
 67        self._async_group = aio.Group()
 68        self._remote_enabled = {i['address']: False
 69                                for i in conf['remote_devices']}
 70        self._remote_confs = {i['address']: i
 71                              for i in conf['remote_devices']}
 72        self._remote_groups = {}
 73        self._log = _create_logger_adapter(conf['name'])
 74
 75        self.async_group.spawn(self._link_loop)
 76        self.async_group.spawn(self._send_loop)
 77
 78    @property
 79    def async_group(self) -> aio.Group:
 80        return self._async_group
 81
 82    async def process_events(self, events: Collection[hat.event.common.Event]):
 83        for event in events:
 84            try:
 85                await self._process_event(event)
 86
 87            except Exception as e:
 88                self._log.warning('error processing event: %s', e, exc_info=e)
 89
 90    async def _link_loop(self):
 91
 92        async def cleanup():
 93            with contextlib.suppress(ConnectionError):
 94                await self._register_status('DISCONNECTED')
 95
 96            if self._link:
 97                await self._link.async_close()
 98
 99        try:
100            if self._conf['link_type'] == 'BALANCED':
101                create_link = link.create_balanced_link
102
103            elif self._conf['link_type'] == 'UNBALANCED':
104                create_link = link.create_master_link
105
106            else:
107                raise ValueError('unsupported link type')
108
109            while True:
110                await self._register_status('CONNECTING')
111
112                try:
113                    self._link = await create_link(
114                        port=self._conf['port'],
115                        address_size=link.AddressSize[
116                            self._conf['device_address_size']],
117                        silent_interval=self._conf['silent_interval'],
118                        baudrate=self._conf['baudrate'],
119                        bytesize=serial.ByteSize[self._conf['bytesize']],
120                        parity=serial.Parity[self._conf['parity']],
121                        stopbits=serial.StopBits[self._conf['stopbits']],
122                        xonxoff=self._conf['flow_control']['xonxoff'],
123                        rtscts=self._conf['flow_control']['rtscts'],
124                        dsrdtr=self._conf['flow_control']['dsrdtr'],
125                        name=self._conf['name'])
126
127                except Exception as e:
128                    self._log.warning(
129                        'link master (endpoint) failed to create: %s',
130                        e, exc_info=e)
131                    await self._register_status('DISCONNECTED')
132                    await asyncio.sleep(self._conf['reconnect_delay'])
133                    continue
134
135                await self._register_status('CONNECTED')
136
137                for address, enabled in self._remote_enabled.items():
138                    if enabled:
139                        self._enable_remote(address)
140
141                await self._link.wait_closed()
142                await self._register_status('DISCONNECTED')
143                self._link = None
144
145        except Exception as e:
146            self._log.error('create link master error: %s', e, exc_info=e)
147
148        finally:
149            self._log.debug('closing link master loop')
150            self.close()
151            self._conns = {}
152            await aio.uncancellable(cleanup())
153
154    async def _send_loop(self):
155        while True:
156            msg, address = await self._send_queue.get()
157
158            conn = self._conns.get(address)
159            if not conn or not conn.is_open:
160                self._log.warning('msg %s not sent, connection to %s closed',
161                                  msg, address)
162                continue
163
164            try:
165                await conn.send([msg])
166                self._log.debug('msg sent asdu=%s', msg.asdu_address)
167
168            except ConnectionError:
169                self._log.warning('msg %s not sent, connection to %s closed',
170                                  msg, address)
171
172    async def _connection_loop(self, group, address):
173
174        async def cleanup():
175            with contextlib.suppress(ConnectionError):
176                await self._register_rmt_status(address, 'DISCONNECTED')
177
178            self._conns.pop(address, None)
179            if conn:
180                await conn.async_close()
181
182        conn = None
183        remote_conf = self._remote_confs[address]
184
185        try:
186            if self._conf['link_type'] == 'BALANCED':
187                conn_args = {
188                    'direction': link.Direction[remote_conf['direction']],
189                    'addr': address,
190                    'response_timeout': remote_conf['response_timeout'],
191                    'send_retry_count': remote_conf['send_retry_count'],
192                    'status_delay': remote_conf['status_delay'],
193                    'name': self._conf['name']}
194
195            elif self._conf['link_type'] == 'UNBALANCED':
196                conn_args = {
197                    'addr': address,
198                    'response_timeout': remote_conf['response_timeout'],
199                    'send_retry_count': remote_conf['send_retry_count'],
200                    'poll_class1_delay': remote_conf['poll_class1_delay'],
201                    'poll_class2_delay': remote_conf['poll_class2_delay'],
202                    'name': self._conf['name']}
203
204            else:
205                raise ValueError('unsupported link type')
206
207            while True:
208                await self._register_rmt_status(address, 'CONNECTING')
209
210                try:
211                    conn = await self._link.open_connection(**conn_args)
212
213                except Exception as e:
214                    self._log.error('connection error to address %s: %s',
215                                    address, e, exc_info=e)
216                    await self._register_rmt_status(address, 'DISCONNECTED')
217                    await asyncio.sleep(remote_conf['reconnect_delay'])
218                    continue
219
220                await self._register_rmt_status(address, 'CONNECTED')
221
222                conn = iec101.Connection(
223                    conn=conn,
224                    cause_size=iec101.CauseSize[self._conf['cause_size']],
225                    asdu_address_size=iec101.AsduAddressSize[
226                        self._conf['asdu_address_size']],
227                    io_address_size=iec101.IoAddressSize[
228                        self._conf['io_address_size']])
229                self._conns[address] = conn
230                group.spawn(self._receive_loop, conn, address)
231
232                if remote_conf['time_sync_delay'] is not None:
233                    group.spawn(self._time_sync_loop, conn,
234                                remote_conf['time_sync_delay'])
235
236                await conn.wait_closed()
237                await self._register_rmt_status(address, 'DISCONNECTED')
238                self._conns.pop(address, None)
239
240        except Exception as e:
241            self._log.error('connection loop error: %s', e, exc_info=e)
242
243        finally:
244            self._log.debug('closing remote device %s', address)
245            group.close()
246            await aio.uncancellable(cleanup())
247
248    async def _receive_loop(self, conn, address):
249        try:
250            while True:
251                try:
252                    msgs = await conn.receive()
253
254                except iec101.AsduTypeError as e:
255                    self._log.warning("asdu type error: %s", e)
256                    continue
257
258                events = collections.deque()
259                for msg in msgs:
260                    if isinstance(msg, iec101.ClockSyncMsg):
261                        continue
262
263                    try:
264                        event = _msg_to_event(self._event_type_prefix, address,
265                                              msg)
266                        events.append(event)
267
268                    except Exception as e:
269                        self._log.warning('message %s ignored due to: %s',
270                                          msg, e, exc_info=e)
271
272                if not events:
273                    continue
274
275                await self._eventer_client.register(events)
276                for e in events:
277                    self._log.debug('registered event %s', e)
278
279        except ConnectionError:
280            self._log.debug('connection closed')
281
282        except Exception as e:
283            self._log.error('receive loop error: %s', e, exc_info=e)
284
285        finally:
286            conn.close()
287
288    async def _time_sync_loop(self, conn, delay):
289        try:
290            while True:
291                time_now = datetime.datetime.now(datetime.timezone.utc)
292                time_iec101 = iec101.time_from_datetime(time_now)
293                msg = iec101.ClockSyncMsg(
294                    is_test=False,
295                    originator_address=0,
296                    asdu_address={
297                        'ONE': 0xFF,
298                        'TWO': 0xFFFF}[self._conf['asdu_address_size']],
299                    time=time_iec101,
300                    is_negative_confirm=False,
301                    cause=iec101.ClockSyncReqCause.ACTIVATION)
302                await conn.send([msg])
303                self._log.debug('time sync sent %s', time_iec101)
304
305                await asyncio.sleep(delay)
306
307        except ConnectionError:
308            self._log.debug('connection closed')
309
310        except Exception as e:
311            self._log.error('time sync loop error: %s', e, exc_info=e)
312
313        finally:
314            conn.close()
315
316    async def _process_event(self, event):
317        match_type = functools.partial(hat.event.common.matches_query_type,
318                                       event.type)
319
320        prefix = (*self._event_type_prefix, 'system', 'remote_device', '?')
321        if not match_type((*prefix, '*')):
322            raise Exception('unexpected event type')
323
324        address = int(event.type[len(prefix) - 1])
325        suffix = event.type[len(prefix):]
326
327        if match_type((*prefix, 'enable')):
328            self._process_event_enable(address, event)
329
330        elif match_type((*prefix, 'command', '?', '?', '?')):
331            cmd_key = common.CommandKey(
332                cmd_type=common.CommandType(suffix[1]),
333                asdu_address=int(suffix[2]),
334                io_address=int(suffix[3]))
335            msg = _command_from_event(cmd_key, event)
336
337            await self._send_queue.put((msg, address))
338            self._log.debug('command asdu=%s io=%s prepared for sending',
339                            cmd_key.asdu_address, cmd_key.io_address)
340
341        elif match_type((*prefix, 'interrogation', '?')):
342            asdu_address = int(suffix[1])
343            msg = _interrogation_from_event(asdu_address, event)
344
345            await self._send_queue.put((msg, address))
346            self._log.debug(
347                "interrogation request asdu=%s prepared for sending",
348                asdu_address)
349
350        elif match_type((*prefix, 'counter_interrogation', '?')):
351            asdu_address = int(suffix[1])
352            msg = _counter_interrogation_from_event(asdu_address, event)
353
354            await self._send_queue.put((msg, address))
355            self._log.debug(
356                "counter interrogation request asdu=%s prepared for sending",
357                asdu_address)
358
359        else:
360            raise Exception('unexpected event type')
361
362    def _process_event_enable(self, address, event):
363        if address not in self._remote_enabled:
364            raise Exception('invalid remote device address')
365
366        enable = event.payload.data
367        if not isinstance(enable, bool):
368            raise Exception('invalid enable event payload')
369
370        if address not in self._remote_enabled:
371            self._log.warning('received enable for unexpected remote device')
372            return
373
374        self._remote_enabled[address] = enable
375
376        if not enable:
377            self._disable_remote(address)
378
379        elif not self._link:
380            return
381
382        else:
383            self._enable_remote(address)
384
385    def _enable_remote(self, address):
386        self._log.debug('enabling device %s', address)
387        remote_group = self._remote_groups.get(address)
388        if remote_group and remote_group.is_open:
389            self._log.debug('device %s is already running', address)
390            return
391
392        remote_group = self._async_group.create_subgroup()
393        self._remote_groups[address] = remote_group
394        remote_group.spawn(self._connection_loop, remote_group, address)
395
396    def _disable_remote(self, address):
397        self._log.debug('disabling device %s', address)
398        if address in self._remote_groups:
399            remote_group = self._remote_groups.pop(address)
400            remote_group.close()
401
402    async def _register_status(self, status):
403        event = hat.event.common.RegisterEvent(
404            type=(*self._event_type_prefix, 'gateway', 'status'),
405            source_timestamp=None,
406            payload=hat.event.common.EventPayloadJson(status))
407        await self._eventer_client.register([event])
408        self._log.debug('device status -> %s', status)
409
410    async def _register_rmt_status(self, address, status):
411        event = hat.event.common.RegisterEvent(
412            type=(*self._event_type_prefix, 'gateway', 'remote_device',
413                  str(address), 'status'),
414            source_timestamp=None,
415            payload=hat.event.common.EventPayloadJson(status))
416        await self._eventer_client.register([event])
417        self._log.debug('remote device %s status -> %s', address, status)
418
419
420def _msg_to_event(event_type_prefix, address, msg):
421    if isinstance(msg, iec101.DataMsg):
422        return _data_to_event(event_type_prefix, address, msg)
423
424    if isinstance(msg, iec101.CommandMsg):
425        return _command_to_event(event_type_prefix, address, msg)
426
427    if isinstance(msg, iec101.InterrogationMsg):
428        return _interrogation_to_event(event_type_prefix, address, msg)
429
430    if isinstance(msg, iec101.CounterInterrogationMsg):
431        return _counter_interrogation_to_event(event_type_prefix, address, msg)
432
433    raise Exception('unsupported message type')
434
435
436def _data_to_event(event_type_prefix, address, msg):
437    data_type = common.get_data_type(msg.data)
438    cause = common.cause_to_json(iec101.DataResCause, msg.cause)
439    data = common.data_to_json(msg.data)
440    event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address),
441                  'data', data_type.value, str(msg.asdu_address),
442                  str(msg.io_address))
443    source_timestamp = common.time_to_source_timestamp(msg.time)
444
445    return hat.event.common.RegisterEvent(
446        type=event_type,
447        source_timestamp=source_timestamp,
448        payload=hat.event.common.EventPayloadJson({'is_test': msg.is_test,
449                                                   'cause': cause,
450                                                   'data': data}))
451
452
453def _command_to_event(event_type_prefix, address, msg):
454    command_type = common.get_command_type(msg.command)
455    cause = common.cause_to_json(iec101.CommandResCause, msg.cause)
456    command = common.command_to_json(msg.command)
457    event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address),
458                  'command', command_type.value, str(msg.asdu_address),
459                  str(msg.io_address))
460
461    return hat.event.common.RegisterEvent(
462        type=event_type,
463        source_timestamp=None,
464        payload=hat.event.common.EventPayloadJson({
465            'is_test': msg.is_test,
466            'is_negative_confirm': msg.is_negative_confirm,
467            'cause': cause,
468            'command': command}))
469
470
471def _interrogation_to_event(event_type_prefix, address, msg):
472    cause = common.cause_to_json(iec101.CommandResCause, msg.cause)
473    event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address),
474                  'interrogation', str(msg.asdu_address))
475
476    return hat.event.common.RegisterEvent(
477        type=event_type,
478        source_timestamp=None,
479        payload=hat.event.common.EventPayloadJson({
480            'is_test': msg.is_test,
481            'is_negative_confirm': msg.is_negative_confirm,
482            'request': msg.request,
483            'cause': cause}))
484
485
486def _counter_interrogation_to_event(event_type_prefix, address, msg):
487    cause = common.cause_to_json(iec101.CommandResCause, msg.cause)
488    event_type = (*event_type_prefix, 'gateway', 'remote_device', str(address),
489                  'counter_interrogation', str(msg.asdu_address))
490
491    return hat.event.common.RegisterEvent(
492        type=event_type,
493        source_timestamp=None,
494        payload=hat.event.common.EventPayloadJson({
495            'is_test': msg.is_test,
496            'is_negative_confirm': msg.is_negative_confirm,
497            'request': msg.request,
498            'freeze': msg.freeze.name,
499            'cause': cause}))
500
501
502def _command_from_event(cmd_key, event):
503    cause = common.cause_from_json(iec101.CommandReqCause,
504                                   event.payload.data['cause'])
505    command = common.command_from_json(cmd_key.cmd_type,
506                                       event.payload.data['command'])
507
508    return iec101.CommandMsg(is_test=event.payload.data['is_test'],
509                             originator_address=0,
510                             asdu_address=cmd_key.asdu_address,
511                             io_address=cmd_key.io_address,
512                             command=command,
513                             is_negative_confirm=False,
514                             cause=cause)
515
516
517def _interrogation_from_event(asdu_address, event):
518    cause = common.cause_from_json(iec101.CommandReqCause,
519                                   event.payload.data['cause'])
520
521    return iec101.InterrogationMsg(is_test=event.payload.data['is_test'],
522                                   originator_address=0,
523                                   asdu_address=asdu_address,
524                                   request=event.payload.data['request'],
525                                   is_negative_confirm=False,
526                                   cause=cause)
527
528
529def _counter_interrogation_from_event(asdu_address, event):
530    freeze = iec101.FreezeCode[event.payload.data['freeze']]
531    cause = common.cause_from_json(iec101.CommandReqCause,
532                                   event.payload.data['cause'])
533
534    return iec101.CounterInterrogationMsg(
535        is_test=event.payload.data['is_test'],
536        originator_address=0,
537        asdu_address=asdu_address,
538        request=event.payload.data['request'],
539        freeze=freeze,
540        is_negative_confirm=False,
541        cause=cause)
542
543
544def _create_logger_adapter(name):
545    extra = {'meta': {'type': 'Iec101MasterDevice',
546                      'name': name}}
547
548    return logging.LoggerAdapter(mlog, extra)
mlog: logging.Logger = <Logger hat.gateway.devices.iec101.master (WARNING)>
async def create( conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str]) -> Iec101MasterDevice:
25async def create(conf: common.DeviceConf,
26                 eventer_client: hat.event.eventer.Client,
27                 event_type_prefix: common.EventTypePrefix
28                 ) -> 'Iec101MasterDevice':
29    event_types = [(*event_type_prefix, 'system', 'remote_device',
30                    str(i['address']), 'enable')
31                   for i in conf['remote_devices']]
32    params = hat.event.common.QueryLatestParams(event_types)
33    result = await eventer_client.query(params)
34
35    device = Iec101MasterDevice(conf=conf,
36                                eventer_client=eventer_client,
37                                event_type_prefix=event_type_prefix)
38    try:
39        await device.process_events(result.events)
40
41    except BaseException:
42        await aio.uncancellable(device.async_close())
43        raise
44
45    return device
info: hat.gateway.common.DeviceInfo = DeviceInfo(type='iec101_master', create=<function create>, json_schema_id='hat-gateway://iec101.yaml#/$defs/master', json_schema_repo={'hat-json://path.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-json://path.yaml', 'title': 'JSON Path', 'oneOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'array', 'items': {'$ref': 'hat-json://path.yaml'}}]}, 'hat-json://logging.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-json://logging.yaml', 'title': 'Logging', 'description': 'Logging configuration', 'type': 'object', 'required': ['version'], 'properties': {'version': {'title': 'Version', 'type': 'integer', 'default': 1}, 'formatters': {'title': 'Formatters', 'type': 'object', 'patternProperties': {'.+': {'title': 'Formatter', 'type': 'object', 'properties': {'format': {'title': 'Format', 'type': 'string', 'default': None}, 'datefmt': {'title': 'Date format', 'type': 'string', 'default': None}}}}}, 'filters': {'title': 'Filters', 'type': 'object', 'patternProperties': {'.+': {'title': 'Filter', 'type': 'object', 'properties': {'name': {'title': 'Logger name', 'type': 'string', 'default': ''}}}}}, 'handlers': {'title': 'Handlers', 'type': 'object', 'patternProperties': {'.+': {'title': 'Handler', 'type': 'object', 'description': 'Additional properties are passed as keyword arguments to\nconstructor\n', 'required': ['class'], 'properties': {'class': {'title': 'Class', 'type': 'string'}, 'level': {'title': 'Level', 'type': 'string'}, 'formatter': {'title': 'Formatter', 'type': 'string'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}}}}}, 'loggers': {'title': 'Loggers', 'type': 'object', 'patternProperties': {'.+': {'title': 'Logger', 'type': 'object', 'properties': {'level': {'title': 'Level', 'type': 'string'}, 'propagate': {'title': 'Propagate', 'type': 'boolean'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}, 'handlers': {'title': 'Handlers', 'type': 'array', 'items': {'title': 'Handler id', 'type': 'string'}}}}}}, 'root': {'title': 'Root logger', 'type': 'object', 'properties': {'level': {'title': 'Level', 'type': 'string'}, 'filters': {'title': 'Filters', 'type': 'array', 'items': {'title': 'Filter id', 'type': 'string'}}, 'handlers': {'title': 'Handlers', 'type': 'array', 'items': {'title': 'Handler id', 'type': 'string'}}}}, 'incremental': {'title': 'Incremental configuration', 'type': 'boolean', 'default': False}, 'disable_existing_loggers': {'title': 'Disable existing loggers', 'type': 'boolean', 'default': True}}}, 'hat-gateway://smpp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://smpp.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'remote_address', 'ssl', 'system_id', 'password', 'enquire_link_delay', 'enquire_link_timeout', 'connect_timeout', 'reconnect_delay', 'short_message', 'priority', 'data_coding', 'message_encoding', 'message_timeout'], 'properties': {'name': {'type': 'string'}, 'remote_address': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'ssl': {'type': 'boolean'}, 'system_id': {'type': 'string'}, 'password': {'type': 'string'}, 'enquire_link_delay': {'type': ['null', 'number']}, 'enquire_link_timeout': {'type': 'number'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'short_message': {'type': 'boolean'}, 'priority': {'enum': ['BULK', 'NORMAL', 'URGENT', 'VERY_URGENT']}, 'data_coding': {'enum': ['DEFAULT', 'ASCII', 'UNSPECIFIED_1', 'LATIN_1', 'UNSPECIFIED_2', 'JIS', 'CYRLLIC', 'LATIN_HEBREW', 'UCS2', 'PICTOGRAM', 'MUSIC', 'EXTENDED_KANJI', 'KS']}, 'message_encoding': {'type': 'string'}, 'message_timeout': {'type': 'number'}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}}, 'system': {'message': {'type': 'object', 'required': ['address', 'message'], 'properties': {'address': {'type': 'string'}, 'message': {'type': 'string'}}}}}}}}, 'hat-gateway://iec61850.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec61850.yaml', '$defs': {'client': {'type': 'object', 'required': ['name', 'connection', 'value_types', 'datasets', 'rcbs', 'data', 'commands', 'changes'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['host', 'port', 'connect_timeout', 'reconnect_delay', 'response_timeout', 'status_delay', 'status_timeout'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}, 'connect_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'response_timeout': {'type': 'number'}, 'status_delay': {'type': 'number'}, 'status_timeout': {'type': 'number'}, 'local_tsel': {'type': 'integer'}, 'remote_tsel': {'type': 'integer'}, 'local_ssel': {'type': 'integer'}, 'remote_ssel': {'type': 'integer'}, 'local_psel': {'type': 'integer'}, 'remote_psel': {'type': 'integer'}, 'local_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'remote_ap_title': {'type': 'array', 'items': {'type': 'integer'}}, 'local_ae_qualifier': {'type': 'integer'}, 'remote_ae_qualifier': {'type': 'integer'}, 'local_detail_calling': {'type': 'integer'}}}, 'value_types': {'type': 'array', 'items': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'name', 'type'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'values', 'dynamic'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'values': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}, 'dynamic': {'type': 'boolean'}}}}, 'rcbs': {'type': 'array', 'items': {'type': 'object', 'required': ['ref', 'report_id', 'dataset'], 'properties': {'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/rcb'}, 'report_id': {'type': 'string'}, 'dataset': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset'}, 'trigger_options': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION']}}, 'optional_fields': {'type': 'array', 'items': {'enum': ['SEQUENCE_NUMBER', 'REPORT_TIME_STAMP', 'REASON_FOR_INCLUSION', 'DATA_SET_NAME', 'DATA_REFERENCE', 'BUFFER_OVERFLOW', 'ENTRY_ID', 'CONF_REVISION']}}, 'conf_revision': {'type': 'integer'}, 'buffer_time': {'type': 'integer'}, 'integrity_period': {'type': 'integer'}, 'purge_buffer': {'type': 'boolean'}, 'reservation_time': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'report_id', 'value'], 'properties': {'name': {'type': 'string'}, 'report_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}, 'commands': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'model', 'ref', 'with_operate_time'], 'properties': {'name': {'type': 'string'}, 'model': {'enum': ['DIRECT_WITH_NORMAL_SECURITY', 'SBO_WITH_NORMAL_SECURITY', 'DIRECT_WITH_ENHANCED_SECURITY', 'SBO_WITH_ENHANCED_SECURITY']}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/command'}, 'with_operate_time': {'type': 'boolean'}}}}, 'changes': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'ref'], 'properties': {'name': {'type': 'string'}, 'ref': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/value'}}}}}}, 'events': {'client': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['reasons'], 'properties': {'reasons': {'type': 'array', 'items': {'enum': ['DATA_CHANGE', 'QUALITY_CHANGE', 'DATA_UPDATE', 'INTEGRITY', 'GENERAL_INTERROGATION', 'APPLICATION_TRIGGER']}}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'quality': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, 'timestamp': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, 'selected': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}}}, 'command': {'allOf': [{'type': 'object', 'required': ['session_id', 'action'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE', 'TERMINATION']}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'service_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}, 'additional_cause': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/additional_cause'}, 'test_error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/test_error'}}}]}]}, 'change': {'allOf': [{'type': 'object', 'required': ['session_id'], 'properties': {'session_id': {'type': 'string'}}}, {'oneOf': [{'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': True}}}, {'type': 'object', 'requried': ['success'], 'properties': {'success': {'const': False}, 'error': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/errors/service_error'}}}]}]}, 'entry_id': {'type': ['string', 'null'], 'description': 'hex encoded bytes'}}, 'system': {'command': {'type': 'object', 'required': ['session_id', 'action', 'value', 'origin', 'test', 'checks'], 'properties': {'session_id': {'type': 'string'}, 'action': {'enum': ['SELECT', 'CANCEL', 'OPERATE']}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}, 'origin': {'type': 'object', 'required': ['category', 'identification'], 'properties': {'category': {'enum': ['BAY_CONTROL', 'STATION_CONTROL', 'REMOTE_CONTROL', 'AUTOMATIC_BAY', 'AUTOMATIC_STATION', 'AUTOMATIC_REMOTE', 'MAINTENANCE', 'PROCESS']}, 'identification': {'type': 'string'}}}, 'test': {'type': 'boolean'}, 'checks': {'type': 'array', 'items': {'enum': ['SYNCHRO', 'INTERLOCK']}}}}, 'change': {'type': 'object', 'requried': ['session_id', 'value'], 'properties': {'session_id': {'type': 'string'}, 'value': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}}}}, 'value': {'anyOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/boolean'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/integer'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/unsigned'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/float'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/bit_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/octet_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/visible_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/mms_string'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/array'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/struct'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/quality'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/timestamp'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/double_point'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/direction'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/severity'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/vector'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/step_position'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/binary_control'}], '$defs': {'boolean': {'type': 'boolean'}, 'integer': {'type': 'integer'}, 'unsigned': {'type': 'integer'}, 'float': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'bit_string': {'type': 'array', 'items': {'type': 'boolean'}}, 'octet_string': {'type': 'string', 'description': 'hex encoded bytes'}, 'visible_string': {'type': 'string'}, 'mms_string': {'type': 'string'}, 'array': {'type': 'array', 'items': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}, 'struct': {'type': 'object', 'patternProperties': {'.+': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value'}}}, 'quality': {'type': 'object', 'required': ['validity', 'details', 'source', 'test', 'operator_blocked'], 'properties': {'validity': {'enum': ['GOOD', 'INVALID', 'RESERVED', 'QUESTIONABLE']}, 'details': {'type': 'array', 'items': {'enum': ['OVERFLOW', 'OUT_OF_RANGE', 'BAD_REFERENCE', 'OSCILLATORY', 'FAILURE', 'OLD_DATA', 'INCONSISTENT', 'INACCURATE']}}, 'source': {'enum': ['PROCESS', 'SUBSTITUTED']}, 'test': {'type': 'boolean'}, 'operator_blocked': {'type': 'boolean'}}}, 'timestamp': {'type': 'object', 'required': ['value', 'leap_second', 'clock_failure', 'not_synchronized'], 'properties': {'value': {'type': 'number', 'description': 'seconds since 1970-01-01'}, 'leap_second': {'type': 'boolean'}, 'clock_failure': {'type': 'boolean'}, 'not_synchronized': {'type': 'boolean'}, 'accuracy': {'type': 'integer'}}}, 'double_point': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'BAD']}, 'direction': {'enum': ['UNKNOWN', 'FORWARD', 'BACKWARD', 'BOTH']}, 'severity': {'enum': ['UNKNOWN', 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING']}, 'analogue': {'type': 'object', 'properties': {'i': {'type': 'integer'}, 'f': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}}}, 'vector': {'type': 'object', 'required': ['magnitude'], 'properties': {'magnitude': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}, 'angle': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value/$defs/analogue'}}}, 'step_position': {'type': 'object', 'required': ['value'], 'properties': {'value': {'type': 'integer'}, 'transient': {'type': 'boolean'}}}, 'binary_control': {'enum': ['STOP', 'LOWER', 'HIGHER', 'RESERVED']}}}, 'value_type': {'oneOf': [{'enum': ['BOOLEAN', 'INTEGER', 'UNSIGNED', 'FLOAT', 'BIT_STRING', 'OCTET_STRING', 'VISIBLE_STRING', 'MMS_STRING', 'QUALITY', 'TIMESTAMP', 'DOUBLE_POINT', 'DIRECTION', 'SEVERITY', 'ANALOGUE', 'VECTOR', 'STEP_POSITION', 'BINARY_CONTROL']}, {'type': 'object', 'required': ['type', 'element_type', 'length'], 'properties': {'type': {'const': 'ARRAY'}, 'element_type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}, 'length': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'elements'], 'properties': {'type': {'const': 'STRUCT'}, 'elements': {'type': 'array', 'items': {'type': 'object', 'requried': ['name', 'type'], 'properties': {'name': {'type': 'string'}, 'type': {'$ref': 'hat-gateway://iec61850.yaml#/$defs/value_type'}}}}}}]}, 'refs': {'value': {'type': 'object', 'required': ['logical_device', 'logical_node', 'fc', 'names'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'fc': {'type': 'string'}, 'names': {'type': 'array', 'items': {'type': ['string', 'integer']}}}}, 'command': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}, 'rcb': {'type': 'object', 'required': ['logical_device', 'logical_node', 'type', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'type': {'enum': ['BUFFERED', 'UNBUFFERED']}, 'name': {'type': 'string'}}}, 'dataset': {'oneOf': [{'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/nonpersisted'}, {'$ref': 'hat-gateway://iec61850.yaml#/$defs/refs/dataset/$defs/persisted'}], '$defs': {'nonpersisted': {'type': 'string'}, 'persisted': {'type': 'object', 'required': ['logical_device', 'logical_node', 'name'], 'properties': {'logical_device': {'type': 'string'}, 'logical_node': {'type': 'string'}, 'name': {'type': 'string'}}}}}}, 'errors': {'service_error': {'enum': ['NO_ERROR', 'INSTANCE_NOT_AVAILABLE', 'INSTANCE_IN_USE', 'ACCESS_VIOLATION', 'ACCESS_NOT_ALLOWED_IN_CURRENT_STATE', 'PARAMETER_VALUE_INAPPROPRIATE', 'PARAMETER_VALUE_INCONSISTENT', 'CLASS_NOT_SUPPORTED', 'INSTANCE_LOCKED_BY_OTHER_CLIENT', 'CONTROL_MUST_BE_SELECTED', 'TYPE_CONFLICT', 'FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT', 'FAILED_DUE_TO_SERVER_CONTRAINT']}, 'additional_cause': {'enum': ['UNKNOWN', 'NOT_SUPPORTED', 'BLOCKED_BY_SWITCHING_HIERARCHY', 'SELECT_FAILED', 'INVALID_POSITION', 'POSITION_REACHED', 'PARAMETER_CHANGE_IN_EXECUTION', 'STEP_LIMIT', 'BLOCKED_BY_MODE', 'BLOCKED_BY_PROCESS', 'BLOCKED_BY_INTERLOCKING', 'BLOCKED_BY_SYNCHROCHECK', 'COMMAND_ALREADY_IN_EXECUTION', 'BLOCKED_BY_HEALTH', 'ONE_OF_N_CONTROL', 'ABORTION_BY_CANCEL', 'TIME_LIMIT_OVER', 'ABORTION_BY_TRIP', 'OBJECT_NOT_SELECTED', 'OBJECT_ALREADY_SELECTED', 'NO_ACCESS_AUTHORITY', 'ENDED_WITH_OVERSHOOT', 'ABORTION_DUE_TO_DEVIATION', 'ABORTION_BY_COMMUNICATION_LOSS', 'BLOCKED_BY_COMMAND', 'NONE', 'INCONSISTENT_PARAMETERS', 'LOCKED_BY_OTHER_CLIENT']}, 'test_error': {'enum': ['NO_ERROR', 'UNKNOWN', 'TIMEOUT_TEST_NOT_OK', 'OPERATOR_TEST_NOT_OK']}}}}, 'hat-gateway://iec103.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec103.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'reconnect_delay', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'data': {'type': 'object', 'required': ['cause', 'value'], 'properties': {'cause': {'oneOf': [{'enum': ['SPONTANEOUS', 'CYCLIC', 'TEST_MODE', 'GENERAL_INTERROGATION', 'LOCAL_OPERATION', 'REMOTE_OPERATION']}, {'type': 'integer', 'description': 'other cause in range [0, 255]\n'}]}, 'value': {'oneOf': [{'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}, {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/measurand'}]}}}, 'command': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'enable': {'type': 'boolean'}, 'command': {'type': 'object', 'required': ['session_id', 'value'], 'properties': {'value': {'$ref': 'hat-gateway://iec103.yaml#/$defs/values/double'}}}}}}, 'values': {'double': {'enum': ['TRANSIENT', 'OFF', 'ON', 'ERROR']}, 'measurand': {'type': 'object', 'required': ['overflow', 'invalid', 'value'], 'properties': {'overflow': {'type': 'boolean'}, 'invalid': {'type': 'boolean'}, 'value': {'type': 'number'}}}}}}, 'hat-gateway://main.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://main.yaml', 'title': 'Gateway', 'description': "Gateway's configuration", 'type': 'object', 'required': ['name', 'event_server', 'devices'], 'properties': {'type': {'const': 'gateway', 'description': 'configuration type identification'}, 'version': {'type': 'string', 'description': 'component version'}, 'log': {'$ref': 'hat-json://logging.yaml'}, 'name': {'type': 'string', 'description': 'component name'}, 'event_server': {'allOf': [{'type': 'object', 'properties': {'require_operational': {'type': 'boolean'}}}, {'oneOf': [{'type': 'object', 'required': ['monitor_component'], 'properties': {'monitor_component': {'type': 'object', 'required': ['host', 'port', 'gateway_group', 'event_server_group'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23010}, 'gateway_group': {'type': 'string'}, 'event_server_group': {'type': 'string'}}}}}, {'type': 'object', 'required': ['eventer_server'], 'properties': {'eventer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23012}}}}}]}]}, 'devices': {'type': 'array', 'items': {'$ref': 'hat-gateway://main.yaml#/$defs/device'}}, 'adminer_server': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string', 'default': '127.0.0.1'}, 'port': {'type': 'integer', 'default': 23016}}}}, '$defs': {'device': {'type': 'object', 'description': 'structure of device configuration depends on device type\n', 'required': ['module', 'name'], 'properties': {'module': {'type': 'string', 'description': 'full python module name that implements device\n'}, 'name': {'type': 'string'}}}}}, 'hat-gateway://snmp.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://snmp.yaml', '$defs': {'manager': {'allOf': [{'oneOf': [{'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v1'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v2c'}, {'$ref': 'hat-gateway://snmp.yaml#/$defs/managers/v3'}]}, {'type': 'object', 'required': ['name', 'remote_host', 'remote_port', 'connect_delay', 'request_timeout', 'request_retry_count', 'request_retry_delay', 'polling_delay', 'polling_oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'Device name\n'}, 'remote_host': {'type': 'string', 'description': 'Remote hostname or IP address\n'}, 'remote_port': {'type': 'integer', 'description': 'Remote UDP port\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of request/response\nexchange\n'}, 'request_retry_count': {'type': 'integer', 'description': 'Number of request retries before remote data is\nconsidered unavailable\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive request\nretries\n'}, 'polling_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive polling\ncycles\n'}, 'polling_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID read during polling cycle formated as integers\nseparated by '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value formated as\nintegers separated by '.'\n"}}}}]}, 'trap_listener': {'type': 'object', 'required': ['name', 'local_host', 'local_port', 'users', 'remote_devices'], 'properties': {'name': {'type': 'string', 'description': 'Device name\n'}, 'local_host': {'type': 'string', 'description': 'Local listening hostname or IP address\n'}, 'local_port': {'type': 'integer', 'description': 'Local listening UDP port\n'}, 'users': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'authentication', 'privacy'], 'properties': {'name': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'remote_devices': {'type': 'array', 'items': {'allOf': [{'oneOf': [{'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'enum': ['V1', 'V2C']}, 'community': {'type': ['null', 'string']}}}, {'type': 'object', 'required': ['version', 'context'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal\ndigits\n'}, 'name': {'type': 'string'}}}]}}}]}, {'type': 'object', 'required': ['name', 'oids', 'string_hex_oids'], 'properties': {'name': {'type': 'string', 'description': 'remote device name\n'}, 'oids': {'type': 'array', 'items': {'type': 'string', 'description': "data OID formated as integers separated\nby '.'\n"}}, 'string_hex_oids': {'type': 'array', 'items': {'type': 'string', 'description': "OID associated to string hex value\nformated as integers separated by '.'\n"}}}}]}}}}, 'managers': {'v1': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V1'}, 'community': {'type': 'string'}}}, 'v2c': {'type': 'object', 'required': ['version', 'community'], 'properties': {'version': {'const': 'V2C'}, 'community': {'type': 'string'}}}, 'v3': {'type': 'object', 'required': ['version', 'context', 'user', 'authentication', 'privacy'], 'properties': {'version': {'const': 'V3'}, 'context': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['engine_id', 'name'], 'properties': {'engine_id': {'type': 'string', 'description': 'sequence of hexadecimal digits\n'}, 'name': {'type': 'string'}}}]}, 'user': {'type': 'string'}, 'authentication': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'enum': ['MD5', 'SHA']}, 'password': {'type': 'string'}}}]}, 'privacy': {'oneOf': [{'type': 'null'}, {'type': 'object', 'required': ['type', 'password'], 'properties': {'type': {'const': 'DES'}, 'password': {'type': 'string'}}}]}}}}, 'events': {'manager': {'gateway': {'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['session_id', 'cause', 'data'], 'properties': {'session_id': {'oneOf': [{'type': 'null', 'description': 'In case of INTERROGATE or CHANGE cause\n'}, {'description': 'In case of REQUESTED cause\n'}]}, 'cause': ['INTERROGATE', 'CHANGE', 'REQUESTED'], 'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}, 'write': {'type': 'object', 'required': ['session_id', 'success'], 'properties': {'success': {'type': 'boolean'}}}}, 'system': {'read': {'type': 'object', 'required': ['session_id']}, 'write': {'type': 'object', 'required': ['session_id', 'data'], 'properties': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}}, 'trap_listener': {'gateway': {'data': {'$ref': 'hat-gateway://snmp.yaml#/$defs/data'}}}}, 'data': {'oneOf': [{'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['INTEGER', 'UNSIGNED', 'COUNTER', 'BIG_COUNTER', 'TIME_TICKS']}, 'value': {'type': 'integer'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'enum': ['STRING', 'STRING_HEX', 'OBJECT_ID', 'IP_ADDRESS', 'ARBITRARY']}, 'value': {'type': 'string'}}}, {'type': 'object', 'required': ['type', 'value'], 'properties': {'type': {'const': 'ERROR'}, 'value': {'enum': ['TOO_BIG', 'NO_SUCH_NAME', 'BAD_VALUE', 'READ_ONLY', 'GEN_ERR', 'NO_ACCESS', 'WRONG_TYPE', 'WRONG_LENGTH', 'WRONG_ENCODING', 'WRONG_VALUE', 'NO_CREATION', 'INCONSISTENT_VALUE', 'RESOURCE_UNAVAILABLE', 'COMMIT_FAILED', 'UNDO_FAILED', 'AUTHORIZATION_ERROR', 'NOT_WRITABLE', 'INCONSISTENT_NAME', 'EMPTY', 'UNSPECIFIED', 'NO_SUCH_OBJECT', 'NO_SUCH_INSTANCE', 'END_OF_MIB_VIEW', 'NOT_IN_TIME_WINDOWS', 'UNKNOWN_USER_NAMES', 'UNKNOWN_ENGINE_IDS', 'WRONG_DIGESTS', 'DECRYPTION_ERRORS']}}}]}}}, 'hat-gateway://iec104.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec104.yaml', '$defs': {'master': {'type': 'object', 'required': ['name', 'remote_addresses', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'reconnect_delay', 'time_sync_delay', 'security'], 'properties': {'name': {'type': 'string'}, 'remote_addresses': {'type': 'array', 'items': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}}}, 'slave': {'type': 'object', 'required': ['local_host', 'local_port', 'remote_hosts', 'max_connections', 'response_timeout', 'supervisory_timeout', 'test_timeout', 'send_window_size', 'receive_window_size', 'security', 'buffers', 'data'], 'properties': {'local_host': {'type': 'string'}, 'local_port': {'type': 'integer'}, 'remote_hosts': {'type': ['array', 'null'], 'description': 'if null, all remote hosts are allowed\n', 'items': {'type': 'string'}}, 'max_connections': {'type': ['null', 'integer']}, 'response_timeout': {'type': 'number'}, 'supervisory_timeout': {'type': 'number'}, 'test_timeout': {'type': 'number'}, 'send_window_size': {'type': 'integer'}, 'receive_window_size': {'type': 'integer'}, 'security': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec104.yaml#/$defs/security'}]}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}}}}}}, 'events': {'master': {'gateway': {'status': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/status'}, 'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/gateway/counter_interrogation'}}, 'system': {'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/command'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/interrogation'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/master/system/counter_interrogation'}}}, 'slave': {'gateway': {'connections': {'$ref': 'hat-gateway://iec104.yaml#/$defs/messages/connections'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/gateway/command'}}, 'system': {'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/data'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/events/slave/system/command'}}}}, 'messages': {'connections': {'type': 'array', 'items': {'type': 'object', 'required': ['connection_id', 'local', 'remote'], 'properties': {'connection_id': {'type': 'integer'}, 'local': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}, 'remote': {'type': 'object', 'required': ['host', 'port'], 'properties': {'host': {'type': 'string'}, 'port': {'type': 'integer'}}}}}}}, 'security': {'type': 'object', 'required': ['cert_path', 'key_path', 'verify_cert', 'ca_path'], 'properties': {'cert_path': {'type': 'string'}, 'key_path': {'type': ['null', 'string']}, 'verify_cert': {'type': 'boolean'}, 'ca_path': {'type': ['null', 'string']}, 'strict_mode': {'type': 'boolean'}, 'renegotiate_delay': {'type': ['null', 'number']}}}}}, 'hat-gateway://iec101.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://iec101.yaml', '$defs': {'master': {'allOf': [{'type': 'object', 'required': ['name', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'cause_size', 'asdu_address_size', 'io_address_size', 'reconnect_delay'], 'properties': {'name': {'type': 'string'}, 'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'reconnect_delay': {'type': 'number'}}}, {'oneOf': [{'type': 'object', 'required': ['link_type', 'device_address_size', 'remote_devices'], 'properties': {'link_type': {'const': 'BALANCED'}, 'device_address_size': {'enum': ['ZERO', 'ONE', 'TWO']}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['direction', 'address', 'response_timeout', 'send_retry_count', 'status_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'direction': {'enum': ['A_TO_B', 'B_TO_A']}, 'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'status_delay': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}, {'type': 'object', 'required': ['link_type', 'device_address_size', 'remote_devices'], 'properties': {'link_type': {'const': 'UNBALANCED'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'response_timeout', 'send_retry_count', 'poll_class1_delay', 'poll_class2_delay', 'reconnect_delay', 'time_sync_delay'], 'properties': {'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'poll_class1_delay': {'type': ['null', 'number']}, 'poll_class2_delay': {'type': ['null', 'number']}, 'reconnect_delay': {'type': 'number'}, 'time_sync_delay': {'type': ['null', 'number']}}}}}}]}]}, 'slave': {'allOf': [{'type': 'object', 'required': ['port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval', 'cause_size', 'asdu_address_size', 'io_address_size', 'buffers', 'data'], 'properties': {'port': {'type': 'string'}, 'baudrate': {'type': 'integer'}, 'bytesize': {'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean'}, 'rtscts': {'type': 'boolean'}, 'dsrdtr': {'type': 'boolean'}}}, 'silent_interval': {'type': 'number'}, 'cause_size': {'enum': ['ONE', 'TWO']}, 'asdu_address_size': {'enum': ['ONE', 'TWO']}, 'io_address_size': {'enum': ['ONE', 'TWO', 'THREE']}, 'buffers': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'size'], 'properties': {'name': {'type': 'string'}, 'size': {'type': 'integer'}}}}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['data_type', 'asdu_address', 'io_address', 'buffer'], 'properties': {'data_type': {'enum': ['SINGLE', 'DOUBLE', 'STEP_POSITION', 'BITSTRING', 'NORMALIZED', 'SCALED', 'FLOATING', 'BINARY_COUNTER', 'PROTECTION', 'PROTECTION_START', 'PROTECTION_COMMAND', 'STATUS']}, 'asdu_address': {'type': 'integer'}, 'io_address': {'type': 'integer'}, 'buffer': {'type': ['null', 'string']}}}}}}, {'oneOf': [{'type': 'object', 'required': ['link_type', 'device_address_size', 'devices'], 'properties': {'link_type': {'const': 'BALANCED'}, 'device_address_size': {'enum': ['ZERO', 'ONE', 'TWO']}, 'devices': {'type': 'array', 'items': {'type': 'object', 'required': ['direction', 'address', 'response_timeout', 'send_retry_count', 'status_delay', 'reconnect_delay'], 'properties': {'direction': {'enum': ['A_TO_B', 'B_TO_A']}, 'address': {'type': 'integer'}, 'response_timeout': {'type': 'number'}, 'send_retry_count': {'type': 'integer'}, 'status_delay': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}}}}}}, {'type': 'object', 'required': ['link_type', 'device_address_size', 'devices'], 'properties': {'link_type': {'const': 'UNBALANCED'}, 'device_address_size': {'enum': ['ONE', 'TWO']}, 'devices': {'type': 'array', 'items': {'type': 'object', 'required': ['address', 'keep_alive_timeout', 'reconnect_delay'], 'properties': {'address': {'type': 'integer'}, 'keep_alive_timeout': {'type': 'number'}, 'reconnect_delay': {'type': 'number'}}}}}}]}]}, 'events': {'master': {'gateway': {'status': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/status'}, 'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/data/res'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/res'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/res'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/counter_interrogation/res'}}, 'system': {'enable': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/enable'}, 'command': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/req'}, 'interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/req'}, 'counter_interrogation': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/counter_interrogation/req'}}}, 'slave': {'gateway': {'connections': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/connections'}, 'command': {'allOf': [{'type': 'object', 'required': ['connection_id'], 'properties': {'connection_id': {'type': 'integer'}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/req'}]}}, 'system': {'data': {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/data/res'}, 'command': {'allOf': [{'type': 'object', 'required': ['connection_id'], 'properties': {'connection_id': {'type': 'integer'}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/command/res'}]}}}}, 'messages': {'enable': {'type': 'boolean'}, 'status': {'enum': ['CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'connections': {'type': 'array', 'items': {'type': 'object', 'required': ['connection_id', 'address'], 'properties': {'connection_id': {'type': 'integer'}, 'address': {'type': 'integer'}}}}, 'data': {'res': {'type': 'object', 'required': ['is_test', 'cause', 'data'], 'properties': {'is_test': {'type': 'boolean'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/data/res'}, 'data': {'oneOf': [{'$ref': 'hat-gateway://iec101.yaml#/$defs/data/single'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/double'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/step_position'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/bitstring'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/normalized'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/scaled'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/floating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/binary_counter'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/protection'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/protection_start'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/protection_command'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/data/status'}]}}}}, 'command': {'req': {'type': 'object', 'required': ['is_test', 'cause', 'command'], 'properties': {'is_test': {'type': 'boolean'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/req'}, 'command': {'oneOf': [{'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/single'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/double'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/regulating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/normalized'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/scaled'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/floating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/bitstring'}]}}}, 'res': {'type': 'object', 'required': ['is_test', 'is_negative_confirm', 'cause', 'command'], 'properties': {'is_test': {'type': 'boolean'}, 'is_negative_confirm': {'type': 'boolean'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/res'}, 'command': {'oneOf': [{'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/single'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/double'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/regulating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/normalized'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/scaled'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/floating'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/commands/bitstring'}]}}}}, 'interrogation': {'req': {'type': 'object', 'required': ['is_test', 'request', 'cause'], 'properties': {'is_test': {'type': 'boolean'}, 'request': {'type': 'integer', 'description': 'request in range [0, 255]\n'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/req'}}}, 'res': {'type': 'object', 'required': ['is_test', 'is_negative_confirm', 'request', 'cause'], 'properties': {'is_test': {'type': 'boolean'}, 'is_negative_confirm': {'type': 'boolean'}, 'request': {'type': 'integer', 'description': 'request in range [0, 255]\n'}, 'cause': {'$ref': 'hat-gateway://iec101.yaml#/$defs/causes/command/res'}}}}, 'counter_interrogation': {'req': {'allOf': [{'type': 'object', 'required': ['freeze'], 'properties': {'freeze': {'enum': ['READ', 'FREEZE', 'FREEZE_AND_RESET', 'RESET']}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/req'}]}, 'res': {'allOf': [{'type': 'object', 'required': ['freeze'], 'properties': {'freeze': {'enum': ['READ', 'FREEZE', 'FREEZE_AND_RESET', 'RESET']}}}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/messages/interrogation/res'}]}}}, 'data': {'single': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/single'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/indication'}}}, 'double': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/double'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/indication'}}}, 'step_position': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/step_position'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'bitstring': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/bitstring'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'normalized': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/normalized'}, 'quality': {'oneOf': [{'type': 'null'}, {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}]}}}, 'scaled': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/scaled'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'floating': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/floating'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}, 'binary_counter': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/binary_counter'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/counter'}}}, 'protection': {'type': 'object', 'required': ['value', 'quality', 'elapsed_time'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/protection'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/protection'}, 'elapsed_time': {'type': 'integer', 'description': 'elapsed_time in range [0, 65535]\n'}}}, 'protection_start': {'type': 'object', 'required': ['value', 'quality', 'duration_time'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/protection_start'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/protection'}, 'duration_time': {'type': 'integer', 'description': 'duration_time in range [0, 65535]\n'}}}, 'protection_command': {'type': 'object', 'required': ['value', 'quality', 'operating_time'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/protection_command'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/protection'}, 'operating_time': {'type': 'integer', 'description': 'operating_time in range [0, 65535]\n'}}}, 'status': {'type': 'object', 'required': ['value', 'quality'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/status'}, 'quality': {'$ref': 'hat-gateway://iec101.yaml#/$defs/qualities/measurement'}}}}, 'commands': {'single': {'type': 'object', 'required': ['value', 'select', 'qualifier'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/single'}, 'select': {'type': 'boolean'}, 'qualifier': {'type': 'integer', 'description': 'qualifier in range [0, 31]\n'}}}, 'double': {'type': 'object', 'required': ['value', 'select', 'qualifier'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/double'}, 'select': {'type': 'boolean'}, 'qualifier': {'type': 'integer', 'description': 'qualifier in range [0, 31]\n'}}}, 'regulating': {'type': 'object', 'required': ['value', 'select', 'qualifier'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/regulating'}, 'select': {'type': 'boolean'}, 'qualifier': {'type': 'integer', 'description': 'qualifier in range [0, 31]\n'}}}, 'normalized': {'type': 'object', 'required': ['value', 'select'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/normalized'}, 'select': {'type': 'boolean'}}}, 'scaled': {'type': 'object', 'required': ['value', 'select'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/scaled'}, 'select': {'type': 'boolean'}}}, 'floating': {'type': 'object', 'required': ['value', 'select'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/floating'}, 'select': {'type': 'boolean'}}}, 'bitstring': {'type': 'object', 'required': ['value'], 'properties': {'value': {'$ref': 'hat-gateway://iec101.yaml#/$defs/values/bitstring'}}}}, 'values': {'single': {'enum': ['OFF', 'ON']}, 'double': {'enum': ['INTERMEDIATE', 'OFF', 'ON', 'FAULT']}, 'regulating': {'enum': ['LOWER', 'HIGHER']}, 'step_position': {'type': 'object', 'required': ['value', 'transient'], 'properties': {'value': {'type': 'integer', 'description': 'value in range [-64, 63]\n'}, 'transient': {'type': 'boolean'}}}, 'bitstring': {'type': 'array', 'description': 'bitstring encoded as 4 bytes\n', 'items': {'type': 'integer'}}, 'normalized': {'type': 'number', 'description': 'value in range [-1.0, 1.0)\n'}, 'scaled': {'type': 'integer', 'description': 'value in range [-2^15, 2^15-1]\n'}, 'floating': {'oneOf': [{'type': 'number'}, {'enum': ['nan', 'inf', '-inf']}]}, 'binary_counter': {'type': 'integer', 'description': 'value in range [-2^31, 2^31-1]\n'}, 'protection': {'enum': ['OFF', 'ON']}, 'protection_start': {'type': 'object', 'required': ['general', 'l1', 'l2', 'l3', 'ie', 'reverse'], 'properties': {'general': {'type': 'boolean'}, 'l1': {'type': 'boolean'}, 'l2': {'type': 'boolean'}, 'l3': {'type': 'boolean'}, 'ie': {'type': 'boolean'}, 'reverse': {'type': 'boolean'}}}, 'protection_command': {'type': 'object', 'required': ['general', 'l1', 'l2', 'l3'], 'properties': {'general': {'type': 'boolean'}, 'l1': {'type': 'boolean'}, 'l2': {'type': 'boolean'}, 'l3': {'type': 'boolean'}}}, 'status': {'type': 'object', 'required': ['value', 'change'], 'properties': {'value': {'type': 'array', 'description': 'value length is 16\n', 'items': {'type': 'boolean'}}, 'change': {'type': 'array', 'description': 'change length is 16\n', 'items': {'type': 'boolean'}}}}}, 'qualities': {'indication': {'type': 'object', 'required': ['invalid', 'not_topical', 'substituted', 'blocked'], 'properties': {'invalid': {'type': 'boolean'}, 'not_topical': {'type': 'boolean'}, 'substituted': {'type': 'boolean'}, 'blocked': {'type': 'boolean'}}}, 'measurement': {'type': 'object', 'required': ['invalid', 'not_topical', 'substituted', 'blocked', 'overflow'], 'properties': {'invalid': {'type': 'boolean'}, 'not_topical': {'type': 'boolean'}, 'substituted': {'type': 'boolean'}, 'blocked': {'type': 'boolean'}, 'overflow': {'type': 'boolean'}}}, 'counter': {'type': 'object', 'required': ['invalid', 'adjusted', 'overflow', 'sequence'], 'properties': {'invalid': {'type': 'boolean'}, 'adjusted': {'type': 'boolean'}, 'overflow': {'type': 'boolean'}, 'sequence': {'type': 'boolean'}}}, 'protection': {'type': 'object', 'required': ['invalid', 'not_topical', 'substituted', 'blocked', 'time_invalid'], 'properties': {'invalid': {'type': 'boolean'}, 'not_topical': {'type': 'boolean'}, 'substituted': {'type': 'boolean'}, 'blocked': {'type': 'boolean'}, 'time_invalid': {'type': 'boolean'}}}}, 'causes': {'data': {'res': {'oneOf': [{'enum': ['PERIODIC', 'BACKGROUND_SCAN', 'SPONTANEOUS', 'REQUEST', 'REMOTE_COMMAND', 'LOCAL_COMMAND', 'INTERROGATED_STATION', 'INTERROGATED_GROUP01', 'INTERROGATED_GROUP02', 'INTERROGATED_GROUP03', 'INTERROGATED_GROUP04', 'INTERROGATED_GROUP05', 'INTERROGATED_GROUP06', 'INTERROGATED_GROUP07', 'INTERROGATED_GROUP08', 'INTERROGATED_GROUP09', 'INTERROGATED_GROUP10', 'INTERROGATED_GROUP11', 'INTERROGATED_GROUP12', 'INTERROGATED_GROUP13', 'INTERROGATED_GROUP14', 'INTERROGATED_GROUP15', 'INTERROGATED_GROUP16', 'INTERROGATED_COUNTER', 'INTERROGATED_COUNTER01', 'INTERROGATED_COUNTER02', 'INTERROGATED_COUNTER03', 'INTERROGATED_COUNTER04']}, {'type': 'integer', 'description': 'other cause in range [0, 63]\n'}]}}, 'command': {'req': {'oneOf': [{'enum': ['ACTIVATION', 'DEACTIVATION']}, {'type': 'integer', 'description': 'other cause in range [0, 63]\n'}]}, 'res': {'oneOf': [{'enum': ['ACTIVATION_CONFIRMATION', 'DEACTIVATION_CONFIRMATION', 'ACTIVATION_TERMINATION', 'UNKNOWN_TYPE', 'UNKNOWN_CAUSE', 'UNKNOWN_ASDU_ADDRESS', 'UNKNOWN_IO_ADDRESS']}, {'type': 'integer', 'description': 'other cause in range [0, 63]\n'}]}}}}}, 'hat-gateway://ping.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://ping.yaml', '$defs': {'device': {'type': 'object', 'required': ['name', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'host', 'ping_delay', 'ping_timeout', 'retry_count', 'retry_delay'], 'properties': {'name': {'type': 'string'}, 'host': {'type': 'string'}, 'ping_delay': {'type': 'number'}, 'ping_timeout': {'type': 'number'}, 'retry_count': {'type': 'number'}, 'retry_delay': {'type': 'number'}}}}}}, 'events': {'status': {'enum': ['AVAILABLE', 'NOT_AVAILABLE']}}}}, 'hat-gateway://modbus.yaml': {'$schema': 'https://json-schema.org/draft/2020-12/schema', '$id': 'hat-gateway://modbus.yaml', 'title': 'Modbus devices', '$defs': {'master': {'type': 'object', 'title': 'Modbus master', 'required': ['name', 'connection', 'remote_devices'], 'properties': {'name': {'type': 'string'}, 'connection': {'type': 'object', 'required': ['modbus_type', 'transport', 'connect_timeout', 'connect_delay', 'request_timeout', 'request_delay', 'request_retry_immediate_count', 'request_retry_delayed_count', 'request_retry_delay'], 'properties': {'modbus_type': {'description': 'Modbus message encoding type\n', 'enum': ['TCP', 'RTU', 'ASCII']}, 'transport': {'oneOf': [{'type': 'object', 'required': ['type', 'host', 'port'], 'properties': {'type': {'const': 'TCP'}, 'host': {'type': 'string', 'description': 'Remote host name\n'}, 'port': {'type': 'integer', 'description': 'Remote host TCP port\n', 'default': 502}}}, {'type': 'object', 'required': ['type', 'port', 'baudrate', 'bytesize', 'parity', 'stopbits', 'flow_control', 'silent_interval'], 'properties': {'type': {'const': 'SERIAL'}, 'port': {'type': 'string', 'description': 'Serial port name (e.g. /dev/ttyS0)\n'}, 'baudrate': {'type': 'integer', 'description': 'Baud rate (e.g. 9600)\n'}, 'bytesize': {'description': 'Number of data bits\n', 'enum': ['FIVEBITS', 'SIXBITS', 'SEVENBITS', 'EIGHTBITS']}, 'parity': {'description': 'Parity checking\n', 'enum': ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE']}, 'stopbits': {'description': 'Number of stop bits\n', 'enum': ['ONE', 'ONE_POINT_FIVE', 'TWO']}, 'flow_control': {'type': 'object', 'required': ['xonxoff', 'rtscts', 'dsrdtr'], 'properties': {'xonxoff': {'type': 'boolean', 'description': 'Enable software flow control\n'}, 'rtscts': {'type': 'boolean', 'description': 'Enable hardware (RTS/CTS) flow control\n'}, 'dsrdtr': {'type': 'boolean', 'description': 'Enable hardware (DSR/DTR) flow control\n'}}}, 'silent_interval': {'type': 'number', 'description': 'Serial communication silet interval\n'}}}]}, 'connect_timeout': {'type': 'number', 'description': 'Maximum number of seconds available to single connection\nattempt\n'}, 'connect_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive connection\nestablishment attempts\n'}, 'request_timeout': {'type': 'number', 'description': 'Maximum duration (in seconds) of read or write\nrequest/response exchange.\n'}, 'request_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive requests\n(minimal duration between response and next request)\n'}, 'request_retry_immediate_count': {'type': 'integer', 'description': 'Number of immediate request retries before remote\ndata is considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delayed_count': {'type': 'integer', 'description': 'Number of delayed request retries before remote data\nis considered unavailable. Total number\nof retries is request_retry_immediate_count *\nrequest_retry_delayed_count.\n'}, 'request_retry_delay': {'type': 'number', 'description': 'Delay (in seconds) between two consecutive delayed\nrequest retries\n'}}}, 'remote_devices': {'type': 'array', 'items': {'type': 'object', 'required': ['device_id', 'timeout_poll_delay', 'data'], 'properties': {'device_id': {'type': 'integer', 'description': 'Modbus device identifier\n'}, 'timeout_poll_delay': {'type': 'number', 'description': 'Delay (in seconds) after read timeout and\nbefore device polling is resumed\n'}, 'data': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'interval', 'data_type', 'start_address', 'bit_offset', 'bit_count'], 'properties': {'name': {'type': 'string', 'description': 'Data point name\n'}, 'interval': {'type': ['number', 'null'], 'description': 'Polling interval in seconds or\nnull if polling is disabled\n'}, 'data_type': {'description': 'Modbus register type\n', 'enum': ['COIL', 'DISCRETE_INPUT', 'HOLDING_REGISTER', 'INPUT_REGISTER', 'QUEUE']}, 'start_address': {'type': 'integer', 'description': 'Starting address of modbus register\n'}, 'bit_offset': {'type': 'integer', 'description': 'Bit offset (number of bits skipped)\n'}, 'bit_count': {'type': 'integer', 'description': 'Number of bits used for\nencoding/decoding value (not\nincluding offset bits)\n'}}}}}}}}}, 'events': {'master': {'gateway': {'status': {'enum': ['DISCONNECTED', 'CONNECTING', 'CONNECTED']}, 'remote_device_status': {'enum': ['DISABLED', 'CONNECTING', 'CONNECTED', 'DISCONNECTED']}, 'read': {'type': 'object', 'required': ['result'], 'properties': {'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND']}, 'value': {'type': 'integer'}, 'cause': {'enum': ['INTERROGATE', 'CHANGE']}}}, 'write': {'type': 'object', 'required': ['request_id', 'result'], 'properties': {'request_id': {'type': 'string'}, 'result': {'enum': ['SUCCESS', 'INVALID_FUNCTION_CODE', 'INVALID_DATA_ADDRESS', 'INVALID_DATA_VALUE', 'FUNCTION_ERROR', 'GATEWAY_PATH_UNAVAILABLE', 'GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND', 'TIMEOUT']}}}}, 'system': {'enable': {'type': 'boolean'}, 'write': {'type': 'object', 'required': ['request_id', 'value'], 'properties': {'request_id': {'type': 'string'}, 'value': {'type': 'integer'}}}}}}}}})
class Iec101MasterDevice(hat.gateway.common.Device):
 55class Iec101MasterDevice(common.Device):
 56
 57    def __init__(self,
 58                 conf: common.DeviceConf,
 59                 eventer_client: hat.event.eventer.Client,
 60                 event_type_prefix: common.EventTypePrefix,
 61                 send_queue_size: int = 1024):
 62        self._conf = conf
 63        self._event_type_prefix = event_type_prefix
 64        self._eventer_client = eventer_client
 65        self._link = None
 66        self._conns = {}
 67        self._send_queue = aio.Queue(send_queue_size)
 68        self._async_group = aio.Group()
 69        self._remote_enabled = {i['address']: False
 70                                for i in conf['remote_devices']}
 71        self._remote_confs = {i['address']: i
 72                              for i in conf['remote_devices']}
 73        self._remote_groups = {}
 74        self._log = _create_logger_adapter(conf['name'])
 75
 76        self.async_group.spawn(self._link_loop)
 77        self.async_group.spawn(self._send_loop)
 78
 79    @property
 80    def async_group(self) -> aio.Group:
 81        return self._async_group
 82
 83    async def process_events(self, events: Collection[hat.event.common.Event]):
 84        for event in events:
 85            try:
 86                await self._process_event(event)
 87
 88            except Exception as e:
 89                self._log.warning('error processing event: %s', e, exc_info=e)
 90
 91    async def _link_loop(self):
 92
 93        async def cleanup():
 94            with contextlib.suppress(ConnectionError):
 95                await self._register_status('DISCONNECTED')
 96
 97            if self._link:
 98                await self._link.async_close()
 99
100        try:
101            if self._conf['link_type'] == 'BALANCED':
102                create_link = link.create_balanced_link
103
104            elif self._conf['link_type'] == 'UNBALANCED':
105                create_link = link.create_master_link
106
107            else:
108                raise ValueError('unsupported link type')
109
110            while True:
111                await self._register_status('CONNECTING')
112
113                try:
114                    self._link = await create_link(
115                        port=self._conf['port'],
116                        address_size=link.AddressSize[
117                            self._conf['device_address_size']],
118                        silent_interval=self._conf['silent_interval'],
119                        baudrate=self._conf['baudrate'],
120                        bytesize=serial.ByteSize[self._conf['bytesize']],
121                        parity=serial.Parity[self._conf['parity']],
122                        stopbits=serial.StopBits[self._conf['stopbits']],
123                        xonxoff=self._conf['flow_control']['xonxoff'],
124                        rtscts=self._conf['flow_control']['rtscts'],
125                        dsrdtr=self._conf['flow_control']['dsrdtr'],
126                        name=self._conf['name'])
127
128                except Exception as e:
129                    self._log.warning(
130                        'link master (endpoint) failed to create: %s',
131                        e, exc_info=e)
132                    await self._register_status('DISCONNECTED')
133                    await asyncio.sleep(self._conf['reconnect_delay'])
134                    continue
135
136                await self._register_status('CONNECTED')
137
138                for address, enabled in self._remote_enabled.items():
139                    if enabled:
140                        self._enable_remote(address)
141
142                await self._link.wait_closed()
143                await self._register_status('DISCONNECTED')
144                self._link = None
145
146        except Exception as e:
147            self._log.error('create link master error: %s', e, exc_info=e)
148
149        finally:
150            self._log.debug('closing link master loop')
151            self.close()
152            self._conns = {}
153            await aio.uncancellable(cleanup())
154
155    async def _send_loop(self):
156        while True:
157            msg, address = await self._send_queue.get()
158
159            conn = self._conns.get(address)
160            if not conn or not conn.is_open:
161                self._log.warning('msg %s not sent, connection to %s closed',
162                                  msg, address)
163                continue
164
165            try:
166                await conn.send([msg])
167                self._log.debug('msg sent asdu=%s', msg.asdu_address)
168
169            except ConnectionError:
170                self._log.warning('msg %s not sent, connection to %s closed',
171                                  msg, address)
172
173    async def _connection_loop(self, group, address):
174
175        async def cleanup():
176            with contextlib.suppress(ConnectionError):
177                await self._register_rmt_status(address, 'DISCONNECTED')
178
179            self._conns.pop(address, None)
180            if conn:
181                await conn.async_close()
182
183        conn = None
184        remote_conf = self._remote_confs[address]
185
186        try:
187            if self._conf['link_type'] == 'BALANCED':
188                conn_args = {
189                    'direction': link.Direction[remote_conf['direction']],
190                    'addr': address,
191                    'response_timeout': remote_conf['response_timeout'],
192                    'send_retry_count': remote_conf['send_retry_count'],
193                    'status_delay': remote_conf['status_delay'],
194                    'name': self._conf['name']}
195
196            elif self._conf['link_type'] == 'UNBALANCED':
197                conn_args = {
198                    'addr': address,
199                    'response_timeout': remote_conf['response_timeout'],
200                    'send_retry_count': remote_conf['send_retry_count'],
201                    'poll_class1_delay': remote_conf['poll_class1_delay'],
202                    'poll_class2_delay': remote_conf['poll_class2_delay'],
203                    'name': self._conf['name']}
204
205            else:
206                raise ValueError('unsupported link type')
207
208            while True:
209                await self._register_rmt_status(address, 'CONNECTING')
210
211                try:
212                    conn = await self._link.open_connection(**conn_args)
213
214                except Exception as e:
215                    self._log.error('connection error to address %s: %s',
216                                    address, e, exc_info=e)
217                    await self._register_rmt_status(address, 'DISCONNECTED')
218                    await asyncio.sleep(remote_conf['reconnect_delay'])
219                    continue
220
221                await self._register_rmt_status(address, 'CONNECTED')
222
223                conn = iec101.Connection(
224                    conn=conn,
225                    cause_size=iec101.CauseSize[self._conf['cause_size']],
226                    asdu_address_size=iec101.AsduAddressSize[
227                        self._conf['asdu_address_size']],
228                    io_address_size=iec101.IoAddressSize[
229                        self._conf['io_address_size']])
230                self._conns[address] = conn
231                group.spawn(self._receive_loop, conn, address)
232
233                if remote_conf['time_sync_delay'] is not None:
234                    group.spawn(self._time_sync_loop, conn,
235                                remote_conf['time_sync_delay'])
236
237                await conn.wait_closed()
238                await self._register_rmt_status(address, 'DISCONNECTED')
239                self._conns.pop(address, None)
240
241        except Exception as e:
242            self._log.error('connection loop error: %s', e, exc_info=e)
243
244        finally:
245            self._log.debug('closing remote device %s', address)
246            group.close()
247            await aio.uncancellable(cleanup())
248
249    async def _receive_loop(self, conn, address):
250        try:
251            while True:
252                try:
253                    msgs = await conn.receive()
254
255                except iec101.AsduTypeError as e:
256                    self._log.warning("asdu type error: %s", e)
257                    continue
258
259                events = collections.deque()
260                for msg in msgs:
261                    if isinstance(msg, iec101.ClockSyncMsg):
262                        continue
263
264                    try:
265                        event = _msg_to_event(self._event_type_prefix, address,
266                                              msg)
267                        events.append(event)
268
269                    except Exception as e:
270                        self._log.warning('message %s ignored due to: %s',
271                                          msg, e, exc_info=e)
272
273                if not events:
274                    continue
275
276                await self._eventer_client.register(events)
277                for e in events:
278                    self._log.debug('registered event %s', e)
279
280        except ConnectionError:
281            self._log.debug('connection closed')
282
283        except Exception as e:
284            self._log.error('receive loop error: %s', e, exc_info=e)
285
286        finally:
287            conn.close()
288
289    async def _time_sync_loop(self, conn, delay):
290        try:
291            while True:
292                time_now = datetime.datetime.now(datetime.timezone.utc)
293                time_iec101 = iec101.time_from_datetime(time_now)
294                msg = iec101.ClockSyncMsg(
295                    is_test=False,
296                    originator_address=0,
297                    asdu_address={
298                        'ONE': 0xFF,
299                        'TWO': 0xFFFF}[self._conf['asdu_address_size']],
300                    time=time_iec101,
301                    is_negative_confirm=False,
302                    cause=iec101.ClockSyncReqCause.ACTIVATION)
303                await conn.send([msg])
304                self._log.debug('time sync sent %s', time_iec101)
305
306                await asyncio.sleep(delay)
307
308        except ConnectionError:
309            self._log.debug('connection closed')
310
311        except Exception as e:
312            self._log.error('time sync loop error: %s', e, exc_info=e)
313
314        finally:
315            conn.close()
316
317    async def _process_event(self, event):
318        match_type = functools.partial(hat.event.common.matches_query_type,
319                                       event.type)
320
321        prefix = (*self._event_type_prefix, 'system', 'remote_device', '?')
322        if not match_type((*prefix, '*')):
323            raise Exception('unexpected event type')
324
325        address = int(event.type[len(prefix) - 1])
326        suffix = event.type[len(prefix):]
327
328        if match_type((*prefix, 'enable')):
329            self._process_event_enable(address, event)
330
331        elif match_type((*prefix, 'command', '?', '?', '?')):
332            cmd_key = common.CommandKey(
333                cmd_type=common.CommandType(suffix[1]),
334                asdu_address=int(suffix[2]),
335                io_address=int(suffix[3]))
336            msg = _command_from_event(cmd_key, event)
337
338            await self._send_queue.put((msg, address))
339            self._log.debug('command asdu=%s io=%s prepared for sending',
340                            cmd_key.asdu_address, cmd_key.io_address)
341
342        elif match_type((*prefix, 'interrogation', '?')):
343            asdu_address = int(suffix[1])
344            msg = _interrogation_from_event(asdu_address, event)
345
346            await self._send_queue.put((msg, address))
347            self._log.debug(
348                "interrogation request asdu=%s prepared for sending",
349                asdu_address)
350
351        elif match_type((*prefix, 'counter_interrogation', '?')):
352            asdu_address = int(suffix[1])
353            msg = _counter_interrogation_from_event(asdu_address, event)
354
355            await self._send_queue.put((msg, address))
356            self._log.debug(
357                "counter interrogation request asdu=%s prepared for sending",
358                asdu_address)
359
360        else:
361            raise Exception('unexpected event type')
362
363    def _process_event_enable(self, address, event):
364        if address not in self._remote_enabled:
365            raise Exception('invalid remote device address')
366
367        enable = event.payload.data
368        if not isinstance(enable, bool):
369            raise Exception('invalid enable event payload')
370
371        if address not in self._remote_enabled:
372            self._log.warning('received enable for unexpected remote device')
373            return
374
375        self._remote_enabled[address] = enable
376
377        if not enable:
378            self._disable_remote(address)
379
380        elif not self._link:
381            return
382
383        else:
384            self._enable_remote(address)
385
386    def _enable_remote(self, address):
387        self._log.debug('enabling device %s', address)
388        remote_group = self._remote_groups.get(address)
389        if remote_group and remote_group.is_open:
390            self._log.debug('device %s is already running', address)
391            return
392
393        remote_group = self._async_group.create_subgroup()
394        self._remote_groups[address] = remote_group
395        remote_group.spawn(self._connection_loop, remote_group, address)
396
397    def _disable_remote(self, address):
398        self._log.debug('disabling device %s', address)
399        if address in self._remote_groups:
400            remote_group = self._remote_groups.pop(address)
401            remote_group.close()
402
403    async def _register_status(self, status):
404        event = hat.event.common.RegisterEvent(
405            type=(*self._event_type_prefix, 'gateway', 'status'),
406            source_timestamp=None,
407            payload=hat.event.common.EventPayloadJson(status))
408        await self._eventer_client.register([event])
409        self._log.debug('device status -> %s', status)
410
411    async def _register_rmt_status(self, address, status):
412        event = hat.event.common.RegisterEvent(
413            type=(*self._event_type_prefix, 'gateway', 'remote_device',
414                  str(address), 'status'),
415            source_timestamp=None,
416            payload=hat.event.common.EventPayloadJson(status))
417        await self._eventer_client.register([event])
418        self._log.debug('remote device %s status -> %s', address, status)

Device interface

Iec101MasterDevice( conf: Union[NoneType, bool, int, float, str, List[ForwardRef('Data')], Dict[str, ForwardRef('Data')]], eventer_client: hat.event.eventer.client.Client, event_type_prefix: tuple[str, str, str], send_queue_size: int = 1024)
57    def __init__(self,
58                 conf: common.DeviceConf,
59                 eventer_client: hat.event.eventer.Client,
60                 event_type_prefix: common.EventTypePrefix,
61                 send_queue_size: int = 1024):
62        self._conf = conf
63        self._event_type_prefix = event_type_prefix
64        self._eventer_client = eventer_client
65        self._link = None
66        self._conns = {}
67        self._send_queue = aio.Queue(send_queue_size)
68        self._async_group = aio.Group()
69        self._remote_enabled = {i['address']: False
70                                for i in conf['remote_devices']}
71        self._remote_confs = {i['address']: i
72                              for i in conf['remote_devices']}
73        self._remote_groups = {}
74        self._log = _create_logger_adapter(conf['name'])
75
76        self.async_group.spawn(self._link_loop)
77        self.async_group.spawn(self._send_loop)
async_group: hat.aio.group.Group
79    @property
80    def async_group(self) -> aio.Group:
81        return self._async_group

Group controlling resource's lifetime.

async def process_events(self, events: Collection[hat.event.common.common.Event]):
83    async def process_events(self, events: Collection[hat.event.common.Event]):
84        for event in events:
85            try:
86                await self._process_event(event)
87
88            except Exception as e:
89                self._log.warning('error processing event: %s', e, exc_info=e)

Process received events

This method can be coroutine or regular function.