Compare commits

...

53 Commits

Author SHA1 Message Date
Andrey
97da209cf0 поддержка PCA9685 и MCP230 2021-02-25 11:52:11 +03:00
Andrey
2c0894356c добавлена возможность восстановления состояний выходов после перезагрузки меги 2021-02-24 10:56:11 +03:00
andvikt
cdc38f2200 Merge pull request #16 from Foroxon/master
UA translation update
2021-02-22 16:15:16 +03:00
Vladyslav Heneraliuk
bab1eab64a updated UA translation 2021-02-22 10:28:17 +02:00
Vladyslav Heneraliuk
c5d8ece77f Repo Sync (#2)
Repo Sync
2021-02-22 10:18:26 +02:00
Andrey
e4c5a4a712 find mega by mdid 2021-02-22 09:53:58 +03:00
Andrey
c60c088795 add allow hosts to web-ui 2021-02-20 18:10:36 +03:00
Andrey
eb3813843c edit readme 2021-02-19 12:46:10 +03:00
Andrey
804a6ad333 add proxy support 2021-02-19 11:10:34 +03:00
Andrey
a3398d0f2a add proxy support 2021-02-19 10:47:08 +03:00
Andrey
1e8777ca6d add proxy support 2021-02-19 10:39:15 +03:00
Andrey
ad2501ca8c add proxy support 2021-02-19 10:32:28 +03:00
Andrey
5681f7315c add proxy support 2021-02-19 10:06:56 +03:00
Andrey
da87c24db9 add proxy support 2021-02-19 09:50:31 +03:00
Andrey
a0dcfa4b9c fix force_d 2021-02-18 20:09:45 +03:00
Andrey
80e43888b9 fix force_d 2021-02-18 15:45:13 +03:00
Andrey
ba41cbb100 fix 1wire bus 2021-02-18 15:41:12 +03:00
Andrey
0b5b9744ba fix 1wire bus 2021-02-18 15:40:59 +03:00
Andrey
9746311f38 fix 1wire bus 2021-02-18 15:40:31 +03:00
Andrey
38a525f2f5 fix 2021-02-18 15:16:43 +03:00
Andrey
6503d6bddd fix 1wire 2021-02-18 15:02:21 +03:00
Andrey
4af40c29a7 fix d 2021-02-18 14:10:04 +03:00
Andrey
75a41c9667 fix i2c 2021-02-18 12:36:43 +03:00
Andrey
4f8f38fde6 fix http response and ds2413 bugs 2021-02-18 12:07:55 +03:00
Andrey
7e02797be8 fix http response 2021-02-18 11:58:33 +03:00
Andrey
035bdebf64 fix ds2413 2021-02-18 11:42:23 +03:00
Andrey
8d92bbdfb3 ad more logging 2021-02-18 11:23:44 +03:00
Andrey
0c43e61c59 make fake http response 2021-02-18 11:11:30 +03:00
Andrey
0a71be693e fix bugs 2021-02-18 11:00:41 +03:00
Andrey
8146148d0c fix bugs 2021-02-18 10:46:17 +03:00
Andrey
e0eaafd0fa fix bugs 2021-02-18 10:40:09 +03:00
Andrey
51f3eb3b19 fix bugs 2021-02-18 10:23:27 +03:00
Andrey
1716651497 fix bugs 2021-02-18 10:12:01 +03:00
Andrey
a87e8139a7 fix bugs 2021-02-18 09:53:30 +03:00
Andrey
358d29f8fd fix bugs 2021-02-18 09:27:40 +03:00
Andrey
fcce9dcfc1 fix bugs 2021-02-17 22:01:33 +03:00
Andrey
4fe2469a01 fix bugs 2021-02-17 19:10:58 +03:00
Andrey
9fab4fce62 fix config scan for ds2413 2021-02-17 18:08:53 +03:00
Andrey
3c28cf4598 Merge remote-tracking branch 'origin/master' 2021-02-17 18:04:53 +03:00
Andrey
af2360b0a0 edit 2021-02-17 18:04:09 +03:00
andvikt
8f7604c9f6 Update bug-report.md 2021-02-17 17:44:56 +03:00
andvikt
f70d72bf39 Update bug-report.md 2021-02-17 17:44:37 +03:00
Andrey
896e02a457 edit readme 2021-02-17 17:40:26 +03:00
Andrey
bb95c9d312 edit readme 2021-02-17 17:23:23 +03:00
Andrey
7ba1562f12 fix assumed state 2021-02-17 17:11:00 +03:00
Andrey
ec505ac2ef edit readme 2021-02-17 16:50:00 +03:00
Andrey
9544f562ba do not update all if "v" parametr 2021-02-15 21:54:21 +03:00
Andrey
c2422cac9c fix manifest 2021-02-15 21:51:31 +03:00
Andrey
0adba7fc0f add http v parsing 2021-02-15 21:50:49 +03:00
Andrey
39642700ca fix force_d 2021-02-15 19:05:00 +03:00
Andrey
07589e8e3a edit readme 2021-02-11 17:35:58 +03:00
Andrey
5a6903c67e fix config bug 2021-02-11 16:03:10 +03:00
Andrey
6758fd8d8e add server response in mqtt mode 2021-02-11 12:19:17 +03:00
18 changed files with 651 additions and 411 deletions

View File

@@ -1,32 +1,43 @@
import asyncio import re
async def handle_echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): PATT_FW = re.compile(r'fw:\s(.+)\)')
data = await reader.read(100) data = """
message = data.decode() <html><head></head><body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body></html>
addr = writer.get_extra_info('peername') <head></head>
<body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body>
print(f"Received {message!r} from {addr!r}") MegaD-2561 by
<a href="http://ab-log.ru">ab-log.ru</a>
print(f"Send: {message!r}") (fw: 4.48b7)
ans = '''HTTP/1.1 200 OK\nContent-Length: 6\n\nhello\n'''.encode() <br>
writer.write(ans) <a href="/sec/?cf=1">Config</a>
await writer.drain() <br>
-- MODS --
print("Close the connection") <br>
writer.transport.close() <a href="/sec/?cf=3">XP1</a>
writer.close() <br>
await writer.wait_closed() <a href="/sec/?cf=4">XP2</a>
<br>
-- XT2 --
async def main(): <br>
server = await asyncio.start_server( <a href="/sec/?pt=30">P30 - OUT</a>
handle_echo, '127.0.0.1', 8888) <br>
<a href="/sec/?pt=31">P31 - OUT</a>
addr = server.sockets[0].getsockname() <br>
print(f'Serving on {addr}') <a href="/sec/?pt=32">P32 - IN</a>
<br>
async with server: <a href="/sec/?pt=33">P33 - I2C/SCL</a>
await server.serve_forever() <br>
<a href="/sec/?pt=34">P34 - DS</a>
asyncio.run(main()) <br>
<a href="/sec/?pt=35">P35 - NC</a>
<br>
-- XP5/6 --
<br>
<a href="/sec/?pt=36">P36 - ADC</a>
<br>
<a href="/sec/?pt=37">P37 - NC</a>
<body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body>
<html><head></head><body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body></html>
"""
print(PATT_FW.search(data).groups()[0])

