Compare commits

...

31 Commits

Author SHA1 Message Date
andvikt
42f44d3020 Fix offline bugs 2021-01-20 23:33:53 +03:00
andvikt
3108927f15 fix offline bug 2021-01-20 23:25:57 +03:00
Andrey
8539b8a6ee fix eng lang in options 2021-01-18 09:32:25 +03:00
Andrey
405fbe4777 fix eng lang in options 2021-01-18 09:28:38 +03:00
Andrey
ed4928011b add refresh devices 2021-01-15 16:05:10 +03:00
Andrey
ed9011a6e1 edit readme 2021-01-15 09:23:32 +03:00
Andrey
c7e8bcb83e edit readme 2021-01-15 09:20:52 +03:00
Andrey
21fd00083c edit readme 2021-01-15 09:13:12 +03:00
Andrey
c4f4510941 add device registry 2021-01-15 09:10:42 +03:00
Andrey
254015be4c edit readme 2021-01-15 08:45:27 +03:00
Andrey
adb65529a2 hotfix 2021-01-14 23:05:30 +03:00
Andrey
768d46d952 hotfix 2021-01-14 22:48:07 +03:00
Andrey
359c6b99b7 hotfix 2021-01-14 22:27:07 +03:00
Andrey
79dc46226a hotfix 2021-01-14 22:19:56 +03:00
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
Andrey
d6191ba46a fix services 2021-01-05 09:02:35 +03:00
Andrey
f19d3daeff add yaml deprecation 2021-01-04 09:59:44 +03:00
Andrey
d56cffa68f fix 37 port 2021-01-02 09:56:47 +03:00
Andrey
a958d56e6b fix brightness 2020-12-30 08:58:57 +03:00
Andrey
577119e41a add services 2020-12-29 12:36:48 +03:00
Andrey
db52d93ee4 add issue tracker 2020-12-29 11:30:49 +03:00
Andrey
a38de65071 add validate 2020-12-29 11:22:24 +03:00
Andrey
6d1aa47e4f fix manifest 2020-12-29 11:13:34 +03:00
Andrey
cc122cf98b add hassfest 2020-12-29 10:06:22 +03:00
Andrey
a6ff177577 fix updater 2020-12-29 08:51:43 +03:00
Andrey
7822f50500 fix some bugs 2020-12-28 23:32:24 +03:00
16 changed files with 496 additions and 273 deletions

14
.github/workflows/hassfest.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Validate with hassfest
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- uses: home-assistant/actions/hassfest@master

17
.github/workflows/validate.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"

View File

@@ -1,17 +1,15 @@
"""The mega integration.""" """The mega integration."""
import asyncio import asyncio
import logging import logging
import typing
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_PLATFORM, CONF_SCAN_INTERVAL, CONF_ID from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.service import bind_hass from homeassistant.helpers.service import bind_hass
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 from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS
from .hub import MegaD from .hub import MegaD
@@ -35,11 +33,7 @@ CONFIG_SCHEMA = vol.Schema(
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 = {}
@@ -51,76 +45,164 @@ async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the mega component.""" """Set up the mega component."""
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
hass.services.async_register(
DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
vol.Optional('mega_id'): str
})
)
hass.services.async_register(
DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({
vol.Optional('mega_id'): str,
vol.Optional('port'): int,
})
)
hass.services.async_register(
DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({
vol.Required('port'): int,
vol.Required('cmd'): str,
vol.Optional('mega_id'): str,
})
)
if conf is None: if conf is None:
return True return True
if CONF_HOST in conf: if CONF_HOST in conf:
conf = {DEF_ID: conf} conf = {DEF_ID: conf}
for id, data in conf.items(): for id, data in conf.items():
_LOGGER.warning('YAML configuration is deprecated, please use web-interface')
await _add_mega(hass, id, data) await _add_mega(hass, id, data)
hass.services.async_register(
DOMAIN, 'save', _save_service, 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) _mqtt = hass.data.get(mqtt.DOMAIN)
if _mqtt is None: if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first') raise Exception('mqtt not configured, please configure mqtt first')
hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER) hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER)
return hub
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id)
hub = await get_hub(hass, entry)
hass.data[DOMAIN][id] = hub
if not await hub.authenticate(): 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()
hub.mqtt_id = mid hub.mqtt_id = mid
_POLL_TASKS[id] = asyncio.create_task(hub.poll())
return hub return hub
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
print(entry.entry_id) hub = await _add_mega(hass, entry)
id = entry.data.get('id', entry.entry_id)
hub = await _add_mega(hass, id, dict(entry.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)
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())
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)
# hub.inverted = map(lambda x: x.strip(), ( entry.data = entry.options
# entry.options.get(CONF_INVERT, '').split(',') 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 = hass.data[DOMAIN]
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)
task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is None:
return
task.cancel()
if hub is None:
return
hub.unsubscribe_all()
unsub = _subs.pop(entry.entry_id) unsub = _subs.pop(entry.entry_id)
unsub() if unsub:
unsub()
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s to version 2", config_entry.version)
hub = await get_hub(hass, config_entry)
new = dict(config_entry.data)
if config_entry.version == 1:
cfg = await hub.get_config()
new.update(cfg)
_LOGGER.debug(f'new config: %s', new)
config_entry.data = new
config_entry.version = 2
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True return True
async def _save_service(hass: HomeAssistant, call: ServiceCall):
mega_id = call.data.get('mega_id')
if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.save()
else:
for hub in hass.data[DOMAIN].values():
await hub.save()
@bind_hass @bind_hass
async def _save_service(hass: HomeAssistant, mega_id='def'): async def _get_port(hass: HomeAssistant, call: ServiceCall):
hub: MegaD = hass.data[DOMAIN][mega_id] port = call.data.get('port')
await hub.save() mega_id = call.data.get('mega_id')
if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id]
if port is None:
await hub.get_all_ports()
else:
await hub.get_port(port)
else:
for hub in hass.data[DOMAIN].values():
if port is None:
await hub.get_all_ports()
else:
await hub.get_port(port)
async def _is_alive(cond: asyncio.Condition, msg): @bind_hass
async with cond: async def _run_cmd(hass: HomeAssistant, call: ServiceCall):
cond.notify_all() port = call.data.get('port')
mega_id = call.data.get('mega_id')
cmd = call.data.get('cmd')
if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.send_command(port=port, cmd=cmd)
else:
for hub in hass.data[DOMAIN].values():
await hub.send_command(port=port, cmd=cmd)

View File

@@ -1,7 +1,6 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio
import json
import logging import logging
import asyncio
import voluptuous as vol import voluptuous as vol
@@ -18,7 +17,6 @@ from homeassistant.const import (
CONF_ID CONF_ID
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.restore_state import RestoreEntity
from .entities import BaseMegaEntity from .entities import BaseMegaEntity
from .hub import MegaD from .hub import MegaD
@@ -63,11 +61,11 @@ 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():
if pty == "0":
sensor = MegaBinarySensor(mega_id=mid, port=port)
devices.append(sensor)
for port, cfg in config_entry.data.get('binary_sensor', {}).items():
hub.lg.debug(f'add binary_sensor on port %s', port)
sensor = MegaBinarySensor(mega_id=mid, port=port, config_entry=config_entry)
devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)

View File

@@ -8,8 +8,8 @@ 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, CONF_INVERT # pylint:disable=unused-import from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -26,6 +26,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
) )
async def get_hub(hass: HomeAssistant, data):
_mqtt = hass.data.get(mqtt.DOMAIN)
if not isinstance(_mqtt, mqtt.MQTT):
raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt)
if not await hub.authenticate():
raise exceptions.InvalidAuth
return hub
async def validate_input(hass: core.HomeAssistant, data): 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 +43,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 +51,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 = 2
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 +64,14 @@ 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)
config = await hub.get_config()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config,
)
except exceptions.CannotConnect: except exceptions.CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except exceptions.InvalidAuth: except exceptions.InvalidAuth:
@@ -69,11 +81,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,10 +99,22 @@ 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:
new = await hub.get_config()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
cfg.update(new)
return self.async_create_entry( 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(
@@ -104,8 +123,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
vol.Optional(CONF_SCAN_INTERVAL, default=e[CONF_SCAN_INTERVAL]): int, vol.Optional(CONF_SCAN_INTERVAL, default=e[CONF_SCAN_INTERVAL]): 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_RELOAD, default=False): bool, vol.Optional(CONF_RELOAD, default=False): bool,
vol.Optional(CONF_INVERT): vol.Set(), # vol.Optional(CONF_INVERT, default=''): str,
}), }),
) )
print(ret)
return ret return ret

View File

@@ -11,4 +11,9 @@ 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'
PLATFORMS = [
"light",
"binary_sensor",
"sensor",
]

View File

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

View File

@@ -1,6 +1,8 @@
import asyncio import asyncio
import json import json
import logging import logging
from collections import defaultdict
from datetime import datetime
from functools import wraps from functools import wraps
import aiohttp import aiohttp
@@ -8,10 +10,27 @@ import typing
from bs4 import BeautifulSoup 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 .const import TEMP, HUM
from .exceptions import CannotConnect from .exceptions import CannotConnect
import re
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PATTERNS = {
TEMP: TEMP_PATT,
HUM: HUM_PATT,
}
UNITS = {
TEMP: '°C',
HUM: '%'
}
CLASSES = {
TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: DEVICE_CLASS_HUMIDITY
}
class MegaD: class MegaD:
"""MegaD Hub""" """MegaD Hub"""
@@ -22,12 +41,12 @@ class MegaD:
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_id: str = None, mqtt_id: str = None,
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
inverted:typing.List[int] = None, inverted: typing.List[int] = None,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
@@ -47,6 +66,7 @@ class MegaD:
self.sensors = [] self.sensors = []
self.port_to_scan = port_to_scan self.port_to_scan = port_to_scan
self.inverted = inverted or [] self.inverted = inverted or []
self.last_update = datetime.now()
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}"
@@ -59,10 +79,11 @@ class MegaD:
self.entities.append(ent) self.entities.append(ent)
async def get_sensors(self): async def get_sensors(self):
self.lg.debug(self.sensors)
_ports = {x.port for x in self.sensors} _ports = {x.port for x in self.sensors}
for x in _ports: for x in _ports:
await self.get_port(x) await self.get_port(x)
await asyncio.sleep(self.poll_interval) await asyncio.sleep(0.1)
async def poll(self): async def poll(self):
""" """
@@ -70,36 +91,37 @@ class MegaD:
offline offline
""" """
self._loop = asyncio.get_event_loop() self._loop = asyncio.get_event_loop()
if self.sensors:
await self.subscribe(self.sensors[0].port, callback=self._notify)
else:
await self.subscribe(self.port_to_scan, callback=self._notify)
while True:
async with self.is_alive:
if len(self.sensors) > 0:
await self.get_sensors()
else:
await self.get_port(self.port_to_scan)
try: while True:
await asyncio.wait_for(self.is_alive.wait(), timeout=5) if len(self.sensors) > 0:
self.hass.states.async_set( await self.get_sensors()
f'mega.{self.id}', else:
'online', await self.get_port(self.port_to_scan)
)
self.online = True await asyncio.sleep(1)
except asyncio.TimeoutError: if (datetime.now() - self.last_update).total_seconds() > (self.poll_interval + 10):
self.online = False 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( self.hass.states.async_set(
f'mega.{self.id}', f'mega.{self.id}',
'offline', 'offline',
) )
for x in self.entities: self.online = False
try: else:
await x.async_update_ha_state() self.hass.states.async_set(
except RuntimeError: f'mega.{self.id}',
pass 'online',
await asyncio.sleep(self.poll_interval) )
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 def _async_notify(self):
async with self.is_alive: async with self.is_alive:
@@ -136,35 +158,47 @@ class MegaD:
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): async def get_port(self, port):
if get_value: """
ftr = asyncio.get_event_loop().create_future() Опрашивает порт с помощью mqtt. Ждет ответ, возвращает ответ.
def cb(msg): :param port:
try: :return:
ftr.set_result(json.loads(msg.payload).get('value')) """
except Exception as exc: ftr = asyncio.get_event_loop().create_future()
self.lg.warning(f'could not parse {msg.payload}: {exc}')
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.lck:
unsub = await self.mqtt.async_subscribe( unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}', topic=f'{self.mqtt_id}/{port}',
msg_callback=cb, msg_callback=cb,
qos=1, 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: 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) return await asyncio.wait_for(ftr, timeout=2)
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.lg.warning(f'timeout on port {port}') self.lg.warning(f'timeout on port {port}')
@@ -173,7 +207,7 @@ class MegaD:
async def get_all_ports(self): async def get_all_ports(self):
for x in range(37): for x in range(37):
await self.get_port(x) asyncio.create_task(self.get_port(x))
async def reboot(self, save=True): async def reboot(self, save=True):
await self.save() await self.save()
@@ -186,6 +220,7 @@ class MegaD:
self.lg.debug( self.lg.debug(
'process incomming message: %s', msg 'process incomming message: %s', msg
) )
self.last_update = datetime.now()
return callback(msg) return callback(msg)
self.lg.debug( self.lg.debug(
@@ -221,39 +256,69 @@ 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'}) tree = BeautifulSoup(html, features="lxml")
if pty is None: pty = tree.find('select', attrs={'name': 'pty'})
return if pty is None:
else:
pty = pty.find(selected=True)
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,):
for x in range(37): for x in range(38):
ret = await self.scan_port(x) ret = await self.scan_port(x)
if ret: if ret:
yield [x, *ret] yield [x, *ret]
async def get_config(self):
ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports():
if pty == "0":
ret['binary_sensor'][port].append({})
elif pty == "1" and m in ['0', '1']:
ret['light'][port].append({'dimmer': m == '1'})
elif pty == '3':
values = await self.get_port(port)
self.lg.debug(f'values: %s', values)
if values is None:
self.lg.warning(f'port {port} is of type sensor but did not respond, skipping it')
continue
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
values = {None: values}
for key in values:
self.lg.debug(f'add sensor {key}')
ret['sensor'][port].append(dict(
key=key,
patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
# TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key,
))
return ret

View File

@@ -1,8 +1,6 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio
import json
import logging import logging
import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
@@ -19,7 +17,6 @@ from homeassistant.const import (
CONF_ID CONF_ID
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.restore_state import RestoreEntity
from .entities import BaseMegaEntity from .entities import BaseMegaEntity
from .hub import MegaD from .hub import MegaD
@@ -80,9 +77,11 @@ 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():
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') for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data)
light = MegaLight(mega_id=mid, port=port, config_entry=config_entry, **data)
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)
@@ -119,7 +118,7 @@ class MegaLight(LightEntity, BaseMegaEntity):
return self._state == 'ON' return self._state == 'ON'
async def async_turn_on(self, brightness=None, **kwargs) -> None: async def async_turn_on(self, brightness=None, **kwargs) -> None:
brightness = brightness or self.brightness brightness = brightness or self.brightness or 255
if self.dimmer and brightness == 0: if self.dimmer and brightness == 0:
cmd = 255 cmd = 255
elif self.dimmer: elif self.dimmer:

View File

@@ -2,7 +2,7 @@
"domain": "mega", "domain": "mega",
"name": "mega", "name": "mega",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mega", "documentation": "https://www.home-assistant.io/integrations/mega_hacs",
"requirements": [ "requirements": [
"beautifulsoup4", "beautifulsoup4",
"lxml" "lxml"
@@ -15,5 +15,6 @@
], ],
"codeowners": [ "codeowners": [
"@andvikt" "@andvikt"
] ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues"
} }

View File

@@ -1,7 +1,6 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio
import logging import logging
import typing
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@@ -70,7 +69,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None):
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,
@@ -79,7 +78,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
) )
@@ -87,24 +87,17 @@ 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():
if pty == "3": for port, cfg in config_entry.data.get('sensor', {}).items():
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_id=mid,
if not isinstance(values, dict): port=port,
values = {None: values} config_entry=config_entry,
for key in values: **data,
hub.lg.debug(f'add sensor {W1}:{key}') )
sensor = _make_entity( devices.append(sensor)
mid=mid,
port=port,
conf={
CONF_TYPE: W1,
CONF_KEY: key,
})
devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
@@ -133,11 +126,6 @@ class Mega1WSensor(BaseMegaEntity):
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
async def async_added_to_hass(self) -> None:
await super(Mega1WSensor, self).async_added_to_hass()
self.mega.sensors.append(self)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
return self._unit_of_measurement return self._unit_of_measurement
@@ -165,7 +153,7 @@ class Mega1WSensor(BaseMegaEntity):
def _update(self, payload: dict): def _update(self, payload: dict):
val = payload.get('value', '') val = payload.get('value', '')
if isinstance(val, str): if isinstance(val, str) and self.patt is not None:
val = self.patt.findall(val) val = self.patt.findall(val)
if val: if val:
self._value = val[0] self._value = val[0]

View File

@@ -1,13 +1,33 @@
save: save:
# Description of the service
description: Сохраняет текущее состояние портов (?cmd=s) description: Сохраняет текущее состояние портов (?cmd=s)
# Different fields that your service accepts
fields: fields:
# Key of the field
mega_id: mega_id:
# Description of the field description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
description: ID меги example: "mega"
# Example value that can be passed for this field
example: "def" get_port:
description: Запросить текущий статус порта (или всех)
fields:
mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega"
port:
description: Номер порта (если не заполнять, будут запрошены все порты сразу)
example: 1
run_cmd:
description: Выполнить любую произвольную команду
fields:
mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega"
port:
description: Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду)
example: 1
cmd:
description: Любая поддерживаемая мегой команда
example: "1:0"

View File

@@ -1,5 +1,4 @@
"""Platform for light integration.""" """Platform for light integration."""
import json
import logging import logging
import voluptuous as vol import voluptuous as vol
@@ -13,11 +12,7 @@ from homeassistant.const import (
CONF_PLATFORM, CONF_PLATFORM,
CONF_PORT, CONF_PORT,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers.restore_state import RestoreEntity
from .entities import BaseMegaEntity from .entities import BaseMegaEntity
from .hub import MegaD
from .const import CONF_DIMMER, CONF_SWITCH from .const import CONF_DIMMER, CONF_SWITCH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

5
install.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
git clone https://github.com/andvikt/mega_hacs.git
mkdir custom_components
cp mega_hacs/custom_components/mega custom_components/mega
rm -fR mega_hacs

140
readme.md
View File

@@ -1,100 +1,84 @@
# 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
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
## Устройства большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
Поддерживаются устройства: light, switch, binary_sensor, sensor. light может работать как диммер выполнении предыдущей.
## Установка
В папке config/custom_components выполнить:
```shell
git clone https://github.com/andvikt/mega.git
```
Обновление:
```shell
git pull
```
Перезагрузить HA
## Зависимости
Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
## Настройка из веб-интерфейса ## Зависимости
**Важно!!** Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
Для максимальной совместимости необходимо обновить ваш контроллер до последней версии, тк были важные обновления в части
mqtt
## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
HACS - Integrations - Explore, в поиске ищем MegaD.
Альтернативный способ установки:
```shell
# из папки с конфигом
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
```
Не забываем перезагрузить HA
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega `Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
## Пример настройки с помощью yaml: Все имеющиеся у вас порты будут настроены автоматически.
```yaml
mega:
mega1:
host: 192.168.0.14
name: hello
password: sec
mqtt_id: mega # это id в конфиге меги
light: Вы можете менять названия, иконки и entity_id так же из интерфейса.
- platform: mega
mega1:
switch:
- 1 # можно просто перечислить порты
- 2
- 3
dimmer:
- port: 7
name: hello # можно использовать расширенный вариант с названиями
- 9
- 10
binary_sensor:
- platform: mega
mega1:
- port: 16
name: sensor1
- port: 18
name: sensor2
sensor:
- platform: mega
mega1:
- port: 10
name: some temp
type: w1
key: temp
- port: 10
name: some hum
type: w1
key: hum
switch:
- platform: mega
mega1:
- 11
```
## Сервисы ## Сервисы
Интеграция предоставляет сервис сохранения состояния портов: `mega.save` Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml ```yaml
action: mega.save:
service: mega.save description: Сохраняет текущее состояние портов (?cmd=s)
data: fields:
mega_id: def mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega"
mega.get_port:
description: Запросить текущий статус порта (или всех)
fields:
mega_id:
description: ID меги, можно оставить пустым, тогда будут порты всех зарегистрированных мег
example: "mega"
port:
description: Номер порта (если не заполнять, будут запрошены все порты сразу)
example: 1
mega.run_cmd:
description: Выполнить любую произвольную команду
fields:
mega_id:
description: ID меги
example: "mega"
port:
description: Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду)
example: 1
cmd:
description: Любая поддерживаемая мегой команда
example: "1:0"
``` ```
## Состояния
Так же каждое устройство megad опрашивается на предмет работоспособности, текущий статус
хранится в mega.<id>
## Отладка ## Отладка
Если возникают проблемы, можно включить детальный лог, для этого в конфиг добавить: Интеграция находится в активной разработке, при возникновении проблем [заводите issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
Просьба прикладывать детальный лог, который можно включить в конфиге так:
```yaml ```yaml
logger: logger:
default: info default: info
logs: logs:
custom_components.mega: debug custom_components.mega: debug
``` ```