Compare commits

...

50 Commits

Author SHA1 Message Date
Andrey
ff6225a959 fix scanning 2021-01-25 17:18:42 +03:00
Andrey
57f355d479 fix scanning 2021-01-25 16:37:48 +03:00
Andrey
7d6273539e fix scanning 2021-01-25 15:57:10 +03:00
Andrey
5da2973351 fix scanning 2021-01-25 15:52:24 +03:00
Andrey
5eadd295f1 Merge branch 'master' of https://github.com/andvikt/mega_hacs 2021-01-25 15:48:29 +03:00
Andrey
5bf432a27f change readme 2021-01-25 15:47:55 +03:00
Andrey
d934e87ae5 force use http while scanning 2021-01-25 15:46:40 +03:00
andvikt
0b54db9c44 Update issue templates 2021-01-25 13:05:31 +03:00
Andrey
2d15b60929 add allowed hosts config 2021-01-25 12:54:15 +03:00
Andrey
b0b4fdd6cf fix dependencies 2021-01-25 12:29:31 +03:00
Andrey
8bbb4ab271 add http support 2021-01-25 12:25:11 +03:00
Andrey
876a1f0cc8 get port one more fix 2021-01-24 11:17:09 +03:00
Andrey
18d0aee391 small fix 2021-01-23 21:08:33 +03:00
Andrey
4e2d659c44 fix mid 2021-01-22 21:32:55 +03:00
Andrey
e0b4fec0ca add name cusomisation for multiple sensors 2021-01-22 12:28:04 +03:00
Andrey
b5e4b2c802 add name cusomisation for multiple sensors 2021-01-22 12:27:07 +03:00
Andrey
ed6a86a721 add name cusomisation for multiple sensors 2021-01-22 12:24:38 +03:00
Andrey
811f2067bb fix save_all service 2021-01-22 12:20:16 +03:00
Andrey
3e1499f78b fix get_port 2021-01-22 12:16:32 +03:00
Andrey
2a4a85a20e small bugfix 2021-01-22 11:12:18 +03:00
Andrey
2a0f60f1b2 edit readme 2021-01-22 10:53:15 +03:00
Andrey
a8eb50ee44 edit readme 2021-01-22 10:52:39 +03:00
Andrey
6b1635f60f edit readme 2021-01-22 10:51:32 +03:00
Andrey
62bdcfeb1b edit readme 2021-01-22 10:50:34 +03:00
Andrey
fa1c3330ba edit readme 2021-01-22 10:48:00 +03:00
Andrey
9755a9c654 edit readme 2021-01-22 10:27:54 +03:00
Andrey
bb4ce882f5 рефакторинг, конфиг yaml 2021-01-22 10:27:02 +03:00
andvikt
c3b9474d56 Merge pull request #3 from Foroxon/master
Added Ukrainian translation
2021-01-20 23:45:39 +03:00
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
Vladyslav Heneraliuk
686d86aec5 renamed UA translation file 2021-01-18 11:30:24 +02:00
Vladyslav Heneraliuk
8d1b828362 added Ukrainian translation 2021-01-18 11:15:39 +02: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
Andrey
012d12437b hotfix 2021-01-14 21:40:19 +03:00
Andrey
7063575957 hotfix 2021-01-14 21:35:15 +03:00
Andrey
6a43198d81 hotfix 2021-01-14 21:33:44 +03:00
Andrey
242386bfe8 edit readme 2021-01-14 21:25:48 +03:00
Andrey
34d31d2879 edit readme 2021-01-14 21:07:30 +03:00
Andrey
6a02a7e98c башфикс сервисов, ускорение загрузки 2021-01-14 20:46:05 +03:00
17 changed files with 1111 additions and 519 deletions

26
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,26 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Описание**
A clear and concise description of what the bug is.
**Версии систем**
Enviroment: raspberry/linux/windows/macos/docker
HA version:
mega_hacs version:
megad firmware version:
**Ожидаемое поведение**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**LOG**
Прочитайте в документации как включить подробный лог интеграции и приложите его здесь

View File