View File

@@ -24,10 +24,4 @@ megad firmware version:
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**LOG** **LOG**
Просьба прикладывать детальный лог, который можно включить в конфиге так: Просьба прикладывать детальный лог, подробная инструкция как включать отладку по [ссылке](https://github.com/andvikt/mega_hacs/wiki/Отладка)
```yaml
logger:
default: info
logs:
custom_components.mega: debug
```

View File

@@ -7,48 +7,69 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN, CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass from homeassistant.helpers.service import bind_hass
from homeassistant.helpers.template import Template
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \ 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, \ CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE
from .hub import MegaD from .hub import MegaD
from .config_flow import ConfigFlow from .config_flow import ConfigFlow
from .http import MegaView from .http import MegaView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CUSTOMIZE_PORT = {
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,
vol.Optional(CONF_CONV_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
CUSTOMIZE_DS2413 = {
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT
}
def extender(x):
if isinstance(x, str) and 'e' in x:
return x
else:
raise ValueError('must has "e" in port name')
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str], vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Required(str, description='id меги из веб-интерфейса'): { vol.Required(str, description='id меги из веб-интерфейса'): {
vol.Optional(int, description='номер порта'): { vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, vol.Optional(
vol.Optional(CONF_INVERT, default=False): bool, CONF_DEF_RESPONSE,
vol.Optional(CONF_NAME): vol.Any(str, { description='Ответ по умолчанию',
vol.Required(str): str default=None
}), ): vol.Any(cv.template, None),
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'), vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'): CUSTOMIZE_PORT,
vol.Any(str, { CUSTOMIZE_DS2413,
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,
vol.Optional(CONF_CONV_TEMPLATE): cv.template
}
} }
} }
}, },
@@ -115,8 +136,8 @@ async def get_hub(hass, entry):
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry): async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
hub = await get_hub(hass, entry) hub = await get_hub(hass, entry)
hass.data[DOMAIN][id] = hass.data[DOMAIN]['__def'] = hub hub.fw = await hub.get_fw()
hass.data[DOMAIN][entry.data.get(CONF_HOST)] = hub hass.data[DOMAIN][id] = hub
hass.data[DOMAIN][CONF_ALL][id] = hub hass.data[DOMAIN][CONF_ALL][id] = hub
if not await hub.authenticate(): if not await hub.authenticate():
raise Exception("not authentificated") raise Exception("not authentificated")
@@ -161,7 +182,7 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
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)
hub: MegaD = hass.data[DOMAIN][id] hub: MegaD = hass.data[DOMAIN].get(id)
if hub is None: if hub is None:
return return
_LOGGER.debug(f'remove {id}') _LOGGER.debug(f'remove {id}')

View File

