- add many i2c sensors

This commit is contained in:
Викторов Андрей Германович
2023-10-15 21:09:46 +03:00
parent bd98fa216d
commit 39696b054f
4 changed files with 183 additions and 90 deletions

View File

@@ -8,9 +8,19 @@ from homeassistant import config_entries, core
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_RELOAD, \ from .const import (
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \ DOMAIN,
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME CONF_RELOAD,
CONF_NPORTS,
CONF_UPDATE_ALL,
CONF_POLL_OUTS,
CONF_FAKE_RESPONSE,
CONF_FORCE_D,
CONF_ALLOW_HOSTS,
CONF_PROTECTED,
CONF_RESTORE_ON_RESTART,
CONF_UPDATE_TIME,
)
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -18,7 +28,7 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema( STEP_USER_DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_ID, default='mega'): str, vol.Required(CONF_ID, default="mega"): str,
vol.Required(CONF_HOST, default="192.168.0.14"): str, vol.Required(CONF_HOST, default="192.168.0.14"): str,
vol.Required(CONF_PASSWORD, default="sec"): str, vol.Required(CONF_PASSWORD, default="sec"): str,
vol.Optional(CONF_SCAN_INTERVAL, default=30): int, vol.Optional(CONF_SCAN_INTERVAL, default=30): int,
@@ -31,7 +41,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_FORCE_D, default=True): bool, vol.Optional(CONF_FORCE_D, default=True): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=True): bool, vol.Optional(CONF_RESTORE_ON_RESTART, default=True): bool,
vol.Optional(CONF_PROTECTED, default=True): bool, vol.Optional(CONF_PROTECTED, default=True): bool,
vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str, vol.Optional(CONF_ALLOW_HOSTS, default="::1;127.0.0.1"): str,
vol.Optional(CONF_UPDATE_TIME, default=True): bool, vol.Optional(CONF_UPDATE_TIME, default=True): bool,
}, },
) )
@@ -41,7 +51,7 @@ async def get_hub(hass: HomeAssistant, data):
# _mqtt = hass.data.get(mqtt.DOMAIN) # _mqtt = hass.data.get(mqtt.DOMAIN)
# if not isinstance(_mqtt, mqtt.MQTT): # if not isinstance(_mqtt, mqtt.MQTT):
# raise exceptions.MqttNotConfigured("mqtt must be configured first") # raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) #mqtt=_mqtt, hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) # mqtt=_mqtt,
hub.mqtt_id = await hub.get_mqtt_id() hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate(): if not await hub.authenticate():
raise exceptions.InvalidAuth raise exceptions.InvalidAuth
@@ -54,7 +64,7 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
""" """
if data[CONF_ID] in hass.data.get(DOMAIN, []): if data[CONF_ID] in hass.data.get(DOMAIN, []):
raise exceptions.DuplicateId('duplicate_id') raise exceptions.DuplicateId("duplicate_id")
hub = await get_hub(hass, data) hub = await get_hub(hass, data)
return hub return hub
@@ -63,7 +73,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega.""" """Handle a config flow for mega."""
VERSION = 26 VERSION = 27
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
@@ -78,12 +88,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
hub = await validate_input(self.hass, user_input) hub = await validate_input(self.hass, user_input)
await hub.start() await hub.start()
hub.new_naming=True hub.new_naming = True
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37)) config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop() await hub.stop()
hub.lg.debug(f'config loaded: %s', config) hub.lg.debug(f"config loaded: %s", config)
config.update(user_input) config.update(user_input)
config['new_naming'] = True config["new_naming"] = True
return self.async_create_entry( return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]), title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config, data=config,
@@ -109,48 +119,66 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: ConfigEntry): def __init__(self, config_entry: ConfigEntry):
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage the options.""" """Manage the options."""
new_naming = self.config_entry.data.get('new_naming', False) new_naming = self.config_entry.data.get("new_naming", False)
if user_input is not None: if user_input is not None:
reload = user_input.pop(CONF_RELOAD) reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data) cfg = dict(self.config_entry.data)
cfg.update(user_input) cfg.update(user_input)
cfg['new_naming'] = new_naming cfg["new_naming"] = new_naming
self.config_entry.data = cfg self.config_entry.data = cfg
await get_hub(self.hass, cfg) await get_hub(self.hass, cfg)
if reload: if reload:
id = self.config_entry.data.get('id', self.config_entry.entry_id) id = self.config_entry.data.get("id", self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(id) hub: MegaD = self.hass.data[DOMAIN].get(id)
cfg = await hub.reload(reload_entry=False) cfg = await hub.reload(reload_entry=False)
return self.async_create_entry( return self.async_create_entry(
title='', title="",
data=cfg, data=cfg,
) )
e = self.config_entry.data e = self.config_entry.data
ret = self.async_show_form( ret = self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema({ data_schema=vol.Schema(
vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int, {
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool, vol.Optional(
# vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int, CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)
# vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool, ): int,
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int, vol.Optional(
vol.Optional(CONF_RELOAD, default=False): bool, CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool, ): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)): bool, # vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)): bool, # vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=e.get(CONF_RESTORE_ON_RESTART, False)): bool, vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)): bool, vol.Optional(CONF_RELOAD, default=False): bool,
vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str, vol.Optional(
vol.Optional(CONF_UPDATE_TIME, default=e.get(CONF_UPDATE_TIME, False)): bool, CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)
# vol.Optional(CONF_INVERT, default=''): str, ): bool,
}), vol.Optional(
CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)
): bool,
vol.Optional(
CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)
): bool,
vol.Optional(
CONF_RESTORE_ON_RESTART,
default=e.get(CONF_RESTORE_ON_RESTART, False),
): bool,
vol.Optional(
CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)
): bool,
vol.Optional(CONF_ALLOW_HOSTS, default="::1;127.0.0.1"): str,
vol.Optional(
CONF_UPDATE_TIME, default=e.get(CONF_UPDATE_TIME, False)
): bool,
# vol.Optional(CONF_INVERT, default=''): str,
}
),
) )
return ret return ret