@@ -4,40 +4,54 @@ import logging
from functools import partial from functools import partial
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_ID
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST
)
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass from homeassistant.helpers.service import bind_hass
from homeassistant.helpers.template import Template
from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD 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
from .hub import MegaD from .hub import MegaD
from .config_flow import ConfigFlow
from .http import MegaView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_MQTT_ID = "mqtt_id"
CONF_PORT_TO_SCAN = 'port_to_scan'
MEGA = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_MQTT_ID, default=""): str,
vol.Optional(CONF_SCAN_INTERVAL, default=60): int,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
}
MEGA_MAPPED = {str: MEGA}
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Any(MEGA, MEGA_MAPPED) DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Required(str, description='id меги из веб-интерфейса'): {
vol.Optional(int, description='номер порта'): {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
vol.Optional(CONF_INVERT, default=False): bool,
vol.Optional(CONF_NAME): vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'),
vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'):
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(
CONF_RESPONSE_TEMPLATE,
description='шаблон ответа когда на этот порт приходит'
'сообщение из меги '): cv.template,
vol.Optional(CONF_ACTION): cv.script_action,
vol.Optional(CONF_GET_VALUE, default=True): bool,
}
}
}
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
PLATFORMS = [
"light",
"binary_sensor",
"sensor",
]
ALIVE_STATE = 'alive' ALIVE_STATE = 'alive'
DEF_ID = 'def' DEF_ID = 'def'
_POLL_TASKS = {} _POLL_TASKS = {}
@@ -46,9 +60,11 @@ _subs = {}
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the mega component.""" """YAML-конфигурация содержит только кастомизации портов"""
conf = config.get(DOMAIN) hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})}
hass.data[DOMAIN] = {} hass.data[DOMAIN][CONF_HTTP] = view = MegaView(cfg=config.get(DOMAIN, {}))
view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, []))
hass.http.register_view(view)
hass.services.async_register( hass.services.async_register(
DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({ DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
vol.Optional('mega_id'): str vol.Optional('mega_id'): str
@@ -67,25 +83,36 @@ async def async_setup(hass: HomeAssistant, config: dict):
vol.Optional('mega_id'): str, vol.Optional('mega_id'): str,
}) })
) )
if conf is None:
return True
if CONF_HOST in conf:
conf = {DEF_ID: conf}
for id, data in conf.items():
_LOGGER.warning('YAML configuration is deprecated, please use web-interface')
await _add_mega(hass, id, data)
for id, hub in hass.data[DOMAIN].items():
_POLL_TASKS[id] = asyncio.create_task(hub.poll())
return True 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) data.update(id=id)
_mqtt = hass.data.get(mqtt.DOMAIN) use_mqtt = data.get(CONF_MQTT_INPUTS, True)
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first') _mqtt = hass.data.get(mqtt.DOMAIN) if use_mqtt else None
hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER) if _mqtt is None and use_mqtt:
for x in range(5):
await asyncio.sleep(5)
_mqtt = hass.data.get(mqtt.DOMAIN)
if _mqtt is not None:
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.mqtt_id = await hub.get_mqtt_id()
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] = hass.data[DOMAIN]['__def'] = hub
hass.data[DOMAIN][entry.data.get(CONF_HOST)] = hub
if not await hub.authenticate(): if not await hub.authenticate():
raise Exception("not authentificated") raise Exception("not authentificated")
mid = await hub.get_mqtt_id() mid = await hub.get_mqtt_id()
@@ -94,58 +121,87 @@ async def _add_mega(hass: HomeAssistant, id, data: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id) hub: MegaD = await _add_mega(hass, entry)
data = dict(entry.data)
data.update(entry.options or {})
hub = await _add_mega(hass, id, data)
_hubs[entry.entry_id] = hub _hubs[entry.entry_id] = hub
_subs[entry.entry_id] = entry.add_update_listener(update) _subs[entry.entry_id] = entry.add_update_listener(updater)
await hub.start()
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup( hass.config_entries.async_forward_entry_setup(
entry, platform entry, platform
) )
) )
_POLL_TASKS[id] = asyncio.create_task(hub.poll()) await hub.updater.async_refresh()
return True 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: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL] hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options[CONF_PORT_TO_SCAN] hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
if entry.options[CONF_RELOAD]: entry.data = entry.options
await async_remove_entry(hass, entry) for platform in PLATFORMS:
await async_setup_entry(hass, entry) await hass.config_entries.async_forward_entry_unload(entry, platform)
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
return True return True
async def async_remove_entry(hass, entry) -> None: async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry.""" """Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
hass.data[DOMAIN][id].unsubscribe_all() hub: MegaD = hass.data[DOMAIN][id]
task: asyncio.Task = _POLL_TASKS.pop(id) if hub is None:
task.cancel() return
_LOGGER.debug(f'remove {id}')
_hubs.pop(entry.entry_id) _hubs.pop(entry.entry_id)
unsub = _subs.pop(entry.entry_id) task: asyncio.Task = _POLL_TASKS.pop(id, None)
unsub() if task is not None:
task.cancel()
if hub is None:
return
await hub.stop()
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s to version %s", config_entry.version, ConfigFlow.VERSION)
hub = await get_hub(hass, config_entry)
new = dict(config_entry.data)
await hub.start()
cfg = await hub.get_config()
await hub.stop()
new.update(cfg)
_LOGGER.debug(f'new config: %s', new)
config_entry.data = new
config_entry.version = ConfigFlow.VERSION
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True
async def _save_service(hass: HomeAssistant, call: ServiceCall): async def _save_service(hass: HomeAssistant, call: ServiceCall):
mega_id = call.data['mega_id'] mega_id = call.data.get('mega_id')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.save() await hub.save()
else: else:
for hub in hass.data[DOMAIN].values(): for hub in hass.data[DOMAIN].values():
await hub.save() if isinstance(hub, MegaD):
await hub.save()
@bind_hass @bind_hass
async def _get_port(hass: HomeAssistant, call: ServiceCall): async def _get_port(hass: HomeAssistant, call: ServiceCall):
port = call.data['port'] port = call.data.get('port')
mega_id = call.data['mega_id'] mega_id = call.data.get('mega_id')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
if port is None: if port is None:
@@ -154,6 +210,8 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
await hub.get_port(port) await hub.get_port(port)
else: else:
for hub in hass.data[DOMAIN].values(): for hub in hass.data[DOMAIN].values():
if not isinstance(hub, MegaD):
continue
if port is None: if port is None:
await hub.get_all_ports() await hub.get_all_ports()
else: else:
@@ -162,9 +220,9 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
@bind_hass @bind_hass
async def _run_cmd(hass: HomeAssistant, call: ServiceCall): async def _run_cmd(hass: HomeAssistant, call: ServiceCall):
port = call.data['port'] port = call.data.get('port')
mega_id = call.data['mega_id'] mega_id = call.data.get('mega_id')
cmd = call.data['cmd'] cmd = call.data.get('cmd')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.send_command(port=port, cmd=cmd) await hub.send_command(port=port, cmd=cmd)

View File

@@ -10,16 +10,17 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID CONF_ID,
CONF_ENTITY_ID,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP
from .entities import MegaPushEntity
from .hub import MegaD from .hub import MegaD
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
@@ -39,20 +40,7 @@ PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM) lg.warning('mega integration does not support yaml for binary_sensors, please use UI configuration')
ents = []
for mid, _config in config.items():
for x in _config:
if isinstance(x, int):
ent = MegaBinarySensor(
mega_id=mid, port=x
)
else:
ent = MegaBinarySensor(
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME]
)
ents.append(ent)
add_entities(ents)
return True return True
@@ -60,26 +48,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
async for port, pty, m in hub.scan_ports(): customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
if pty == "0": for port, cfg in config_entry.data.get('binary_sensor', {}).items():
sensor = MegaBinarySensor(mega_id=mid, port=port) port = int(port)
devices.append(sensor) c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False):
continue
hub.lg.debug(f'add binary_sensor on port %s', port)
sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry)
devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity): class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_on = None self._is_on = None
self._attrs = None
@property
def state_attributes(self):
return self._attrs
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
if self._is_on is not None: val = self.mega.values.get(self.port, {}).get("value") \
return self._is_on or self.mega.values.get(self.port, {}).get('m')
return self._state == 'ON' if val is None and self._state is not None:
return self._state == 'ON'
elif val is not None:
return val == 'ON' or val == 1
def _update(self, payload: dict): def _update(self, payload: dict):
val = payload.get("value") self.mega.values[self.port] = payload
self._is_on = val == 'ON'

View File

@@ -1,5 +1,5 @@
"""Пока не сделано""" """Пока не сделано"""
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@@ -8,8 +8,9 @@ from homeassistant import config_entries, core
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD # pylint:disable=unused-import from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \
CONF_NPORTS, CONF_UPDATE_ALL # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -20,12 +21,26 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_ID, default='def'): str, vol.Required(CONF_ID, default='def'): str,
vol.Required(CONF_HOST, default="192.168.0.14"): str, vol.Required(CONF_HOST, default="192.168.0.14"): str,
vol.Required(CONF_PASSWORD, default="sec"): str, vol.Required(CONF_PASSWORD, default="sec"): str,
vol.Optional(CONF_SCAN_INTERVAL, default=60): int, vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=True): bool,
vol.Optional(CONF_NPORTS, default=37): int,
# vol.Optional(CONF_UPDATE_ALL, default=True): bool,
}, },
) )
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, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate():
raise exceptions.InvalidAuth
return hub
async def validate_input(hass: core.HomeAssistant, data): async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
@@ -33,12 +48,7 @@ async def validate_input(hass: core.HomeAssistant, data):
""" """
if data[CONF_ID] in hass.data.get(DOMAIN, []): if data[CONF_ID] in hass.data.get(DOMAIN, []):
raise exceptions.DuplicateId('duplicate_id') raise exceptions.DuplicateId('duplicate_id')
_mqtt = hass.data.get(mqtt.DOMAIN) hub = await get_hub(hass, data)
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 return hub
@@ -46,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega.""" """Handle a config flow for mega."""
VERSION = 1 VERSION = 4
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
@@ -59,7 +69,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
try: try:
await validate_input(self.hass, user_input) hub = await validate_input(self.hass, user_input)
await hub.start()
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
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: except exceptions.CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except exceptions.InvalidAuth: except exceptions.InvalidAuth:
@@ -69,11 +88,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors[CONF_ID] = str(exc) 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( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -92,18 +106,36 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage the options.""" """Manage the options."""
if user_input is not None: 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:
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)
cfg.update(new)
return self.async_create_entry( return self.async_create_entry(
title='', title='',
data={**user_input, **{CONF_ID: self.config_entry.data[CONF_ID]}}, data=cfg,
) )
e = self.config_entry.data e = self.config_entry.data
ret = self.async_show_form( ret = self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Optional(CONF_SCAN_INTERVAL, default=e[CONF_SCAN_INTERVAL]): int, vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int, vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_RELOAD, default=False): bool, vol.Optional(CONF_RELOAD, default=False): bool,
# vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
# vol.Optional(CONF_INVERT, default=''): str, # vol.Optional(CONF_INVERT, default=''): str,
}), }),
) )

View File

@@ -1,4 +1,5 @@
"""Constants for the mega integration.""" """Constants for the mega integration."""
import re
DOMAIN = "mega" DOMAIN = "mega"
CONF_MEGA_ID = "mega_id" CONF_MEGA_ID = "mega_id"
@@ -11,4 +12,23 @@ W1 = 'w1'
W1BUS = 'w1bus' W1BUS = 'w1bus'
CONF_PORT_TO_SCAN = 'port_to_scan' CONF_PORT_TO_SCAN = 'port_to_scan'
CONF_RELOAD = 'reload' CONF_RELOAD = 'reload'
CONF_INVERT = 'invert' CONF_INVERT = 'invert'
CONF_PORTS = 'ports'
CONF_CUSTOM = '__custom'
CONF_HTTP = '__http'
CONF_SKIP = 'skip'
CONF_MQTT_INPUTS = 'mqtt_inputs'
CONF_NPORTS = 'nports'
CONF_RESPONSE_TEMPLATE = 'response_template'
CONF_ACTION = 'action'
CONF_UPDATE_ALL = 'update_all'
CONF_GET_VALUE = 'get_value'
CONF_ALLOW_HOSTS = 'allow_hosts'
PLATFORMS = [
"light",
"switch",
"binary_sensor",
"sensor",
]
EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor'
PATT_SPLIT = re.compile('[;/]')

View File

@@ -1,36 +1,70 @@
import asyncio
import json
import logging import logging
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import State from homeassistant.core import State
from .hub import MegaD from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from .const import DOMAIN from .hub import MegaD
from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT
class BaseMegaEntity(RestoreEntity): class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
""" """
Base Mega's entity. It is responsible for storing reference to mega hub Base Mega's entity. It is responsible for storing reference to mega hub
Also provides some basic entity information: unique_id, name, availiability Also provides some basic entity information: unique_id, name, availiability
It also makes subscription to port states All base entities are polled in order to be online or offline
""" """
def __init__( def __init__(
self, self,
mega_id: str, mega: MegaD,
port: int, port: int,
config_entry: ConfigEntry = None,
id_suffix=None, id_suffix=None,
name=None, name=None,
unique_id=None unique_id=None,
): ):
super().__init__(mega.updater)
self._state: State = None self._state: State = None
self.port = port self.port = port
self._mega_id = mega_id self.config_entry = config_entry
self.mega = mega
mega.entities.append(self)
self._mega_id = mega.id
self._lg = None self._lg = None
self._unique_id = unique_id or f"mega_{mega_id}_{port}" + \ self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._name = name or f"{mega_id}_{port}" + \ self._name = name or f"{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None
@property
def customize(self):
if self.hass is None:
return {}
if self._customize is None:
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {}
c = c.get(self.port) or {}
self._customize = c
return self._customize
@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 @property
def lg(self) -> logging.Logger: def lg(self) -> logging.Logger:
@@ -38,38 +72,124 @@ class BaseMegaEntity(RestoreEntity):
self._lg = self.mega.lg.getChild(self._name or self.unique_id) self._lg = self.mega.lg.getChild(self._name or self.unique_id)
return self._lg return self._lg
@property
def mega(self) -> MegaD:
return self.hass.data[DOMAIN][self._mega_id]
@property @property
def available(self) -> bool: def available(self) -> bool:
return self.mega.online return self.mega.online
@property @property
def name(self): def name(self):
return self._name or f"{self.mega.id}_p{self.port}" c = self.customize.get(CONF_NAME)
if not isinstance(c, str):
c = self._name or f"{self.mega.id}_p{self.port}"
return c
@property @property
def unique_id(self): def unique_id(self):
return self._unique_id return self._unique_id
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
await self.mega.subscribe(self.port, callback=self.__update) await super().async_added_to_hass()
self._state = await self.async_get_last_state() self._state = await self.async_get_last_state()
await asyncio.sleep(0.1)
await self.mega.get_port(self.port)
def __update(self, msg): async def get_state(self):
try: if self.mega.mqtt is None:
value = json.loads(msg.payload) self.async_write_ha_state()
except Exception as exc:
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return class MegaPushEntity(BaseMegaEntity):
"""
Updates on messages from mqtt
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mega.subscribe(self.port, callback=self.__update)
self.is_first_update = True
def __update(self, value: dict):
self._update(value) self._update(value)
self.hass.async_create_task(self.async_update_ha_state()) self.async_write_ha_state()
self.lg.debug(f'state after update %s', self.state) self.lg.debug(f'state after update %s', self.state)
self.is_first_update = False
return return
def _update(self, payload: dict): def _update(self, payload: dict):
raise NotImplementedError pass
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
if self.mega.mqtt is not None:
asyncio.create_task(self.mega.get_port(self.port))
class MegaOutPort(MegaPushEntity):
def __init__(
self,
dimmer=False,
*args, **kwargs
):
super().__init__(
*args, **kwargs
)
self._brightness = None
self._is_on = None
self.dimmer = dimmer
@property
def invert(self):
return self.customize.get(CONF_INVERT, False)
@property
def brightness(self):
val = self.mega.values.get(self.port, {}).get("value")
if val is None and self._state is not None:
return self._state.attributes.get("brightness")
elif val is not None:
try:
val = int(val)
return val
except Exception:
pass
@property
def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}).get("value")
if val is None and self._state is not None:
return self._state == 'ON'
elif val is not None:
if not self.invert:
return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0)
else:
return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0)
async def async_turn_on(self, brightness=None, **kwargs) -> None:
brightness = brightness or self.brightness or 255
if self.dimmer and brightness == 0:
cmd = 255
elif self.dimmer:
cmd = brightness
else:
cmd = 1 if not self.invert else 0
await self.mega.send_command(self.port, f"{self.port}:{cmd}")
self.mega.values[self.port] = {'value': cmd}
await self.get_state()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0" if not self.invert else "1"
await self.mega.send_command(self.port, f"{self.port}:{cmd}")
self.mega.values[self.port] = {'value': cmd}
await self.get_state()
def safe_int(v):
if v in ['ON', 'OFF']:
return None
try:
return int(v)
except ValueError:
return None

