Compare commits

..

90 Commits

Author SHA1 Message Date
Andrey
a0900052dc fix bugs 2021-03-05 00:02:43 +03:00
Andrey
b8d355f412 fix bugs 2021-03-04 22:54:21 +03:00
Andrey
d3f76a88df fix bugs 2021-03-04 22:52:15 +03:00
Andrey
fa2bb2674e fix updater 2021-03-04 21:52:25 +03:00
Andrey
a1ae4e294b fix pop 2021-03-04 21:28:01 +03:00
Andrey
81d85ba1ed remove crap entities 2021-03-04 20:45:42 +03:00
Andrey
7135bb273e add sync_time 2021-03-04 20:25:59 +03:00
Andrey
7d8554a7aa add force_i2c, add new port namings 2021-03-04 19:45:37 +03:00
Andrey
40dcadc109 fix ptsensor 2021-03-04 16:52:07 +03:00
Andrey
35f99877ca add delay on ptsensor 2021-03-04 16:49:02 +03:00
Andrey
6c50b81bff remove scl 2021-03-04 16:22:33 +03:00
Andrey
c810693ba5 remove scl 2021-03-04 16:21:59 +03:00
Andrey
1fd321d4c1 fix errors 2021-03-04 15:01:00 +03:00
Andrey
6732e1b7a2 add more logs on i2c update process 2021-03-03 13:55:35 +03:00
Andrey
f784c1c261 edit readme 2021-03-03 13:15:05 +03:00
Andrey
ab572c4db5 edit readme 2021-03-03 12:34:08 +03:00
Andrey
e55184f565 edit readme 2021-03-03 12:20:44 +03:00
Andrey
3f74f9739c edit readme 2021-03-03 12:20:09 +03:00
Andrey
49fcf880d9 add i2c sensors 2021-03-03 12:01:02 +03:00
Andrey
289f52ef73 fix errors 2021-02-28 22:27:57 +03:00
Andrey
ce589c97b9 fix errors 2021-02-28 22:22:17 +03:00
Andrey
22a6f8f444 fix errors 2021-02-28 22:06:36 +03:00
Andrey
9a53de1d5d fix errors 2021-02-28 22:02:22 +03:00
Andrey
bd8b07dd90 fix errors 2021-02-28 21:52:34 +03:00
Andrey
d9b6ba3a50 fix errors 2021-02-28 21:52:06 +03:00
Andrey
1042592a31 fix errors 2021-02-28 21:33:26 +03:00
Andrey
137eb8b6ba fix errors 2021-02-28 21:15:48 +03:00
Andrey
a2f412b89e fix errors 2021-02-28 20:16:04 +03:00
Andrey
8fa14cdbc5 fix errors 2021-02-28 20:15:47 +03:00
Andrey
fc17b82021 support response for extenders 2021-02-28 15:01:39 +03:00
Andrey
1aeaabfb3c fix errors 2021-02-28 14:50:47 +03:00
Andrey
a0bd8acac0 fix errors 2021-02-28 14:49:38 +03:00
andvikt
c48a3632d2 Update http.py 2021-02-28 13:04:00 +03:00
Andrey
e06ba65ead fix errors 2021-02-28 09:49:16 +03:00
Andrey
22720a27bd fix errors 2021-02-28 09:46:06 +03:00
Andrey
d0769b5b02 fix errors 2021-02-27 17:39:54 +03:00
Andrey
9ae093dd91 fix errors 2021-02-27 14:44:20 +03:00
Andrey
f88109c3a6 fix int(port) 2021-02-26 16:18:05 +03:00
Andrey
8742bb975f поддержка PCA9685 и MCP230 2021-02-25 12:11:05 +03:00
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
22 changed files with 1154 additions and 455 deletions

View File

@@ -1,32 +1,121 @@
import asyncio
from urllib.parse import urlparse, parse_qsl
from bs4 import BeautifulSoup
page = '''
<html><head></head><body><a href="/sec/?pt=33">Back</a><br>0x15 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=t67xx">T67XX</a><br>0x40 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=htu21d">HTU21D</a>/PCA9685/HM3301<br>0x4a - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=max44009">MAX44009</a><br>
</body></html>
'''
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
)
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()
def parse_scan_page(page: str):
ret = []
req = []
page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'):
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
if 'i2c_dev' in params:
dev = params['i2c_dev']
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
else:
suffix = ''
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
_dev = f'{dev}_{suffix}'
else:
_dev = dev
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
})
req.append(params)
return req, ret
async def main():
server = await asyncio.start_server(
handle_echo, '127.0.0.1', 8888)
class Skip:
pass
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
class Request:
pass
asyncio.run(main())
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
],
'ptsensor': [
Request, # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
]
}
print(parse_scan_page(page))