@@ -10,7 +10,8 @@ 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, HomeAssistant from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \ from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS # pylint:disable=unused-import CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -24,9 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_SCAN_INTERVAL, default=0): int, vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_POLL_OUTS, default=False): bool, vol.Optional(CONF_POLL_OUTS, default=False): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=True): bool, vol.Optional(CONF_MQTT_INPUTS, default=False): bool,
vol.Optional(CONF_NPORTS, default=37): int, vol.Optional(CONF_NPORTS, default=37): int,
vol.Optional(CONF_UPDATE_ALL, default=True): bool, vol.Optional(CONF_UPDATE_ALL, default=True): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=True): bool,
vol.Optional(CONF_FORCE_D, default=True): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=True): bool,
vol.Optional(CONF_PROTECTED, default=True): bool,
vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str,
}, },
) )
@@ -57,7 +63,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 = 4 VERSION = 11
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):
@@ -107,7 +113,7 @@ 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."""
hub = await get_hub(self.hass, self.config_entry.data)
if user_input is not None: if user_input is not None:
reload = user_input.pop(CONF_RELOAD) reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data) cfg = dict(self.config_entry.data)
@@ -138,6 +144,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int, vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_RELOAD, default=False): bool, vol.Optional(CONF_RELOAD, default=False): bool,
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool, vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)): bool,
vol.Optional(CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=e.get(CONF_RESTORE_ON_RESTART, False)): bool,
vol.Optional(CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)): bool,
vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str,
# vol.Optional(CONF_INVERT, default=''): str, # vol.Optional(CONF_INVERT, default=''): str,
}), }),
) )

View File

@@ -0,0 +1,52 @@
from dataclasses import dataclass, field
from bs4 import BeautifulSoup
@dataclass(frozen=True, eq=True)
class Config:
pty: str = None
m: str = None
gr: str = None
d: str = None
inta: str = field(compare=False, hash=False, default=None)
ety: str = None
misc: str = field(compare=False, hash=False, default=None)
def parse_config(page: str):
page = BeautifulSoup(page, features="lxml")
ret = {}
for x in [
'pty',
'm',
'gr',
'd',
'ety',
]:
v = page.find('select', attrs={'name': x})
if v is None:
continue
else:
v = v.find(selected=True)
if v:
v = v['value']
ret[x] = v
v = page.find('input', attrs={'name': 'inta'})
if v:
ret['inta'] = v['value']
v = page.find('input', attrs={'name': 'misc'})
if v:
ret['misc'] = v.get('checked', False)
return Config(**ret)
DIGITAL_IN = Config(pty="0")
RELAY_OUT = Config(pty="1", m="0")
PWM_OUT = Config(pty="1", m="1")
DS2413 = Config(pty="1", m="2")
MCP230 = Config(pty="4", m="1", gr="3", d="20")
MCP230_OUT = Config(ety="1")
MCP230_IN = Config(ety="0")
PCA9685 = Config(pty="4", m="1", gr="3", d="21")
OWIRE_BUS = Config(pty="3", d="5")

View File

@@ -23,11 +23,15 @@ CONF_NPORTS = 'nports'
CONF_RESPONSE_TEMPLATE = 'response_template' CONF_RESPONSE_TEMPLATE = 'response_template'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_UPDATE_ALL = 'update_all' CONF_UPDATE_ALL = 'update_all'
CONF_FAKE_RESPONSE = 'fake_response'
CONF_GET_VALUE = 'get_value' CONF_GET_VALUE = 'get_value'
CONF_ALLOW_HOSTS = 'allow_hosts' CONF_ALLOW_HOSTS = 'allow_hosts'
CONF_PROTECTED = 'protected'
CONF_CONV_TEMPLATE = 'conv_template' CONF_CONV_TEMPLATE = 'conv_template'
CONF_POLL_OUTS = 'poll_outs' CONF_POLL_OUTS = 'poll_outs'
CONF_FORCE_D = 'force_d' CONF_FORCE_D = 'force_d'
CONF_DEF_RESPONSE = 'def_response'
CONF_RESTORE_ON_RESTART = 'restore_on_restart'
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",
@@ -46,3 +50,5 @@ PRESS = 'press'
LUX = 'lux' LUX = 'lux'
SINGLE_CLICK = 'single' SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double' DOUBLE_CLICK = 'double'
PATT_FW = re.compile(r'fw:\s(.+)\)')

View File

@@ -43,8 +43,14 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
id_suffix=None, id_suffix=None,
name=None, name=None,
unique_id=None, unique_id=None,
http_cmd='get',
addr: str=None,
index=None,
): ):
super().__init__(mega.updater) super().__init__(mega.updater)
self.http_cmd = http_cmd
self._state: State = None self._state: State = None
self.port = port self.port = port
self.config_entry = config_entry self.config_entry = config_entry
@@ -57,6 +63,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._name = name or f"{mega.id}_{port}" + \ self._name = name or f"{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None self._customize: dict = None
self.index = index
self.addr = addr
if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port}
@property @property
def customize(self): def customize(self):
@@ -66,7 +76,11 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {} c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {} c = c.get(self._mega_id) or {}
c = c.get(self.port) or {} c = c.get(self.port) or {}
if self.addr is not None and self.index is not None and isinstance(c, dict):
idx = self.addr.lower() + f'_a' if self.index == 0 else '_b'
c = c.get(idx, {})
self._customize = c self._customize = c
return self._customize return self._customize
@property @property
@@ -82,7 +96,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
"name": f'{self._mega_id} port {self.port}', "name": f'{self._mega_id} port {self.port}',
"manufacturer": 'ab-log.ru', "manufacturer": 'ab-log.ru',
# "model": self.light.productname, # "model": self.light.productname,
# "sw_version": self.light.swversion, "sw_version": self.mega.fw,
"via_device": (DOMAIN, self._mega_id), "via_device": (DOMAIN, self._mega_id),
} }
@@ -115,7 +129,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
_task_set_ev_on = asyncio.create_task(_set_events_on()) _task_set_ev_on = asyncio.create_task(_set_events_on())
async def get_state(self): async def get_state(self):
if self.mega.mqtt is None: self.lg.debug(f'state is %s', self.state)
if not self.mega.mqtt_inputs:
self.async_write_ha_state() self.async_write_ha_state()
@@ -199,6 +214,7 @@ class MegaOutPort(MegaPushEntity):
def __init__( def __init__(
self, self,
dimmer=False, dimmer=False,
dimmer_scale=1,
*args, **kwargs *args, **kwargs
): ):
super().__init__( super().__init__(
@@ -207,6 +223,11 @@ class MegaOutPort(MegaPushEntity):
self._brightness = None self._brightness = None
self._is_on = None self._is_on = None
self.dimmer = dimmer self.dimmer = dimmer
self.dimmer_scale = dimmer_scale
# @property
# def assumed_state(self) -> bool:
# return True if self.index is not None or self.mega.mqtt is None else False
@property @property
def invert(self): def invert(self):
@@ -214,10 +235,24 @@ class MegaOutPort(MegaPushEntity):
@property @property
def brightness(self): def brightness(self):
val = self.mega.values.get(self.port, {}).get("value") if not self.dimmer:
if val is None and self._state is not None: return
val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None:
return self._state.attributes.get("brightness") return self._state.attributes.get("brightness")
elif isinstance(self.port, str) and 'e' in self.port:
if isinstance(val, str):
val = safe_int(val)
else:
val = 0
if val == 0:
return self._brightness
else:
return val
elif val is not None: elif val is not None:
val = val.get("value")
if val is None:
return
try: try:
val = int(val) val = int(val)
return val return val
@@ -227,35 +262,98 @@ class MegaOutPort(MegaPushEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}) val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None:
if val is None and self._state is not None:
return self._state == 'ON' return self._state == 'ON'
elif isinstance(self.port, str) and 'e' in self.port and val:
if val is None:
return
if self.dimmer:
val = safe_int(val)
return val > 0 if not self.invert else val == 0
else:
return val == 'ON' if not self.invert else val == 'OFF'
elif val is not None: elif val is not None:
val = val.get("value") val = val.get("value")
if not isinstance(val, str) and self.index is not None and self.addr is not None:
if not isinstance(val, dict):
self.mega.lg.warning(f'{self.entity_id}: {val} is not a dict')
return
_val = val.get(self.addr, val.get(self.addr.lower(), val.get(self.addr.upper())))
if not isinstance(_val, str):
self.mega.lg.warning(f'{self.entity_id}: can not get {self.addr} from {val}, recieved {_val}')
return
_val = _val.split('/')
if len(_val) >= 2:
self.mega.lg.debug('%s parsed values: %s[%s]="%s"', self.entity_id, _val, self.index, _val)
val = _val[self.index]
else:
self.mega.lg.warning(f'{self.entity_id}: {_val} has wrong length')
return
elif self.index is not None and self.addr is None:
self.mega.lg.warning(f'{self.entity_id} does not has addr')
return
self.mega.lg.debug('%s.state = %s', self.entity_id, val)
if not self.invert: if not self.invert:
return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0) return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0)
else: else:
return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0) return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0)
@property
def cmd_port(self):
if self.index is not None:
return f'{self.port}A' if self.index == 0 else f'{self.port}B'
else:
return self.port
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 or 255 brightness = brightness or self.brightness or 255
self._brightness = brightness
if self.dimmer and brightness == 0: if self.dimmer and brightness == 0:
cmd = 255 cmd = 255 * self.dimmer_scale
elif self.dimmer: elif self.dimmer:
cmd = brightness cmd = brightness * self.dimmer_scale
else: else:
cmd = 1 if not self.invert else 0 cmd = 1 if not self.invert else 0
await self.mega.request(cmd=f"{self.port}:{cmd}") _cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
self.mega.values[self.port] = {'value': cmd} if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
port=self.port,
force_http=True,
conv=False,
http_cmd='list',
)
elif isinstance(self.port, str) and 'e' in self.port:
if not self.dimmer:
self.mega.values[self.port] = 'ON' if not self.invert else 'OFF'
else:
self.mega.values[self.port] = cmd
else:
self.mega.values[self.port] = {'value': cmd}
await self.get_state() await self.get_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
cmd = "0" if not self.invert else "1" cmd = "0" if not self.invert else "1"
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
await self.mega.request(cmd=f"{self.port}:{cmd}") if self.addr:
self.mega.values[self.port] = {'value': cmd} _cmd['addr'] = self.addr
await self.mega.request(**_cmd)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
port=self.port,
force_http=True,
conv=False,
http_cmd='list',
)
elif isinstance(self.port, str) and 'e' in self.port:
self.mega.values[self.port] = 'OFF' if not self.invert else 'ON'
else:
self.mega.values[self.port] = {'value': cmd}
await self.get_state() await self.get_state()

View File

@@ -15,3 +15,7 @@ class DuplicateId(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth.""" """Error to indicate there is invalid auth."""
class NoPort(Exception):
pass