View File

@@ -0,0 +1,95 @@
import asyncio
import logging
import typing
from collections import defaultdict
from aiohttp.web_request import Request
from aiohttp.web_response import Response
from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, CONF_HTTP, DOMAIN, CONF_CUSTOM, CONF_RESPONSE_TEMPLATE
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback, HomeAssistant
from . import hub
_LOGGER = logging.getLogger(__name__).getChild('http')
class MegaView(HomeAssistantView):
"""Handle Yandex Smart Home unauthorized requests."""
url = '/mega'
name = 'mega'
requires_auth = False
def __init__(self, cfg: dict):
self._try = 0
self.allowed_hosts = {'::1'}
self.callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] \
= defaultdict(list)
self.templates: typing.Dict[str, typing.Dict[str, Template]] = {
mid: {
pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE]
for pt in cfg[mid]
if CONF_RESPONSE_TEMPLATE in cfg[mid][pt]
} for mid in cfg
}
_LOGGER.debug('templates: %s', self.templates)
async def get(self, request: Request) -> Response:
auth = False
for x in self.allowed_hosts:
if request.remote.startswith(x):
auth = True
break
if not auth:
_LOGGER.warning(f'unauthorised attempt to connect from {request.remote}')
return Response(status=401)
hass: HomeAssistant = request.app['hass']
hub: 'hub.MegaD' = hass.data.get(DOMAIN).get(request.remote) # TODO: проверить какой remote
if hub is None and request.remote == '::1':
hub = hass.data.get(DOMAIN).get('__def')
if hub is None:
return Response(status=400)
data = dict(request.query)
hass.bus.async_fire(
EVENT_BINARY_SENSOR,
data,
)
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data)
port = data.get('pt')
data = data.copy()
ret = 'd'
if port is not None:
for cb in self.callbacks[port]:
cb(data)
template: Template = self.templates.get(hub.id, {}).get(port)
if hub.update_all:
asyncio.create_task(self.later_update(hub))
if template is not None:
template.hass = hass
ret = template.async_render(data)
_LOGGER.debug('response %s', ret)
ret = Response(body=ret or 'd', content_type='text/plain', headers={})
ret.headers.clear()
return ret
async def later_update(self, hub):
_LOGGER.debug('force update')
await asyncio.sleep(1)
await hub.updater.async_refresh()
def make_ints(d: dict):
for x in d:
try:
d[x] = float(d[x])
except ValueError:
pass
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0