View File

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

View File

@@ -7,48 +7,74 @@ import voluptuous as vol
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass
from homeassistant.helpers.template import Template
from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, 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_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN
from .hub import MegaD
from .config_flow import ConfigFlow
from .http import MegaView
_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_DEVICE_CLASS):
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,
vol.Optional(CONF_FORCE_I2C_SCAN): bool,
}
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(
{
DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
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,
vol.Optional(CONF_CONV_TEMPLATE): cv.template
}
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional(
CONF_DEF_RESPONSE,
description='Ответ по умолчанию',
default=None
): vol.Any(cv.template, None),
vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
CUSTOMIZE_PORT,
CUSTOMIZE_DS2413,
)
}
}
},
@@ -115,8 +141,8 @@ async def get_hub(hass, entry):
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
hub.fw = await hub.get_fw()
hass.data[DOMAIN][id] = hub
hass.data[DOMAIN][CONF_ALL][id] = hub
if not await hub.authenticate():
raise Exception("not authentificated")
@@ -161,7 +187,7 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id)
hub: MegaD = hass.data[DOMAIN][id]
hub: MegaD = hass.data[DOMAIN].get(id)
if hub is None:
return
_LOGGER.debug(f'remove {id}')

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP, CONF_INVERT, CONF_RESPONSE_TEMPLATE
from .entities import MegaPushEntity
from .hub import MegaD
from .tools import int_ignore
lg = logging.getLogger(__name__)
@@ -51,12 +51,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('binary_sensor', {}).items():
port = int(port)
port = int_ignore(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)
if '<' in sensor.name:
continue
devices.append(sensor)
async_add_devices(devices)
@@ -67,6 +69,10 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
super().__init__(*args, **kwargs)
self._is_on = None
self._attrs = None
self._click_task = None
async def _click(self):
await self.customize.get
@property
def state_attributes(self):
@@ -78,14 +84,15 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
@property
def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}).get("value") \
or self.mega.values.get(self.port, {}).get('m')
val = self.mega.values.get(self.port, {})
if isinstance(val, dict):
val = val.get("value", val.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' if not self.invert else val == 'OFF'
else:
if val in ['ON', 'OFF', '1', '0']:
return val in ['ON', '1'] if not self.invert else val in ['OFF', '0']
elif isinstance(val, int):
return val != 1 if not self.invert else val == 1
def _update(self, payload: dict):

View File

@@ -10,7 +10,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, 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, CONF_UPDATE_TIME, \
REMOVE_CONFIG # pylint:disable=unused-import
from .hub import MegaD
from . import exceptions
@@ -23,10 +25,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD, default="sec"): str,
vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_POLL_OUTS, default=False): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=True): bool,
# vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=False): bool,
vol.Optional(CONF_NPORTS, default=37): int,
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,
vol.Optional(CONF_UPDATE_TIME, default=True): bool,
},
)
@@ -57,7 +65,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 4
VERSION = 22
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -76,6 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await hub.stop()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
config['new_naming'] = True
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config,
@@ -107,12 +116,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Manage the options."""
new_naming = self.config_entry.data.get('new_naming', False)
hub = await get_hub(self.hass, self.config_entry.data)
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)
hub = await get_hub(self.hass, cfg)
if reload:
await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
@@ -122,7 +132,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
for x in REMOVE_CONFIG:
cfg.pop(x, None)
cfg.update(new)
cfg['new_naming'] = new_naming
return self.async_create_entry(
title='',
data=cfg,
@@ -133,11 +147,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
data_schema=vol.Schema({
vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
# vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_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_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_UPDATE_TIME, default=e.get(CONF_UPDATE_TIME, False)): bool,
# vol.Optional(CONF_INVERT, default=''): str,
}),
)

View File

@@ -0,0 +1,59 @@
from dataclasses import dataclass, field
from bs4 import BeautifulSoup
inputs = [
'eact',
'inta',
'misc',
]
selectors = [
'pty',
'm',
'gr',
'd',
'ety',
]
@dataclass(frozen=True, eq=True)
class Config:
pty: str = None
m: str = None
gr: str = None
d: str = None
ety: str = None
inta: str = field(compare=False, hash=False, default=None)
misc: str = field(compare=False, hash=False, default=None)
eact: str = field(compare=False, hash=False, default=None)
src: BeautifulSoup = field(compare=False, hash=False, default=None)
def parse_config(page: str):
page = BeautifulSoup(page, features="lxml")
ret = {}
for x in selectors:
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
for x in inputs:
v = page.find('input', attrs={'name': x})
if v:
ret[x] = v['value']
return Config(**ret, src=page)
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,19 @@ CONF_NPORTS = 'nports'
CONF_RESPONSE_TEMPLATE = 'response_template'
CONF_ACTION = 'action'
CONF_UPDATE_ALL = 'update_all'
CONF_FAKE_RESPONSE = 'fake_response'
CONF_GET_VALUE = 'get_value'
CONF_ALLOW_HOSTS = 'allow_hosts'
CONF_PROTECTED = 'protected'
CONF_CONV_TEMPLATE = 'conv_template'
CONF_POLL_OUTS = 'poll_outs'
CONF_FORCE_D = 'force_d'
CONF_DEF_RESPONSE = 'def_response'
CONF_RESTORE_ON_RESTART = 'restore_on_restart'
CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_time'
CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
CONF_UPDATE_TIME = 'update_time'
PLATFORMS = [
"light",
"switch",
@@ -46,3 +54,16 @@ PRESS = 'press'
LUX = 'lux'
SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double'
PATT_FW = re.compile(r'fw:\s(.+)\)')
REMOVE_CONFIG = [
'extenders',
'ext_in',
'ext_acts',
'i2c_sensors',
'binary_sensor',
'light',
'i2c',
'sensor',
]

View File

@@ -43,8 +43,14 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
id_suffix=None,
name=None,
unique_id=None,
http_cmd='get',
addr: str=None,
index=None,
):
super().__init__(mega.updater)
self.http_cmd = http_cmd
self._state: State = None
self.port = port
self.config_entry = config_entry
@@ -54,9 +60,21 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._lg = None
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}" + \
_pt = port if not mega.new_naming else f'{port:02}' if isinstance(port, int) else port
self._name = name or f"{mega.id}_{_pt}" + \
(f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None
self.index = index
self.addr = addr
if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port}
@property
def enabled(self):
if '<' in self.name:
return False
else:
return super().enabled
@property
def customize(self):
@@ -66,11 +84,16 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) 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
return self._customize
@property
def device_info(self):
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
return {
"identifiers": {
# Serial numbers are unique identifiers within a specific domain
@@ -79,10 +102,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
"config_entries": [
self.config_entry,
],
"name": f'{self._mega_id} port {self.port}',
"name": f'{self._mega_id} port {_pt}',
"manufacturer": 'ab-log.ru',
# "model": self.light.productname,
# "sw_version": self.light.swversion,
"sw_version": self.mega.fw,
"via_device": (DOMAIN, self._mega_id),
}
@@ -100,7 +123,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def name(self):
c = self.customize.get(CONF_NAME)
if not isinstance(c, str):
c = self._name or f"{self.mega.id}_p{self.port}"
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
c = self._name or f"{self.mega.id}_p{_pt}"
return c
@property
@@ -115,7 +139,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
_task_set_ev_on = asyncio.create_task(_set_events_on())
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()
@@ -132,6 +157,8 @@ class MegaPushEntity(BaseMegaEntity):
def __update(self, value: dict):
self._update(value)
if self.hass is None:
return
self.async_write_ha_state()
self.lg.debug(f'state after update %s', self.state)
if self.mega.mqtt_inputs and not _events_on:
@@ -199,6 +226,7 @@ class MegaOutPort(MegaPushEntity):
def __init__(
self,
dimmer=False,
dimmer_scale=1,
*args, **kwargs
):
super().__init__(
@@ -207,6 +235,11 @@ class MegaOutPort(MegaPushEntity):
self._brightness = None
self._is_on = None
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
def invert(self):
@@ -214,10 +247,24 @@ class MegaOutPort(MegaPushEntity):
@property
def brightness(self):
val = self.mega.values.get(self.port, {}).get("value")
if val is None and self._state is not None:
if not self.dimmer:
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")
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
elif isinstance(val, (int, float)):
return int(val / self.dimmer_scale)
elif val is not None:
val = val.get("value")
if val is None:
return
try:
val = int(val)
return val
@@ -227,41 +274,107 @@ class MegaOutPort(MegaPushEntity):
@property
def is_on(self) -> bool:
val = self.mega.values.get(self.port, {})
if val is None and self._state is not None:
if isinstance(val, dict) and len(val) == 0 and self._state is not None:
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)
if val is not None:
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:
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:
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)
@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:
brightness = brightness or self.brightness or 255
self._brightness = brightness
if self.dimmer and brightness == 0:
cmd = 255
cmd = 255 * self.dimmer_scale
elif self.dimmer:
cmd = brightness
cmd = brightness * self.dimmer_scale
else:
cmd = 1 if not self.invert else 0
await self.mega.request(cmd=f"{self.port}:{cmd}")
self.mega.values[self.port] = {'value': cmd}
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd, priority=-1)
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()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0" if not self.invert else "1"
await self.mega.request(cmd=f"{self.port}:{cmd}")
self.mega.values[self.port] = {'value': cmd}
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd, priority=-1)
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()
def safe_int(v):
if v in ['ON', 'OFF']:
return None
if v == 'ON':
return 1
elif v == 'OFF':
return 0
try:
return int(v)
except (ValueError, TypeError):

View File

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

View File

@@ -8,16 +8,21 @@ 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 .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_RESPONSE_TEMPLATE
from .tools import make_ints
from . import hub as h
_LOGGER = logging.getLogger(__name__).getChild('http')
def is_ext(data: typing.Dict[str, typing.Any]):
for x in data:
if x.startswith('ext'):
return True
class MegaView(HomeAssistantView):
"""Handle Yandex Smart Home unauthorized requests."""
url = '/mega'
name = 'mega'
@@ -25,33 +30,55 @@ class MegaView(HomeAssistantView):
def __init__(self, cfg: dict):
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.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]
if isinstance(pt, int) and CONF_RESPONSE_TEMPLATE in cfg[mid][pt]
} for mid in cfg if isinstance(cfg[mid], dict)
}
_LOGGER.debug('templates: %s', self.templates)
self.hubs = {}
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)
_LOGGER.debug('request from %s %s', request.remote, request.headers)
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:
if self.protected:
auth = False
for x in self.allowed_hosts:
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)
data = dict(request.query)
hass.bus.async_fire(
@@ -60,26 +87,81 @@ class MegaView(HomeAssistantView):
)
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
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')
data = data.copy()
update_all = True
if 'v' in data:
update_all = False
data['value'] = data.pop('v')
data['mega_id'] = hub.id
ret = 'd'
ret = 'd' if hub.force_d else ''
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:
if is_ext(data):
# ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
if port in hub.extenders:
pt_orig = port
else:
pt_orig = hub.ext_in.get(port, hub.ext_in.get(str(port)))
if pt_orig is None:
hub.lg.warning(f'can not find extender for int port {port}, '
f'have ext_int: {hub.ext_in}, ext: {hub.extenders}')
return Response(status=200)
for e, v in data.items():
_data = data.copy()
if e.startswith('ext'):
idx = e[3:]
pt = f'{pt_orig}e{idx}'
_data['pt_orig'] = pt_orig
_data['value'] = 'ON' if v == '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)
act = hub.ext_act.get(pt)
hub.lg.debug(f'act on port {pt}: {act}, all acts are: {hub.ext_act}')
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)
hub.lg.debug(f'response={ret}, template={template}')
if ret == 'd' and act:
await hub.request(cmd=act.replace(':3', f':{v}'))
ret = 'd' if hub.force_d else ''
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))
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'})
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
async def later_restore(self, hub):
"""
Восстановление всех выходов с небольшой задержкой. Задержка нужна чтобы ответ прошел успешно
:param hub:
:return:
"""
await asyncio.sleep(0.2)
await hub.restore_states()
async def later_update(self, hub):
_LOGGER.debug('force update')
await asyncio.sleep(1)
_LOGGER.debug('force update')
await hub.updater.async_refresh()

View File

@@ -10,16 +10,22 @@ import json
from bs4 import BeautifulSoup
from homeassistant.components import mqtt
from homeassistant.const import (DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX)
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
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, \
CONF_FORCE_D
from .entities import set_events_off, BaseMegaEntity
from .exceptions import CannotConnect
from .tools import make_ints
from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
from .const import (
TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN
)
from .entities import set_events_off, BaseMegaEntity, MegaOutPort
from .exceptions import CannotConnect, NoPort
from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -44,13 +50,11 @@ CLASSES = {
LUX: DEVICE_CLASS_ILLUMINANCE
}
I2C_DEVICE_TYPES = {
"2": LUX, # BH1750
"3": LUX, # TSL2591
"7": LUX, # MAX44009
"70": LUX, # OPT3001
"2": LUX, # BH1750
"3": LUX, # TSL2591
"7": LUX, # MAX44009
"70": LUX, # OPT3001
}
class NoPort(Exception):
pass
class MegaD:
@@ -70,9 +74,19 @@ class MegaD:
scan_interval=60,
port_to_scan=0,
nports=38,
inverted: typing.List[int] = None,
update_all=True,
poll_outs=False,
update_all: bool=True,
poll_outs: bool=False,
fake_response: bool=True,
force_d: bool=None,
allow_hosts: str=None,
protected=True,
restore_on_restart=False,
extenders=None,
ext_in=None,
ext_acts=None,
i2c_sensors=None,
new_naming=False,
update_time=False,
**kwargs,
):
"""Initialize."""
@@ -80,11 +94,23 @@ class MegaD:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None:
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:
self.http = None
self.new_naming = new_naming
self.extenders = extenders or []
self.ext_in = ext_in or {}
self.ext_act = ext_acts or {}
self.i2c_sensors = i2c_sensors or []
self._update_time = update_time
self.poll_outs = poll_outs
self.update_all = update_all if update_all is not None else True
self.nports = nports
self.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None
self.hass = hass
@@ -94,11 +120,12 @@ class MegaD:
self.id = id
self.lck = asyncio.Lock()
self.last_long = {}
self._http_lck = asyncio.Lock()
self._http_lck = PriorityLock()
self._notif_lck = asyncio.Lock()
self.cnd = asyncio.Condition()
self.online = True
self.entities: typing.List[BaseMegaEntity] = []
self.ds2413_ports = set()
self.poll_interval = scan_interval
self.subs = None
self.lg: logging.Logger = lg.getChild(self.id)
@@ -114,16 +141,29 @@ class MegaD:
self.updater = DataUpdateCoordinator(
hass,
self.lg,
name="sensors",
name="megad",
update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
)
self.updaters = []
self.fw = ''
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.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):
self.loop = asyncio.get_event_loop()
@@ -168,6 +208,10 @@ class MegaD:
def force_d(self):
return self.customize.get(CONF_FORCE_D, False)
@property
def def_response(self):
return self.customize.get(CONF_DEF_RESPONSE, None)
@property
def is_online(self):
return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
@@ -189,12 +233,39 @@ class MegaD:
)
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):
"""
Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes
offline
Polling ports
"""
self.lg.debug('poll')
if self._update_time:
await self.update_time()
for x in self.i2c_sensors:
if not isinstance(x, dict):
continue
ret = await self._update_i2c(x)
if isinstance(ret, dict):
self.values.update(ret)
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:
await self.get_all_ports()
await self.get_sensors(only_list=True)
@@ -202,8 +273,9 @@ class MegaD:
await self.get_all_ports(check_skip=True)
elif len(self.sensors) > 0:
await self.get_sensors()
else:
await self.get_port(self.port_to_scan)
# else:
# await self.get_port(self.port_to_scan)
await self._get_ds2413()
return self.values
async def get_mqtt_id(self):
@@ -217,14 +289,20 @@ class MegaD:
_id = _id['value']
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):
return await self.request(pt=port, cmd=cmd)
async def request(self, **kwargs):
async def request(self, priority=0, **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}"
url = f"http://{self.host}/{self.sec}"
if cmd:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url)
async with self._http_lck:
async with self._http_lck(priority):
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())
@@ -237,13 +315,18 @@ class MegaD:
async def save(self):
await self.send_command(cmd='s')
def parse_response(self, ret):
def parse_response(self, ret, cmd='get'):
if ret is None:
raise NoPort()
if 'busy' in ret:
return None
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([
x.split(':') for x in ret if x.count(':') == 1
])}
@@ -255,17 +338,17 @@ class MegaD:
ret = {'value': 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
"""
self.lg.debug(f'get port %s', port)
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 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
while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1)
@@ -298,6 +381,8 @@ class MegaD:
if not self.mqtt_inputs:
ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports:
continue
if check_skip and not port in self.ports:
continue
ret = self.parse_response(x)
@@ -328,7 +413,7 @@ class MegaD:
if port == 'cmd':
return
try:
port = int(port)
port = int_ignore(port)
except:
self.lg.warning('can not process %s', msg)
return
@@ -357,7 +442,7 @@ class MegaD:
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
def subscribe(self, port, callback):
port = int(port)
port = int_ignore(port)
self.lg.debug(
f'subscribe %s %s', port, callback
)
@@ -383,70 +468,121 @@ class MegaD:
return await req.text()
async def scan_port(self, port):
async with self.lck:
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
elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
m = tree.find('select', attrs={'name': 'd'})
if m:
m = m.find(selected=True)['value']
self._scanned[port] = (pty, m or '0')
return pty, m or '0'
data = await self.request(pt=port)
return parse_config(data)
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]
yield x, ret
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 _update_i2c(self, params):
"""
Обновление портов i2c
:param params: параметры url
:return:
"""
_params = tuple(params.items())
delay = None
if 'delay' in params:
delay = params.pop('delay')
ret = {
_params: await self.request(**params)
}
self.lg.debug('i2c response: %s', ret)
if delay:
self.lg.debug('delay %s', delay)
await asyncio.sleep(delay)
return ret
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['mqtt_id'] = await self.get_mqtt_id()
ret['extenders'] = extenders = []
ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = []
async for port, cfg in self.scan_ports(nports):
_cust = self.customize.get(port)
if not isinstance(_cust, dict):
_cust = {}
if cfg.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 in ('3', '2', '4'):
try:
http_cmd = 'get'
elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
ret['light'][port].append({'dimmer': cfg.m == '1'})
elif cfg == DS2413:
# ds2413
_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)
if cfg.inta:
ext_int[int_ignore(cfg.inta)] = 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)
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
if ext_cfg.ety == '1':
ret['light'][pt].append({})
elif ext_cfg.ety == '0':
if ext_cfg.eact:
ext_acts[pt] = ext_cfg.eact
ret['binary_sensor'][pt].append({})
elif cfg == PCA9685:
extenders.append(port)
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty == '4' and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)):
# i2c в режиме ANY
scan = cfg.src.find('a', text='I2C Scan')
self.lg.debug(f'find scan link: %s', scan)
if scan is not None:
page = await self.request(pt=port, cmd='scan')
req, parsed = parse_scan_page(page)
self.lg.debug(f'scan results: %s', (req, parsed))
ret['i2c'][port].extend(parsed)
i2c_sensors.extend(req)
elif cfg.pty == '4' and cfg.m == '2':
# scl исключаем из сканирования
continue
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)
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')
@@ -456,8 +592,8 @@ class MegaD:
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
if pty == '4' and m in I2C_DEVICE_TYPES:
values = {I2C_DEVICE_TYPES[m]: values}
if cfg.pty == '4' and cfg.d in I2C_DEVICE_TYPES:
values = {I2C_DEVICE_TYPES.get(cfg.m): values}
else:
values = {None: values}
for key in values:
@@ -471,4 +607,16 @@ class MegaD:
))
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()
async def update_time(self):
await self.request(
cf=7,
stime=datetime.now().strftime('%H:%M:%S')
)

