рефакторинг, конфиг yaml

This commit is contained in:
Andrey
2021-01-22 10:27:02 +03:00
parent c3b9474d56
commit bb4ce882f5
12 changed files with 409 additions and 400 deletions

View File

@@ -4,36 +4,41 @@ 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,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN
from .hub import MegaD
from .config_flow import ConfigFlow
_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.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): str,
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'),
vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'):
vol.Any(str, {
vol.Required(str): str
}),
}
}
}
},
extra=vol.ALLOW_EXTRA,
)
ALIVE_STATE = 'alive'
DEF_ID = 'def'
_POLL_TASKS = {}
@@ -42,9 +47,9 @@ _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.services.async_register(
DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
vol.Optional('mega_id'): str
@@ -63,16 +68,6 @@ 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
@@ -84,7 +79,8 @@ async def get_hub(hass, entry):
_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)
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
return hub
@@ -100,17 +96,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 +121,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 +131,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)
@@ -189,6 +184,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,15 +48,19 @@ 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)
@@ -82,5 +73,13 @@ class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity):
return self._state == 'ON'
def _update(self, payload: dict):
data = {CONF_ENTITY_ID: self.entity_id}
payload = payload.copy()
payload.pop(CONF_PORT)
data.update(payload)
self.hass.bus.async_fire(
EVENT_BINARY_SENSOR,
data,
)
val = payload.get("value")
self._is_on = val == 'ON'

View File

@@ -1,5 +1,5 @@
"""Пока не сделано"""
import asyncio
import logging
import voluptuous as vol
@@ -20,7 +20,7 @@ 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,
},
)
@@ -30,7 +30,8 @@ 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)
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 +52,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 = 3
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -65,7 +66,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
hub = await validate_input(self.hass, user_input)
await hub.start()
config = await hub.get_config()
await hub.stop()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
return self.async_create_entry(
@@ -106,7 +109,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
cfg.update(user_input)
hub = await get_hub(self.hass, self.config_entry.data)
if reload:
await hub.start()
new = await hub.get_config()
await hub.stop()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
@@ -120,7 +126,7 @@ 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_RELOAD, default=False): bool,
# vol.Optional(CONF_INVERT, default=''): str,

View File

@@ -12,8 +12,13 @@ W1BUS = 'w1bus'
CONF_PORT_TO_SCAN = 'port_to_scan'
CONF_RELOAD = 'reload'
CONF_INVERT = 'invert'
CONF_PORTS = 'ports'
CONF_CUSTOM = '__custom'
CONF_SKIP = 'skip'
PLATFORMS = [
"light",
"switch",
"binary_sensor",
"sensor",
]
]
EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor'

View File

@@ -1,39 +1,48 @@
import asyncio
import json
import logging
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
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:
self._customize = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(self._mega_id).get(self.port, {})
return self._customize
@property
def device_info(self):
@@ -58,38 +67,112 @@ 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}"
return self.customize.get(CONF_NAME) or self._name or f"{self.mega.id}_p{self.port}"
@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
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)
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)
return
def _update(self, payload: dict):
raise NotImplementedError
pass
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
self.hass.async_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):
if self._brightness is not None:
return self._brightness
if self._state:
return self._state.attributes.get("brightness")
@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 not self.invert else 0
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 not self.invert else "1"
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:
if not self.invert:
self._is_on = val == 'ON'
else:
self._is_on = val == 'OFF'

View File