View File

@@ -1,17 +1,40 @@
import asyncio import asyncio
import json
import logging import logging
from datetime import datetime from collections import defaultdict
from functools import wraps from datetime import datetime, timedelta
import aiohttp import aiohttp
import typing import typing
from bs4 import BeautifulSoup import re
import json
from bs4 import BeautifulSoup
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .exceptions import CannotConnect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import TEMP, HUM, PATT_SPLIT, DOMAIN, CONF_HTTP
from .exceptions import CannotConnect, MqttNotConfigured
from .http import MegaView
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 NoPort(Exception):
pass
class MegaD: class MegaD:
@@ -20,97 +43,133 @@ class MegaD:
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
loop: asyncio.AbstractEventLoop,
host: str, host: str,
password: str, password: str,
mqtt: mqtt.MQTT, mqtt: mqtt.MQTT,
lg: logging.Logger, lg: logging.Logger,
id: str, id: str,
mqtt_inputs: bool = True,
mqtt_id: str = None, mqtt_id: str = None,
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
nports=38,
inverted: typing.List[int] = None, inverted: typing.List[int] = None,
update_all=True,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data[DOMAIN][CONF_HTTP]
self.http.allowed_hosts |= {host}
else:
self.http = None
self.update_all = update_all if update_all is not None else True
self.nports = nports
self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None
self.hass = hass self.hass = hass
self.host = host self.host = host
self.sec = password self.sec = password
self.mqtt = mqtt self.mqtt = mqtt
self.id = id self.id = id
self.lck = asyncio.Lock() self.lck = asyncio.Lock()
self.is_alive = asyncio.Condition() self._http_lck = asyncio.Lock()
self._notif_lck = asyncio.Lock()
self.cnd = asyncio.Condition()
self.online = True self.online = True
self.entities: typing.List[Entity] = [] self.entities: typing.List[Entity] = []
self.poll_interval = scan_interval self.poll_interval = scan_interval
self.subscriptions = [] self.subs = None
self.lg: logging.Logger = lg.getChild(self.id) self.lg: logging.Logger = lg.getChild(self.id)
self._scanned = {} self._scanned = {}
self.sensors = [] self.sensors = []
self.port_to_scan = port_to_scan self.port_to_scan = port_to_scan
self.inverted = inverted or []
self.last_update = datetime.now() self.last_update = datetime.now()
self._callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] = defaultdict(list)
self._loop = loop
self.values = {}
self.last_port = None
self.updater = DataUpdateCoordinator(
hass,
self.lg,
name="sensors",
update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
)
self.notifiers = defaultdict(asyncio.Condition)
if not mqtt_id: if not mqtt_id:
_id = host.split(".")[-1] _id = host.split(".")[-1]
self.mqtt_id = f"megad/{_id}" self.mqtt_id = f"megad/{_id}"
else: else:
self.mqtt_id = mqtt_id self.mqtt_id = mqtt_id
self._loop: asyncio.AbstractEventLoop = None
async def start(self):
self.loop = asyncio.get_event_loop()
if self.mqtt is not None:
self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg,
qos=0,
)
async def stop(self):
if self.subs is not None:
self.subs()
for x in self._callbacks.values():
x.clear()
async def add_entity(self, ent): async def add_entity(self, ent):
async with self.lck: async with self.lck:
self.entities.append(ent) self.entities.append(ent)
async def get_sensors(self): async def get_sensors(self, only_list=False):
self.lg.debug(self.sensors) self.lg.debug(self.sensors)
_ports = {x.port for x in self.sensors} ports = []
for x in _ports: for x in self.sensors:
await self.get_port(x) if only_list and x.http_cmd != 'list':
await asyncio.sleep(0.1) continue
if x.port in ports:
continue
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
ports.append(x.port)
@property
def is_online(self):
return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
def _warn_offline(self):
if self.online:
self.lg.warning('mega is offline')
self.hass.states.async_set(
f'mega.{self.id}',
'offline',
)
self.online = False
def _notify_online(self):
if not self.online:
self.hass.states.async_set(
f'mega.{self.id}',
'online',
)
self.online = True
async def poll(self): async def poll(self):
""" """
Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes
offline offline
""" """
self._loop = asyncio.get_event_loop() self.lg.debug('poll')
if self.mqtt is None:
while True: await self.get_all_ports()
if len(self.sensors) > 0: await self.get_sensors(only_list=True)
await self.get_sensors() return
else: if len(self.sensors) > 0:
await self.get_port(self.port_to_scan) await self.get_sensors()
else:
await asyncio.sleep(1) await self.get_port(self.port_to_scan)
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval: return self.values
await self.get_port(self.port_to_scan)
await asyncio.sleep(1)
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval:
self.lg.warning('mega is offline')
self.hass.states.async_set(
f'mega.{self.id}',
'offline',
)
self.online = False
else:
self.hass.states.async_set(
f'mega.{self.id}',
'online',
)
self.online = True
for x in self.entities:
try:
await x.async_update_ha_state()
except RuntimeError:
pass
await asyncio.sleep(self.poll_interval - 1)
async def _async_notify(self):
async with self.is_alive:
self.is_alive.notify_all()
def _notify(self, *args):
asyncio.run_coroutine_threadsafe(self._async_notify(), self._loop)
async def get_mqtt_id(self): async def get_mqtt_id(self):
async with aiohttp.request( async with aiohttp.request(
@@ -124,91 +183,130 @@ class MegaD:
return _id or 'megad/' + self.host.split('.')[-1] return _id or 'megad/' + self.host.split('.')[-1]
async def send_command(self, port=None, cmd=None): async def send_command(self, port=None, cmd=None):
if port: return await self.request(pt=port, cmd=cmd)
url = f"http://{self.host}/{self.sec}/?pt={port}&cmd={cmd}"
else: async def request(self, **kwargs):
url = f"http://{self.host}/{self.sec}/?cmd={cmd}" cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
self.lg.debug('run command: %s', url) url = f"http://{self.host}/{self.sec}/?{cmd}"
async with self.lck: self.lg.debug('request: %s', url)
async with self._http_lck:
async with aiohttp.request("get", url=url) as req: async with aiohttp.request("get", url=url) as req:
if req.status != 200: if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text()) self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return False return None
else: else:
return True ret = await req.text()
self.lg.debug('response %s', ret)
return ret
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
async def get_port(self, port, get_value=False): def parse_response(self, ret):
if get_value: if ret is None:
ftr = asyncio.get_event_loop().create_future() raise NoPort()
if ':' in ret:
ret = PATT_SPLIT.split(ret)
ret = dict([
x.split(':') for x in ret if x.count(':') == 1
])
elif 'ON' in ret:
ret = {'value': 'ON'}
elif 'OFF' in ret:
ret = {'value': 'OFF'}
else:
ret = {'value': ret}
return ret
def cb(msg): async def get_port(self, port, force_http=False, http_cmd='get'):
"""
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values
"""
self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http:
ret = await self.request(pt=port, cmd=http_cmd)
ret = self.parse_response(ret)
self.lg.debug('parsed: %s', ret)
if http_cmd == 'list' and isinstance(ret, dict) and 'value' in ret:
await asyncio.sleep(1)
ret = await self.request(pt=port, http_cmd=http_cmd)
ret = self.parse_response(ret)
self.values[port] = ret
return ret
async with self._notif_lck:
async with self.notifiers[port]:
cnd = self.notifiers[port]
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
qos=2,
retain=False,
)
try: try:
ftr.set_result(json.loads(msg.payload).get('value')) await asyncio.wait_for(cnd.wait(), timeout=10)
except Exception as exc: return self.values.get(port)
self.lg.warning(f'could not parse {msg.payload}: {exc}') except asyncio.TimeoutError:
ftr.set_result(None) self.lg.error(f'timeout when getting port {port}')
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
self.lg.debug(
f'get port: %s', port
)
async with self.lck:
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
qos=0,
retain=False,
)
await asyncio.sleep(0.1)
if get_value:
try:
return await asyncio.wait_for(ftr, timeout=2)
except asyncio.TimeoutError:
self.lg.warning(f'timeout on port {port}')
finally:
unsub()
async def get_all_ports(self): async def get_all_ports(self):
for x in range(37): if not self.mqtt_inputs:
asyncio.create_task(self.get_port(x)) ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')):
ret = self.parse_response(x)
self.values[port] = ret
else:
for x in range(self.nports + 1):
await self.get_port(x)
async def reboot(self, save=True): async def reboot(self, save=True):
await self.save() await self.save()
# await self.send_command(cmd=)
async def subscribe(self, port, callback): async def _notify(self, port, value):
async with self.notifiers[port]:
cnd = self.notifiers[port]
cnd.notify_all()
@wraps(callback) def _process_msg(self, msg):
def wrapper(msg): try:
self.lg.debug( d = msg.topic.split('/')
'process incomming message: %s', msg port = d[-1]
) except ValueError:
self.last_update = datetime.now() self.lg.warning('can not process %s', msg)
return callback(msg) return
if port == 'cmd':
return
try:
port = int(port)
except:
self.lg.warning('can not process %s', msg)
return
self.lg.debug( self.lg.debug(
f'subscribe %s %s', port, wrapper 'process incomming message: %s', msg
) )
subs = await self.mqtt.async_subscribe( value = None
topic=f"{self.mqtt_id}/{port}", try:
msg_callback=wrapper, value = json.loads(msg.payload)
qos=0, self.values[port] = value
) for cb in self._callbacks[port]:
self.subscriptions.append(subs) cb(value)
except Exception as exc:
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return
finally:
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
def unsubscribe_all(self): def subscribe(self, port, callback):
self.lg.info('unsubscribe') port = int(port)
for x in self.subscriptions: self.lg.debug(
self.lg.debug('unsubscribe %s', x) f'subscribe %s %s', port, callback
x() )
if self.mqtt_inputs:
self._callbacks[port].append(callback)
else:
self.http.callbacks[port].append(callback)
async def authenticate(self) -> bool: async def authenticate(self) -> bool:
"""Test if we can authenticate with the host.""" """Test if we can authenticate with the host."""
@@ -227,39 +325,83 @@ class MegaD:
return await req.text() return await req.text()
async def scan_port(self, port): async def scan_port(self, port):
if port in self._scanned: async with self.lck:
return self._scanned[port] if port in self._scanned:
url = f'http://{self.host}/{self.sec}/?pt={port}' return self._scanned[port]
self.lg.debug( url = f'http://{self.host}/{self.sec}/?pt={port}'
f'scan port %s: %s', port, url self.lg.debug(
) f'scan port %s: %s', port, url
async with aiohttp.request('get', url) as req: )
html = await req.text() async with aiohttp.request('get', url) as req:
tree = BeautifulSoup(html, features="lxml") html = await req.text()
pty = tree.find('select', attrs={'name': 'pty'}) if req.status != 200:
if pty is None: return
return tree = BeautifulSoup(html, features="lxml")
else: pty = tree.find('select', attrs={'name': 'pty'})
pty = pty.find(selected=True) if pty is None:
if pty:
pty = pty['value']
else:
return return
if pty in ['0', '1']: else:
m = tree.find('select', attrs={'name': 'm'}) pty = pty.find(selected=True)
if m: if pty:
m = m.find(selected=True)['value'] pty = pty['value']
self._scanned[port] = (pty, m) else:
return pty, m return
elif pty == '3': if pty in ['0', '1']:
m = tree.find('select', attrs={'name': 'd'}) m = tree.find('select', attrs={'name': 'm'})
if m: if m:
m = m.find(selected=True)['value'] m = m.find(selected=True)['value']
self._scanned[port] = (pty, m) self._scanned[port] = (pty, m)
return 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 def scan_ports(self, nports=37):
for x in range(38): for x in range(1, nports+1):
ret = await self.scan_port(x) ret = await self.scan_port(x)
if ret: if ret:
yield [x, *ret] yield [x, *ret]
self.nports = nports+1
async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports(nports):
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':
try:
http_cmd = 'get'
values = await self.get_port(port, force_http=True)
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')):
values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list'
except asyncio.TimeoutError:
self.lg.warning(f'timout on port {port}')
continue
self.lg.debug(f'values: %s', values)
if values is None:
self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
continue
if isinstance(values, dict) and 'value' in values:
values = values['value']
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,
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key,
http_cmd=http_cmd,
))
return ret

