Compare commits

...

14 Commits

Author SHA1 Message Date
andvikt
42f44d3020 Fix offline bugs 2021-01-20 23:33:53 +03:00
andvikt
3108927f15 fix offline bug 2021-01-20 23:25:57 +03:00
Andrey
8539b8a6ee fix eng lang in options 2021-01-18 09:32:25 +03:00
Andrey
405fbe4777 fix eng lang in options 2021-01-18 09:28:38 +03:00
Andrey
ed4928011b add refresh devices 2021-01-15 16:05:10 +03:00
Andrey
ed9011a6e1 edit readme 2021-01-15 09:23:32 +03:00
Andrey
c7e8bcb83e edit readme 2021-01-15 09:20:52 +03:00
Andrey
21fd00083c edit readme 2021-01-15 09:13:12 +03:00
Andrey
c4f4510941 add device registry 2021-01-15 09:10:42 +03:00
Andrey
254015be4c edit readme 2021-01-15 08:45:27 +03:00
Andrey
adb65529a2 hotfix 2021-01-14 23:05:30 +03:00
Andrey
768d46d952 hotfix 2021-01-14 22:48:07 +03:00
Andrey
359c6b99b7 hotfix 2021-01-14 22:27:07 +03:00
Andrey
79dc46226a hotfix 2021-01-14 22:19:56 +03:00
10 changed files with 264 additions and 137 deletions

View File