View File

@@ -8,16 +8,17 @@ from aiohttp.web_request import Request
from aiohttp.web_response import Response from aiohttp.web_response import Response
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_RESPONSE_TEMPLATE
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_RESPONSE_TEMPLATE
from .tools import make_ints from .tools import make_ints
from . import hub as h from . import hub as h
_LOGGER = logging.getLogger(__name__).getChild('http') _LOGGER = logging.getLogger(__name__).getChild('http')
ext = {f'ext{x}' for x in range(16)}
class MegaView(HomeAssistantView): class MegaView(HomeAssistantView):
"""Handle Yandex Smart Home unauthorized requests."""
url = '/mega' url = '/mega'
name = 'mega' name = 'mega'
@@ -25,33 +26,55 @@ class MegaView(HomeAssistantView):
def __init__(self, cfg: dict): def __init__(self, cfg: dict):
self._try = 0 self._try = 0
self.allowed_hosts = {'::1'} self.protected = True
self.allowed_hosts = {'::1', '127.0.0.1'}
self.notified_attempts = defaultdict(lambda : False)
self.callbacks = defaultdict(lambda: defaultdict(list)) self.callbacks = defaultdict(lambda: defaultdict(list))
self.templates: typing.Dict[str, typing.Dict[str, Template]] = { self.templates: typing.Dict[str, typing.Dict[str, Template]] = {
mid: { mid: {
pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE] pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE]
for pt in cfg[mid] for pt in cfg[mid]
if CONF_RESPONSE_TEMPLATE in cfg[mid][pt] if isinstance(pt, int) and CONF_RESPONSE_TEMPLATE in cfg[mid][pt]
} for mid in cfg } for mid in cfg if isinstance(cfg[mid], dict)
} }
_LOGGER.debug('templates: %s', self.templates) _LOGGER.debug('templates: %s', self.templates)
self.hubs = {}
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
_LOGGER.debug('request from %s %s', request.remote, request.headers)
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'] hass: HomeAssistant = request.app['hass']
hub: 'h.MegaD' = hass.data.get(DOMAIN).get(request.remote) # TODO: проверить какой remote if self.protected:
if hub is None and request.remote == '::1': auth = False
hub = hass.data.get(DOMAIN).get('__def') for x in self.allowed_hosts:
if hub is None: if request.remote.startswith(x):
auth = True
break
if not auth:
msg = f"Non-authorised request from {request.remote} to `/mega`. "\
f"If you want to accept requests from this host "\
f"please add it to allowed hosts in `mega` UI-configuration"
if not self.notified_attempts[request.remote]:
await hass.services.async_call(
'persistent_notification',
'create',
{
"notification_id": request.remote,
"title": "Non-authorised request",
"message": msg
}
)
_LOGGER.warning(msg)
return Response(status=401)
remote = request.headers.get('X-Real-IP', request.remote)
hub: 'h.MegaD' = self.hubs.get(remote)
if hub is None and 'mdid' in request.query:
hub = self.hubs.get(request.query['mdid'])
if hub is None:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
if hub is None and request.remote in ['::1', '127.0.0.1']:
hub = self.hubs.get('__def')
elif hub is None:
return Response(status=400) return Response(status=400)
data = dict(request.query) data = dict(request.query)
hass.bus.async_fire( hass.bus.async_fire(
@@ -60,26 +83,61 @@ class MegaView(HomeAssistantView):
) )
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote) _LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data) make_ints(data)
if data.get('st') == '1' and hub.restore_on_restart:
asyncio.create_task(self.later_restore(hub))
return Response(status=200)
port = data.get('pt') port = data.get('pt')
data = data.copy() data = data.copy()
update_all = True
if 'v' in data:
update_all = False
data['value'] = data.pop('v')
data['mega_id'] = hub.id data['mega_id'] = hub.id
ret = 'd' ret = 'd' if hub.force_d else ''
if port is not None: if port is not None:
hub.values[port] = data if set(data).issubset(ext):
for cb in self.callbacks[hub.id][port]: ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
cb(data) for e in ext:
template: Template = self.templates.get(hub.id, {}).get(port) if e in data:
if hub.update_all: idx = e[-1]
pt = f'{port}e{idx}'
data['value'] = 'ON' if data[e] == '1' else 'OFF'
data['m'] = 1 if data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
hub.values[pt] = data
for cb in self.callbacks[hub.id][pt]:
cb(data)
else:
hub.values[port] = data
for cb in self.callbacks[hub.id][port]:
cb(data)
template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response)
if template is not None:
template.hass = hass
ret = template.async_render(data)
if hub.update_all and update_all:
asyncio.create_task(self.later_update(hub)) 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) _LOGGER.debug('response %s', ret)
ret = Response(body=ret or 'd', content_type='text/plain', headers={'Server': 's', 'Date': 'n'}) Response(body='' if hub.fake_response else ret, content_type='text/plain')
if hub.fake_response and 'value' not in data and 'pt' in data:
if 'd' in ret:
await hub.request(pt=port, cmd=ret)
else:
await hub.request(cmd=ret)
return ret return ret
async def later_restore(self, hub):
"""
Восстановление всех выходов с небольшой задержкой. Задержка нужна чтобы ответ прошел успешно
:param hub:
:return:
"""
await asyncio.sleep(0.2)
await hub.restore_states()
async def later_update(self, hub): async def later_update(self, hub):
_LOGGER.debug('force update')
await asyncio.sleep(1) await asyncio.sleep(1)
_LOGGER.debug('force update')
await hub.updater.async_refresh() await hub.updater.async_refresh()