View File

@@ -0,0 +1,118 @@
from dataclasses import dataclass, field
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
)
def parse_scan_page(page: str):
ret = []
req = []
page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'):
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
dev = params.get('i2c_dev')
if dev is None:
continue
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, Request):
if c.delay:
params = params.copy()
params['delay'] = c.delay
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
else:
suffix = ''
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
_dev = f'{dev}_{suffix}'
else:
_dev = dev
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
})
req.append(params)
return req, ret
class Skip:
pass
@dataclass
class Request:
delay: float = None
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
],
'ptsensor': [
Skip,
Request(delay=1), # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
]
}

View File

@@ -26,6 +26,7 @@ from .const import (
CONF_CUSTOM,
CONF_SKIP,
)
from .tools import int_ignore
lg = logging.getLogger(__name__)
@@ -61,13 +62,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items():
port = int(port)
port = int_ignore(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)
if '<' in light.name:
continue
devices.append(light)
async_add_devices(devices)

View File

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

View File

@@ -13,7 +13,8 @@ from homeassistant.const import (
CONF_PORT,
CONF_UNIQUE_ID,
CONF_ID,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_DEVICE_CLASS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
@@ -22,6 +23,8 @@ from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE
from .hub import MegaD
import re
from .tools import int_ignore
lg = logging.getLogger(__name__)
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -80,21 +83,44 @@ 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)
for tp in ['sensor', 'i2c']:
for port, cfg in config_entry.data.get(tp, {}).items():
port = int_ignore(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = _constructors[tp](
mega=hub,
port=port,
config_entry=config_entry,
**data,
)
if '<' in sensor.name:
continue
devices.append(sensor)
async_add_devices(devices)
class MegaI2C(MegaPushEntity):
def __init__(self, *args, device_class: str, params: dict, **kwargs):
self._device_class = device_class
self._params = tuple(params.items())
super().__init__(*args, **kwargs)
def device_class(self):
return self._device_class
@property
def state(self):
# self.lg.debug(f'get % all states: %', self._params, self.mega.values)
return self.mega.values.get(self._params)
@property
def device_class(self):
return self._device_class
class Mega1WSensor(MegaPushEntity):
def __init__(
@@ -102,7 +128,6 @@ class Mega1WSensor(MegaPushEntity):
unit_of_measurement,
device_class,
key=None,
http_cmd='get',
*args,
**kwargs
):
@@ -118,7 +143,6 @@ class Mega1WSensor(MegaPushEntity):
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):
@@ -141,7 +165,15 @@ class Mega1WSensor(MegaPushEntity):
@property
def device_class(self):
return self._device_class
_u = self.customize.get(CONF_DEVICE_CLASS, None)
if _u is None:
return self._device_class
elif isinstance(_u, str):
return _u
elif isinstance(_u, dict) and self.key in _u:
return _u[self.key]
else:
return self._device_class
@property
def state(self):
@@ -165,7 +197,7 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret)
except:
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:
tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
@@ -177,4 +209,10 @@ class Mega1WSensor(MegaPushEntity):
c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict):
c = c.get(self.key)
return c or n
return c or n
_constructors = {
'sensor': Mega1WSensor,
'i2c': MegaI2C,
}

