diff --git a/.experiment.py b/.experiment.py new file mode 100644 index 0000000..0ae89f6 --- /dev/null +++ b/.experiment.py @@ -0,0 +1,35 @@ +import asyncio +from asyncio import Event, FIRST_COMPLETED +import signal + + +stop = Event() +loop = asyncio.get_event_loop() + + +async def handler( + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, +): + await reader.read(100) + ans = b'HTTP/1.1 200 OK\r\nContent-Length:1\r\n\r\nd' + writer.write(ans) + await writer.drain() + writer.close() + await writer.wait_closed() + + +async def serve(): + server = await asyncio.start_server( + handler, + host='0.0.0.0', + port=8888, + ) + async with server: + await asyncio.wait((server.serve_forever(), stop.wait()), return_when=FIRST_COMPLETED) + +if __name__ == '__main__': + loop.add_signal_handler( + signal.SIGINT, stop.set + ) + loop.run_until_complete(serve()) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 691be27..6355bf5 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -8,19 +8,20 @@ assignees: '' --- **Описание** -A clear and concise description of what the bug is. +Описание проблемы **Версии систем** Enviroment: raspberry/linux/windows/macos/docker HA version: mega_hacs version: megad firmware version: +используется mqtt: true/false **Ожидаемое поведение** -A clear and concise description of what you expected to happen. +Описание правильного поведения **Screenshots** If applicable, add screenshots to help explain your problem. **LOG** -Прочитайте в документации как включить подробный лог интеграции и приложите его здесь +Просьба прикладывать детальный лог, подробная инструкция как включать отладку по [ссылке](https://github.com/andvikt/mega_hacs/wiki/Отладка) diff --git a/custom_components/mega/.experiment.py b/custom_components/mega/.experiment.py deleted file mode 100644 index 485f324..0000000 --- a/custom_components/mega/.experiment.py +++ /dev/null @@ -1,36 +0,0 @@ -import asyncio -from bs4 import BeautifulSoup -import aiohttp - -host = '192.168.88.14/sec' - - - - -# page = ''' -# Back
P7/ON
ON OFF
Type
Default:
Mode
Group
-# -# Back
P7/ON
ON OFF
Type
Default:
Mode
Group
-# Back -#
-# P7/ON -#
-# ON -# OFF -#
-#
Type
Default:
Mode
Group
-# Back
P7/ON
ON OFF
Type
Default:
Mode
Group
-# Back
P7/ON
ON OFF
Type
Default:
Mode
Group
-# ''' -# tree = BeautifulSoup(page, features="lxml") -# pty = tree.find('select', attrs={'name': 'pty'}).find(selected=True)['value'] -# m = tree.find('select', attrs={'name': 'm'}) -# if m: -# m = m.find(selected=True)['value'] -# -# print(pty, m) - -if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete( - scan_port(0) - ) \ No newline at end of file diff --git a/custom_components/mega/__init__.py b/custom_components/mega/__init__.py index d4d14e5..3757f3e 100644 --- a/custom_components/mega/__init__.py +++ b/custom_components/mega/__init__.py @@ -7,45 +7,61 @@ 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 ) 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_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \ + CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE 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_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 +} + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { vol.Optional(CONF_ALLOW_HOSTS): [str], vol.Required(str, description='id меги из веб-интерфейса'): { - vol.Optional(int, description='номер порта'): { - vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, - vol.Optional(CONF_INVERT, default=False): bool, - vol.Optional(CONF_NAME): vol.Any(str, { - vol.Required(str): str - }), - vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'), - vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'): - vol.Any(str, { - vol.Required(str): str - }), - vol.Optional( - CONF_RESPONSE_TEMPLATE, - description='шаблон ответа когда на этот порт приходит' - 'сообщение из меги '): cv.template, - vol.Optional(CONF_ACTION): cv.script_action, - vol.Optional(CONF_GET_VALUE, default=True): bool, - } + 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(int, description='номер порта'): vol.Any( + CUSTOMIZE_PORT, + CUSTOMIZE_DS2413, + ) } } }, @@ -63,6 +79,7 @@ async def async_setup(hass: HomeAssistant, config: dict): """YAML-конфигурация содержит только кастомизации портов""" hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})} hass.data[DOMAIN][CONF_HTTP] = view = MegaView(cfg=config.get(DOMAIN, {})) + hass.data[DOMAIN][CONF_ALL] = {} view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, [])) hass.http.register_view(view) hass.services.async_register( @@ -73,12 +90,12 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.services.async_register( DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({ vol.Optional('mega_id'): str, - vol.Optional('port'): int, + vol.Optional('port'): vol.Any(int, [int]), }) ) hass.services.async_register( DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({ - vol.Required('port'): int, + vol.Optional('port'): int, vol.Required('cmd'): str, vol.Optional('mega_id'): str, }) @@ -111,8 +128,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 + hass.data[DOMAIN][id] = hub + hass.data[DOMAIN][CONF_ALL][id] = hub if not await hub.authenticate(): raise Exception("not authentificated") mid = await hub.get_mqtt_id() @@ -160,7 +177,9 @@ async def async_remove_entry(hass, entry) -> None: if hub is None: return _LOGGER.debug(f'remove {id}') - _hubs.pop(entry.entry_id) + _hubs.pop(id, None) + hass.data[DOMAIN].pop(id, None) + hass.data[DOMAIN][CONF_ALL].pop(id, None) task: asyncio.Task = _POLL_TASKS.pop(id, None) if task is not None: task.cancel() @@ -205,27 +224,32 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall): if mega_id: hub: MegaD = hass.data[DOMAIN][mega_id] if port is None: - await hub.get_all_ports() - else: + await hub.get_all_ports(check_skip=True) + elif isinstance(port, int): await hub.get_port(port) + elif isinstance(port, list): + for x in port: + await hub.get_port(x) else: - for hub in hass.data[DOMAIN].values(): + for hub in hass.data[DOMAIN][CONF_ALL].values(): if not isinstance(hub, MegaD): continue if port is None: - await hub.get_all_ports() - else: + await hub.get_all_ports(check_skip=True) + elif isinstance(port, int): await hub.get_port(port) + elif isinstance(port, list): + for x in port: + await hub.get_port(x) @bind_hass async def _run_cmd(hass: HomeAssistant, call: ServiceCall): - port = call.data.get('port') mega_id = call.data.get('mega_id') cmd = call.data.get('cmd') if mega_id: hub: MegaD = hass.data[DOMAIN][mega_id] - await hub.send_command(port=port, cmd=cmd) + await hub.request(cmd=cmd) else: for hub in hass.data[DOMAIN].values(): - await hub.send_command(port=port, cmd=cmd) + await hub.request(cmd=cmd) diff --git a/custom_components/mega/binary_sensor.py b/custom_components/mega/binary_sensor.py index 39607be..874af69 100644 --- a/custom_components/mega/binary_sensor.py +++ b/custom_components/mega/binary_sensor.py @@ -16,7 +16,8 @@ from homeassistant.const import ( CONF_ENTITY_ID, ) from homeassistant.core import HomeAssistant -from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP +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 @@ -71,6 +72,10 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity): def state_attributes(self): return self._attrs + @property + def invert(self): + return self.customize.get(CONF_INVERT, False) + @property def is_on(self) -> bool: val = self.mega.values.get(self.port, {}).get("value") \ @@ -78,8 +83,27 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity): if val is None and self._state is not None: return self._state == 'ON' elif val is not None: - return val == 'ON' or val == 1 + if val in ['ON', 'OFF']: + return val == 'ON' if not self.invert else val == 'OFF' + else: + return val != 1 if not self.invert else val == 1 def _update(self, payload: dict): self.mega.values[self.port] = payload + if not self.mega.mqtt_inputs: + return + + template: Template = self.customize.get(CONF_RESPONSE_TEMPLATE, None) + if template is not None: + template.hass = self.hass + ret = template.async_render(payload) + self.mega.lg.debug(f'response: %s', ret) + self.hass.async_create_task( + self.mega.request(pt=self.port, cmd=ret) + ) + elif self.mega.force_d: + self.mega.lg.debug(f'response d') + self.hass.async_create_task( + self.mega.request(pt=self.port, cmd='d') + ) diff --git a/custom_components/mega/config_flow.py b/custom_components/mega/config_flow.py index f454bd9..2ebdade 100644 --- a/custom_components/mega/config_flow.py +++ b/custom_components/mega/config_flow.py @@ -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.core import callback, HomeAssistant from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \ - CONF_NPORTS, CONF_UPDATE_ALL # pylint:disable=unused-import + CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \ + CONF_ALLOW_HOSTS, CONF_PROTECTED # pylint:disable=unused-import from .hub import MegaD from . import exceptions @@ -18,14 +19,19 @@ _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_ID, default='def'): str, + vol.Required(CONF_ID, default='mega'): str, vol.Required(CONF_HOST, default="192.168.0.14"): str, 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_MQTT_INPUTS, default=False): bool, 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_PROTECTED, default=True): bool, + vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str, }, ) @@ -56,7 +62,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 = 11 CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED async def async_step_user(self, user_input=None): @@ -106,7 +112,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the options.""" - + 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) @@ -131,11 +137,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): step_id="init", 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_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_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_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, }), ) diff --git a/custom_components/mega/const.py b/custom_components/mega/const.py index 00b694a..2212242 100644 --- a/custom_components/mega/const.py +++ b/custom_components/mega/const.py @@ -16,14 +16,21 @@ CONF_INVERT = 'invert' CONF_PORTS = 'ports' CONF_CUSTOM = '__custom' CONF_HTTP = '__http' +CONF_ALL = '__all' CONF_SKIP = 'skip' CONF_MQTT_INPUTS = 'mqtt_inputs' CONF_NPORTS = 'nports' CONF_RESPONSE_TEMPLATE = 'response_template' CONF_ACTION = 'action' CONF_UPDATE_ALL = 'update_all' +CONF_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' PLATFORMS = [ "light", "switch", @@ -31,4 +38,14 @@ PLATFORMS = [ "sensor", ] EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor' -PATT_SPLIT = re.compile('[;/]') \ No newline at end of file +EVENT_BINARY = f'{DOMAIN}.binary' + +PATT_SPLIT = re.compile('[;/]') + +LONG = 'long' +RELEASE = 'release' +LONG_RELEASE = 'long_release' +PRESS = 'press' +LUX = 'lux' +SINGLE_CLICK = 'single' +DOUBLE_CLICK = 'double' diff --git a/custom_components/mega/entities.py b/custom_components/mega/entities.py index 270fc4b..d4ab7cf 100644 --- a/custom_components/mega/entities.py +++ b/custom_components/mega/entities.py @@ -1,12 +1,32 @@ import logging import asyncio + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import State from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.restore_state import RestoreEntity -from .hub import MegaD -from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT +from . import hub as h +from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \ + LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY + +_events_on = False +_LOGGER = logging.getLogger(__name__) + + +async def _set_events_on(): + global _events_on, _task_set_ev_on + await asyncio.sleep(10) + _LOGGER.debug('events on') + _events_on = True + + +def set_events_off(): + global _events_on, _task_set_ev_on + _events_on = False + _task_set_ev_on = None + +_task_set_ev_on = None class BaseMegaEntity(CoordinatorEntity, RestoreEntity): @@ -17,14 +37,20 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): """ def __init__( self, - mega: MegaD, + mega: 'h.MegaD', port: int, config_entry: ConfigEntry = None, 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 @@ -37,6 +63,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): self._name = name or f"{mega.id}_{port}" + \ (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 customize(self): @@ -46,7 +76,11 @@ 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 @@ -88,11 +122,15 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): return self._unique_id async def async_added_to_hass(self) -> None: + global _task_set_ev_on await super().async_added_to_hass() self._state = await self.async_get_last_state() + if self.mega.mqtt_inputs and _task_set_ev_on is None: + _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() @@ -111,7 +149,55 @@ class MegaPushEntity(BaseMegaEntity): self._update(value) self.async_write_ha_state() self.lg.debug(f'state after update %s', self.state) - self.is_first_update = False + if self.mega.mqtt_inputs and not _events_on: + _LOGGER.debug('skip event because events are off') + return + if not self.entity_id.startswith('binary_sensor'): + _LOGGER.debug('skip event because not a bnary sens') + return + ll: bool = self.mega.last_long.get(self.port, False) + if safe_int(value.get('click', 0)) == 1: + self.hass.bus.async_fire( + event_type=EVENT_BINARY, + event_data={ + 'entity_id': self.entity_id, + 'type': SINGLE_CLICK + } + ) + elif safe_int(value.get('click', 0)) == 2: + self.hass.bus.async_fire( + event_type=EVENT_BINARY, + event_data={ + 'entity_id': self.entity_id, + 'type': DOUBLE_CLICK + } + ) + elif safe_int(value.get('m', 0)) == 2: + self.mega.last_long[self.port] = True + self.hass.bus.async_fire( + event_type=EVENT_BINARY, + event_data={ + 'entity_id': self.entity_id, + 'type': LONG + } + ) + elif safe_int(value.get('m', 0)) == 1: + self.hass.bus.async_fire( + event_type=EVENT_BINARY, + event_data={ + 'entity_id': self.entity_id, + 'type': LONG_RELEASE if ll else RELEASE, + } + ) + elif safe_int(value.get('m', None)) == 0: + self.hass.bus.async_fire( + event_type=EVENT_BINARY, + event_data={ + 'entity_id': self.entity_id, + 'type': PRESS, + } + ) + self.mega.last_long[self.port] = False return def _update(self, payload: dict): @@ -137,12 +223,18 @@ class MegaOutPort(MegaPushEntity): self._is_on = None self.dimmer = dimmer + # @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): return self.customize.get(CONF_INVERT, False) @property def brightness(self): + if not self.dimmer: + return val = self.mega.values.get(self.port, {}).get("value") if val is None and self._state is not None: return self._state.attributes.get("brightness") @@ -161,11 +253,37 @@ class MegaOutPort(MegaPushEntity): return self._state == 'ON' 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 @@ -175,17 +293,39 @@ class MegaOutPort(MegaPushEntity): cmd = brightness else: cmd = 1 if not self.invert else 0 - await self.mega.send_command(self.port, f"{self.port}:{cmd}") - self.mega.values[self.port] = {'value': cmd} + _cmd = {"cmd": f"{self.cmd_port}:{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', + ) + 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.send_command(self.port, 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) + if self.index is not None: + # обновление текущего стейта для ds2413 + await self.mega.get_port( + port=self.port, + force_http=True, + conv=False, + http_cmd='list', + ) + else: + self.mega.values[self.port] = {'value': cmd} await self.get_state() diff --git a/custom_components/mega/exceptions.py b/custom_components/mega/exceptions.py index 9165b10..ec65f3f 100644 --- a/custom_components/mega/exceptions.py +++ b/custom_components/mega/exceptions.py @@ -15,3 +15,7 @@ class DuplicateId(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class NoPort(Exception): + pass \ No newline at end of file diff --git a/custom_components/mega/http.py b/custom_components/mega/http.py index ab52f3b..b46a362 100644 --- a/custom_components/mega/http.py +++ b/custom_components/mega/http.py @@ -8,16 +8,15 @@ 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') class MegaView(HomeAssistantView): - """Handle Yandex Smart Home unauthorized requests.""" url = '/mega' name = 'mega' @@ -25,32 +24,54 @@ 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] - } for mid in cfg + 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: 'hub.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) + + hub: 'h.MegaD' = self.hubs.get(request.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( @@ -61,24 +82,34 @@ class MegaView(HomeAssistantView): make_ints(data) 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: + template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response) + 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: + if 'd' in ret: + await hub.request(pt=port, cmd=ret) + else: + await hub.request(cmd=ret) return ret async def later_update(self, hub): - _LOGGER.debug('force update') await asyncio.sleep(1) + _LOGGER.debug('force update') await hub.updater.async_refresh() - diff --git a/custom_components/mega/hub.py b/custom_components/mega/hub.py index 0c6debb..801b2fa 100644 --- a/custom_components/mega/hub.py +++ b/custom_components/mega/hub.py @@ -10,32 +10,49 @@ import json from bs4 import BeautifulSoup from homeassistant.components import mqtt -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY +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, PATT_SPLIT, DOMAIN, CONF_HTTP, EVENT_BINARY_SENSOR -from .exceptions import CannotConnect, MqttNotConfigured -from .http import MegaView +from .const import ( + TEMP, HUM, PRESS, + LUX, PATT_SPLIT, DOMAIN, + CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE +) +from .entities import set_events_off, BaseMegaEntity +from .exceptions import CannotConnect, NoPort from .tools import make_ints TEMP_PATT = re.compile(r'temp:([01234567890\.]+)') HUM_PATT = re.compile(r'hum:([01234567890\.]+)') +PRESS_PATT = re.compile(r'press:([01234567890\.]+)') +LUX_PATT = re.compile(r'lux:([01234567890\.]+)') PATTERNS = { TEMP: TEMP_PATT, HUM: HUM_PATT, + PRESS: PRESS_PATT, + LUX: LUX_PATT } UNITS = { - TEMP: '°C', - HUM: '%' + TEMP: TEMP_CELSIUS, + HUM: PERCENTAGE, + PRESS: 'mmHg', + LUX: LIGHT_LUX } CLASSES = { TEMP: DEVICE_CLASS_TEMPERATURE, - HUM: DEVICE_CLASS_HUMIDITY + HUM: DEVICE_CLASS_HUMIDITY, + PRESS: DEVICE_CLASS_PRESSURE, + LUX: DEVICE_CLASS_ILLUMINANCE +} +I2C_DEVICE_TYPES = { + "2": LUX, # BH1750 + "3": LUX, # TSL2591 + "7": LUX, # MAX44009 + "70": LUX, # OPT3001 } - -class NoPort(Exception): - pass class MegaD: @@ -55,8 +72,12 @@ class MegaD: scan_interval=60, port_to_scan=0, nports=38, - inverted: typing.List[int] = None, - update_all=True, + update_all: bool=True, + poll_outs: bool=False, + fake_response: bool=True, + force_d: bool=None, + allow_hosts: str=None, + protected=True, **kwargs, ): """Initialize.""" @@ -64,10 +85,17 @@ 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.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 @@ -76,11 +104,13 @@ class MegaD: self.mqtt = mqtt self.id = id self.lck = asyncio.Lock() + self.last_long = {} self._http_lck = asyncio.Lock() self._notif_lck = asyncio.Lock() self.cnd = asyncio.Condition() self.online = True - self.entities: typing.List[Entity] = [] + self.entities: typing.List[BaseMegaEntity] = [] + self.ds2413_ports = set() self.poll_interval = scan_interval self.subs = None self.lg: logging.Logger = lg.getChild(self.id) @@ -90,6 +120,7 @@ class MegaD: self.last_update = datetime.now() self._callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] = defaultdict(list) self._loop = loop + self._customize = None self.values = {} self.last_port = None self.updater = DataUpdateCoordinator( @@ -106,9 +137,21 @@ class MegaD: else: self.mqtt_id = mqtt_id + 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() if self.mqtt is not None: + set_events_off() self.subs = await self.mqtt.async_subscribe( topic=f"{self.mqtt_id}/+", msg_callback=self._process_msg, @@ -136,6 +179,22 @@ class MegaD: await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd) ports.append(x.port) + @property + def customize(self): + if self._customize is None: + c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {} + c = c.get(self.id) or {} + self._customize = c + return self._customize + + @property + 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) @@ -157,20 +216,35 @@ 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. becomes online else mega. becomes - offline + Polling ports """ self.lg.debug('poll') if self.mqtt is None: await self.get_all_ports() await self.get_sensors(only_list=True) - return - if len(self.sensors) > 0: + elif self.poll_outs: + 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) + await self._get_ds2413() return self.values async def get_mqtt_id(self): @@ -204,14 +278,21 @@ 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) - ret = dict([ + 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 - ]) + ])} elif 'ON' in ret: ret = {'value': 'ON'} elif 'OFF' in ret: @@ -220,20 +301,23 @@ 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: - ret = await self.request(pt=port, cmd=http_cmd) - ret = self.parse_response(ret) - self.lg.debug('parsed: %s', ret) - if http_cmd == 'list' and isinstance(ret, dict) and 'value' in ret: + if http_cmd == 'list' and conv: + await self.request(pt=port, cmd='conv') await asyncio.sleep(1) - ret = await self.request(pt=port, http_cmd=http_cmd) - ret = self.parse_response(ret) + 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) + ret = self.parse_response(await self.request(pt=port, cmd=http_cmd)) + ntry += 1 + self.lg.debug('parsed: %s', ret) self.values[port] = ret return ret @@ -252,15 +336,26 @@ class MegaD: except asyncio.TimeoutError: self.lg.error(f'timeout when getting port {port}') - async def get_all_ports(self): + @property + def ports(self): + return {e.port for e in self.entities} + + async def get_all_ports(self, only_out=False, check_skip=False): 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) self.values[port] = ret - else: + elif not check_skip: for x in range(self.nports + 1): await self.get_port(x) + else: + for x in self.ports: + await self.get_port(x) async def reboot(self, save=True): await self.save() @@ -369,6 +464,12 @@ class MegaD: 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' async def scan_ports(self, nports=37): for x in range(0, nports+1): @@ -379,18 +480,36 @@ class MegaD: async def get_config(self, nports=37): ret = defaultdict(lambda: defaultdict(list)) + ret['mqtt_id'] = await self.get_mqtt_id() async for port, pty, m in self.scan_ports(nports): if pty == "0": ret['binary_sensor'][port].append({}) elif pty == "1" and (m in ['0', '1', '3'] or m is None): ret['light'][port].append({'dimmer': m == '1'}) - elif pty == '3': + elif pty == "1" and m == "2": + # 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 pty in ('3', '2', '4'): try: http_cmd = 'get' - values = await self.get_port(port, force_http=True) - if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')): + if m == '5' and 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 @@ -403,7 +522,10 @@ class MegaD: if isinstance(values, str) and TEMP_PATT.search(values): values = {TEMP: values} elif not isinstance(values, dict): - values = {None: values} + if pty == '4' and m in I2C_DEVICE_TYPES: + values = {I2C_DEVICE_TYPES[m]: values} + else: + values = {None: values} for key in values: self.lg.debug(f'add sensor {key}') ret['sensor'][port].append(dict( diff --git a/custom_components/mega/manifest.json b/custom_components/mega/manifest.json index 8d2d30d..af5e967 100644 --- a/custom_components/mega/manifest.json +++ b/custom_components/mega/manifest.json @@ -14,5 +14,6 @@ "codeowners": [ "@andvikt" ], - "issue_tracker": "https://github.com/andvikt/mega_hacs/issues" + "issue_tracker": "https://github.com/andvikt/mega_hacs/issues", + "version": "v0.4.2b1" } \ No newline at end of file diff --git a/custom_components/mega/sensor.py b/custom_components/mega/sensor.py index cf3a7d3..a6417a8 100644 --- a/custom_components/mega/sensor.py +++ b/custom_components/mega/sensor.py @@ -13,11 +13,12 @@ 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, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.template import Template from .entities import MegaPushEntity -from .const import CONF_KEY, TEMP, HUM, W1, W1BUS +from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE from .hub import MegaD import re @@ -101,7 +102,6 @@ class Mega1WSensor(MegaPushEntity): unit_of_measurement, device_class, key=None, - http_cmd='get', *args, **kwargs ): @@ -117,7 +117,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): @@ -149,7 +148,9 @@ class Mega1WSensor(MegaPushEntity): try: ret = self.mega.values.get(self.port, {}) if isinstance(ret, dict): - ret = ret.get(self.key) + ret = ret.get('value', {}) + if isinstance(ret, dict): + ret = ret.get(self.key) except: self.lg.error(self.mega.values.get(self.port, {}).get('value', {})) return @@ -162,6 +163,10 @@ class Mega1WSensor(MegaPushEntity): ret = str(ret) except: ret = None + 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}) return ret @property diff --git a/custom_components/mega/services.yaml b/custom_components/mega/services.yaml index 55a207c..2147513 100644 --- a/custom_components/mega/services.yaml +++ b/custom_components/mega/services.yaml @@ -15,7 +15,7 @@ get_port: description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги example: "mega" port: - description: Номер порта (если не заполнять, будут запрошены все порты сразу) + description: Номер порта или список портов (если не заполнять, будут запрошены все порты сразу) example: 1 run_cmd: @@ -25,9 +25,10 @@ run_cmd: mega_id: description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги example: "mega" - port: - description: Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду) - example: 1 cmd: description: Любая поддерживаемая мегой команда example: "1:0" + port: + description: (Deprecated, больше не нужен) Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду) + example: 1 + diff --git a/custom_components/mega/strings.json b/custom_components/mega/strings.json index 9fab033..ddfbf91 100644 --- a/custom_components/mega/strings.json +++ b/custom_components/mega/strings.json @@ -14,7 +14,12 @@ "invert": "[%key:common::config_flow::data::invert%]", "mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]", "nports": "[%key:common::config_flow::data::nports%]", - "update_all": "[%key:common::config_flow::data::update_all%]" + "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%]", + "poll_outs": "[%key:common::config_flow::data::poll_outs%]" } } }, @@ -38,7 +43,8 @@ "invert": "[%key:common::config_flow::data::invert%]", "mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]", "nports": "[%key:common::config_flow::data::nports%]", - "update_all": "[%key:common::config_flow::data::update_all%]" + "update_all": "[%key:common::config_flow::data::update_all%]", + "poll_outs": "[%key:common::config_flow::data::poll_outs%]" } } } diff --git a/custom_components/mega/switch.py b/custom_components/mega/switch.py index 1462e63..1178874 100644 --- a/custom_components/mega/switch.py +++ b/custom_components/mega/switch.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_DOMAIN, ) from homeassistant.core import HomeAssistant -from .entities import MegaD +from . import hub as h from .entities import MegaOutPort from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP @@ -45,7 +45,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices): mid = config_entry.data[CONF_ID] - hub: MegaD = hass.data['mega'][mid] + hub: 'h.MegaD' = hass.data['mega'][mid] devices = [] customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) diff --git a/custom_components/mega/tools.py b/custom_components/mega/tools.py index d1d8876..e93b0f5 100644 --- a/custom_components/mega/tools.py +++ b/custom_components/mega/tools.py @@ -1,7 +1,9 @@ +_params = ['m', 'click', 'cnt', 'pt'] + def make_ints(d: dict): - for x in d: + for x in _params: try: - d[x] = float(d[x]) + d[x] = int(d.get(x, 0)) except (ValueError, TypeError): pass if 'm' not in d: diff --git a/custom_components/mega/translations/en.json b/custom_components/mega/translations/en.json index 39960df..1d2f31c 100644 --- a/custom_components/mega/translations/en.json +++ b/custom_components/mega/translations/en.json @@ -22,7 +22,12 @@ "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" + "mqtt_inputs": "Use MQTT (Deprecated)", + "fake_response": "Fake response", + "force_d": "Force 'd' response", + "protected": "Protected", + "allow_hosts": "Allowed hosts", + "poll_outs": "Poll outs" } } } @@ -34,8 +39,13 @@ "scan_interval": "Scan interval (sec), 0 - don't update", "port_to_scan": "Port to poll aliveness (needed only if no sensors used)", "reload": "Reload objects", - "mqtt_inputs": "Use MQTT", - "update_all": "Update all outs when input" + "mqtt_inputs": "Use MQTT (Deprecated)", + "update_all": "Update all outs when input", + "fake_response": "Fake response", + "force_d": "Force 'd' response", + "protected": "Protected", + "allow_hosts": "Allowed hosts", + "poll_outs": "Poll outs" } } } diff --git a/custom_components/mega/translations/ru.json b/custom_components/mega/translations/ru.json index 9eb0033..f2a2b1d 100644 --- a/custom_components/mega/translations/ru.json +++ b/custom_components/mega/translations/ru.json @@ -19,9 +19,14 @@ "mqtt_id": "MQTT id", "scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять", "port_to_scan": "Порт, который сканируется когда нет датчиков", - "mqtt_inputs": "Использовать MQTT", + "mqtt_inputs": "Использовать MQTT (Не рекомендуется)", "nports": "Кол-во портов", - "update_all": "Обновить все выходы когда срабатывает вход" + "update_all": "Обновить все выходы когда срабатывает вход", + "fake_response": "Имитация http-ответа", + "force_d": "Ответ 'd' по умолчанию", + "protected": "Блокировать неразрешенные соединения", + "allow_hosts": "Разрешенные ip (через ;)", + "poll_outs": "Обновлять выходы (регулярно)" } } } @@ -34,9 +39,14 @@ "port_to_scan": "Порт, который сканируется когда нет датчиков", "reload": "Обновить объекты", "invert": "Список портов (через ,) с инвертированной логикой", - "mqtt_inputs": "Использовать MQTT", + "mqtt_inputs": "Использовать MQTT (Не рекомендуется)", + "fake_response": "Имитация http-ответа", + "force_d": "Ответ 'd' по умолчанию", "nports": "Кол-во портов", - "update_all": "Обновить все выходы когда срабатывает вход" + "update_all": "Обновить все выходы когда срабатывает вход", + "protected": "Блокировать неразрешенные соединения", + "allow_hosts": "Разрешенные ip (через ;)", + "poll_outs": "Обновлять выходы (регулярно)" } } } diff --git a/custom_components/mega/translations/uk.json b/custom_components/mega/translations/uk.json index 14e669b..32b67eb 100644 --- a/custom_components/mega/translations/uk.json +++ b/custom_components/mega/translations/uk.json @@ -19,9 +19,14 @@ "mqtt_id": "MQTT id", "scan_interval": "Період оновлення (сек.), 0 - не оновлювати", "port_to_scan": "Порт для сканування при відсутності датчиків", - "mqtt_inputs": "Використовувати MQTT", + "mqtt_inputs": "Використовувати MQTT (Deprecated)", "nports": "Кількість портів", - "update_all": "Оновити всі виходи коли спрацьовує вхід" + "update_all": "Оновити всі виходи коли спрацьовує вхід", + "fake_response": "Имитация http-ответа", + "force_d": "Ответ 'd' по умолчанию", + "protected": "Блокировать неразрешенные соединения", + "allow_hosts": "Разрешенные ip (через ;)", + "poll_outs": "Оновити виходи" } } } @@ -34,9 +39,14 @@ "port_to_scan": "Порт для сканування при відсутності датчиків", "reload": "Оновити об'єкти", "invert": "Список портів з інвертованою логікою (через ,)", - "mqtt_inputs": "Використовувати MQTT", + "mqtt_inputs": "Використовувати MQTT (Deprecated)", "nports": "Кількість портів", - "update_all": "Оновити всі виходи коли спрацьовує вхід" + "fake_response": "Имитация http-ответа", + "force_d": "Ответ 'd' по умолчанию", + "update_all": "Оновити всі виходи коли спрацьовує вхід", + "protected": "Блокировать неразрешенные соединения", + "allow_hosts": "Разрешенные ip (через ;)", + "poll_outs": "Оновити виходи" } } } diff --git a/readme.md b/readme.md index 52c802c..fa54f85 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,46 @@ + + # MegaD HomeAssistant integration +[![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) ## Установка Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation): HACS - Integrations - Explore, в поиске ищем MegaD. +Чтобы включить возможность использования бета-версий, зайдите в HACS, найдите интеграцию MegaD, нажмите три точки, +там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии" + +Обновления выполняются так же в меню HACS. +Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений +нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию` + Альтернативный способ установки: ```shell # из папки с конфигом @@ -31,206 +53,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" - 14: - name: какой-то датчик - unit_of_measurement: "°C" # если датчик один, то просто строчкой -``` - -## Зависимости -Для совместимости c mqtt необходимо настроить интеграцию [mqtt](https://www.home-assistant.io/integrations/mqtt/) -в HomeAssistant, а так же обновить ваш контроллер до последней версии, тк были важные обновления в части mqtt - -## HTTP in -Начиная с версии `0.3.1` интеграция стала поддерживать обратную связь без mqtt, используя http-сервер. Для этого в настройках -интеграции необходимо снять галку с `использовать mqtt` - В самой меге необходимо прописать настройки: ```yaml srv: "192.168.1.4:8123" # ip:port вашего HA script: "mega" # это api интеграции, к которому будет обращаться контроллер ``` +Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным. -Входы будут доступны как binary_sensor, а так же в виде событий `mega.sensor`. -События можно обрабатывать так: -```yaml -- alias: some double click - trigger: - - platform: event - event_type: mega.sensor - event_data: - pt: 1 - click: 2 - action: - - service: light.toggle - entity_id: light.some_light -``` -Для binary_sensor имеет смысл использовать режим P&R, для остальных режимов - лучше пользоваться событиями. +При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить +объекты` -Примеры использования binary_sensor: -```yaml -- alias: обработка долгих/коротких нажатий - trigger: - - platform: state - entity_id: binary_sensor.some_sensor - to: on - for: 1 # задержка на секунду +## Зависимости +Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в +настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться +[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy) - action: - - choose: - # если кнопка все еще нажата - значит это долгое нажатие - - conditions: "{{ is_state('binary_sensor.some_sensor', 'on')}}" - sequence: - - service: light.turn_on - entity_id: light.some_light - # если кнопка уже не нажата - значит это короткое нажатие - - conditions: "{{ is_state('binary_sensor.some_sensor', 'off')}}" - sequence: - - service: light.turn_off - entity_id: light.some_light -``` - -## Ответ на входящие события от контроллера -Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает -запустить то что прописано в поле act в настройках порта). - -Поддерживаеются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не -умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id) - -Примеры: -```yaml -mega: - mega1: # id меги, который вы сами придумываете в конфиге в UI - 4: # номер порта, с которого ожидаются события - response_template: 5:2 # простейший пример без шаблона. Каждый раз когда будет приходить сообщение на этот порт, - # будем менять состояние на противоположное - 5: - # пример с использованием шаблона, порт 1 будет выключен если он сейчас включен и включен с последней сохраненной - # яркостью если он сейчас выключен - response_template: >- - {% if is_state('light.some_port_1', 'on') %} - 1:0 - {% else %} - 1:{{state_attr('light.some_port_1', 'brightness')}} - {% endif %} - 6: - # в шаблон так же передаются все параметры, которые передает контроллер (pt, cnt, m, click) - # эти параметры можно использовать в условиях или непосредственно в шаблоне в виде {{pt}} - response_template: >- - {% if m==2 %}1:0{% else %}d{% endif %} - -``` -## Отладка ответов -Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли -HA: -```shell -curl -v -X GET 'http://localhost:8123/mega?pt=5&m=1' -``` -Если доступа нет, нужно в файл конфигурации добавить ip компьюетра, с которого вы хотите делать запросы, например: -```yaml -mega: - allow_hosts: - - 192.168.1.1 -``` -И тогда можно с локальной машины делать запросы на ваш сервер HA: -```shell -curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1' -``` -В ответ будет приходить либо `d`, либо скрипт, который вы настроили +Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать +из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git) -## События -`binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать -только с режимом входа P&R - -При каждом срабатывании `binary_sensor` так же сообщает о событии типа `mega.sensor`. -События можно использовать в автоматизациях, например так: -```yaml -- alias: some double click - trigger: - - platform: event - event_type: mega.sensor - event_data: - pt: 1 - cnt: 2 - action: - - service: light.toggle - entity_id: light.some_light -``` -События могут содержать следующие поля: -- mega_id: id как в конфиге HA -- pt: номер порта -- cnt: счетчик срабатываний -- mdid: if как в конфиге контроллера -- click: клик (подробнее в документации меги) -- value: текущее значение (только для mqtt) -- port: номер порта - -Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться -на вкладке события на событие `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 -``` +Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)