View File

@@ -10,15 +10,20 @@ import json
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, DEVICE_CLASS_PRESSURE, from homeassistant.const import (
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX) DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import TEMP, HUM, PRESS, LUX, PATT_SPLIT, DOMAIN, CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_SKIP, \ from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
CONF_FORCE_D from .const import (
from .entities import set_events_off, BaseMegaEntity TEMP, HUM, PRESS,
from .exceptions import CannotConnect LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW
)
from .entities import set_events_off, BaseMegaEntity, MegaOutPort
from .exceptions import CannotConnect, NoPort
from .tools import make_ints from .tools import make_ints
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)') TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
@@ -44,13 +49,11 @@ CLASSES = {
LUX: DEVICE_CLASS_ILLUMINANCE LUX: DEVICE_CLASS_ILLUMINANCE
} }
I2C_DEVICE_TYPES = { I2C_DEVICE_TYPES = {
"2": LUX, # BH1750 "2": LUX, # BH1750
"3": LUX, # TSL2591 "3": LUX, # TSL2591
"7": LUX, # MAX44009 "7": LUX, # MAX44009
"70": LUX, # OPT3001 "70": LUX, # OPT3001
} }
class NoPort(Exception):
pass
class MegaD: class MegaD:
@@ -70,9 +73,14 @@ class MegaD:
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
nports=38, nports=38,
inverted: typing.List[int] = None, update_all: bool=True,
update_all=True, poll_outs: bool=False,
poll_outs=False, fake_response: bool=True,
force_d: bool=None,
allow_hosts: str=None,
protected=True,
restore_on_restart=False,
extenders=None,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
@@ -80,11 +88,18 @@ class MegaD:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP) self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None: if not self.http is None:
self.http.allowed_hosts |= {host} self.http.allowed_hosts |= {host}
self.http.hubs[host] = self
if len(self.http.hubs) == 1:
self.http.hubs['__def'] = self
if mqtt_id:
self.http.hubs[mqtt_id] = self
else: else:
self.http = None self.http = None
self.extenders = extenders or []
self.poll_outs = poll_outs self.poll_outs = poll_outs
self.update_all = update_all if update_all is not None else True self.update_all = update_all if update_all is not None else True
self.nports = nports self.nports = nports
self.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None self.loop: asyncio.AbstractEventLoop = None
self.hass = hass self.hass = hass
@@ -99,6 +114,7 @@ class MegaD:
self.cnd = asyncio.Condition() self.cnd = asyncio.Condition()
self.online = True self.online = True
self.entities: typing.List[BaseMegaEntity] = [] self.entities: typing.List[BaseMegaEntity] = []
self.ds2413_ports = set()
self.poll_interval = scan_interval self.poll_interval = scan_interval
self.subs = None self.subs = None
self.lg: logging.Logger = lg.getChild(self.id) self.lg: logging.Logger = lg.getChild(self.id)
@@ -114,16 +130,29 @@ class MegaD:
self.updater = DataUpdateCoordinator( self.updater = DataUpdateCoordinator(
hass, hass,
self.lg, self.lg,
name="sensors", name="megad",
update_method=self.poll, update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None, update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
) )
self.updaters = []
self.fw = ''
self.notifiers = defaultdict(asyncio.Condition) self.notifiers = defaultdict(asyncio.Condition)
if not mqtt_id: if not mqtt_id:
_id = host.split(".")[-1] _id = host.split(".")[-1]
self.mqtt_id = f"megad/{_id}" self.mqtt_id = f"megad/{_id}"
else: else:
self.mqtt_id = mqtt_id self.mqtt_id = mqtt_id
self.restore_on_restart = restore_on_restart
if force_d is not None:
self.customize[CONF_FORCE_D] = force_d
try:
if allow_hosts is not None:
allow_hosts = set(allow_hosts.split(';'))
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
hass.data[DOMAIN][CONF_HTTP].protected = protected
except Exception:
self.lg.exception('while setting allowed hosts')
async def start(self): async def start(self):
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
@@ -168,6 +197,10 @@ class MegaD:
def force_d(self): def force_d(self):
return self.customize.get(CONF_FORCE_D, False) return self.customize.get(CONF_FORCE_D, False)
@property
def def_response(self):
return self.customize.get(CONF_DEF_RESPONSE, None)
@property @property
def is_online(self): def is_online(self):
return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10) return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
@@ -189,12 +222,31 @@ class MegaD:
) )
self.online = True self.online = True
async def _get_ds2413(self):
"""
обновление ds2413 устройств
:return:
"""
for x in self.ds2413_ports:
self.lg.debug(f'poll ds2413 for %s', x)
await self.get_port(
port=x,
force_http=True,
http_cmd='list',
conv=False
)
async def poll(self): async def poll(self):
""" """
Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes Polling ports
offline
""" """
self.lg.debug('poll') self.lg.debug('poll')
for x in self.extenders:
ret = await self._update_extender(x)
if not isinstance(ret, dict):
self.lg.warning(f'wrong updater result: {ret} from extender {x}')
continue
self.values.update(ret)
if self.mqtt is None: if self.mqtt is None:
await self.get_all_ports() await self.get_all_ports()
await self.get_sensors(only_list=True) await self.get_sensors(only_list=True)
@@ -204,6 +256,7 @@ class MegaD:
await self.get_sensors() await self.get_sensors()
else: else:
await self.get_port(self.port_to_scan) await self.get_port(self.port_to_scan)
await self._get_ds2413()
return self.values return self.values
async def get_mqtt_id(self): async def get_mqtt_id(self):
@@ -217,12 +270,18 @@ class MegaD:
_id = _id['value'] _id = _id['value']
return _id or 'megad/' + self.host.split('.')[-1] return _id or 'megad/' + self.host.split('.')[-1]
async def get_fw(self):
data = await self.request()
return PATT_FW.search(data).groups()[0]
async def send_command(self, port=None, cmd=None): async def send_command(self, port=None, cmd=None):
return await self.request(pt=port, cmd=cmd) return await self.request(pt=port, cmd=cmd)
async def request(self, **kwargs): async def request(self, **kwargs):
cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None]) cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
url = f"http://{self.host}/{self.sec}/?{cmd}" url = f"http://{self.host}/{self.sec}"
if cmd:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url) self.lg.debug('request: %s', url)
async with self._http_lck: async with self._http_lck:
async with aiohttp.request("get", url=url) as req: async with aiohttp.request("get", url=url) as req:
@@ -237,13 +296,18 @@ class MegaD:
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
def parse_response(self, ret): def parse_response(self, ret, cmd='get'):
if ret is None: if ret is None:
raise NoPort() raise NoPort()
if 'busy' in ret: if 'busy' in ret:
return None return None
if ':' in ret: if ':' in ret:
ret = PATT_SPLIT.split(ret) if ';' in ret:
ret = ret.split(';')
elif '/' in ret and not cmd == 'list':
ret = ret.split('/')
else:
ret = [ret]
ret = {'value': dict([ ret = {'value': dict([
x.split(':') for x in ret if x.count(':') == 1 x.split(':') for x in ret if x.count(':') == 1
])} ])}
@@ -255,17 +319,17 @@ class MegaD:
ret = {'value': ret} ret = {'value': ret}
return ret return ret
async def get_port(self, port, force_http=False, http_cmd='get'): async def get_port(self, port, force_http=False, http_cmd='get', conv=True):
""" """
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values хранилище values
""" """
self.lg.debug(f'get port %s', port) self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http: if self.mqtt is None or force_http:
if http_cmd == 'list': if http_cmd == 'list' and conv:
await self.request(pt=port, cmd='conv') await self.request(pt=port, cmd='conv')
await asyncio.sleep(1) await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd)) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ntry = 0 ntry = 0
while http_cmd == 'list' and ret is None and ntry < 3: while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1) await asyncio.sleep(1)
@@ -298,6 +362,8 @@ class MegaD:
if not self.mqtt_inputs: if not self.mqtt_inputs:
ret = await self.request(cmd='all') ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')): for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports:
continue
if check_skip and not port in self.ports: if check_skip and not port in self.ports:
continue continue
ret = self.parse_response(x) ret = self.parse_response(x)
@@ -383,70 +449,116 @@ class MegaD:
return await req.text() return await req.text()
async def scan_port(self, port): async def scan_port(self, port):
async with self.lck: data = await self.request(pt=port)
if port in self._scanned: return parse_config(data)
return self._scanned[port] # async with self.lck:
url = f'http://{self.host}/{self.sec}/?pt={port}' # if port in self._scanned:
self.lg.debug( # return self._scanned[port]
f'scan port %s: %s', port, url # url = f'http://{self.host}/{self.sec}/?pt={port}'
) # self.lg.debug(
async with aiohttp.request('get', url) as req: # f'scan port %s: %s', port, url
html = await req.text() # )
if req.status != 200: # async with aiohttp.request('get', url) as req:
return # html = await req.text()
tree = BeautifulSoup(html, features="lxml") # if req.status != 200:
pty = tree.find('select', attrs={'name': 'pty'}) # return
if pty is None: # tree = BeautifulSoup(html, features="lxml")
return # pty = tree.find('select', attrs={'name': 'pty'})
else: # if pty is None:
pty = pty.find(selected=True) # return
if pty: # else:
pty = pty['value'] # pty = pty.find(selected=True)
else: # if pty:
return # pty = pty['value']
if pty in ['0', '1']: # else:
m = tree.find('select', attrs={'name': 'm'}) # return
if m: # if pty in ['0', '1']:
m = m.find(selected=True)['value'] # m = tree.find('select', attrs={'name': 'm'})
self._scanned[port] = (pty, m) # if m:
return pty, m # m = m.find(selected=True)['value']
elif pty == '3': # self._scanned[port] = (pty, m)
m = tree.find('select', attrs={'name': 'd'}) # return pty, m
if m: # elif pty == '3':
m = m.find(selected=True)['value'] # m = tree.find('select', attrs={'name': 'd'})
self._scanned[port] = (pty, m) # if m:
return pty, m # m = m.find(selected=True)['value']
elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно # self._scanned[port] = (pty, m)
m = tree.find('select', attrs={'name': 'd'}) # return pty, m
if m: # elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
m = m.find(selected=True)['value'] # m = tree.find('select', attrs={'name': 'd'})
self._scanned[port] = (pty, m or '0') # if m:
return pty, m or '0' # m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m or '0')
# return pty, m or '0'
async def scan_ports(self, nports=37): async def scan_ports(self, nports=37):
for x in range(0, nports+1): for x in range(0, nports+1):
ret = await self.scan_port(x) ret = await self.scan_port(x)
if ret: if ret:
yield [x, *ret] yield x, ret
self.nports = nports+1 self.nports = nports+1
async def _update_extender(self, port):
"""
Обновление mcp230, так же подходит для PCA9685
:param port:
:return:
"""
values = await self.request(pt=port, cmd='get')
ret = {}
for i, x in enumerate(values.split(';')):
ret[f'{port}e{i}'] = x
return ret
async def get_config(self, nports=37): async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list)) ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports(nports): ret['mqtt_id'] = await self.get_mqtt_id()
if pty == "0": ret['extenders'] = extenders = []
async for port, cfg in self.scan_ports(nports):
if cfg.pty == "0":
ret['binary_sensor'][port].append({}) ret['binary_sensor'][port].append({})
elif pty == "1" and (m in ['0', '1', '3'] or m is None): elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
ret['light'][port].append({'dimmer': m == '1'}) ret['light'][port].append({'dimmer': cfg.m == '1'})
elif pty in ('3', '2', '4'): elif cfg == DS2413:
try: # ds2413
http_cmd = 'get' _data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
data = _data.get('value', {})
if not isinstance(data, dict):
self.lg.warning(f'can not add ds2413 on port {port}, it has wrong data: {_data}')
continue
for addr, state in data.items():
ret['light'][port].extend([
{"index": 0, "addr": addr, "id_suffix": f'{addr}_a', "http_cmd": 'ds2413'},
{"index": 1, "addr": addr, "id_suffix": f'{addr}_b', "http_cmd": 'ds2413'},
])
elif cfg == MCP230:
extenders.append(port)
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page)
if ext_cfg.ety == '1':
ret['light'][f'{port}e{n}'].append({})
elif ext_cfg.ety == '0':
ret['binary_sensor'][f'{port}e{n}'].append({})
elif cfg == PCA9685:
extenders.append(port)
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty in ('3', '2', '4'):
http_cmd = 'get'
if cfg.d == '5' and cfg.pty == '3':
# 1-wire bus
values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list'
else:
values = await self.get_port(port, force_http=True) values = await self.get_port(port, force_http=True)
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')): 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') values = await self.get_port(port, force_http=True, http_cmd='list')
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) self.lg.debug(f'values: %s', values)
if values is None: if values is None:
self.lg.warning(f'port {port} is of type sensor but response is None, skipping it') self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
@@ -456,8 +568,8 @@ class MegaD:
if isinstance(values, str) and TEMP_PATT.search(values): if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values} values = {TEMP: values}
elif not isinstance(values, dict): elif not isinstance(values, dict):
if pty == '4' and m in I2C_DEVICE_TYPES: if cfg.pty == '4' and cfg.d in I2C_DEVICE_TYPES:
values = {I2C_DEVICE_TYPES[m]: values} values = {I2C_DEVICE_TYPES.get(cfg.m): values}
else: else:
values = {None: values} values = {None: values}
for key in values: for key in values:
@@ -471,4 +583,12 @@ class MegaD:
)) ))
return ret return ret
async def restore_states(self):
for x in self.entities:
if isinstance(x, MegaOutPort):
if x.is_on:
await x.async_turn_on(brightness=x.brightness)
else:
await x.async_turn_off()