View File

@@ -591,3 +591,10 @@ def safe_int(v, def_on=1, def_off=0, def_val=None):
return int(v) return int(v)
except (ValueError, TypeError): except (ValueError, TypeError):
return def_val return def_val
def safe_float(v):
try:
return float(v)
except:
return None

View File

@@ -32,7 +32,7 @@ from .const import (
CONF_FORCE_I2C_SCAN, CONF_FORCE_I2C_SCAN,
REMOVE_CONFIG, REMOVE_CONFIG,
) )
from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int, safe_float
from .exceptions import CannotConnect, NoPort from .exceptions import CannotConnect, NoPort
from .i2c import parse_scan_page from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock from .tools import make_ints, int_ignore, PriorityLock
@@ -168,6 +168,9 @@ class MegaD:
except Exception: except Exception:
self.lg.exception("while setting allowed hosts") self.lg.exception("while setting allowed hosts")
self.binary_sensors = [] self.binary_sensors = []
self.sht31inited = (
set()
) # список портов sht31 которые уже успешно проинициализированы были
async def start(self): async def start(self):
pass pass
@@ -487,16 +490,39 @@ class MegaD:
:return: :return:
""" """
pt = params.get("pt") pt = params.get("pt")
i2c_dev = params.get("i2c_dev", None)
if pt in self.skip_ports: if pt in self.skip_ports:
return return
if pt is not None: if pt is None:
pass return
_params = tuple(params.items()) _params = tuple(params.items())
if i2c_dev is not None and i2c_dev == "sht31" and pt not in self.sht31inited:
__params = params.copy()
__params["i2c_par"] = 9
# инициализация сенсора
await self.request(i2c_dev=i2c_dev, **__params)
await asyncio.sleep(0.1)
self.sht31inited |= pt
delay = None delay = None
idx: int = params.pop("idx", None)
pt: int = params.get("pt", None)
if "delay" in params: if "delay" in params:
delay = params.pop("delay") delay = params.pop("delay")
try: try:
ret = {_params: await self.request(**params)} if idx is None or idx == 0:
v: str = await self.request(**params)
# scd4x фактически отдает сразу 3 датчика на одном запросе, не ложится
# в общую архитектуру, поэтому используется такой костыль с кешем
self.values[f"chache_{pt}"] = v
elif idx is not None and idx > 0:
v: str = self.values.get(f"chache_{pt}")
if idx is not None:
v = safe_float(v.split("/")[idx])
ret = {_params: v}
except Exception:
self.lg.exception(f"while getting i2c {params=}")
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
self.lg.debug("i2c response: %s", ret) self.lg.debug("i2c response: %s", ret)

View File