View File

@@ -1,6 +1,5 @@
"""Platform for light integration.""" """Platform for light integration."""
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
@@ -11,17 +10,22 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID CONF_ID,
CONF_DOMAIN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity from .entities import MegaOutPort
from .hub import MegaD from .hub import MegaD
from .const import CONF_DIMMER, CONF_SWITCH from .const import (
CONF_DIMMER,
CONF_SWITCH,
DOMAIN,
CONF_CUSTOM,
CONF_SKIP,
)
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
@@ -47,29 +51,7 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM) lg.warning('mega integration does not support yaml for lights, please use UI configuration')
ents = []
for mid, _config in config.items():
for x in _config["dimmer"]:
if isinstance(x, int):
ent = MegaLight(
mega_id=mid, port=x, dimmer=True)
else:
ent = MegaLight(
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME], dimmer=True
)
ents.append(ent)
for x in _config["switch"]:
if isinstance(x, int):
ent = MegaLight(
mega_id=mid, port=x, dimmer=False
)
else:
ent = MegaLight(
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME], dimmer=False
)
ents.append(ent)
add_entities(ents)
return True return True
@@ -77,75 +59,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
async for port, pty, m in hub.scan_ports(): customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
if pty == "1" and m in ['0', '1']: for port, cfg in config_entry.data.get('light', {}).items():
light = MegaLight(mega_id=mid, port=port, dimmer=m == '1') port = int(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light':
continue
for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data)
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)
class MegaLight(LightEntity, BaseMegaEntity): class MegaLight(MegaOutPort, LightEntity):
def __init__(
self,
dimmer=False,
*args, **kwargs
):
super().__init__(
*args, **kwargs
)
self._brightness = None
self._is_on = None
self.dimmer = dimmer
@property
def brightness(self):
if self._brightness is not None:
return self._brightness
if self._state:
return self._state.attributes.get("brightness")
@property @property
def supported_features(self): def supported_features(self):
return SUPPORT_BRIGHTNESS if self.dimmer else 0 return SUPPORT_BRIGHTNESS if self.dimmer else 0
@property
def is_on(self) -> bool:
if self._is_on is not None:
return self._is_on
return self._state == 'ON'
async def async_turn_on(self, brightness=None, **kwargs) -> None:
brightness = brightness or self.brightness or 255
if self.dimmer and brightness == 0:
cmd = 255
elif self.dimmer:
cmd = brightness
else:
cmd = 1
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = True
self._brightness = brightness
await self.async_update_ha_state()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0"
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = False
await self.async_update_ha_state()
def _update(self, payload: dict):
val = payload.get("value")
try:
val = int(val)
except Exception:
pass
if isinstance(val, int):
self._is_on = val
if val > 0:
self._brightness = val
else:
self._is_on = val == 'ON'

