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)
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.