2021-05-26 17:28:14 +00:00
|
|
|
"""Validate Modbus configuration."""
|
2021-05-28 09:38:31 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-05-26 17:28:14 +00:00
|
|
|
import logging
|
|
|
|
import struct
|
2021-05-28 09:38:31 +00:00
|
|
|
from typing import Any
|
2021-05-26 17:28:14 +00:00
|
|
|
|
2021-05-28 09:38:31 +00:00
|
|
|
import voluptuous as vol
|
2021-05-26 17:28:14 +00:00
|
|
|
|
2021-05-28 09:38:31 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_COUNT,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_SCAN_INTERVAL,
|
|
|
|
CONF_STRUCTURE,
|
|
|
|
CONF_TIMEOUT,
|
|
|
|
)
|
2021-05-26 17:28:14 +00:00
|
|
|
|
|
|
|
from .const import (
|
|
|
|
CONF_DATA_TYPE,
|
|
|
|
CONF_SWAP,
|
|
|
|
CONF_SWAP_BYTE,
|
|
|
|
CONF_SWAP_NONE,
|
|
|
|
DATA_TYPE_CUSTOM,
|
2021-07-12 05:58:45 +00:00
|
|
|
DATA_TYPE_FLOAT,
|
|
|
|
DATA_TYPE_FLOAT16,
|
|
|
|
DATA_TYPE_FLOAT32,
|
|
|
|
DATA_TYPE_FLOAT64,
|
|
|
|
DATA_TYPE_INT,
|
|
|
|
DATA_TYPE_INT16,
|
|
|
|
DATA_TYPE_INT32,
|
|
|
|
DATA_TYPE_INT64,
|
|
|
|
DATA_TYPE_UINT,
|
|
|
|
DATA_TYPE_UINT16,
|
|
|
|
DATA_TYPE_UINT32,
|
|
|
|
DATA_TYPE_UINT64,
|
2021-05-28 09:38:31 +00:00
|
|
|
DEFAULT_SCAN_INTERVAL,
|
2021-05-26 17:28:14 +00:00
|
|
|
DEFAULT_STRUCT_FORMAT,
|
2021-05-28 09:38:31 +00:00
|
|
|
PLATFORMS,
|
2021-05-26 17:28:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2021-07-21 05:49:54 +00:00
|
|
|
OLD_DATA_TYPES = {
|
2021-07-12 05:58:45 +00:00
|
|
|
DATA_TYPE_INT: {
|
|
|
|
1: DATA_TYPE_INT16,
|
|
|
|
2: DATA_TYPE_INT32,
|
|
|
|
4: DATA_TYPE_INT64,
|
|
|
|
},
|
|
|
|
DATA_TYPE_UINT: {
|
|
|
|
1: DATA_TYPE_UINT16,
|
|
|
|
2: DATA_TYPE_UINT32,
|
|
|
|
4: DATA_TYPE_UINT64,
|
|
|
|
},
|
|
|
|
DATA_TYPE_FLOAT: {
|
|
|
|
1: DATA_TYPE_FLOAT16,
|
|
|
|
2: DATA_TYPE_FLOAT32,
|
|
|
|
4: DATA_TYPE_FLOAT64,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-05-26 17:28:14 +00:00
|
|
|
|
2021-07-21 05:49:54 +00:00
|
|
|
def struct_validator(config):
|
2021-05-26 17:28:14 +00:00
|
|
|
"""Sensor schema validator."""
|
|
|
|
|
2021-07-12 05:58:45 +00:00
|
|
|
data_type = config[CONF_DATA_TYPE]
|
2021-07-21 05:49:54 +00:00
|
|
|
count = config.get(CONF_COUNT, 1)
|
2021-07-12 05:58:45 +00:00
|
|
|
name = config[CONF_NAME]
|
|
|
|
structure = config.get(CONF_STRUCTURE)
|
|
|
|
swap_type = config.get(CONF_SWAP)
|
|
|
|
if data_type in [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]:
|
2021-07-21 05:49:54 +00:00
|
|
|
error = f"{name} with {data_type} is not valid, trying to convert"
|
2021-07-12 05:58:45 +00:00
|
|
|
_LOGGER.warning(error)
|
2021-05-26 17:28:14 +00:00
|
|
|
try:
|
2021-07-21 05:49:54 +00:00
|
|
|
data_type = OLD_DATA_TYPES[data_type][config.get(CONF_COUNT, 1)]
|
2021-07-12 05:58:45 +00:00
|
|
|
except KeyError as exp:
|
2021-07-21 05:49:54 +00:00
|
|
|
error = f"{name} cannot convert automatically {data_type}"
|
|
|
|
raise vol.Invalid(error) from exp
|
2021-07-12 05:58:45 +00:00
|
|
|
if config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
|
2021-07-21 05:49:54 +00:00
|
|
|
if structure:
|
|
|
|
error = f"{name} structure: cannot be mixed with {data_type}"
|
|
|
|
raise vol.Invalid(error)
|
|
|
|
structure = f">{DEFAULT_STRUCT_FORMAT[data_type][0]}"
|
|
|
|
if CONF_COUNT not in config:
|
|
|
|
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type][1]
|
2021-05-26 17:28:14 +00:00
|
|
|
else:
|
2021-07-12 05:58:45 +00:00
|
|
|
if not structure:
|
2021-07-21 05:49:54 +00:00
|
|
|
error = (
|
|
|
|
f"Error in sensor {name}. The `{CONF_STRUCTURE}` field can not be empty"
|
2021-07-12 05:58:45 +00:00
|
|
|
)
|
2021-07-21 05:49:54 +00:00
|
|
|
raise vol.Invalid(error)
|
2021-07-12 05:58:45 +00:00
|
|
|
try:
|
|
|
|
size = struct.calcsize(structure)
|
|
|
|
except struct.error as err:
|
|
|
|
raise vol.Invalid(f"Error in {name} structure: {str(err)}") from err
|
2021-05-26 17:28:14 +00:00
|
|
|
|
2021-07-21 05:49:54 +00:00
|
|
|
count = config.get(CONF_COUNT, 1)
|
2021-07-12 05:58:45 +00:00
|
|
|
bytecount = count * 2
|
|
|
|
if bytecount != size:
|
2021-05-28 09:38:31 +00:00
|
|
|
raise vol.Invalid(
|
2021-07-12 05:58:45 +00:00
|
|
|
f"Structure request {size} bytes, "
|
|
|
|
f"but {count} registers have a size of {bytecount} bytes"
|
2021-05-26 17:28:14 +00:00
|
|
|
)
|
2021-07-12 05:58:45 +00:00
|
|
|
|
|
|
|
if swap_type != CONF_SWAP_NONE:
|
|
|
|
if swap_type == CONF_SWAP_BYTE:
|
|
|
|
regs_needed = 1
|
|
|
|
else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD
|
|
|
|
regs_needed = 2
|
|
|
|
if count < regs_needed or (count % regs_needed) != 0:
|
|
|
|
raise vol.Invalid(
|
|
|
|
f"Error in sensor {name} swap({swap_type}) "
|
|
|
|
f"not possible due to the registers "
|
|
|
|
f"count: {count}, needed: {regs_needed}"
|
|
|
|
)
|
2021-05-26 17:28:14 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
**config,
|
|
|
|
CONF_STRUCTURE: structure,
|
|
|
|
CONF_SWAP: swap_type,
|
|
|
|
}
|
2021-05-28 09:38:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
def number_validator(value: Any) -> int | float:
|
|
|
|
"""Coerce a value to number without losing precision."""
|
|
|
|
if isinstance(value, int):
|
|
|
|
return value
|
|
|
|
if isinstance(value, float):
|
|
|
|
return value
|
|
|
|
|
|
|
|
try:
|
|
|
|
value = int(value)
|
|
|
|
return value
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
value = float(value)
|
|
|
|
return value
|
|
|
|
except (TypeError, ValueError) as err:
|
|
|
|
raise vol.Invalid(f"invalid number {value}") from err
|
|
|
|
|
|
|
|
|
|
|
|
def scan_interval_validator(config: dict) -> dict:
|
|
|
|
"""Control scan_interval."""
|
|
|
|
for hub in config:
|
|
|
|
minimum_scan_interval = DEFAULT_SCAN_INTERVAL
|
|
|
|
for component, conf_key in PLATFORMS:
|
|
|
|
if conf_key not in hub:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for entry in hub[conf_key]:
|
|
|
|
scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
2021-06-04 16:06:44 +00:00
|
|
|
if scan_interval == 0:
|
|
|
|
continue
|
|
|
|
if scan_interval < 5:
|
2021-05-28 09:38:31 +00:00
|
|
|
_LOGGER.warning(
|
2021-06-04 16:06:44 +00:00
|
|
|
"%s %s scan_interval(%d) is lower than 5 seconds, "
|
|
|
|
"which may cause Home Assistant stability issues",
|
2021-05-28 09:38:31 +00:00
|
|
|
component,
|
|
|
|
entry.get(CONF_NAME),
|
|
|
|
scan_interval,
|
|
|
|
)
|
|
|
|
entry[CONF_SCAN_INTERVAL] = scan_interval
|
|
|
|
minimum_scan_interval = min(scan_interval, minimum_scan_interval)
|
2021-06-04 16:06:44 +00:00
|
|
|
if (
|
|
|
|
CONF_TIMEOUT in hub
|
|
|
|
and hub[CONF_TIMEOUT] > minimum_scan_interval - 1
|
|
|
|
and minimum_scan_interval > 1
|
|
|
|
):
|
2021-05-28 09:38:31 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"Modbus %s timeout(%d) is adjusted(%d) due to scan_interval",
|
|
|
|
hub.get(CONF_NAME, ""),
|
|
|
|
hub[CONF_TIMEOUT],
|
|
|
|
minimum_scan_interval - 1,
|
|
|
|
)
|
2021-06-04 16:06:44 +00:00
|
|
|
hub[CONF_TIMEOUT] = minimum_scan_interval - 1
|
2021-05-28 09:38:31 +00:00
|
|
|
return config
|