View File

@@ -10,9 +10,7 @@
"ssdp": [], "ssdp": [],
"zeroconf": [], "zeroconf": [],
"homekit": {}, "homekit": {},
"dependencies": [ "after_dependencies": ["mqtt"],
"mqtt"
],
"codeowners": [ "codeowners": [
"@andvikt" "@andvikt"
], ],

View File

@@ -10,14 +10,13 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID, CONF_ID,
CONF_TYPE, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS from .const import CONF_KEY, TEMP, HUM, W1, W1BUS
from .hub import MegaD from .hub import MegaD
import re import re
@@ -58,17 +57,11 @@ PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM) lg.warning('mega integration does not support yaml for sensors, please use UI configuration')
ents = []
for mid, _config in config.items():
for x in _config:
ent = _make_entity(mid, **x)
ents.append(ent)
add_entities(ents)
return True 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] key = conf[CONF_KEY]
return Mega1WSensor( return Mega1WSensor(
key=key, key=key,
@@ -77,7 +70,8 @@ def _make_entity(mid: str, port: int, conf: dict):
patt=PATTERNS.get(key), patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]), device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key id_suffix=key,
config_entry=config_entry
) )
@@ -85,39 +79,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
async for port, pty, m in hub.scan_ports(): for port, cfg in config_entry.data.get('sensor', {}).items():
if pty == "3": port = int(port)
values = await hub.get_port(port, get_value=True) for data in cfg:
lg.debug(f'values: %s', values) hub.lg.debug(f'add sensor on port %s with data %s', port, data)
if values is None: sensor = Mega1WSensor(
continue mega=hub,
if isinstance(values, str) and TEMP_PATT.search(values): port=port,
values = {TEMP: values} config_entry=config_entry,
elif not isinstance(values, dict): **data,
values = {None: values} )
for key in values: devices.append(sensor)
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)
async_add_devices(devices) async_add_devices(devices)
class Mega1WSensor(BaseMegaEntity): class Mega1WSensor(MegaPushEntity):
def __init__( def __init__(
self, self,
unit_of_measurement, unit_of_measurement,
device_class, device_class,
patt=None,
key=None, key=None,
http_cmd='get',
*args, *args,
**kwargs **kwargs
): ):
@@ -130,13 +114,22 @@ class Mega1WSensor(BaseMegaEntity):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._value = None self._value = None
self.key = key self.key = key
self.patt = patt
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self.mega.sensors.append(self)
self.http_cmd = http_cmd
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
return self._unit_of_measurement _u = self.customize.get(CONF_UNIT_OF_MEASUREMENT, None)
if _u is None:
return self._unit_of_measurement
elif isinstance(_u, str):
return _u
elif isinstance(_u, dict) and self.key in _u:
return _u[self.key]
else:
return self._unit_of_measurement
@property @property
def unique_id(self): def unique_id(self):
@@ -150,26 +143,31 @@ class Mega1WSensor(BaseMegaEntity):
return self._device_class return self._device_class
@property @property
def should_poll(self): def state(self):
return False ret = None
if self.key:
try:
ret = self.mega.values.get(self.port, {})
if isinstance(ret, dict):
ret = ret.get(self.key)
except:
self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
return
else:
ret = self.mega.values.get(self.port, {}).get('value')
if ret is None and self._state is not None:
ret = self._state.state
try:
ret = float(ret)
ret = str(ret)
except:
ret = None
return ret
@property @property
def state(self): def name(self):
if self._value is None and self._state is not None: n = super().name
return self._state.state c = self.customize.get(CONF_NAME, {})
return self._value if isinstance(c, dict):
c = c.get(self.key)
def _update(self, payload: dict): return c or n
val = payload.get('value', '')
if isinstance(val, str) and self.patt is not None:
val = self.patt.findall(val)
if val:
self._value = val[0]
else:
self.lg.warning(f'could not parse: {payload}')
elif isinstance(val, dict) and self.key is not None:
self._value = val.get(self.key)
elif isinstance(val, (float, int)):
self._value = val
else:
self.lg.warning(f'could not parse: {payload}')

