Compare commits

...

9 Commits

Author SHA1 Message Date
Andrey
5edf000ce8 tune restore 2021-03-05 12:38:14 +03:00
Andrey
b821d182b2 edit readme 2021-03-05 12:25:42 +03:00
Andrey
149d30e921 add hex_to_float option
add auto config reload on megad restart
fix reloading issues
2021-03-05 11:45:41 +03:00
Andrey
84f677656a add hex_to_float option
add auto config reload on megad restart
fix reloading issues
2021-03-05 11:02:49 +03:00
Andrey
a0900052dc fix bugs 2021-03-05 00:02:43 +03:00
Andrey
b8d355f412 fix bugs 2021-03-04 22:54:21 +03:00
Andrey
d3f76a88df fix bugs 2021-03-04 22:52:15 +03:00
Andrey
fa2bb2674e fix updater 2021-03-04 21:52:25 +03:00
Andrey
a1ae4e294b fix pop 2021-03-04 21:28:01 +03:00
10 changed files with 82 additions and 203 deletions

View File

@@ -1,121 +1,3 @@
from urllib.parse import urlparse, parse_qsl
import struct
from bs4 import BeautifulSoup
page = '''
<html><head></head><body><a href="/sec/?pt=33">Back</a><br>0x15 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=t67xx">T67XX</a><br>0x40 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=htu21d">HTU21D</a>/PCA9685/HM3301<br>0x4a - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=max44009">MAX44009</a><br>
</body></html>
'''
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,
]
}
print(parse_scan_page(page))
print(struct.unpack('!f', bytes.fromhex('435c028f'))[0])

View File

