add i2c sensors

This commit is contained in:
Andrey
2021-03-03 12:01:02 +03:00
parent 289f52ef73
commit 49fcf880d9
11 changed files with 420 additions and 65 deletions

View File

@@ -7,7 +7,7 @@ import voluptuous as vol
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass
@@ -34,6 +34,10 @@ CUSTOMIZE_PORT = {
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(CONF_DEVICE_CLASS):
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(
CONF_RESPONSE_TEMPLATE,
description='шаблон ответа когда на этот порт приходит'

View File

@@ -63,7 +63,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 17
VERSION = 18
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):

View File

@@ -25,6 +25,7 @@ class Config:
inta: str = field(compare=False, hash=False, default=None)
misc: str = field(compare=False, hash=False, default=None)
eact: str = field(compare=False, hash=False, default=None)
src: BeautifulSoup = field(compare=False, hash=False, default=None)
def parse_config(page: str):
@@ -43,7 +44,7 @@ def parse_config(page: str):
v = page.find('input', attrs={'name': x})
if v:
ret[x] = v['value']
return Config(**ret)
return Config(**ret, src=page)
DIGITAL_IN = Config(pty="0")

View File

@@ -318,7 +318,7 @@ class MegaOutPort(MegaPushEntity):
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd)
await self.mega.request(**_cmd, priority=-1)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
@@ -342,7 +342,7 @@ class MegaOutPort(MegaPushEntity):
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd)
await self.mega.request(**_cmd, priority=-1)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(

View File

@@ -24,7 +24,8 @@ from .const import (
)
from .entities import set_events_off, BaseMegaEntity, MegaOutPort
from .exceptions import CannotConnect, NoPort
from .tools import make_ints, int_ignore
from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -83,6 +84,7 @@ class MegaD:
extenders=None,
ext_in=None,
ext_acts=None,
i2c_sensors=None,
**kwargs,
):
"""Initialize."""
@@ -100,6 +102,7 @@ class MegaD:
self.extenders = extenders or []
self.ext_in = ext_in or {}
self.ext_act = ext_acts or {}
self.i2c_sensors = i2c_sensors or []
self.poll_outs = poll_outs
self.update_all = update_all if update_all is not None else True
self.nports = nports
@@ -113,7 +116,7 @@ class MegaD:
self.id = id
self.lck = asyncio.Lock()
self.last_long = {}
self._http_lck = asyncio.Lock()
self._http_lck = PriorityLock()
self._notif_lck = asyncio.Lock()
self.cnd = asyncio.Condition()
self.online = True
@@ -245,6 +248,12 @@ class MegaD:
Polling ports
"""
self.lg.debug('poll')
for x in self.i2c_sensors:
if not isinstance(x, dict):
continue
ret = await self._update_i2c(x)
if isinstance(ret, dict):
self.values.update(ret)
for x in self.extenders:
ret = await self._update_extender(x)
if not isinstance(ret, dict):
@@ -281,13 +290,13 @@ class MegaD:
async def send_command(self, port=None, cmd=None):
return await self.request(pt=port, cmd=cmd)
async def request(self, **kwargs):
async def request(self, priority=0, **kwargs):
cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
url = f"http://{self.host}/{self.sec}"
if cmd:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url)
async with self._http_lck:
async with self._http_lck(priority):
async with aiohttp.request("get", url=url) as req:
if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
@@ -514,12 +523,24 @@ class MegaD:
ret[f'{port}e{i}'] = x
return ret
async def _update_i2c(self, params):
"""
Обновление портов i2c
:param params: параметры url
:return:
"""
_params = tuple(params.items())
return {
_params: await self.request(**params)
}
async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list))
ret['mqtt_id'] = await self.get_mqtt_id()
ret['extenders'] = extenders = []
ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = []
async for port, cfg in self.scan_ports(nports):
if cfg.pty == "0":
ret['binary_sensor'][port].append({})
@@ -559,6 +580,16 @@ class MegaD:
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty == '4' and cfg.gr == '0':
# i2c в режиме ANY
scan = cfg.src.find('a', text='I2C Scan')
self.lg.debug(f'find scan link: %s', scan)
if scan is not None:
page = await self.request(pt=port, cmd='scan')
req, parsed = parse_scan_page(page)
self.lg.debug(f'scan results: %s', (req, parsed))
ret['i2c'][port].append(parsed)
i2c_sensors.extend(req)
elif cfg.pty in ('3', '2', '4'):
http_cmd = 'get'
if cfg.d == '5' and cfg.pty == '3':

View File

@@ -0,0 +1,108 @@
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
)
def parse_scan_page(page: str):
ret = []
req = []
page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'):
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
if 'i2c_dev' in params:
dev = params['i2c_dev']
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
else:
suffix = ''
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
_dev = f'{dev}_{suffix}'
else:
_dev = dev
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
})
req.append(params)
return req, ret
class Skip:
pass
class Request:
pass
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
],
'ptsensor': [
Request, # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
]
}

View File

@@ -15,5 +15,5 @@
"@andvikt"
],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v0.5.1b2"
"version": "v0.6.1b1"
}

View File

@@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_UNIQUE_ID,
CONF_ID,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_DEVICE_CLASS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
@@ -82,21 +83,40 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
for port, cfg in config_entry.data.get('sensor', {}).items():
port = int_ignore(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = Mega1WSensor(
mega=hub,
port=port,
config_entry=config_entry,
**data,
)
devices.append(sensor)
for tp in ['sensor', 'i2c']:
for port, cfg in config_entry.data.get(tp, {}).items():
port = int_ignore(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = _constructors[tp](
mega=hub,
port=port,
config_entry=config_entry,
**data,
)
devices.append(sensor)
async_add_devices(devices)
class MegaI2C(MegaPushEntity):
def __init__(self, *args, device_class: str, params: dict, **kwargs):
self._device_class = device_class
self._params = tuple(params.items())
super().__init__(*args, **kwargs)
def device_class(self):
return self._device_class
def state(self):
return self.mega.values[self._params]
@property
def device_class(self):
return self._device_class
class Mega1WSensor(MegaPushEntity):
def __init__(
@@ -141,7 +161,15 @@ class Mega1WSensor(MegaPushEntity):
@property
def device_class(self):
return self._device_class
_u = self.customize.get(CONF_DEVICE_CLASS, None)
if _u is None:
return self._device_class
elif isinstance(_u, str):
return _u
elif isinstance(_u, dict) and self.key in _u:
return _u[self.key]
else:
return self._device_class
@property
def state(self):
@@ -177,4 +205,10 @@ class Mega1WSensor(MegaPushEntity):
c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict):
c = c.get(self.key)
return c or n
return c or n
_constructors = {
'sensor': Mega1WSensor,
'i2c': MegaI2C,
}

View File

@@ -1,3 +1,9 @@
import asyncio
import itertools
from heapq import heappush
from contextlib import asynccontextmanager
_params = ['m', 'click', 'cnt', 'pt']
@@ -17,4 +23,95 @@ def int_ignore(x):
try:
return int(x)
except (TypeError, ValueError):
return x
return x
class PriorityLock(asyncio.Lock):
"""
You can acquire lock with some kind of priority in mind, so that locks with higher priority will be released first.
priority can be set with lck.acquire(1)
or by using context manager:
>>> lck = PriorityLock()
... async with lck(1):
... # do something
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cnt = itertools.count()
def __call__(self, priority=0):
return self._with_priority(priority)
@asynccontextmanager
async def _with_priority(self, p):
await self.acquire(p)
try:
yield
finally:
self.release()
async def acquire(self, priority=0) -> bool:
"""Acquire a lock.
This method blocks until the lock is unlocked, then sets it to
locked and returns True.
"""
if (not self._locked and (self._waiters is None or
all(w.cancelled() for _, w in self._waiters))):
self._locked = True
return True
if self._waiters is None:
self._waiters = []
fut = self._loop.create_future()
cnt = next(self._cnt)
heappush(self._waiters, (priority, cnt, fut))
# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
try:
await fut
finally:
self._waiters.remove((priority, cnt, fut))
except asyncio.exceptions.CancelledError:
if not self._locked:
self._wake_up_first()
raise
self._locked = True
return True
def release(self):
"""Release a lock.
When the lock is locked, reset it to unlocked, and return.
If any other coroutines are blocked waiting for the lock to become
unlocked, allow exactly one of them to proceed.
When invoked on an unlocked lock, a RuntimeError is raised.
There is no return value.
"""
if self._locked:
self._locked = False
self._wake_up_first()
else:
raise RuntimeError('Lock is not acquired.')
def _wake_up_first(self):
"""Wake up the first waiter if it isn't done."""
if not self._waiters:
return
try:
_, _, fut = self._waiters[0]
except IndexError:
return
# .done() necessarily means that a waiter will wake up later on and
# either take the lock, or, if it was cancelled and lock wasn't
# taken already, will hit this again and wake up a new waiter.
if not fut.done():
fut.set_result(True)