Merge pull request #1 from andvikt/master

Sync
This commit is contained in:
Vladyslav Heneraliuk
2021-01-26 12:10:46 +02:00
committed by GitHub
17 changed files with 900 additions and 449 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,36 +4,54 @@ import logging
from functools import partial
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.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.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS
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 .config_flow import ConfigFlow
from .http import MegaView
_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(
{
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,
)
ALIVE_STATE = 'alive'
DEF_ID = 'def'
_POLL_TASKS = {}
@@ -42,9 +60,11 @@ _subs = {}
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the mega component."""
conf = config.get(DOMAIN)
hass.data[DOMAIN] = {}
"""YAML-конфигурация содержит только кастомизации портов"""
hass.data[DOMAIN] = {CONF_CUSTOM: config.get(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(
DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
vol.Optional('mega_id'): str
@@ -63,16 +83,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
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
@@ -81,17 +92,27 @@ async def get_hub(hass, entry):
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')
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER)
use_mqtt = data.get(CONF_MQTT_INPUTS, True)
_mqtt = hass.data.get(mqtt.DOMAIN) if use_mqtt else None
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] = hub
hass.data[DOMAIN][id] = hass.data[DOMAIN]['__def'] = hub
hass.data[DOMAIN][entry.data.get(CONF_HOST)] = hub
if not await hub.authenticate():
raise Exception("not authentificated")
mid = await hub.get_mqtt_id()
@@ -100,17 +121,17 @@ async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hub = await _add_mega(hass, entry)
hub: MegaD = await _add_mega(hass, entry)
_hubs[entry.entry_id] = hub
_subs[entry.entry_id] = entry.add_update_listener(updater)
await hub.start()
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
entry, platform
)
)
_POLL_TASKS[id] = asyncio.create_task(hub.poll())
await hub.updater.async_refresh()
return True
@@ -125,6 +146,8 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
return True
@@ -133,34 +156,31 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id)
hub = hass.data[DOMAIN]
hub: MegaD = hass.data[DOMAIN][id]
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 task is not None:
task.cancel()
if hub is None:
return
hub.unsubscribe_all()
unsub = _subs.pop(entry.entry_id)
if unsub:
unsub()
await hub.stop()
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s to version 2", config_entry.version)
_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)
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
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)
@@ -174,7 +194,8 @@ async def _save_service(hass: HomeAssistant, call: ServiceCall):
await hub.save()
else:
for hub in hass.data[DOMAIN].values():
await hub.save()
if isinstance(hub, MegaD):
await hub.save()
@bind_hass
@@ -189,6 +210,8 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
await hub.get_port(port)
else:
for hub in hass.data[DOMAIN].values():
if not isinstance(hub, MegaD):
continue
if port is None:
await hub.get_all_ports()
else:

View File

@@ -1,6 +1,5 @@
"""Platform for light integration."""
import logging
import asyncio
import voluptuous as vol
@@ -11,16 +10,17 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
CONF_PLATFORM,
CONF_PORT,
CONF_UNIQUE_ID,
CONF_ID
CONF_ID,
CONF_ENTITY_ID,
)
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
lg = logging.getLogger(__name__)
@@ -40,20 +40,7 @@ PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM)
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)
lg.warning('mega integration does not support yaml for binary_sensors, please use UI configuration')
return True
@@ -61,26 +48,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('binary_sensor', {}).items():
port = int(port)
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_id=mid, port=port, config_entry=config_entry)
sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry)
devices.append(sensor)
async_add_devices(devices)
class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity):
class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_on = None
self._attrs = None
@property
def state_attributes(self):
return self._attrs
@property
def is_on(self) -> bool:
if self._is_on is not None:
return self._is_on
return self._state == 'ON'
val = self.mega.values.get(self.port, {}).get("value") \
or self.mega.values.get(self.port, {}).get('m')
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):
val = payload.get("value")
self._is_on = val == 'ON'
self.mega.values[self.port] = payload

View File

@@ -1,5 +1,5 @@
"""Пока не сделано"""
import asyncio
import logging
import voluptuous as vol
@@ -9,7 +9,8 @@ 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, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS # 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 . import exceptions
@@ -20,17 +21,21 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_ID, default='def'): str,
vol.Required(CONF_HOST, default="192.168.0.14"): 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_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)
# 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
@@ -51,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 2
VERSION = 4
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -65,7 +70,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
hub = await validate_input(self.hass, user_input)
config = await hub.get_config()
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(
@@ -106,7 +113,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
cfg.update(user_input)
hub = await get_hub(self.hass, self.config_entry.data)
if reload:
new = await hub.get_config()
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:
@@ -120,9 +130,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
ret = self.async_show_form(
step_id="init",
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_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_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
# vol.Optional(CONF_INVERT, default=''): str,
}),
)

View File

@@ -1,4 +1,5 @@
"""Constants for the mega integration."""
import re
DOMAIN = "mega"
CONF_MEGA_ID = "mega_id"
@@ -12,8 +13,22 @@ W1BUS = 'w1bus'
CONF_PORT_TO_SCAN = 'port_to_scan'
CONF_RELOAD = 'reload'
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,39 +1,53 @@
import asyncio
import json
import logging
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import State
from .hub import MegaD
from homeassistant.helpers.update_coordinator import CoordinatorEntity
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
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__(
self,
mega_id: str,
mega: MegaD,
port: int,
config_entry: ConfigEntry = None,
id_suffix=None,
name=None,
unique_id=None,
):
super().__init__(mega.updater)
self._state: State = None
self.port = port
self.config_entry = config_entry
self._mega_id = mega_id
self.mega = mega
mega.entities.append(self)
self._mega_id = mega.id
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 "")
self._name = name or f"{mega_id}_{port}" + \
self._name = name or f"{mega.id}_{port}" + \
(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):
@@ -45,7 +59,7 @@ class BaseMegaEntity(RestoreEntity):
"config_entries": [
self.config_entry,
],
"name": f'port {self.port}',
"name": f'{self._mega_id} port {self.port}',
"manufacturer": 'ab-log.ru',
# "model": self.light.productname,
# "sw_version": self.light.swversion,
@@ -58,38 +72,127 @@ class BaseMegaEntity(RestoreEntity):
self._lg = self.mega.lg.getChild(self._name or self.unique_id)
return self._lg
@property
def mega(self) -> MegaD:
return self.hass.data[DOMAIN][self._mega_id]
@property
def available(self) -> bool:
return self.mega.online
@property
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
def unique_id(self):
return self._unique_id
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()
await asyncio.sleep(0.1)
await self.mega.get_port(self.port)
def __update(self, msg):
try:
value = json.loads(msg.payload)
except Exception as exc:
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return
async def get_state(self):
if self.mega.mqtt is None:
self.async_write_ha_state()
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.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.is_first_update = False
return
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, {})
if val is None and self._state is not None:
return self._state == 'ON'
elif val is not None:
val = val.get("value")
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, TypeError):
return None

View File

@@ -0,0 +1,84 @@
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, DOMAIN, CONF_RESPONSE_TEMPLATE
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant
from .tools import make_ints
_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 = defaultdict(lambda: 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()
data['mega_id'] = hub.id
ret = 'd'
if port is not None:
for cb in self.callbacks[hub.id][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={'Server': 's', 'Date': 'n'})
return ret
async def later_update(self, hub):
_LOGGER.debug('force update')
await asyncio.sleep(1)
await hub.updater.async_refresh()

View File

@@ -1,21 +1,23 @@
import asyncio
import json
import logging
from collections import defaultdict
from datetime import datetime
from functools import wraps
from datetime import datetime, timedelta
import aiohttp
import typing
from bs4 import BeautifulSoup
import re
import json
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
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import TEMP, HUM, PATT_SPLIT, DOMAIN, CONF_HTTP, EVENT_BINARY_SENSOR
from .exceptions import CannotConnect, MqttNotConfigured
from .http import MegaView
from .tools import make_ints
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -32,103 +34,144 @@ CLASSES = {
HUM: DEVICE_CLASS_HUMIDITY
}
class NoPort(Exception):
pass
class MegaD:
"""MegaD Hub"""
def __init__(
self,
hass: HomeAssistant,
loop: asyncio.AbstractEventLoop,
host: str,
password: str,
mqtt: mqtt.MQTT,
lg: logging.Logger,
id: str,
mqtt_inputs: bool = True,
mqtt_id: str = None,
scan_interval=60,
port_to_scan=0,
nports=38,
inverted: typing.List[int] = None,
update_all=True,
**kwargs,
):
"""Initialize."""
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None:
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.host = host
self.sec = password
self.mqtt = mqtt
self.id = id
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.entities: typing.List[Entity] = []
self.poll_interval = scan_interval
self.subscriptions = []
self.subs = None
self.lg: logging.Logger = lg.getChild(self.id)
self._scanned = {}
self.sensors = []
self.port_to_scan = port_to_scan
self.inverted = inverted or []
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:
_id = host.split(".")[-1]
self.mqtt_id = f"megad/{_id}"
else:
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 with self.lck:
self.entities.append(ent)
async def get_sensors(self):
async def get_sensors(self, only_list=False):
self.lg.debug(self.sensors)
_ports = {x.port for x in self.sensors}
for x in _ports:
await self.get_port(x)
await asyncio.sleep(0.1)
ports = []
for x in self.sensors:
if only_list and x.http_cmd != 'list':
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):
"""
Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes
offline
"""
self._loop = asyncio.get_event_loop()
while True:
if len(self.sensors) > 0:
await self.get_sensors()
else:
await self.get_port(self.port_to_scan)
await asyncio.sleep(1)
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval:
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)
self.lg.debug('poll')
if self.mqtt is None:
await self.get_all_ports()
await self.get_sensors(only_list=True)
return
if len(self.sensors) > 0:
await self.get_sensors()
else:
await self.get_port(self.port_to_scan)
return self.values
async def get_mqtt_id(self):
async with aiohttp.request(
@@ -142,102 +185,139 @@ class MegaD:
return _id or 'megad/' + self.host.split('.')[-1]
async def send_command(self, port=None, cmd=None):
if port:
url = f"http://{self.host}/{self.sec}/?pt={port}&cmd={cmd}"
else:
url = f"http://{self.host}/{self.sec}/?cmd={cmd}"
self.lg.debug('run command: %s', url)
async with self.lck:
return await self.request(pt=port, cmd=cmd)
async def request(self, **kwargs):
cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
url = f"http://{self.host}/{self.sec}/?{cmd}"
self.lg.debug('request: %s', url)
async with self._http_lck:
async with aiohttp.request("get", url=url) as req:
if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return False
return None
else:
return True
ret = await req.text()
self.lg.debug('response %s', ret)
return ret
async def save(self):
await self.send_command(cmd='s')
async def get_port(self, port):
def parse_response(self, ret):
if ret is None:
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
async def get_port(self, port, force_http=False, http_cmd='get'):
"""
Опрашивает порт с помощью mqtt. Ждет ответ, возвращает ответ.
:param port:
:return:
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values
"""
ftr = asyncio.get_event_loop().create_future()
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
def cb(msg):
try:
if '"value":NA' in msg.payload.decode():
if not ftr.done():
ftr.set_result(None)
return
ret = json.loads(msg.payload).get('value')
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}')
ftr.set_result(None)
self.lg.debug(
f'port: %s response: %s', port, ret
)
async with self.lck:
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
try:
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=1,
qos=2,
retain=False,
)
return await asyncio.wait_for(ftr, timeout=2)
except asyncio.TimeoutError:
self.lg.warning(f'timeout on port {port}')
finally:
unsub()
try:
await asyncio.wait_for(cnd.wait(), timeout=10)
return self.values.get(port)
except asyncio.TimeoutError:
self.lg.error(f'timeout when getting port {port}')
async def get_all_ports(self):
for x in range(37):
asyncio.create_task(self.get_port(x))
if not self.mqtt_inputs:
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):
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 wrapper(msg):
self.lg.debug(
'process incomming message: %s', msg
)
self.last_update = datetime.now()
return callback(msg)
def _process_msg(self, msg):
try:
d = msg.topic.split('/')
port = d[-1]
except ValueError:
self.lg.warning('can not process %s', msg)
return
if port == 'cmd':
return
try:
port = int(port)
except:
self.lg.warning('can not process %s', msg)
return
self.lg.debug(
f'subscribe %s %s', port, wrapper
'process incomming message: %s', msg
)
subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/{port}",
msg_callback=wrapper,
qos=0,
)
self.subscriptions.append(subs)
value = None
try:
value = json.loads(msg.payload)
if isinstance(value, dict):
make_ints(value)
self.values[port] = value
for cb in self._callbacks[port]:
cb(value)
if isinstance(value, dict):
value = value.copy()
value['mega_id'] = self.id
self.hass.bus.async_fire(
EVENT_BINARY_SENSOR,
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):
self.lg.info('unsubscribe')
for x in self.subscriptions:
self.lg.debug('unsubscribe %s', x)
x()
def subscribe(self, port, callback):
port = int(port)
self.lg.debug(
f'subscribe %s %s', port, callback
)
if self.mqtt_inputs:
self._callbacks[port].append(callback)
else:
self.http.callbacks[self.id][port].append(callback)
async def authenticate(self) -> bool:
"""Test if we can authenticate with the host."""
@@ -265,6 +345,8 @@ class MegaD:
)
async with aiohttp.request('get', url) as req:
html = await req.text()
if req.status != 200:
return
tree = BeautifulSoup(html, features="lxml")
pty = tree.find('select', attrs={'name': 'pty'})
if pty is None:
@@ -288,25 +370,36 @@ class MegaD:
self._scanned[port] = (pty, m)
return pty, m
async def scan_ports(self,):
for x in range(38):
async def scan_ports(self, nports=37):
for x in range(0, nports+1):
ret = await self.scan_port(x)
if ret:
yield [x, *ret]
self.nports = nports+1
async def get_config(self):
async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports():
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']:
elif pty == "1" and (m in ['0', '1', '3'] or m is None):
ret['light'][port].append({'dimmer': m == '1'})
elif pty == '3':
values = await self.get_port(port)
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 did not respond, skipping it')
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):
@@ -315,10 +408,11 @@ class MegaD:
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,
http_cmd=http_cmd,
))
return ret

View File

@@ -1,6 +1,5 @@
"""Platform for light integration."""
import logging
import asyncio
import voluptuous as vol
from homeassistant.components.light import (
@@ -11,17 +10,22 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
CONF_PLATFORM,
CONF_PORT,
CONF_UNIQUE_ID,
CONF_ID
CONF_ID,
CONF_DOMAIN,
)
from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity
from .entities import MegaOutPort
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__)
@@ -47,29 +51,7 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM)
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)
lg.warning('mega integration does not support yaml for lights, please use UI configuration')
return True
@@ -77,77 +59,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items():
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_id=mid, port=port, config_entry=config_entry, **data)
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
devices.append(light)
async_add_devices(devices)
class MegaLight(LightEntity, BaseMegaEntity):
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")
class MegaLight(MegaOutPort, LightEntity):
@property
def supported_features(self):
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": [],
"zeroconf": [],
"homekit": {},
"dependencies": [
"mqtt"
],
"after_dependencies": ["mqtt"],
"codeowners": [
"@andvikt"
],

View File

@@ -1,5 +1,4 @@
"""Platform for light integration."""
import asyncio
import logging
import voluptuous as vol
@@ -11,14 +10,13 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
CONF_PLATFORM,
CONF_PORT,
CONF_UNIQUE_ID,
CONF_ID,
CONF_TYPE,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity
from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS
from .hub import MegaD
import re
@@ -59,13 +57,7 @@ PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM)
ents = []
for mid, _config in config.items():
for x in _config:
ent = _make_entity(mid, **x)
ents.append(ent)
add_entities(ents)
lg.warning('mega integration does not support yaml for sensors, please use UI configuration')
return True
@@ -87,12 +79,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
for port, cfg in config_entry.data.get('sensor', {}).items():
port = int(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = Mega1WSensor(
mega_id=mid,
mega=hub,
port=port,
config_entry=config_entry,
**data,
@@ -102,14 +94,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
async_add_devices(devices)
class Mega1WSensor(BaseMegaEntity):
class Mega1WSensor(MegaPushEntity):
def __init__(
self,
unit_of_measurement,
device_class,
patt=None,
key=None,
http_cmd='get',
*args,
**kwargs
):
@@ -122,13 +114,22 @@ class Mega1WSensor(BaseMegaEntity):
super().__init__(*args, **kwargs)
self._value = None
self.key = key
self.patt = patt
self._device_class = device_class
self._unit_of_measurement = unit_of_measurement
self.mega.sensors.append(self)
self.http_cmd = http_cmd
@property
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
def unique_id(self):
@@ -142,26 +143,31 @@ class Mega1WSensor(BaseMegaEntity):
return self._device_class
@property
def should_poll(self):
return False
def state(self):
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
def state(self):
if self._value is None and self._state is not None:
return self._state.state
return self._value
def _update(self, payload: dict):
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}')
def name(self):
n = super().name
c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict):
c = c.get(self.key)
return c or n

View File

@@ -11,7 +11,10 @@
"mqtt_id": "[%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%]",
"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%]",
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
"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,
SwitchEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
CONF_PLATFORM,
CONF_PORT,
CONF_ID,
CONF_DOMAIN,
)
from .entities import BaseMegaEntity
from .const import CONF_DIMMER, CONF_SWITCH
from homeassistant.core import HomeAssistant
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
@@ -33,50 +37,29 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
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
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):
super().__init__(*args, **kwargs)
self._is_on = None
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items():
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:
cmd = 1
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'
class MegaSwitch(MegaOutPort, SwitchEntity):
pass

View File

@@ -0,0 +1,10 @@
def make_ints(d: dict):
for x in d:
try:
d[x] = float(d[x])
except (ValueError, TypeError):
pass
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0

View File

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

View File

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

172
readme.md
View File

@@ -3,22 +3,17 @@
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
## Основные особенности:
- Настройка в веб-интерфейсе
- Настройка в веб-интерфейсе + yaml
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`)
- Возможность работы с несколькими megad
- Обратная связь по mqtt
- Обратная связь по mqtt или http (на выбор)
- События на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей.
## Зависимости
**Важно!!** Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
Для максимальной совместимости необходимо обновить ваш контроллер до последней версии, тк были важные обновления в части
mqtt
## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
@@ -34,9 +29,166 @@ wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
Все имеющиеся у вас порты будут настроены автоматически.
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml
# 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
click: 2
action:
- service: light.toggle
entity_id: light.some_light
```
Для binary_sensor имеет смысл использовать режим P&R, для остальных режимов - лучше пользоваться событиями.
Примеры использования binary_sensor:
```yaml
- alias: обработка долгих/коротких нажатий
trigger:
- platform: state
entity_id: binary_sensor.some_sensor
to: on
for: 1 # задержка на секунду
action:
- choose:
# если кнопка все еще нажата - значит это долгое нажатие
- conditions: "{{ is_state('binary_sensor.some_sensor', 'on')}}"
sequence:
- service: light.turn_on
entity_id: light.some_light
# если кнопка уже не нажата - значит это короткое нажатие
- conditions: "{{ is_state('binary_sensor.some_sensor', 'off')}}"
sequence:
- service: light.turn_off
entity_id: light.some_light
```
## Ответ на входящие события от контроллера
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает
запустить то что прописано в поле act в настройках порта).
Поддерживаеются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id)
Примеры:
```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_id: id как в конфиге HA
- pt: номер порта
- cnt: счетчик срабатываний
- mdid: if как в конфиге контроллера
- click: клик (подробнее в документации меги)
- value: текущее значение (только для mqtt)
- port: номер порта
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `mega.sensor`, понажимать кнопки.
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования