diff --git a/.experiment.py b/.experiment.py index 9d7532b..3f7dbaf 100644 --- a/.experiment.py +++ b/.experiment.py @@ -1,32 +1,61 @@ import asyncio +from urllib.parse import urlparse, parse_qsl +from asyncio import Event, FIRST_COMPLETED +import signal +import typing +from logging import getLogger, DEBUG -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() +stop = Event() +loop = asyncio.get_event_loop() +lg = getLogger(__name__) +lg.setLevel(DEBUG) -async def main(): +def make_handler(get_ans: typing.Callable[[dict], str]): + + async def handler( + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, + ): + data = await reader.read(200) + print(data) + message = data.decode() + addr = writer.get_extra_info('peername') + lg.debug('process msg "%s" from %s', message, addr) + try: + (_, p, *_) = message.split(' ') + p = dict(parse_qsl(urlparse(p).query)) + lg.debug('query %s', p) + ans = get_ans(p) + ans = f'''HTTP/1.1 200 OK\nContent-Length: {len(ans)}\n\n{ans}'''.encode() # \nContent-Length: 6 + ans = b'HTTP/1.1 200 OK\r\n\r\n7:2' + print(ans) + except Exception as exc: + print(exc) + lg.exception('process msg "%s" from %s', message, addr) + ans = '''HTTP/1.1 500\n\n'''.encode() + writer.write(ans) + await writer.drain() + # writer.transport.close() + writer.close() + await writer.wait_closed() + return handler + + +async def serve(): server = await asyncio.start_server( - handle_echo, '127.0.0.1', 8888) - + make_handler(lambda x: '7:2'), + host='0.0.0.0', + port=1111, + ) addr = server.sockets[0].getsockname() print(f'Serving on {addr}') - async with server: - await server.serve_forever() + await asyncio.wait((server.serve_forever(), stop.wait()), return_when=FIRST_COMPLETED) -asyncio.run(main()) \ No newline at end of file +if __name__ == '__main__': + loop.add_signal_handler( + signal.SIGINT, stop.set + ) + loop.run_until_complete(serve()) \ No newline at end of file diff --git a/custom_components/mega/__init__.py b/custom_components/mega/__init__.py index 17aa573..f159030 100644 --- a/custom_components/mega/__init__.py +++ b/custom_components/mega/__init__.py @@ -24,6 +24,29 @@ from .http import MegaView _LOGGER = logging.getLogger(__name__) +CUSTOMIZE_PORT = vol.Schema({ + 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 +}) +CUSTOMIZE_DS2413 = vol.Schema({ + vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT +}) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { @@ -31,25 +54,10 @@ CONFIG_SCHEMA = vol.Schema( # vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool, vol.Required(str, description='id меги из веб-интерфейса'): { vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool, - 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(int, description='номер порта'): vol.Any( + CUSTOMIZE_PORT, + CUSTOMIZE_DS2413, + ) } } }, diff --git a/custom_components/mega/config_flow.py b/custom_components/mega/config_flow.py index b572932..b1414c0 100644 --- a/custom_components/mega/config_flow.py +++ b/custom_components/mega/config_flow.py @@ -57,7 +57,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 = 5 CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED async def async_step_user(self, user_input=None): diff --git a/custom_components/mega/entities.py b/custom_components/mega/entities.py index 7f6f6b5..06bc025 100644 --- a/custom_components/mega/entities.py +++ b/custom_components/mega/entities.py @@ -43,6 +43,9 @@ 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._state: State = None @@ -57,6 +60,9 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): self._name = name or f"{mega.id}_{port}" + \ (f"_{id_suffix}" if id_suffix else "") self._customize: dict = None + self.http_cmd = http_cmd + self.index = index + self.addr = addr @property def customize(self): @@ -66,7 +72,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 @@ -208,12 +218,17 @@ class MegaOutPort(MegaPushEntity): self._is_on = None self.dimmer = dimmer + 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") @@ -232,11 +247,33 @@ class MegaOutPort(MegaPushEntity): return self._state == 'ON' elif val is not None: val = val.get("value") + if self.index and self.addr: + _val = val.get(self.addr) + if not isinstance(val, str): + self.mega.lg.warning(f'{self} has wrong state: {val}') + return + _val = _val.split('/') + if len(_val) >= 2: + val = val[self.index] + else: + self.mega.lg.warning(f'{self} has wrong state: {val}') + return + elif self.index and self.addr is None: + self.mega.lg.warning(f'{self} does not has addr') + return + 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 @@ -246,16 +283,39 @@ class MegaOutPort(MegaPushEntity): cmd = brightness 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) + if self.index is not None: + # обновление текущего стейта для ds2413 + self.hass.async_create_task(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.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) + if self.index is not None: + # обновление текущего стейта для ds2413 + self.hass.async_create_task(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/hub.py b/custom_components/mega/hub.py index 3a076e3..6d3b379 100644 --- a/custom_components/mega/hub.py +++ b/custom_components/mega/hub.py @@ -196,6 +196,15 @@ class MegaD: Polling ports """ self.lg.debug('poll') + for x in self.entities: + # обновление ds2413 устройств + if x.http_cmd == 'ds2413': + await self.get_port( + port=x.port, + force_http=True, + http_cmd='list', + conv=False + ) if self.mqtt is None: await self.get_all_ports() await self.get_sensors(only_list=True) @@ -256,14 +265,14 @@ 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)) @@ -438,6 +447,18 @@ class MegaD: 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 == "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].append([ + {"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' diff --git a/custom_components/mega/manifest.json b/custom_components/mega/manifest.json index c80195c..5107dd8 100644 --- a/custom_components/mega/manifest.json +++ b/custom_components/mega/manifest.json @@ -15,5 +15,5 @@ "@andvikt" ], "issue_tracker": "https://github.com/andvikt/mega_hacs/issues", - "version": "v0.3.19" + "version": "v0.4.1b" } \ No newline at end of file diff --git a/custom_components/mega/sensor.py b/custom_components/mega/sensor.py index 3a1e5fd..73a2855 100644 --- a/custom_components/mega/sensor.py +++ b/custom_components/mega/sensor.py @@ -102,7 +102,6 @@ class Mega1WSensor(MegaPushEntity): unit_of_measurement, device_class, key=None, - http_cmd='get', *args, **kwargs ): @@ -118,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): diff --git a/readme.md b/readme.md index 341dc94..e3bfe92 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,10 @@ Интеграция с [MegaD-2561, MegaD-328](https://www.ab-log.ru/smart-house/ethernet/megad-2561) +Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если +интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната ) + +Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git) ## Основные особенности: - Настройка в веб-интерфейсе + yaml - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается @@ -17,6 +21,7 @@ - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о выполнении предыдущей. +- поддержка ds2413 (начиная с версии 0.4.1) ## Установка Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation): @@ -48,6 +53,15 @@ mega: 8: # исключить из сканирования skip: true + 10: + skip: false # если нужен skip, то он работает только целиком для всего порта + # в случае если порт настроен как ds2413 + address_a: #address - это адрес ds2413 + # здесь внутри работают все стандартные поля конфигурирования порта + invert: true + domain: switch + c6c439000000_b: + name: какое-то имя 33: # для датчиков можно кастомизировать только имя и unit_of_measurement # для температуры и влажность unit определяется автоматически, для остальных юнита нет