View File

@@ -15,7 +15,14 @@
"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%]",
"poll_outs": "[%key:common::config_flow::data::poll_outs%]"
"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%]",
"update_time": "[%key:common::config_flow::data::update_time%]"
}
}
},

View File

@@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant
from . import hub as h
from .entities import MegaOutPort
from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP
from .tools import int_ignore
_LOGGER = lg = logging.getLogger(__name__)
@@ -50,13 +51,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items():
port = int(port)
port = int_ignore(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)
if '<' in light.name:
continue
devices.append(light)
async_add_devices(devices)

View File

@@ -1,5 +1,12 @@
import asyncio
import itertools
from heapq import heappush
from contextlib import asynccontextmanager
_params = ['m', 'click', 'cnt', 'pt']
def make_ints(d: dict):
for x in _params:
try:
@@ -9,4 +16,102 @@ def make_ints(d: dict):
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0
d['click'] = 0
def int_ignore(x):
try:
return int(x)
except (TypeError, ValueError):
return x
class PriorityLock(asyncio.Lock):
"""
You can acquire lock with some kind of priority in mind, so that locks with higher priority will be released first.
priority can be set with lck.acquire(1)
or by using context manager:
>>> lck = PriorityLock()
... async with lck(1):
... # do something
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cnt = itertools.count()
def __call__(self, priority=0):
return self._with_priority(priority)
@asynccontextmanager
async def _with_priority(self, p):
await self.acquire(p)
try:
yield
finally:
self.release()
async def acquire(self, priority=0) -> bool:
"""Acquire a lock.
This method blocks until the lock is unlocked, then sets it to
locked and returns True.
"""
if (not self._locked and (self._waiters is None or
all(w.cancelled() for _, _, w in self._waiters))):
self._locked = True
return True
if self._waiters is None:
self._waiters = []
fut = self._loop.create_future()
cnt = next(self._cnt)
heappush(self._waiters, (priority, cnt, fut))
# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
try:
await fut
finally:
self._waiters.remove((priority, cnt, fut))
except asyncio.exceptions.CancelledError:
if not self._locked:
self._wake_up_first()
raise
self._locked = True
return True
def release(self):
"""Release a lock.
When the lock is locked, reset it to unlocked, and return.
If any other coroutines are blocked waiting for the lock to become
unlocked, allow exactly one of them to proceed.
When invoked on an unlocked lock, a RuntimeError is raised.
There is no return value.
"""
if self._locked:
self._locked = False
self._wake_up_first()
else:
raise RuntimeError('Lock is not acquired.')
def _wake_up_first(self):
"""Wake up the first waiter if it isn't done."""
if not self._waiters:
return
try:
_, _, fut = self._waiters[0]
except IndexError:
return
# .done() necessarily means that a waiter will wake up later on and
# either take the lock, or, if it was cancelled and lock wasn't
# taken already, will hit this again and wake up a new waiter.
if not fut.done():
fut.set_result(True)