@@ -16,27 +16,33 @@ from collections import namedtuple
# DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix') # DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
@dataclass @dataclass
class DeviceType: class DeviceType:
device_class: typing.Optional[str] = None device_class: typing.Optional[str] = None
unit_of_measurement: typing.Optional[str] = None unit_of_measurement: typing.Optional[str] = None
suffix: typing.Optional[str] = None suffix: typing.Optional[str] = None
delay: typing.Optional[float] = None delay: typing.Optional[float] = None
i2c_par: typing.Optional[int] = None
idx: typing.Optional[
int
] = None # на случай если все значения представлены одной строчкой (как с scd4x)
def parse_scan_page(page: str): def parse_scan_page(page: str):
ret = [] ret = []
req = [] req = []
page = BeautifulSoup(page, features="lxml") page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'): for x in page.find_all("a"):
params = x.get('href') params = x.get("href")
if params is None: if params is None:
continue continue
params = dict(parse_qsl(urlparse(params).query)) params = dict(parse_qsl(urlparse(params).query))
dev = params.get('i2c_dev') dev = params.get("i2c_dev")
if dev is None: if dev is None:
continue continue
classes = i2c_classes.get(dev, []) classes = i2c_classes.get(dev, [])
i2c_par, idx = (None, None)
for i, c in enumerate(classes): for i, c in enumerate(classes):
_params = params.copy() _params = params.copy()
if c is Skip: if c is Skip:
@@ -46,31 +52,39 @@ def parse_scan_page(page: str):
continue continue
elif isinstance(c, Request): elif isinstance(c, Request):
if c.delay: if c.delay:
_params['delay'] = c.delay _params["delay"] = c.delay
req.append(_params) req.append(_params)
continue continue
elif isinstance(c, DeviceType): elif isinstance(c, DeviceType):
c, m, suffix, delay = astuple(c) c, m, suffix, delay, i2c_par, idx = astuple(c)
if delay is not None: if delay is not None:
_params['delay'] = delay _params["delay"] = delay
else: else:
continue continue
suffix = suffix or c suffix = suffix or c
if 'addr' in _params: if "addr" in _params:
suffix += f"_{_params['addr']}" if suffix else str(_params['addr']) suffix += f"_{_params['addr']}" if suffix else str(_params["addr"])
if suffix: if suffix:
_dev = f'{dev}_{suffix}' _dev = f"{dev}_{suffix}"
else: else:
_dev = dev _dev = dev
if i > 0: if i > 0 and i2c_par is None:
_params['i2c_par'] = i _params["i2c_par"] = i
# i2c_par может быть явно указан в DeviceType
elif i2c_par is not None and i2c_par > 0:
_params["i2c_par"] = i2c_par
# idx - тема фактически только для scd4x, означает номер внутри текстового значения разделенного знаком "/"
if idx is not None:
_params["idx"] = idx
ret.append({ ret.append(
'id_suffix': _dev, {
'device_class': c, "id_suffix": _dev,
'params': _params, "device_class": c,
'unit_of_measurement': m, "params": _params,
}) "unit_of_measurement": m,
}
)
req.append(_params) req.append(_params)
return req, ret return req, ret
@@ -85,67 +99,85 @@ class Request:
i2c_classes = { i2c_classes = {
'htu21d': [ "htu21d": [
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None), DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'sht31': [ "sht31": [
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=1.5), DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=0.1),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'max44009': [ "max44009": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None) "bh1750": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
], "tsl2591": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
'bh1750': [ "bmp180": [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
],
'tsl2591': [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
],
'bmp180': [
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'bmx280': [ "bmx280": [
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None) DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
], ],
'dps368': [ "dps368": [
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mlx90614': [ "mlx90614": [
Skip, Skip,
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'temp'), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "temp"),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'object'), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "object"),
], ],
'ptsensor': [ "ptsensor": [
Skip, Skip,
Request(delay=3), # запрос на измерение Request(delay=3), # запрос на измерение
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mcp9600': [ "mcp9600": [
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # термопара DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # термопара
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему DeviceType(
SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None
), # сенсор встроенный в микросхему
], ],
't67xx': [ "t67xx": [DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None)],
DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None) "tmp117": [
],
'tmp117': [
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'ads1115': [ "ads1115": [
DeviceType(None, None, 'ch0'), DeviceType(None, None, "ch0"),
DeviceType(None, None, 'ch1'), DeviceType(None, None, "ch1"),
DeviceType(None, None, 'ch2'), DeviceType(None, None, "ch2"),
DeviceType(None, None, 'ch3'), DeviceType(None, None, "ch3"),
], ],
'ads1015': [ "ads1015": [
DeviceType(None, None, 'ch0'), DeviceType(None, None, "ch0"),
DeviceType(None, None, 'ch1'), DeviceType(None, None, "ch1"),
DeviceType(None, None, 'ch2'), DeviceType(None, None, "ch2"),
DeviceType(None, None, 'ch3'), DeviceType(None, None, "ch3"),
],
"opt3001": [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None),
],
"scd4x": [
DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
],
"ina226": [
Skip,
DeviceType(SensorDeviceClass.CURRENT, "A", None),
DeviceType(SensorDeviceClass.VOLTAGE, "V", None),
],
"scd4x": [
DeviceType(
SensorDeviceClass.CO2,
CONCENTRATION_PARTS_PER_MILLION,
None,
i2c_par=0,
idx=0,
),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None, i2c_par=0, idx=1),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, i2c_par=0, idx=2),
], ],
} }