diff --git a/.experiment.py b/.experiment.py
index cdbe35f..4419e7a 100644
--- a/.experiment.py
+++ b/.experiment.py
@@ -1,43 +1,121 @@
-import re
+from urllib.parse import urlparse, parse_qsl
+
+from bs4 import BeautifulSoup
+
+page = '''
+
Back
0x15 - T67XX
0x40 - HTU21D/PCA9685/HM3301
0x4a - MAX44009
+
+'''
+
+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,
+)
-PATT_FW = re.compile(r'fw:\s(.+)\)')
-data = """
-MegaD-2561 by ab-log.ru (fw: 4.48b7)
Config
-- MODS --
XP1
XP2
-- XT2 --
P30 - OUT
P31 - OUT
P32 - IN
P33 - I2C/SCL
P34 - DS
P35 - NC
-- XP5/6 --
P36 - ADC
P37 - NC
-
-MegaD-2561 by ab-log.ru (fw: 4.48b7)
Config
-- MODS --
XP1
XP2
-- XT2 --
P30 - OUT
P31 - OUT
P32 - IN
P33 - I2C/SCL
P34 - DS
P35 - NC
-- XP5/6 --
P36 - ADC
P37 - NC
-MegaD-2561 by
-ab-log.ru
- (fw: 4.48b7)
-
-Config
-
--- MODS --
-
-XP1
-
-XP2
-
--- XT2 --
-
-P30 - OUT
-
-P31 - OUT
-
-P32 - IN
-
-P33 - I2C/SCL
-
-P34 - DS
-
-P35 - NC
-
--- XP5/6 --
-
-P36 - ADC
-
-P37 - NC
-MegaD-2561 by ab-log.ru (fw: 4.48b7)
Config
-- MODS --
XP1
XP2
-- XT2 --
P30 - OUT
P31 - OUT
P32 - IN
P33 - I2C/SCL
P34 - DS
P35 - NC
-- XP5/6 --
P36 - ADC
P37 - NC
-MegaD-2561 by ab-log.ru (fw: 4.48b7)
Config
-- MODS --
XP1
XP2
-- XT2 --
P30 - OUT
P31 - OUT
P32 - IN
P33 - I2C/SCL
P34 - DS
P35 - NC
-- XP5/6 --
P36 - ADC
P37 - NC
-"""
-print(PATT_FW.search(data).groups()[0])
\ No newline at end of file
+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,
+ ]
+}
+
+print(parse_scan_page(page))
\ No newline at end of file
diff --git a/custom_components/mega/__init__.py b/custom_components/mega/__init__.py
index 57b53e1..4a36cf0 100644
--- a/custom_components/mega/__init__.py
+++ b/custom_components/mega/__init__.py
@@ -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='шаблон ответа когда на этот порт приходит'
diff --git a/custom_components/mega/config_flow.py b/custom_components/mega/config_flow.py
index f6bbd38..aa03c78 100644
--- a/custom_components/mega/config_flow.py
+++ b/custom_components/mega/config_flow.py
@@ -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):
diff --git a/custom_components/mega/config_parser.py b/custom_components/mega/config_parser.py
index 602cddf..49860c2 100644
--- a/custom_components/mega/config_parser.py
+++ b/custom_components/mega/config_parser.py
@@ -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")
diff --git a/custom_components/mega/entities.py b/custom_components/mega/entities.py
index 4884aef..015cb2b 100644
--- a/custom_components/mega/entities.py
+++ b/custom_components/mega/entities.py
@@ -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(
diff --git a/custom_components/mega/hub.py b/custom_components/mega/hub.py
index af296a4..5b43d75 100644
--- a/custom_components/mega/hub.py
+++ b/custom_components/mega/hub.py
@@ -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':
diff --git a/custom_components/mega/i2c.py b/custom_components/mega/i2c.py
new file mode 100644
index 0000000..752dfec
--- /dev/null
+++ b/custom_components/mega/i2c.py
@@ -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,
+ ]
+}
diff --git a/custom_components/mega/manifest.json b/custom_components/mega/manifest.json
index e33c76b..2e58e32 100644
--- a/custom_components/mega/manifest.json
+++ b/custom_components/mega/manifest.json
@@ -15,5 +15,5 @@
"@andvikt"
],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
- "version": "v0.5.1b2"
+ "version": "v0.6.1b1"
}
\ No newline at end of file
diff --git a/custom_components/mega/sensor.py b/custom_components/mega/sensor.py
index a25e9df..613dc25 100644
--- a/custom_components/mega/sensor.py
+++ b/custom_components/mega/sensor.py
@@ -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
\ No newline at end of file
+ return c or n
+
+
+_constructors = {
+ 'sensor': Mega1WSensor,
+ 'i2c': MegaI2C,
+}
\ No newline at end of file
diff --git a/custom_components/mega/tools.py b/custom_components/mega/tools.py
index 43973ea..bdc7e2a 100644
--- a/custom_components/mega/tools.py
+++ b/custom_components/mega/tools.py
@@ -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
\ No newline at end of file
+ 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)
diff --git a/readme.md b/readme.md
index 89d85f1..644a073 100644
--- a/readme.md
+++ b/readme.md
@@ -30,6 +30,8 @@
выполнении предыдущей.
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1)
+- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков датчиков
+ [по ссылке](https://github.com/andvikt/mega_hacs/wiki/ш2с)
## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):