View File

@@ -15,5 +15,5 @@
"@andvikt" "@andvikt"
], ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues", "issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v0.3.17" "version": "v0.5.1b1"
} }

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID, CONF_ID,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
@@ -102,7 +102,6 @@ class Mega1WSensor(MegaPushEntity):
unit_of_measurement, unit_of_measurement,
device_class, device_class,
key=None, key=None,
http_cmd='get',
*args, *args,
**kwargs **kwargs
): ):
@@ -118,7 +117,6 @@ class Mega1WSensor(MegaPushEntity):
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self.mega.sensors.append(self) self.mega.sensors.append(self)
self.http_cmd = http_cmd
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@@ -165,7 +163,7 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret) ret = str(ret)
except: except:
ret = None ret = None
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE) tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if tmpl is not None and self.hass is not None: if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret}) ret = tmpl.async_render({'value': ret})

View File

@@ -15,6 +15,11 @@
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]", "mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]", "nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]", "update_all": "[%key:common::config_flow::data::update_all%]",
"fake_response": "[%key:common::config_flow::data::fake_response%]",
"force_d": "[%key:common::config_flow::data::force_d%]",
"protected": "[%key:common::config_flow::data::protected%]",
"allow_hosts": "[%key:common::config_flow::data::allow_hosts%]",
"restore_on_restart": "[%key:common::config_flow::data::restore_on_restart%]",
"poll_outs": "[%key:common::config_flow::data::poll_outs%]" "poll_outs": "[%key:common::config_flow::data::poll_outs%]"
} }
} }

