mirror of
https://github.com/andvikt/mega_hacs.git
synced 2025-12-11 00:54:28 +05:00
add i2c sensors
This commit is contained in:
@@ -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='шаблон ответа когда на этот порт приходит'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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':
|
||||
|
||||
108
custom_components/mega/i2c.py
Normal file
108
custom_components/mega/i2c.py
Normal 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,
|
||||
]
|
||||
}
|
||||
@@ -15,5 +15,5 @@
|
||||
"@andvikt"
|
||||
],
|
||||
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
|
||||
"version": "v0.5.1b2"
|
||||
"version": "v0.6.1b1"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user