View File

@@ -11,7 +11,10 @@
"mqtt_id": "[%key:common::config_flow::data::mqtt_id%]", "mqtt_id": "[%key:common::config_flow::data::mqtt_id%]",
"scan_interval": "[%key:common::config_flow::data::mqtt_id%]", "scan_interval": "[%key:common::config_flow::data::mqtt_id%]",
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]", "port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
"invert": "[%key:common::config_flow::data::invert%]" "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]"
} }
} }
}, },
@@ -32,7 +35,10 @@
"scan_interval": "[%key:common::config_flow::data::scan_interval%]", "scan_interval": "[%key:common::config_flow::data::scan_interval%]",
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]", "port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
"reload": "[%key:common::config_flow::data::reload%]", "reload": "[%key:common::config_flow::data::reload%]",
"invert": "[%key:common::config_flow::data::invert%]" "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]"
} }
} }
} }

View File

@@ -7,15 +7,19 @@ from homeassistant.components.switch import (
PLATFORM_SCHEMA as LIGHT_SCHEMA, PLATFORM_SCHEMA as LIGHT_SCHEMA,
SwitchEntity, SwitchEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_ID,
CONF_DOMAIN,
) )
from .entities import BaseMegaEntity from homeassistant.core import HomeAssistant
from .const import CONF_DIMMER, CONF_SWITCH from .entities import MegaD
from .entities import MegaOutPort
from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP
_LOGGER = logging.getLogger(__name__) _LOGGER = lg = logging.getLogger(__name__)
# Validation of the user's configuration # Validation of the user's configuration
@@ -33,50 +37,29 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM)
ents = []
for mid, _config in config.items():
mega = hass.data["mega"][mid]
for x in _config:
if isinstance(x, int):
ent = MegaSwitch(hass, mega=mega, port=x)
else:
ent = MegaSwitch(
hass, mega=mega, port=x[CONF_PORT], name=x[CONF_NAME]
)
ents.append(ent)
add_entities(ents) async def async_setup_platform(hass, config, add_entities, discovery_info=None):
lg.warning('mega integration does not support yaml for switches, please use UI configuration')
return True return True
class MegaSwitch(SwitchEntity, BaseMegaEntity): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
def __init__(self, *args, **kwargs): customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
super().__init__(*args, **kwargs) for port, cfg in config_entry.data.get('light', {}).items():
self._is_on = None port = int(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'switch':
continue
for data in cfg:
hub.lg.debug(f'add switch on port %s with data %s', port, data)
light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data)
devices.append(light)
async_add_devices(devices)
@property
def is_on(self) -> bool:
if self._is_on is not None:
return self._is_on
return self._state == 'ON'
async def async_turn_on(self, **kwargs) -> None: class MegaSwitch(MegaOutPort, SwitchEntity):
cmd = 1 pass
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = True
await self.async_update_ha_state()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0"
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = False
await self.async_update_ha_state()
def _update(self, payload: dict):
val = payload.get("value")
self._is_on = val == 'ON'

View File

@@ -7,7 +7,8 @@
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error", "unknown": "Unexpected error",
"duplicate_id": "Duplicate ID" "duplicate_id": "Duplicate ID",
"mqtt_inputs": "Use MQTT"
}, },
"step": { "step": {
"user": { "user": {
@@ -17,8 +18,24 @@
"username": "Username", "username": "Username",
"id": "ID", "id": "ID",
"mqtt_id": "MQTT id", "mqtt_id": "MQTT id",
"scan_interval": "Scan interval (sec) (used for aliveness and sensors)", "scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)" "port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"nports": "Number of ports",
"update_all": "Update all outs when input",
"mqtt_inputs": "Use MQTT"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"reload": "Reload objects",
"mqtt_inputs": "Use MQTT",
"update_all": "Update all outs when input"
} }
} }
} }

View File

@@ -17,8 +17,11 @@
"username": "Пользователь", "username": "Пользователь",
"id": "ID", "id": "ID",
"mqtt_id": "MQTT id", "mqtt_id": "MQTT id",
"scan_interval": "Периодичность обновлений (сек.)", "scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять",
"port_to_scan": "Порт, который сканируется когда нет датчиков" "port_to_scan": "Порт, который сканируется когда нет датчиков",
"mqtt_inputs": "Использовать MQTT",
"nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход"
} }
} }
} }
@@ -27,10 +30,13 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"scan_interval": "Периодичность обновлений (сек.)", "scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять",
"port_to_scan": "Порт, который сканируется когда нет датчиков", "port_to_scan": "Порт, который сканируется когда нет датчиков",
"reload": "Обновить объекты", "reload": "Обновить объекты",
"invert": "Список портов (через ,) с инвертированной логикой" "invert": "Список портов (через ,) с инвертированной логикой",
"mqtt_inputs": "Использовать MQTT",
"nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход"
} }
} }
} }

View File