@@ -1,21 +1,21 @@
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import TEMP, HUM
from .exceptions import CannotConnect
import re
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -38,6 +38,7 @@ class MegaD:
def __init__(
self,
hass: HomeAssistant,
loop: asyncio.AbstractEventLoop,
host: str,
password: str,
mqtt: mqtt.MQTT,
@@ -50,29 +51,51 @@ class MegaD:
**kwargs,
):
"""Initialize."""
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.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.updater = DataUpdateCoordinator(
hass,
self.lg,
name="sensors",
update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
)
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()
self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg,
qos=0,
)
async def stop(self):
self.subs()
for x in self._callbacks.values():
x.clear()
async def add_entity(self, ent):
async with self.lck:
@@ -80,55 +103,41 @@ class MegaD:
async def get_sensors(self):
self.lg.debug(self.sensors)
_ports = {x.port for x in self.sensors}
for x in _ports:
for x in self.sensors:
await self.get_port(x)
await asyncio.sleep(0.1)
@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 + 10):
await self.get_port(self.port_to_scan)
await asyncio.sleep(1)
if (datetime.now() - self.last_update).total_seconds() > (self.poll_interval + 10):
self.lg.warning('mega is offline')
self.hass.states.async_set(
f'mega.{self.id}',
'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 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(
@@ -159,85 +168,63 @@ class MegaD:
await self.send_command(cmd='s')
async def get_port(self, port):
"""
Опрашивает порт с помощью mqtt. Ждет ответ, возвращает ответ.
:param port:
:return:
"""
ftr = asyncio.get_event_loop().create_future()
def cb(msg):
self.last_update = datetime.now()
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.cnd:
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
qos=2,
retain=False,
)
async with self.lck:
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
try:
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
qos=1,
retain=False,
)
return await asyncio.wait_for(ftr, timeout=2)
except asyncio.TimeoutError:
self.lg.warning(f'timeout on port {port}')
finally:
unsub()
await asyncio.wait_for(self.cnd.wait(), timeout=15)
async def get_all_ports(self):
for x in range(37):
asyncio.create_task(self.get_port(x))
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):
@wraps(callback)
def wrapper(msg):
self.lg.debug(
'process incomming message: %s', msg
)
async def _notify(self, port, value):
async with self.cnd:
self.last_update = datetime.now()
return callback(msg)
self.values[port] = value
self.cnd.notify()
def _process_msg(self, msg):
try:
_, port = msg.topic.split('/')
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)
for cb in self._callbacks[port]:
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):
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
)
self._callbacks[port].append(callback)
async def authenticate(self) -> bool:
"""Test if we can authenticate with the host."""
@@ -302,11 +289,18 @@ class MegaD:
elif pty == "1" and m in ['0', '1']:
ret['light'][port].append({'dimmer': m == '1'})
elif pty == '3':
values = await self.get_port(port)
try:
await self.get_port(port)
values = self.values.get(port)
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 +309,10 @@ 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,
))
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

@@ -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,13 +94,12 @@ 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,
*args,
**kwargs
@@ -120,15 +111,25 @@ class Mega1WSensor(BaseMegaEntity):
:param patt: pattern to extract value, must have at least one group that will contain parsed value
"""
super().__init__(*args, **kwargs)
self.mega.sensors.append(self)
self._value = None
self.key = key
self.patt = patt
self._device_class = device_class
self._unit_of_measurement = unit_of_measurement
if self.port not in self.mega.sensors:
self.mega.sensors.append(self.port)
@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):
@@ -141,27 +142,15 @@ class Mega1WSensor(BaseMegaEntity):
def device_class(self):
return self._device_class
@property
def should_poll(self):
return False
@property
def state(self):
if self._value is None and self._state is not None:
return self._state.state
return self._value
if self.key:
ret = self.mega.values.get(self.port, {}).get('value', {}).get(self.key)
else:
ret = self.mega.values.get(self.port, {}).get('value')
if ret is None and self._state is not None:
ret = self._state.state
return ret
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}')
self.mega.values[self.port] = payload

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

@@ -17,7 +17,7 @@
"username": "Username",
"id": "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)"
}
}
@@ -27,7 +27,7 @@
"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"
}

View File

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

View File

@@ -38,6 +38,32 @@ wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.
Вы можете менять названия, иконки и entity_id так же из интерфейса.
По умолчанию все порты настроенные как сенсоры запрашиваются с периодичностью, указанной в настройках. Если это значение
выставить в 0, сканирование выполняться не будет.
####Так же доступна кастомизация устройств с помощью 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
name: какой-то датчик
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
14:
name: какой-то датчик
hum: "%" # если датчик один, то просто строчкой
```
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml