Compare commits

...

60 Commits

Author SHA1 Message Date
Andrey
c36c1369aa fix binary sensors 2021-01-28 10:29:16 +03:00
Andrey
6d3391bc45 add new events, fix binsensor 2021-01-27 21:40:55 +03:00
Andrey
a7d7738a5c fix mqtt 2021-01-26 08:34:27 +03:00
Andrey
c0b1247b9e smaller headers 2021-01-25 21:35:06 +03:00
Andrey
1548e8c364 fix multiple megas 2021-01-25 20:17:15 +03:00
Andrey
39c4ab0e3b fix device name 2021-01-25 18:45:23 +03:00
Andrey
a002e48e04 fix old mega out type 0 2021-01-25 18:13:25 +03:00
Andrey
dc6bdfc8f4 fix yaml exclusion 2021-01-25 17:56:47 +03:00
Andrey
e51b50797c fix yaml exclusion 2021-01-25 17:46:26 +03:00
Andrey
c4205c7ddc fix sw-link 2021-01-25 17:23:33 +03:00
Andrey
6164966d0b fix scanning 2021-01-25 17:21:55 +03:00
Andrey
ff6225a959 fix scanning 2021-01-25 17:18:42 +03:00
Andrey
57f355d479 fix scanning 2021-01-25 16:37:48 +03:00
Andrey
7d6273539e fix scanning 2021-01-25 15:57:10 +03:00
Andrey
5da2973351 fix scanning 2021-01-25 15:52:24 +03:00
Andrey
5eadd295f1 Merge branch 'master' of https://github.com/andvikt/mega_hacs 2021-01-25 15:48:29 +03:00
Andrey
5bf432a27f change readme 2021-01-25 15:47:55 +03:00
Andrey
d934e87ae5 force use http while scanning 2021-01-25 15:46:40 +03:00
andvikt
0b54db9c44 Update issue templates 2021-01-25 13:05:31 +03:00
Andrey
2d15b60929 add allowed hosts config 2021-01-25 12:54:15 +03:00
Andrey
b0b4fdd6cf fix dependencies 2021-01-25 12:29:31 +03:00
Andrey
8bbb4ab271 add http support 2021-01-25 12:25:11 +03:00
Andrey
876a1f0cc8 get port one more fix 2021-01-24 11:17:09 +03:00
Andrey
18d0aee391 small fix 2021-01-23 21:08:33 +03:00
Andrey
4e2d659c44 fix mid 2021-01-22 21:32:55 +03:00
Andrey
e0b4fec0ca add name cusomisation for multiple sensors 2021-01-22 12:28:04 +03:00
Andrey
b5e4b2c802 add name cusomisation for multiple sensors 2021-01-22 12:27:07 +03:00
Andrey
ed6a86a721 add name cusomisation for multiple sensors 2021-01-22 12:24:38 +03:00
Andrey
811f2067bb fix save_all service 2021-01-22 12:20:16 +03:00
Andrey
3e1499f78b fix get_port 2021-01-22 12:16:32 +03:00
Andrey
2a4a85a20e small bugfix 2021-01-22 11:12:18 +03:00
Andrey
2a0f60f1b2 edit readme 2021-01-22 10:53:15 +03:00
Andrey
a8eb50ee44 edit readme 2021-01-22 10:52:39 +03:00
Andrey
6b1635f60f edit readme 2021-01-22 10:51:32 +03:00
Andrey
62bdcfeb1b edit readme 2021-01-22 10:50:34 +03:00
Andrey
fa1c3330ba edit readme 2021-01-22 10:48:00 +03:00
Andrey
9755a9c654 edit readme 2021-01-22 10:27:54 +03:00
Andrey
bb4ce882f5 рефакторинг, конфиг yaml 2021-01-22 10:27:02 +03:00
andvikt
c3b9474d56 Merge pull request #3 from Foroxon/master
Added Ukrainian translation
2021-01-20 23:45:39 +03:00
andvikt
42f44d3020 Fix offline bugs 2021-01-20 23:33:53 +03:00
andvikt
3108927f15 fix offline bug 2021-01-20 23:25:57 +03:00
Vladyslav Heneraliuk
686d86aec5 renamed UA translation file 2021-01-18 11:30:24 +02:00
Vladyslav Heneraliuk
8d1b828362 added Ukrainian translation 2021-01-18 11:15:39 +02:00
Andrey
8539b8a6ee fix eng lang in options 2021-01-18 09:32:25 +03:00
Andrey
405fbe4777 fix eng lang in options 2021-01-18 09:28:38 +03:00
Andrey
ed4928011b add refresh devices 2021-01-15 16:05:10 +03:00
Andrey
ed9011a6e1 edit readme 2021-01-15 09:23:32 +03:00
Andrey
c7e8bcb83e edit readme 2021-01-15 09:20:52 +03:00
Andrey
21fd00083c edit readme 2021-01-15 09:13:12 +03:00
Andrey
c4f4510941 add device registry 2021-01-15 09:10:42 +03:00
Andrey
254015be4c edit readme 2021-01-15 08:45:27 +03:00
Andrey
adb65529a2 hotfix 2021-01-14 23:05:30 +03:00
Andrey
768d46d952 hotfix 2021-01-14 22:48:07 +03:00
Andrey
359c6b99b7 hotfix 2021-01-14 22:27:07 +03:00
Andrey
79dc46226a hotfix 2021-01-14 22:19:56 +03:00
Andrey
012d12437b hotfix 2021-01-14 21:40:19 +03:00
Andrey
7063575957 hotfix 2021-01-14 21:35:15 +03:00
Andrey
6a43198d81 hotfix 2021-01-14 21:33:44 +03:00
Andrey
242386bfe8 edit readme 2021-01-14 21:25:48 +03:00
Andrey
34d31d2879 edit readme 2021-01-14 21:07:30 +03:00
20 changed files with 1288 additions and 573 deletions

32
.experiment.py Normal file
View File

@@ -0,0 +1,32 @@
import asyncio
async def handle_echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print(f"Received {message!r} from {addr!r}")
print(f"Send: {message!r}")
ans = '''HTTP/1.1 200 OK\nContent-Length: 6\n\nhello\n'''.encode()
writer.write(ans)
await writer.drain()
print("Close the connection")
writer.transport.close()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(
handle_echo, '127.0.0.1', 8888)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
asyncio.run(main())

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

@@ -1,36 +0,0 @@
import asyncio
from bs4 import BeautifulSoup
import aiohttp
host = '192.168.88.14/sec'
# page = '''
# <html><head><style>input,select{margin:1px}</style></head><body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body></html>
# <head><style>input,select{margin:1px}</style></head>
# <body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body>
# <a href="/sec/?cf=3">Back</a>
# <br>
# P7/ON
# <br>
# <a href="/sec/?pt=7&amp;cmd=7:1">ON</a>
# <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a>
# <br>
# <form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form>
# <body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body>
# <html><head><style>input,select{margin:1px}</style></head><body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body></html>
# '''
# tree = BeautifulSoup(page, features="lxml")
# pty = tree.find('select', attrs={'name': 'pty'}).find(selected=True)['value']
# m = tree.find('select', attrs={'name': 'm'})
# if m:
# m = m.find(selected=True)['value']
#
# print(pty, m)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(
scan_port(0)
)

View File

@@ -4,40 +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
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,
)
PLATFORMS = [
"light",
"binary_sensor",
"sensor",
]
ALIVE_STATE = 'alive'
DEF_ID = 'def'
_POLL_TASKS = {}
@@ -46,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
@@ -67,25 +83,36 @@ 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
async def _add_mega(hass: HomeAssistant, id, data: dict):
async def get_hub(hass, entry):
id = entry.data.get('id', entry.entry_id)
data = dict(entry.data)
data.update(entry.options or {})
data.update(id=id)
_mqtt = hass.data.get(mqtt.DOMAIN)
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first')
hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER)
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] = 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()
@@ -94,42 +121,70 @@ async def _add_mega(hass: HomeAssistant, id, data: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id)
data = dict(entry.data)
data.update(entry.options or {})
hub = await _add_mega(hass, id, data)
hub: MegaD = await _add_mega(hass, entry)
_hubs[entry.entry_id] = hub
_subs[entry.entry_id] = entry.add_update_listener(update)
_subs[entry.entry_id] = entry.add_update_listener(updater)
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
async def update(hass: HomeAssistant, entry: ConfigEntry):
async def updater(hass: HomeAssistant, entry: ConfigEntry):
"""
Обновляется конфигурация
:param hass:
:param entry:
:return:
"""
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options[CONF_PORT_TO_SCAN]
if entry.options[CONF_RELOAD]:
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options
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
async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id)
hass.data[DOMAIN][id].unsubscribe_all()
task: asyncio.Task = _POLL_TASKS.pop(id)
task.cancel()
hub: MegaD = hass.data[DOMAIN][id]
if hub is None:
return
_LOGGER.debug(f'remove {id}')
_hubs.pop(entry.entry_id)
unsub = _subs.pop(entry.entry_id)
unsub()
task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is not None:
task.cancel()
if hub is None:
return
await hub.stop()
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s to version %s", config_entry.version, ConfigFlow.VERSION)
hub = await get_hub(hass, config_entry)
new = dict(config_entry.data)
await hub.start()
cfg = await hub.get_config()
await hub.stop()
new.update(cfg)
_LOGGER.debug(f'new config: %s', new)
config_entry.data = new
config_entry.version = ConfigFlow.VERSION
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True
async def _save_service(hass: HomeAssistant, call: ServiceCall):
@@ -139,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
@@ -154,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,5 +1,4 @@
"""Platform for light integration."""
import asyncio
import logging
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,29 +48,41 @@ 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=hub, port=port, config_entry=config_entry)
devices.append(sensor)
async_add_devices(devices)
async def scan():
async for port, pty, m in hub.scan_ports():
if pty == "0":
sensor = MegaBinarySensor(mega_id=mid, port=port)
devices.append(sensor)
async_add_devices(devices)
asyncio.create_task(scan())
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:
if val in ['ON', 'OFF']:
return val == 'ON'
else:
return 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
@@ -8,8 +8,9 @@ from homeassistant import config_entries, core
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD # pylint:disable=unused-import
from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \
CONF_NPORTS, CONF_UPDATE_ALL # pylint:disable=unused-import
from .hub import MegaD
from . import exceptions
@@ -20,12 +21,26 @@ 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, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate():
raise exceptions.InvalidAuth
return hub
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
@@ -33,12 +48,7 @@ async def validate_input(hass: core.HomeAssistant, data):
"""
if data[CONF_ID] in hass.data.get(DOMAIN, []):
raise exceptions.DuplicateId('duplicate_id')
_mqtt = hass.data.get(mqtt.DOMAIN)
if not isinstance(_mqtt, mqtt.MQTT):
raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt)
if not await hub.authenticate():
raise exceptions.InvalidAuth
hub = await get_hub(hass, data)
return hub
@@ -46,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 1
VERSION = 4
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -59,7 +69,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
try:
await validate_input(self.hass, user_input)
hub = await validate_input(self.hass, user_input)
await hub.start()
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config,
)
except exceptions.CannotConnect:
errors["base"] = "cannot_connect"
except exceptions.InvalidAuth:
@@ -69,11 +88,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors[CONF_ID] = str(exc)
else:
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=user_input,
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -92,18 +106,36 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data)
cfg.update(user_input)
hub = await get_hub(self.hass, self.config_entry.data)
if reload:
await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
cfg.update(new)
return self.async_create_entry(
title='',
data={**user_input, **{CONF_ID: self.config_entry.data[CONF_ID]}},
data=cfg,
)
e = self.config_entry.data
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,3 +13,31 @@ 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'
EVENT_BINARY = f'{DOMAIN}.binary'
PATT_SPLIT = re.compile('[;/]')
LONG = 'long'
RELEASE = 'release'
LONG_RELEASE = 'long_release'
PRESS = 'press'
SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double'

View File

@@ -1,36 +1,83 @@
import logging
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, EVENT_BINARY_SENSOR, LONG, \
LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY
_events_on = False
_LOGGER = logging.getLogger(__name__)
async def _set_events_on():
global _events_on
await asyncio.sleep(10)
_LOGGER.debug('events on')
_events_on = True
_task_set_ev_on = None
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
unique_id=None,
):
super().__init__(mega.updater)
self._state: State = None
self.port = port
self._mega_id = mega_id
self.config_entry = config_entry
self.mega = mega
mega.entities.append(self)
self._mega_id = mega.id
self._lg = None
self._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):
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'{self._mega_id} port {self.port}',
"manufacturer": 'ab-log.ru',
# "model": self.light.productname,
# "sw_version": self.light.swversion,
"via_device": (DOMAIN, self._mega_id),
}
@property
def lg(self) -> logging.Logger:
@@ -38,38 +85,178 @@ 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)
global _task_set_ev_on
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)
if self.mega.mqtt_inputs and _task_set_ev_on is None:
_task_set_ev_on = asyncio.create_task(_set_events_on())
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)
if self.mega.mqtt_inputs and not _events_on:
_LOGGER.debug('skip event because events are off')
return
if not self.entity_id.startswith('binary_sensor'):
_LOGGER.debug('skip event because not a bnary sens')
return
ll: bool = self.mega.last_long.get(self.port, False)
if safe_int(value.get('click', 0)) == 1:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': SINGLE_CLICK
}
)
elif safe_int(value.get('click', 0)) == 2:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': DOUBLE_CLICK
}
)
elif safe_int(value.get('m', 0)) == 2:
self.mega.last_long[self.port] = True
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': LONG
}
)
elif safe_int(value.get('m', 0)) == 1:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': LONG_RELEASE if ll else RELEASE,
}
)
elif safe_int(value.get('m', None)) == 0:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': PRESS,
}
)
self.mega.last_long[self.port] = 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,85 @@
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
from . import hub as h
_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: 'h.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:
hub.values[port] = data
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,17 +1,40 @@
import asyncio
import json
import logging
from datetime import datetime
from functools import wraps
from collections import defaultdict
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, PATT_SPLIT, DOMAIN, CONF_HTTP, EVENT_BINARY_SENSOR
from .exceptions import CannotConnect
from .tools import make_ints
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PATTERNS = {
TEMP: TEMP_PATT,
HUM: HUM_PATT,
}
UNITS = {
TEMP: '°C',
HUM: '%'
}
CLASSES = {
TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: DEVICE_CLASS_HUMIDITY
}
class NoPort(Exception):
pass
class MegaD:
@@ -20,97 +43,135 @@ class MegaD:
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.last_long = {}
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(
@@ -124,100 +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():
ftr.set_result(None)
return
ret = json.loads(msg.payload).get('value')
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:
try:
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
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))
async def get_all_ports(self, only_out=False):
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."""
@@ -236,40 +336,83 @@ class MegaD:
return await req.text()
async def scan_port(self, port):
if port in self._scanned:
return self._scanned[port]
url = f'http://{self.host}/{self.sec}/?pt={port}'
self.lg.debug(
f'scan port %s: %s', port, url
)
async with aiohttp.request('get', url) as req:
html = await req.text()
tree = BeautifulSoup(html, features="lxml")
pty = tree.find('select', attrs={'name': 'pty'})
if pty is None:
return
else:
pty = pty.find(selected=True)
if pty:
pty = pty['value']
else:
return
if pty in ['0', '1']:
m = tree.find('select', attrs={'name': 'm'})
if m:
m = m.find(selected=True)['value']
self._scanned[port] = (pty, m)
return pty, m
elif pty == '3':
m = tree.find('select', attrs={'name': 'd'})
if m:
m = m.find(selected=True)['value']
self._scanned[port] = (pty, m)
return pty, m
async def scan_ports(self,):
async with self.lck:
for x in range(38):
ret = await self.scan_port(x)
if ret:
yield [x, *ret]
if port in self._scanned:
return self._scanned[port]
url = f'http://{self.host}/{self.sec}/?pt={port}'
self.lg.debug(
f'scan port %s: %s', port, url
)
async with aiohttp.request('get', url) as req:
html = await req.text()
if req.status != 200:
return
tree = BeautifulSoup(html, features="lxml")
pty = tree.find('select', attrs={'name': 'pty'})
if pty is None:
return
else:
pty = pty.find(selected=True)
if pty:
pty = pty['value']
else:
return
if pty in ['0', '1']:
m = tree.find('select', attrs={'name': 'm'})
if m:
m = m.find(selected=True)['value']
self._scanned[port] = (pty, m)
return pty, m
elif pty == '3':
m = tree.find('select', attrs={'name': 'd'})
if m:
m = m.find(selected=True)['value']
self._scanned[port] = (pty, m)
return pty, m
async def scan_ports(self, 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, nports=37):
ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports(nports):
if pty == "0":
ret['binary_sensor'][port].append({})
elif pty == "1" and (m in ['0', '1', '3'] or m is None):
ret['light'][port].append({'dimmer': m == '1'})
elif pty == '3':
try:
http_cmd = 'get'
values = await self.get_port(port, force_http=True)
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')):
values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list'
except asyncio.TimeoutError:
self.lg.warning(f'timout on port {port}')
continue
self.lg.debug(f'values: %s', values)
if values is None:
self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
continue
if isinstance(values, dict) and 'value' in values:
values = values['value']
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
values = {None: values}
for key in values:
self.lg.debug(f'add sensor {key}')
ret['sensor'][port].append(dict(
key=key,
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key,
http_cmd=http_cmd,
))
return ret

View File

@@ -1,7 +1,5 @@
"""Platform for light integration."""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.light import (
@@ -12,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__)
@@ -48,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
@@ -78,79 +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 = []
async def scan_ports():
async for port, pty, m in hub.scan_ports():
if pty == "1" and m in ['0', '1']:
light = MegaLight(mega_id=mid, port=port, dimmer=m == '1')
devices.append(light)
async_add_devices(devices)
asyncio.create_task(scan_ports())
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=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,17 +57,11 @@ 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
def _make_entity(mid: str, port: int, conf: dict):
def _make_entity(config_entry, mid: str, port: int, conf: dict):
key = conf[CONF_KEY]
return Mega1WSensor(
key=key,
@@ -78,7 +70,8 @@ def _make_entity(mid: str, port: int, conf: dict):
patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key
id_suffix=key,
config_entry=config_entry
)
@@ -86,42 +79,29 @@ 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=hub,
port=port,
config_entry=config_entry,
**data,
)
devices.append(sensor)
async def scan():
async for port, pty, m in hub.scan_ports():
if pty == "3":
values = await hub.get_port(port)
lg.debug(f'values: %s', values)
if values is None:
continue
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
values = {None: values}
for key in values:
hub.lg.debug(f'add sensor {W1}:{key}')
sensor = _make_entity(
mid=mid,
port=port,
conf={
CONF_TYPE: W1,
CONF_KEY: key,
})
devices.append(sensor)
hub.sensors.append(sensor)
async_add_devices(devices)
async_add_devices(devices)
asyncio.create_task(scan())
class Mega1WSensor(BaseMegaEntity):
class Mega1WSensor(MegaPushEntity):
def __init__(
self,
unit_of_measurement,
device_class,
patt=None,
key=None,
http_cmd='get',
*args,
**kwargs
):
@@ -134,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):
@@ -154,26 +143,33 @@ 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('value', {})
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,12 @@
_params = ['m', 'click', 'cnt', 'pt']
def make_ints(d: dict):
for x in _params:
try:
d[x] = int(d.get(x, 0))
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,24 @@
"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"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"reload": "Reload objects",
"mqtt_inputs": "Use MQTT",
"update_all": "Update all outs when input"
}
}
}

View File

@@ -17,8 +17,11 @@
"username": "Пользователь",
"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": "Обновить все выходы когда срабатывает вход"
}
}
}

View File

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

231
readme.md
View File

@@ -1,50 +1,229 @@
# MegaD HomeAssistant custom component
# MegaD HomeAssistant integration
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
## Основные особенности:
- Настройка в веб-интерфейсе + yaml
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для температурных датчиков
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`)
- Возможность работы с несколькими megad
- Обратная связь по mqtt
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad
- Поддержка температурных датчиков в режиме шины
## Устройства
Поддерживаются устройства: light, switch, binary_sensor, sensor. light может работать как диммер
- Обратная связь по mqtt или http (на выбор)
- События на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей.
## Установка
Рекомендованнй способ - через [HACS](https://hacs.xyz/docs/installation/installation).
После установки HACS, нужно перейти в меню hacs -> integrations, далее в верхнем правом углу
нажать три точки, где будет `Custom repositories`, открыть, нажать add и добавить `https://github.com/andvikt/mega_hacs.git`
Рекомендованный способ с поддержкой обновлений - [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
Не забываем перезагрузить HA
Для обновления повторить
## Зависимости
Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
## Настройка из веб-интерфейса
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
## Сервисы
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml
save:
# configuration.yaml
mega:
hello: # ID меги, как в UI
7: # номер порта
domain: switch # тип устройства (switch или light, по умолчанию для цифровых выходов используется light)
invert: true # инвертировать или нет (по умолчанию false)
name: Насос # имя устройства
8:
# исключить из сканирования
skip: true
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
14:
name: какой-то датчик
unit_of_measurement: "°C" # если датчик один, то просто строчкой
```
## Зависимости
Для совместимости c mqtt необходимо настроить интеграцию [mqtt](https://www.home-assistant.io/integrations/mqtt/)
в HomeAssistant, а так же обновить ваш контроллер до последней версии, тк были важные обновления в части mqtt
## HTTP in
Начиная с версии `0.3.1` интеграция стала поддерживать обратную связь без mqtt, используя http-сервер. Для этого в настройках
интеграции необходимо снять галку с `использовать mqtt`
В самой меге необходимо прописать настройки:
```yaml
srv: "192.168.1.4:8123" # ip:port вашего HA
script: "mega" # это api интеграции, к которому будет обращаться контроллер
```
Входы будут доступны как binary_sensor, а так же в виде событий `mega.sensor`.
События можно обрабатывать так:
```yaml
- alias: some double click
trigger:
- platform: event
event_type: mega.sensor
event_data:
pt: 1
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: номер порта
Начиная с версии 0.3.7 появилось так же событие типа mega.binary:
```yaml
# Пример события с полями как есть прямо из меги
- alias: some long click
trigger:
- platform: event
event_type: mega.binary
event_data:
entity_id: binary_sensor.some_id
type: long
action:
- service: light.toggle
entity_id: light.some_light
```
Возможные варианты поля `type`:
- `long`: долгое нажатие
- `release`: размыкание (с гарантией что не было долгого нажатия)
- `long_release`: размыкание после долгого нажатия
- `press`: замыкание
- `single`: одинарный клик (в режиме кликов)
- `double`: двойной клик
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `mega.sensor`, понажимать кнопки.
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml
mega.save:
description: Сохраняет текущее состояние портов (?cmd=s)
fields:
mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega"
get_port:
mega.get_port:
description: Запросить текущий статус порта (или всех)
fields:
mega_id:
@@ -54,10 +233,9 @@ get_port:
description: Номер порта (если не заполнять, будут запрошены все порты сразу)
example: 1
run_cmd:
mega.run_cmd:
description: Выполнить любую произвольную команду
fields:
mega_id:
description: ID меги
example: "mega"
@@ -67,11 +245,12 @@ run_cmd:
cmd:
description: Любая поддерживаемая мегой команда
example: "1:0"
```
## Отладка
Если возникают проблемы, можно включить детальный лог, для этого в конфиг добавить:
Интеграция находится в активной разработке, при возникновении проблем [заводите issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
Просьба прикладывать детальный лог, который можно включить в конфиге так:
```yaml
logger:
default: info