hat.gateway.devices.modbus.slave

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

Resource with lifetime control based on Group.

async_group: hat.aio.group.Group
77    @property
78    def async_group(self) -> aio.Group:
79        return self._async_group

Group controlling resource's lifetime.

async def process_event(self, event: hat.event.common.common.Event):
81    async def process_event(self, event: hat.event.common.Event):
82        try:
83            self._log.debug('received event: %s', event)
84            await self._process_event(event)
85
86        except Exception as e:
87            self._log.warning('error processing event: %s', e, exc_info=e)