@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS
from .hub import MegaD
@@ -33,11 +33,7 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
PLATFORMS = [
"light",
"binary_sensor",
"sensor",
]
ALIVE_STATE = 'alive'
DEF_ID = 'def'
_POLL_TASKS = {}
@@ -80,12 +76,22 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
async def _add_mega(hass: HomeAssistant, id, data: dict):
async def get_hub(hass, entry):
id = entry.data.get('id', entry.entry_id)
data = dict(entry.data)
data.update(entry.options or {})
data.update(id=id)
_mqtt = hass.data.get(mqtt.DOMAIN)
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first')
hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER)
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER)
return hub
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id)
hub = await get_hub(hass, entry)
hass.data[DOMAIN][id] = hub
if not await hub.authenticate():
raise Exception("not authentificated")
mid = await hub.get_mqtt_id()
@@ -94,12 +100,9 @@ async def _add_mega(hass: HomeAssistant, id, data: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id)
data = dict(entry.data)
data.update(entry.options or {})
hub = await _add_mega(hass, id, data)
hub = await _add_mega(hass, entry)
_hubs[entry.entry_id] = hub
_subs[entry.entry_id] = entry.add_update_listener(update)
_subs[entry.entry_id] = entry.add_update_listener(updater)
for platform in PLATFORMS:
hass.async_create_task(
@@ -111,25 +114,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return True
async def update(hass: HomeAssistant, entry: ConfigEntry):
async def updater(hass: HomeAssistant, entry: ConfigEntry):
"""
Обновляется конфигурация
:param hass:
: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[CONF_PORT_TO_SCAN]
if entry.options[CONF_RELOAD]:
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
return True
async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id)
hass.data[DOMAIN][id].unsubscribe_all()
task: asyncio.Task = _POLL_TASKS.pop(id)
task.cancel()
hub = hass.data[DOMAIN]
if hub is None:
return
_LOGGER.debug(f'remove {id}')
_hubs.pop(entry.entry_id)
task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is None:
return
task.cancel()
if hub is None:
return
hub.unsubscribe_all()
unsub = _subs.pop(entry.entry_id)
unsub()
if unsub:
unsub()
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s to version 2", config_entry.version)
hub = await get_hub(hass, config_entry)
new = dict(config_entry.data)
if config_entry.version == 1:
cfg = await hub.get_config()
new.update(cfg)
_LOGGER.debug(f'new config: %s', new)
config_entry.data = new
config_entry.version = 2
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True
async def _save_service(hass: HomeAssistant, call: ServiceCall):

View File

@@ -1,5 +1,6 @@
"""Platform for light integration."""
import logging
import asyncio
import voluptuous as vol
@@ -61,15 +62,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
hub: MegaD = hass.data['mega'][mid]
devices = []
async def scan():
async for port, pty, m in hub.scan_ports():
if pty == "0":
sensor = MegaBinarySensor(mega_id=mid, port=port)
devices.append(sensor)
for port, cfg in config_entry.data.get('binary_sensor', {}).items():
hub.lg.debug(f'add binary_sensor on port %s', port)
sensor = MegaBinarySensor(mega_id=mid, port=port, config_entry=config_entry)
devices.append(sensor)
async_add_devices(devices)
async_add_devices(devices)
await scan()
class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity):

View File

@@ -8,8 +8,8 @@ from homeassistant import config_entries, core
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD # pylint:disable=unused-import
from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS # pylint:disable=unused-import
from .hub import MegaD
from . import exceptions
@@ -26,6 +26,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
)
async def get_hub(hass: HomeAssistant, data):
_mqtt = hass.data.get(mqtt.DOMAIN)
if not isinstance(_mqtt, mqtt.MQTT):
raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt)
if not await hub.authenticate():
raise exceptions.InvalidAuth
return hub
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
@@ -33,12 +43,7 @@ async def validate_input(hass: core.HomeAssistant, data):
"""
if data[CONF_ID] in hass.data.get(DOMAIN, []):
raise exceptions.DuplicateId('duplicate_id')
_mqtt = hass.data.get(mqtt.DOMAIN)
if not isinstance(_mqtt, mqtt.MQTT):
raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt)
if not await hub.authenticate():
raise exceptions.InvalidAuth
hub = await get_hub(hass, data)
return hub
@@ -46,7 +51,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 1
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -59,7 +64,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
try:
await validate_input(self.hass, user_input)
hub = await validate_input(self.hass, user_input)
config = await hub.get_config()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config,
)
except exceptions.CannotConnect:
errors["base"] = "cannot_connect"
except exceptions.InvalidAuth:
@@ -69,11 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors[CONF_ID] = str(exc)
else:
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=user_input,
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -92,10 +99,22 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Manage the options."""
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, self.config_entry.data)
if reload:
new = await hub.get_config()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
cfg.update(new)
return self.async_create_entry(
title='',
data={**user_input, **{CONF_ID: self.config_entry.data[CONF_ID]}},
data=cfg,
)
e = self.config_entry.data
ret = self.async_show_form(

View File

@@ -11,4 +11,9 @@ W1 = 'w1'
W1BUS = 'w1bus'
CONF_PORT_TO_SCAN = 'port_to_scan'
CONF_RELOAD = 'reload'
CONF_INVERT = 'invert'
CONF_INVERT = 'invert'
PLATFORMS = [
"light",
"binary_sensor",
"sensor",
]

View File

@@ -3,6 +3,7 @@ import asyncio
import json
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import State
from .hub import MegaD
from homeassistant.helpers.restore_state import RestoreEntity
@@ -19,12 +20,14 @@ class BaseMegaEntity(RestoreEntity):
self,
mega_id: str,
port: int,
config_entry: ConfigEntry = None,
id_suffix=None,
name=None,
unique_id=None
unique_id=None,
):
self._state: State = None
self.port = port
self.config_entry = config_entry
self._mega_id = mega_id
self._lg = None
self._unique_id = unique_id or f"mega_{mega_id}_{port}" + \
@@ -32,6 +35,23 @@ class BaseMegaEntity(RestoreEntity):
self._name = name or f"{mega_id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "")
@property
def device_info(self):
return {
"identifiers": {
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, f'{self._mega_id}', self.port),
},
"config_entries": [
self.config_entry,
],
"name": f'port {self.port}',
"manufacturer": 'ab-log.ru',
# "model": self.light.productname,
# "sw_version": self.light.swversion,
"via_device": (DOMAIN, self._mega_id),
}
@property
def lg(self) -> logging.Logger:
if self._lg is None:

View File

@@ -1,6 +1,7 @@
import asyncio
import json
import logging
from collections import defaultdict
from datetime import datetime
from functools import wraps
@@ -9,10 +10,27 @@ import typing
from bs4 import BeautifulSoup
from homeassistant.components import mqtt
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from .const import TEMP, HUM
from .exceptions import CannotConnect
import re
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PATTERNS = {
TEMP: TEMP_PATT,
HUM: HUM_PATT,
}
UNITS = {
TEMP: '°C',
HUM: '%'
}
CLASSES = {
TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: DEVICE_CLASS_HUMIDITY
}
class MegaD:
"""MegaD Hub"""
@@ -81,10 +99,10 @@ class MegaD:
await self.get_port(self.port_to_scan)
await asyncio.sleep(1)
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval:
if (datetime.now() - self.last_update).total_seconds() > (self.poll_interval + 10):
await self.get_port(self.port_to_scan)
await asyncio.sleep(1)
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval:
if (datetime.now() - self.last_update).total_seconds() > (self.poll_interval + 10):
self.lg.warning('mega is offline')
self.hass.states.async_set(
f'mega.{self.id}',
@@ -150,13 +168,15 @@ class MegaD:
ftr = asyncio.get_event_loop().create_future()
def cb(msg):
self.last_update = datetime.now()
try:
if '"value":NA' in msg.payload.decode():
ftr.set_result(None)
if not ftr.done():
ftr.set_result(None)
return
ret = json.loads(msg.payload).get('value')
ftr.set_result(ret)
if not ftr.done():
ftr.set_result(ret)
except Exception as exc:
ret = None
self.lg.exception(f'while parsing response from port {port}: {msg.payload}')
@@ -167,12 +187,12 @@ class MegaD:
)
async with self.lck:
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
try:
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
@@ -236,40 +256,69 @@ class MegaD:
return await req.text()
async def scan_port(self, port):
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()
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:
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()
tree = BeautifulSoup(html, features="lxml")
pty = tree.find('select', attrs={'name': 'pty'})
if pty is None:
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
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
async def scan_ports(self,):
async with self.lck:
for x in range(38):
ret = await self.scan_port(x)
if ret:
yield [x, *ret]
for x in range(38):
ret = await self.scan_port(x)
if ret:
yield [x, *ret]
async def get_config(self):
ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports():
if pty == "0":
ret['binary_sensor'][port].append({})
elif pty == "1" and m in ['0', '1']:
ret['light'][port].append({'dimmer': m == '1'})
elif pty == '3':
values = await self.get_port(port)
self.lg.debug(f'values: %s', values)
if values is None:
self.lg.warning(f'port {port} is of type sensor but did not respond, skipping it')
continue
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
values = {None: values}
for key in values:
self.lg.debug(f'add sensor {key}')
ret['sensor'][port].append(dict(
key=key,
patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
# TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key,
))
return ret

View File

@@ -1,6 +1,6 @@
"""Platform for light integration."""
import logging
import asyncio
import voluptuous as vol
from homeassistant.components.light import (
@@ -78,14 +78,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
hub: MegaD = hass.data['mega'][mid]
devices = []
async def scan_ports():
async for port, pty, m in hub.scan_ports():
if pty == "1" and m in ['0', '1']:
light = MegaLight(mega_id=mid, port=port, dimmer=m == '1')
devices.append(light)
async_add_devices(devices)
await scan_ports()
for port, cfg in config_entry.data.get('light', {}).items():
for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data)
light = MegaLight(mega_id=mid, port=port, config_entry=config_entry, **data)
devices.append(light)
async_add_devices(devices)
class MegaLight(LightEntity, BaseMegaEntity):

View File

@@ -1,4 +1,5 @@
"""Platform for light integration."""
import asyncio
import logging
import voluptuous as vol
@@ -68,7 +69,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None):
return True
def _make_entity(mid: str, port: int, conf: dict):
def _make_entity(config_entry, mid: str, port: int, conf: dict):
key = conf[CONF_KEY]
return Mega1WSensor(
key=key,
@@ -77,7 +78,8 @@ def _make_entity(mid: str, port: int, conf: dict):
patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key
id_suffix=key,
config_entry=config_entry
)
@@ -86,32 +88,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
hub: MegaD = hass.data['mega'][mid]
devices = []
async def scan():
async for port, pty, m in hub.scan_ports():
if pty == "3":
values = await hub.get_port(port)
lg.debug(f'values: %s', values)
if values is None:
continue
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
values = {None: values}
for key in values:
hub.lg.debug(f'add sensor {W1}:{key}')
sensor = _make_entity(
mid=mid,
port=port,
conf={
CONF_TYPE: W1,
CONF_KEY: key,
})
devices.append(sensor)
hub.sensors.append(sensor)
for port, cfg in config_entry.data.get('sensor', {}).items():
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = Mega1WSensor(
mega_id=mid,
port=port,
config_entry=config_entry,
**data,
)
devices.append(sensor)
async_add_devices(devices)
await scan()
async_add_devices(devices)
class Mega1WSensor(BaseMegaEntity):

View File

@@ -23,5 +23,16 @@
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Scan interval (sec) (used for aliveness and sensors)",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"reload": "Reload objects"
}
}
}
},
"title": "mega"
}

View File

@@ -3,13 +3,15 @@
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
## Основные особенности:
- Настройка в веб-интерфейсе
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для температурных датчиков
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`)
- Возможность работы с несколькими megad
- Обратная связь по mqtt
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad
- Поддержка температурных датчиков в режиме шины
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей.
## Зависимости
**Важно!!** Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
@@ -18,8 +20,9 @@
mqtt
## Установка
Рекомендованнй способ с поддержкой обновлений - через [HACS](https://hacs.xyz/docs/installation/installation).
После установки перейти в меню HACS - Integrations - Explore, в поиске ищем MegaD
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
HACS - Integrations - Explore, в поиске ищем MegaD.
Альтернативный способ установки:
```shell
@@ -27,12 +30,14 @@ mqtt
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
```
Не забываем перезагрузить HA
## Устройства
Поддерживаются устройства: light, switch, binary_sensor, sensor. light может работать как диммер
## Настройка из веб-интерфейса
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
Все имеющиеся у вас порты будут настроены автоматически.
Вы можете менять названия, иконки и entity_id так же из интерфейса.
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml
@@ -65,7 +70,6 @@ mega.run_cmd:
cmd:
description: Любая поддерживаемая мегой команда
example: "1:0"
```
## Отладка