@@ -16,7 +16,7 @@ from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT
from .hub import MegaD
from .config_flow import ConfigFlow
from .http import MegaView
@@ -47,6 +47,7 @@ CUSTOMIZE_PORT = {
vol.Optional(CONF_CONV_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_FORCE_I2C_SCAN): bool,
vol.Optional(CONF_HEX_TO_FLOAT): bool,
}
CUSTOMIZE_DS2413 = {
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT
@@ -133,7 +134,7 @@ async def get_hub(hass, entry):
break
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first')
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
hub = MegaD(hass, config=entry, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
return hub
@@ -173,18 +174,14 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
:param entry:
:return:
"""
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
# hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
# hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
# hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
await hass.config_entries.async_reload(entry.entry_id)
return True
async def async_remove_entry(hass, entry) -> None:
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id)
hub: MegaD = hass.data[DOMAIN].get(id)
@@ -194,12 +191,17 @@ async def async_remove_entry(hass, entry) -> None:
_hubs.pop(id, None)
hass.data[DOMAIN].pop(id, None)
hass.data[DOMAIN][CONF_ALL].pop(id, None)
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is not None:
task.cancel()
if hub is None:
return
await hub.stop()
return True
async_unload_entry = async_remove_entry
async def async_migrate_entry(hass, config_entry: ConfigEntry):

View File

@@ -65,7 +65,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 20
VERSION = 23
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -117,26 +117,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Manage the options."""
new_naming = self.config_entry.data.get('new_naming', False)
hub = await get_hub(self.hass, self.config_entry.data)
if user_input is not None:
reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data)
cfg.update(user_input)
hub = await get_hub(self.hass, cfg)
if reload:
await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
for x in REMOVE_CONFIG:
cfg.pop(x, default=None)
cfg.update(new)
cfg['new_naming'] = new_naming
self.config_entry.data = cfg
await get_hub(self.hass, cfg)
if reload:
id = self.config_entry.data.get('id', self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(id)
await hub.reload(reload_entry=False)
return self.async_create_entry(
title='',
data=cfg,

View File

@@ -36,6 +36,7 @@ CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_time'
CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
CONF_UPDATE_TIME = 'update_time'
CONF_HEX_TO_FLOAT = 'hex_to_float'
PLATFORMS = [
"light",
"switch",

View File

@@ -60,7 +60,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._lg = None
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "")
_pt = port if not mega.new_naming else f'{port:02}'
_pt = port if not mega.new_naming else f'{port:02}' if isinstance(port, int) else port
self._name = name or f"{mega.id}_{_pt}" + \
(f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None
@@ -93,7 +93,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
@property
def device_info(self):
_pt = self.port if not self.mega.new_naming else f'{self.port:02}'
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
return {
"identifiers": {
# Serial numbers are unique identifiers within a specific domain
@@ -123,7 +123,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def name(self):
c = self.customize.get(CONF_NAME)
if not isinstance(c, str):
_pt = self.port if not self.mega.new_naming else f'{self.port:02}'
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
c = self._name or f"{self.mega.id}_p{_pt}"
return c
@@ -157,6 +157,8 @@ class MegaPushEntity(BaseMegaEntity):
def __update(self, value: dict):
self._update(value)
if self.hass is None:
return
self.async_write_ha_state()
self.lg.debug(f'state after update %s', self.state)
if self.mega.mqtt_inputs and not _events_on:

View File

@@ -77,7 +77,11 @@ class MegaView(HomeAssistantView):
if hub is None:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
if hub is None and request.remote in ['::1', '127.0.0.1']:
hub = self.hubs.get('__def')
try:
hub = list(self.hubs.values())[0]
except IndexError:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
return Response(status=400)
elif hub is None:
return Response(status=400)
data = dict(request.query)
@@ -87,8 +91,8 @@ class MegaView(HomeAssistantView):
)
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data)
if data.get('st') == '1' and hub.restore_on_restart:
asyncio.create_task(self.later_restore(hub))
if data.get('st') == '1':
hass.async_create_task(self.later_restore(hub))
return Response(status=200)
port = data.get('pt')
data = data.copy()
@@ -158,10 +162,11 @@ class MegaView(HomeAssistantView):
:return:
"""
await asyncio.sleep(0.2)
await hub.restore_states()
if hub.restore_on_restart:
await hub.restore_states()
await hub.reload()
async def later_update(self, hub):
await asyncio.sleep(1)
_LOGGER.debug('force update')
await hub.updater.async_refresh()
await hub.updater.async_refresh()

View File

@@ -10,6 +10,7 @@ import json
from bs4 import BeautifulSoup
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
@@ -20,7 +21,8 @@ from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN,
from .const import (
TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN,
REMOVE_CONFIG
)
from .entities import set_events_off, BaseMegaEntity, MegaOutPort
from .exceptions import CannotConnect, NoPort
@@ -69,6 +71,7 @@ class MegaD:
mqtt: mqtt.MQTT,
lg: logging.Logger,
id: str,
config: ConfigEntry = None,
mqtt_inputs: bool = True,
mqtt_id: str = None,
scan_interval=60,
@@ -90,6 +93,7 @@ class MegaD:
**kwargs,
):
"""Initialize."""
self.config = config
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None:
@@ -470,45 +474,6 @@ class MegaD:
async def scan_port(self, port):
data = await self.request(pt=port)
return parse_config(data)
# async with self.lck:
# if port in self._scanned:
# return self._scanned[port]
# url = f'http://{self.host}/{self.sec}/?pt={port}'
# self.lg.debug(
# f'scan port %s: %s', port, url
# )
# async with aiohttp.request('get', url) as req:
# html = await req.text()
# if req.status != 200:
# return
# tree = BeautifulSoup(html, features="lxml")
# pty = tree.find('select', attrs={'name': 'pty'})
# if pty is None:
# return
# else:
# pty = pty.find(selected=True)
# if pty:
# pty = pty['value']
# else:
# return
# if pty in ['0', '1']:
# m = tree.find('select', attrs={'name': 'm'})
# if m:
# m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m)
# return pty, m
# elif pty == '3':
# m = tree.find('select', attrs={'name': 'd'})
# if m:
# m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m)
# return pty, m
# elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
# m = tree.find('select', attrs={'name': 'd'})
# if m:
# m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m or '0')
# return pty, m or '0'
async def scan_ports(self, nports=37):
for x in range(0, nports+1):
@@ -584,7 +549,7 @@ class MegaD:
for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page)
pt = f'{port}e{n}'
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
if ext_cfg.ety == '1':
ret['light'][pt].append({})
elif ext_cfg.ety == '0':
@@ -596,7 +561,8 @@ class MegaD:
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty == '4' and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)):
# i2c в режиме ANY
scan = cfg.src.find('a', text='I2C Scan')
@@ -657,4 +623,15 @@ class MegaD:
await self.request(
cf=7,
stime=datetime.now().strftime('%H:%M:%S')
)
)
async def reload(self, reload_entry=True):
new = await self.get_config(nports=self.nports)
self.lg.debug(f'new config: %s', new)
cfg = dict(self.config.data)
for x in REMOVE_CONFIG:
cfg.pop(x, None)
cfg.update(new)
self.config.data = cfg
if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id)

View File

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

View File

@@ -1,6 +1,7 @@
"""Platform for light integration."""
import logging
import voluptuous as vol
import struct
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA,
@@ -19,7 +20,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT
from .hub import MegaD
import re
@@ -114,7 +115,17 @@ class MegaI2C(MegaPushEntity):
@property
def state(self):
# self.lg.debug(f'get % all states: %', self._params, self.mega.values)
return self.mega.values.get(self._params)
ret = self.mega.values.get(self._params)
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex('41973333'))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
return ret
@property
def device_class(self):
@@ -197,6 +208,11 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret)
except:
ret = None
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex(ret))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass

View File

@@ -21,9 +21,10 @@
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`)
- Возможность работы с несколькими megad
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt
будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Автоматическое добавление/изменение объектов после перезагрузки контроллера
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о