View File

@@ -1,5 +1,6 @@
_params = ['m', 'click', 'cnt', 'pt'] _params = ['m', 'click', 'cnt', 'pt']
def make_ints(d: dict): def make_ints(d: dict):
for x in _params: for x in _params:
try: try:

View File

@@ -22,7 +22,12 @@
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)", "port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"nports": "Number of ports", "nports": "Number of ports",
"update_all": "Update all outs when input", "update_all": "Update all outs when input",
"mqtt_inputs": "Use MQTT", "mqtt_inputs": "Use MQTT (Deprecated)",
"fake_response": "Fake response",
"force_d": "Force 'd' response",
"protected": "Protected",
"allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart",
"poll_outs": "Poll outs" "poll_outs": "Poll outs"
} }
} }
@@ -35,8 +40,13 @@
"scan_interval": "Scan interval (sec), 0 - don't update", "scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)", "port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"reload": "Reload objects", "reload": "Reload objects",
"mqtt_inputs": "Use MQTT", "mqtt_inputs": "Use MQTT (Deprecated)",
"update_all": "Update all outs when input", "update_all": "Update all outs when input",
"fake_response": "Fake response",
"force_d": "Force 'd' response",
"protected": "Protected",
"allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart",
"poll_outs": "Poll outs" "poll_outs": "Poll outs"
} }
} }

View File

@@ -19,9 +19,14 @@
"mqtt_id": "MQTT id", "mqtt_id": "MQTT id",
"scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять", "scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять",
"port_to_scan": "Порт, который сканируется когда нет датчиков", "port_to_scan": "Порт, который сканируется когда нет датчиков",
"mqtt_inputs": "Использовать MQTT", "mqtt_inputs": "Использовать MQTT (Не рекомендуется)",
"nports": "Кол-во портов", "nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход", "update_all": "Обновить все выходы когда срабатывает вход",
"fake_response": "Имитация http-ответа",
"force_d": "Ответ 'd' по умолчанию",
"protected": "Блокировать неразрешенные соединения",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"allow_hosts": "Разрешенные ip (через ;)",
"poll_outs": "Обновлять выходы (регулярно)" "poll_outs": "Обновлять выходы (регулярно)"
} }
} }
@@ -35,10 +40,15 @@
"port_to_scan": "Порт, который сканируется когда нет датчиков", "port_to_scan": "Порт, который сканируется когда нет датчиков",
"reload": "Обновить объекты", "reload": "Обновить объекты",
"invert": "Список портов (через ,) с инвертированной логикой", "invert": "Список портов (через ,) с инвертированной логикой",
"mqtt_inputs": "Использовать MQTT", "mqtt_inputs": "Использовать MQTT (Не рекомендуется)",
"nports": "Кол-во портов", "fake_response": "Имитация http-ответа",
"update_all": "Обновить все выходы когда срабатывает вход", "force_d": "Ответ 'd' по умолчанию",
"poll_outs": "Обновлять выходы (регулярно)" "nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход",
"protected": "Блокировать неразрешенные соединения",
"allow_hosts": "Разрешенные ip (через ;)",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"poll_outs": "Обновлять выходы (регулярно)"
} }
} }
} }