View File

@@ -22,8 +22,14 @@
"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",
"poll_outs": "Poll outs"
"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",
"update_time": "Sync time"
}
}
}
@@ -35,9 +41,15 @@
"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",
"mqtt_inputs": "Use MQTT (Deprecated)",
"update_all": "Update all outs when input",
"poll_outs": "Poll outs"
"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",
"update_time": "Sync time"
}
}
}

View File

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

View File

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

245
readme.md
View File

@@ -4,25 +4,47 @@
[![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)
Интеграция с [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`, для датчиков
`sensor`)
- Возможность работы с несколькими 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, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей.
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1)
- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков
[по ссылке](https://github.com/andvikt/mega_hacs/wiki/i2c) (начиная с версии 0.6.1)
## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
HACS - Integrations - Explore, в поиске ищем MegaD.
Чтобы включить возможность использования бета-версий, зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии"
Обновления выполняются так же в меню HACS.
Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений
нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию`
Альтернативный способ установки:
```shell
# из папки с конфигом
@@ -35,218 +57,23 @@ wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и 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
srv: "192.168.1.4:8123" # ip:port вашего HA
script: "mega" # это api интеграции, к которому будет обращаться контроллер
```
Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным.
#### Ответ на входящие события от контроллера
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает
запустить то что прописано в поле act в настройках порта).
При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
объекты`
Поддерживаются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id)
## Зависимости
Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
Примеры:
```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 %}
```
Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
Начиная с версии v0.3.17 ответ можно слать так же и в режиме MQTT. Аналогично, темплейт должен возвращать готовую команду
такую же как требует команда cmd, так же можно использовать d, но d не отправляется по умолчанию, это сделано чтобы не
сломать текущую логику у пользователей предыдущих версий. Чтобы включить для всех входов в режиме mqtt отправку команды
d необходимо в конфиге прописать следующее:
```yaml
mega:
mega1:
force_d: true
```
**Внимание!** Нельзя использовать чекбокс напротив поля act если планируется использовать ответ сервера - у вас и
сработает act и команда от сервера, а вслучае ответа d сработает act два раза.
Так же следует понимать, что это не "ответ" в нормальном понимании - это вызов следом за полученным mqtt-сообщением
http команды такого вида `http://megaurl/?pt=port&cmd=rendered_template`, где `port` - это номер порта сработавшего входа,
а `cmd` - текст команды, который получен из темплейта. Те это имитация ответа. У этого подхода есть минус - задержка в
исполнении будет значительно выше чем при ответе в режиме http, но тем не менее эта задержка скорее всего не будет
сильно заметна.
## binary_sensor и события
Входы будут доступны как 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`, либо скрипт, который вы настроили
Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)