@@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Вже налаштовано"
},
"error": {
"cannot_connect": "Неможливо підключитись",
"invalid_auth": "Неправильний пароль",
"unknown": "Невідома помилка",
"duplicate_id": "Дублікат ID"
},
"step": {
"user": {
"data": {
"host": "Хост",
"password": "Пароль",
"username": "Користувач",
"id": "ID",
"mqtt_id": "MQTT id",
"scan_interval": "Період оновлення (сек.)",
"port_to_scan": "Порт для сканування при відсутності датчиків"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Період оновлення (сек.)",
"port_to_scan": "Порт для сканування при відсутності датчиків",
"reload": "Оновити об'єкти",
"invert": "Список портів з інвертованою логікою (через ,)"
}
}
}
},
"title": "mega"
}

175
readme.md
View File

@@ -1,50 +1,173 @@
# MegaD HomeAssistant custom component # MegaD HomeAssistant integration
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561) Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
## Основные особенности: ## Основные особенности:
- Настройка в веб-интерфейсе + yaml
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для температурных датчиков `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`) `sensor`)
- Возможность работы с несколькими megad - Возможность работы с несколькими megad
- Обратная связь по mqtt - Обратная связь по mqtt или http (на выбор)
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad - События на двойные/долгие нажатия
- Поддержка температурных датчиков в режиме шины - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
## Устройства выполнении предыдущей.
Поддерживаются устройства: light, switch, binary_sensor, sensor. light может работать как диммер
## Установка ## Установка
Рекомендованнй способ - через [HACS](https://hacs.xyz/docs/installation/installation). Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
После установки HACS, нужно перейти в меню hacs -> integrations, далее в верхнем правом углу
нажать три точки, где будет `Custom repositories`, открыть, нажать add и добавить `https://github.com/andvikt/mega_hacs.git` HACS - Integrations - Explore, в поиске ищем MegaD.
Альтернативный способ установки: Альтернативный способ установки:
```shell ```shell
# из папки с конфигом # из папки с конфигом
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash - wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
``` ```
Перезагрузить HA Не забываем перезагрузить HA
Для обновления повторить ## Настройка
## Зависимости
Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
## Настройка из веб-интерфейса
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega `Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
## Сервисы Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml ```yaml
save: # configuration.yaml
mega:
hello: # ID меги, как в UI
7: # номер порта
domain: switch # тип устройства (switch или light, по умолчанию для цифровых выходов используется light)
invert: true # инвертировать или нет (по умолчанию false)
name: Насос # имя устройства
8:
# исключить из сканирования
skip: true
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
14:
name: какой-то датчик
unit_of_measurement: "°C" # если датчик один, то просто строчкой
```
## Зависимости
Для совместимости c mqtt необходимо настроить интеграцию [mqtt](https://www.home-assistant.io/integrations/mqtt/)
в HomeAssistant, а так же обновить ваш контроллер до последней версии, тк были важные обновления в части mqtt
## HTTP in
Начиная с версии `0.3.1` интеграция стала поддерживать обратную связь без mqtt, используя http-сервер. Для этого в настройках
интеграции необходимо снять галку с `использовать mqtt`
В самой меге необходимо прописать настройки:
```yaml
srv: "192.168.1.4:8123" # ip:port вашего HA
script: "mega" # это api интеграции, к которому будет обращаться контроллер
```
Входы будут доступны как binary_sensor, а так же в виде событий `mega.sensor`.
События можно обрабатывать так:
```yaml
- alias: some double click
trigger:
- platform: event
event_type: mega.sensor
event_data:
pt: 1
action:
- service: light.toggle
entity_id: light.some_light
```
Для binary_sensor имеет смысл использовать режим P&R, для остальных режимов - лучше пользоваться событиями.
## Ответ на входящие события от контроллера
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает
запустить то что прописано в поле act в настройках порта).
Поддерживаеются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, value)
Примеры:
```yaml
mega:
mega1: # id меги, который вы сами придумываете в конфиге в UI
4: # номер порта, с которого ожидаются события
response_template: 5:2 # простейший пример без шаблона. Каждый раз когда будет приходить сообщение на этот порт,
# будем менять состояние на противоположное
5:
# пример с использованием шаблона, порт 1 будет выключен если он сейчас включен и включен с последней сохраненной
# яркостью если он сейчас выключен
response_template: >-
{% if is_state('light.some_port_1', 'on') %}
1:0
{% else %}
1:{{state_attr('light.some_port_1', 'brightness')}}
{% endif %}
6:
# в шаблон так же передаются все параметры, которые передает контроллер (pt, cnt, m, click)
# эти параметры можно использовать в условиях или непосредственно в шаблоне в виде {{pt}}
response_template: >-
{% if m==2 %}1:0{% else %}d{% endif %}
```
## Отладка ответов
Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли
HA:
```shell
curl -v -X GET 'http://localhost:8123/mega?pt=5&m=1'
```
Если доступа нет, нужно в файл конфигурации добавить ip компьюетра, с которого вы хотите делать запросы, например:
```yaml
mega:
allow_hosts:
- 192.168.1.1
```
И тогда можно с локальной машины делать запросы на ваш сервер HA:
```shell
curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1'
```
В ответ будет приходить либо `d`, либо скрипт, который вы настроили
## События
`binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать
только с режимом входа P&R
При каждом срабатывании `binary_sensor` так же сообщает о событии типа `mega.sensor`.
События можно использовать в автоматизациях, например так:
```yaml
- alias: some double click
trigger:
- platform: event
event_type: mega.sensor
event_data:
pt: 1
cnt: 2
action:
- service: light.toggle
entity_id: light.some_light
```
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `mega.sensor`, понажимать кнопки.
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml
mega.save:
description: Сохраняет текущее состояние портов (?cmd=s) description: Сохраняет текущее состояние портов (?cmd=s)
fields: fields:
mega_id: mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega" example: "mega"
get_port: mega.get_port:
description: Запросить текущий статус порта (или всех) description: Запросить текущий статус порта (или всех)
fields: fields:
mega_id: mega_id:
@@ -54,10 +177,9 @@ get_port:
description: Номер порта (если не заполнять, будут запрошены все порты сразу) description: Номер порта (если не заполнять, будут запрошены все порты сразу)
example: 1 example: 1
run_cmd: mega.run_cmd:
description: Выполнить любую произвольную команду description: Выполнить любую произвольную команду
fields: fields:
mega_id: mega_id:
description: ID меги description: ID меги
example: "mega" example: "mega"
@@ -67,11 +189,12 @@ run_cmd:
cmd: cmd:
description: Любая поддерживаемая мегой команда description: Любая поддерживаемая мегой команда
example: "1:0" example: "1:0"
``` ```
## Отладка ## Отладка
Если возникают проблемы, можно включить детальный лог, для этого в конфиг добавить: Интеграция находится в активной разработке, при возникновении проблем [заводите issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
Просьба прикладывать детальный лог, который можно включить в конфиге так:
```yaml ```yaml
logger: logger:
default: info default: info