View File

@@ -19,9 +19,14 @@
"mqtt_id": "MQTT id", "mqtt_id": "MQTT id",
"scan_interval": "Період оновлення (сек.), 0 - не оновлювати", "scan_interval": "Період оновлення (сек.), 0 - не оновлювати",
"port_to_scan": "Порт для сканування при відсутності датчиків", "port_to_scan": "Порт для сканування при відсутності датчиків",
"mqtt_inputs": "Використовувати MQTT", "mqtt_inputs": "Використовувати MQTT (Deprecated)",
"nports": "Кількість портів", "nports": "Кількість портів",
"update_all": "Оновити всі виходи коли спрацьовує вхід", "update_all": "Оновити всі виходи коли спрацьовує вхід",
"fake_response": "Імітація http-відповіді",
"force_d": "Неявно відповідати 'd'",
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи" "poll_outs": "Оновити виходи"
} }
} }
@@ -31,13 +36,18 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"scan_interval": "Період оновлення (сек.)", "scan_interval": "Період оновлення (сек.)",
"port_to_scan": "Порт для сканування при відсутності датчиків", "port_to_scan": "Порт для сканування при відсутності датчиків",
"reload": "Оновити об'єкти", "reload": "Оновити об'єкти",
"invert": "Список портів з інвертованою логікою (через ,)", "invert": "Список портів з інвертованою логікою (через ,)",
"mqtt_inputs": "Використовувати MQTT", "mqtt_inputs": "Використовувати MQTT (Deprecated)",
"nports": "Кількість портів", "nports": "Кількість портів",
"update_all": "Оновити всі виходи коли спрацьовує вхід", "fake_response": "Імітація http-відповіді",
"force_d": "Неявно відповідати 'd'",
"update_all": "Оновити всі виходи коли спрацьовує вхід",
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи" "poll_outs": "Оновити виходи"
} }
} }

237
readme.md
View File

@@ -4,25 +4,45 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
[![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://yoomoney.ru/to/410013955329136) [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://yoomoney.ru/to/410013955329136)
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561) Интеграция с [MegaD-2561, MegaD-328](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
Предложения по доработкам просьба писать в [discussions](https://github.com/andvikt/mega_hacs/discussions), о проблемах
создавать [issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
## Основные особенности: ## Основные особенности:
- Настройка в веб-интерфейсе + yaml - Настройка в веб-интерфейсе + [yaml](https://github.com/andvikt/mega_hacs/wiki/Кастомизация)
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`) `sensor`)
- Возможность работы с несколькими megad - Возможность работы с несколькими megad
- Обратная связь по mqtt или http (на выбор) - Автоматическое восстановление состояний выходов после перезагрузки контроллера
- События на двойные/долгие нажатия - Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt
будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей. выполнении предыдущей.
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1)
## Установка ## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation): Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
HACS - Integrations - Explore, в поиске ищем MegaD. HACS - Integrations - Explore, в поиске ищем MegaD.
Чтобы включить возможность использования бета-версий, зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии"
Обновления выполняются так же в меню HACS.
Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений
нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию`
Альтернативный способ установки: Альтернативный способ установки:
```shell ```shell
# из папки с конфигом # из папки с конфигом
@@ -35,212 +55,23 @@ wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса. Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml
# configuration.yaml
mega:
hello: # ID меги, как в UI
7: # номер порта
domain: switch # тип устройства (switch или light, по умолчанию для цифровых выходов используется light)
invert: true # инвертировать или нет (по умолчанию false)
name: Насос # имя устройства
8:
# исключить из сканирования
skip: true
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
# можно так же указать шаблон для конвертации значения, может быть полезно для ацп-входа
# текущее значение порта передается в шаблон в переменной "value"
conv_template: "{{(value|float)/100}}"
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`
**Внимание!** Не используйте srv loop на контроллере, это может приводить к ложным срабатываниям входов. Вместо srv loop
интеграция будет сама обновлять все состояния портов с заданным интервалом
В самой меге необходимо прописать настройки: В самой меге необходимо прописать настройки:
```yaml ```yaml
srv: "192.168.1.4:8123" # ip:port вашего HA srv: "192.168.1.4:8123" # ip:port вашего HA
script: "mega" # это api интеграции, к которому будет обращаться контроллер script: "mega" # это api интеграции, к которому будет обращаться контроллер
``` ```
Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным.
#### Ответ на входящие события от контроллера При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает объекты`
запустить то что прописано в поле act в настройках порта).
Поддерживаются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не ## Зависимости
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id) Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
Примеры: Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
```yaml из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
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 %}
```
Начиная с версии v0.3.17 ответ можно слать так же и в режиме MQTT. Аналогично, темплейт должен возвращать готовую команду
такую же как требует команда cmd, так же можно использовать d, но d не отправляется по умолчанию, это сделано чтобы не
сломать текущую логику у пользователей предыдущих версий. Чтобы включить для всех входов в режиме mqtt отправку команды
d необходимо в конфиге прописать следующее:
```yaml
mega:
mega1:
force_d: true
```
**Внимание!** Нельзя использовать чекбокс напротив поля act если планируется использовать ответ сервера - у вас и
сработает act и команда от сервера, а вслучае ответа d сработает act два раза.
## binary_sensor и события Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
Входы будут доступны как binary_sensor, а так же в виде событий `mega.sensor` и `mega.binary`.
Для корректной работы binary_sensor имеет смысл использовать режим P&R, для остальных режимов - лучше пользоваться
событиями.
События можно использовать в автоматизациях, например так:
```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
```
События могут содержать следующие поля:
- `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`: двойной клик
**гарантия есть только при использовании http-метода синхронизации, mqtt не гарантирует порядок доставки сообщений, хотя
маловероятно, что порядок будет нарушен, но все же сам протокол не дает таких гарантий.
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `mega.sensor`, понажимать кнопки.
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml
mega.save:
description: Сохраняет текущее состояние портов (?cmd=s)
fields:
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"
```
## Отладка
Интеграция находится в активной разработке, при возникновении проблем [заводите issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
Просьба прикладывать детальный лог, который можно включить в конфиге так:
```yaml
logger:
default: info
logs:
custom_components.mega: debug
```
#### Отладка ответов http-сервера
Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли
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`, либо скрипт, который вы настроили