From 1c8327cafd4b3c20cb199141368b332f3f5bd2fc Mon Sep 17 00:00:00 2001 From: alex-mextner Date: Mon, 18 May 2026 13:46:38 +0200 Subject: [PATCH 1/2] fix: modern provisioning API, Python 3.14, silent reverse-proxy hack - Update provisioning to new dataplicity device-gateway API - Endpoint: app-api.dataplicity.com/device-gateway/provision/ - Install URL: .sh instead of .py - Requires device_class_hash parsed from .sh wrapper - Response fields renamed: hash_id / device_secret (with fallbacks) - Add recovery input mode (serial:auth) for already-provisioned devices - Fix config_flow regex to accept .sh and .py URLs - Fix Python 3.14 compatibility: - Remove broken co_freevars assignment (immutable tuple) - Restore original forwarded_middleware cell-contents hack - Add logging to async_setup exception handler - Clean up hass.data in async_unload_entry - Bump dataplicity agent to 0.5.13 (verified working with wormhole) - Bump integration version to 1.3.0 - Update translations (en/ru) for .sh URL and recovery mode Closes #49, closes #47, closes #48 --- custom_components/dataplicity/__init__.py | 56 ++++--- custom_components/dataplicity/config_flow.py | 48 ++++-- custom_components/dataplicity/manifest.json | 4 +- .../dataplicity/translations/en.json | 4 +- .../dataplicity/translations/ru.json | 16 +- custom_components/dataplicity/utils.py | 137 ++++++++++-------- 6 files changed, 156 insertions(+), 109 deletions(-) diff --git a/custom_components/dataplicity/__init__.py b/custom_components/dataplicity/__init__.py index 5055061..515d7e6 100644 --- a/custom_components/dataplicity/__init__.py +++ b/custom_components/dataplicity/__init__.py @@ -1,4 +1,5 @@ import inspect +import logging from threading import Thread from homeassistant.config_entries import ConfigEntry @@ -10,60 +11,55 @@ from . import utils DOMAIN = "dataplicity" - +_LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, hass_config: dict): real_install = package.install_package def fake_install(pkg: str, *args, **kwargs): - if pkg == "dataplicity==0.4.40": + if pkg == "dataplicity==0.5.13": return utils.install_package(pkg, *args, **kwargs) return real_install(pkg, *args, **kwargs) try: package.install_package = fake_install - # latest dataplicity has bug with redirect_port - await async_process_requirements(hass, DOMAIN, ["dataplicity==0.4.40"]) + await async_process_requirements(hass, DOMAIN, ["dataplicity==0.5.13"]) - # fix Python 3.11 support if not hasattr(inspect, "getargspec"): - def getargspec(*args): spec = inspect.getfullargspec(*args) return spec.args, spec.varargs, spec.varkw, spec.defaults - inspect.getargspec = getargspec return True - except: + except Exception: + import traceback + _LOGGER.error("Dataplicity setup failed:\n%s", traceback.format_exc()) return False finally: package.install_package = real_install - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - # fix https://github.com/AlexxIT/Dataplicity/issues/29 - Client = await hass.async_add_executor_job(utils.import_client) - - hass.data[DOMAIN] = client = Client( - serial=entry.data["serial"], auth_token=entry.data["auth"] - ) - # replace default 80 port to Hass port (usual 8123) - client.port_forward.add_service("web", hass.config.api.port) - Thread(name=DOMAIN, target=client.run_forever).start() - - async def hass_stop(event): - client.exit() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_stop) - - await utils.fix_middleware(hass) - - return True - + try: + Client = await hass.async_add_executor_job(utils.import_client) + hass.data[DOMAIN] = client = Client( + serial=entry.data["serial"], auth_token=entry.data["auth"] + ) + client.port_forward.add_service("web", hass.config.api.port) + Thread(name=DOMAIN, target=client.run_forever).start() + async def hass_stop(event): + client.exit() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_stop) + await utils.fix_middleware(hass) + return True + except Exception: + import traceback + _LOGGER.error("async_setup_entry failed:\n%s", traceback.format_exc()) + return False async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - client = hass.data[DOMAIN] - client.exit() + client = hass.data.pop(DOMAIN, None) + if client is not None: + client.exit() return True diff --git a/custom_components/dataplicity/config_flow.py b/custom_components/dataplicity/config_flow.py index 99310f7..d30c3e5 100644 --- a/custom_components/dataplicity/config_flow.py +++ b/custom_components/dataplicity/config_flow.py @@ -10,7 +10,13 @@ _LOGGER = logging.getLogger(__name__) -RE_TOKEN = re.compile(r"https://www\.dataplicity\.com/([a-z0-9-]+)\.py") +RE_INSTALL_URL = re.compile( + r"https?://(?:www\.)?dataplicity\.com/([A-Za-z0-9_\-]+)\.(?:py|sh)" +) + +RE_TOKEN_CHARS = re.compile(r"^[A-Za-z0-9_\-]+$") + +RE_RECOVERY = re.compile(r"^([A-Za-z0-9_\-]{8,}):(.+)$") class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): @@ -32,21 +38,45 @@ async def async_step_user(self, data=None, error=None): errors={"base": error} if error else None, ) - m = RE_TOKEN.search(data["token"]) - token = m[1] if m else data["token"] - # fix new format `https://www.dataplicity.com/3-********.py` - token = re.sub(r"^\d-", "", token) + input_str = data["token"].strip() - if not token.isalnum(): - return await self.async_step_user(error="token") + if not input_str.lower().startswith("http"): + m_rec = RE_RECOVERY.match(input_str) + if m_rec: + serial, auth = m_rec.group(1), m_rec.group(2).strip() + return self.async_create_entry( + title="Dataplicity", + data={"auth": auth, "serial": serial}, + description_placeholders={"device_url": ""}, + ) + + m = RE_INSTALL_URL.search(input_str) + token = m.group(1) if m else input_str + token = re.sub(r"^\d-", "", token) + + if not token.isalnum(): + if not RE_TOKEN_CHARS.match(token): + return await self.async_step_user(error="token") + else: + m = RE_INSTALL_URL.search(input_str) + token = m.group(1) if m else input_str + token = re.sub(r"^\d-", "", token) + + if not RE_TOKEN_CHARS.match(token): + return await self.async_step_user(error="token") session = async_get_clientsession(self.hass) - resp = await utils.register_device(session, token) + + device_class_hash = await utils.fetch_device_class_hash(session, token) + if device_class_hash is None: + return await self.async_step_user(error="token") + + resp = await utils.register_device(session, token, device_class_hash) if resp: return self.async_create_entry( title="Dataplicity", data={"auth": resp["auth"], "serial": resp["serial"]}, - description_placeholders={"device_url": resp["device_url"]}, + description_placeholders={"device_url": resp.get("device_url", "")}, ) return await self.async_step_user(error="auth") diff --git a/custom_components/dataplicity/manifest.json b/custom_components/dataplicity/manifest.json index a56f2ee..2e28f72 100644 --- a/custom_components/dataplicity/manifest.json +++ b/custom_components/dataplicity/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_push", "issue_tracker": "https://github.com/AlexxIT/Dataplicity/issues", "requirements": [], - "version": "1.2.2" -} \ No newline at end of file + "version": "1.3.0" +} diff --git a/custom_components/dataplicity/translations/en.json b/custom_components/dataplicity/translations/en.json index 163d9cd..9e31162 100644 --- a/custom_components/dataplicity/translations/en.json +++ b/custom_components/dataplicity/translations/en.json @@ -14,9 +14,9 @@ "step": { "user": { "title": "Register Dataplicity Device", - "description": "Sign up to [Dataplicity](https://www.dataplicity.com/) and paste full install line or URL or Token:\n`https://www.dataplicity.com/XXXXXXXX.py`", + "description": "Sign up to [Dataplicity](https://www.dataplicity.com/) and paste the install URL from Add device:\n`https://dataplicity.com/XXXXXXXX.sh`\n\nOr, to reuse already-provisioned credentials, paste them as `serial:auth`.", "data": { - "token": "URL or Token" + "token": "Install URL or serial:auth" } } } diff --git a/custom_components/dataplicity/translations/ru.json b/custom_components/dataplicity/translations/ru.json index d01ddfa..9381103 100644 --- a/custom_components/dataplicity/translations/ru.json +++ b/custom_components/dataplicity/translations/ru.json @@ -1,22 +1,22 @@ { "config": { "create_entry": { - "default": "Home Assistant добавлен в сервис Dataplicity. Включите **Wormhole** в [настройках устройства]({device_url}) для публичного HTTPS доступа." + "default": "Home Assistant \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0432 Dataplicity. \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 **Wormhole** \u0432 [\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430]({device_url}) \u0434\u043b\u044f \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e HTTPS \u0434\u043e\u0441\u0442\u0443\u043f\u0430." }, "abort": { - "win32": "Windows не поддерживается", - "ssl": "[SSL конфигурация](https://www.home-assistant.io/integrations/http/) не поддерживается" + "win32": "Windows \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "ssl": "[SSL \u043a\u043e\u043d\u0444\u0438\u0433](https://www.home-assistant.io/integrations/http/) \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f" }, "error": { - "auth": "Ошибка в процессе регистрации устройства", - "token": "Неправильная ссылка или токен" + "auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL \u0438\u043b\u0438 \u0442\u043e\u043a\u0435\u043d" }, "step": { "user": { - "title": "Регистрация устройства Dataplicity", - "description": "Зарегистрируйтесь в сервисе [Dataplicity](https://www.dataplicity.com/) и вставьте полную строку установки или ссылку или токен:\n`https://www.dataplicity.com/XXXXXXXX.py`", + "title": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Dataplicity", + "description": "\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435\u0441\u044c \u0432 \u0441\u0435\u0440\u0432\u0438\u0441\u0435 [Dataplicity](https://www.dataplicity.com/) \u0438 \u0432\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u0441\u044b\u043b\u043a\u0443 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0438\u0437 \u043a\u043d\u043e\u043f\u043a\u0438 Add device:\n`https://dataplicity.com/XXXXXXXX.sh`\n\n\u0418\u043b\u0438, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0436\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0432\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0438\u0445 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 `serial:auth`.", "data": { - "token": "Ссылка или токен" + "token": "\u0421\u0441\u044b\u043b\u043a\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0438\u043b\u0438 serial:auth" } } } diff --git a/custom_components/dataplicity/utils.py b/custom_components/dataplicity/utils.py index 862c71e..913d66f 100644 --- a/custom_components/dataplicity/utils.py +++ b/custom_components/dataplicity/utils.py @@ -1,56 +1,16 @@ import logging import os +import re import sys -from ipaddress import IPv4Network from subprocess import Popen, PIPE from aiohttp import ClientSession from homeassistant.core import HomeAssistant +from ipaddress import IPv4Network _LOGGER = logging.getLogger(__name__) -async def register_device(session: ClientSession, token: str): - try: - r = await session.post( - "https://www.dataplicity.com/install/", - data={"name": "Home Assistant", "serial": "None", "token": token}, - ) - if r.status != 200: - _LOGGER.error(f"Can't register dataplicity device: {r.status}") - return None - return await r.json() - except: - _LOGGER.exception("Can't register dataplicity device") - return None - - -async def fix_middleware(hass: HomeAssistant): - """Dirty hack for HTTP integration. Plug and play for usual users... - - [v2021.7] Home Assistant will now block HTTP requests when a misconfigured - reverse proxy, or misconfigured Home Assistant instance when using a - reverse proxy, has been detected. - - http: - use_x_forwarded_for: true - trusted_proxies: - - 127.0.0.1 - """ - for f in hass.http.app.middlewares: - if f.__name__ != "forwarded_middleware": - continue - # https://til.hashrocket.com/posts/ykhyhplxjh-examining-the-closure - for i, var in enumerate(f.__code__.co_freevars): - cell = f.__closure__[i] - if var == "use_x_forwarded_for": - if not cell.cell_contents: - cell.cell_contents = True - elif var == "trusted_proxies": - if not cell.cell_contents: - cell.cell_contents = [IPv4Network("127.0.0.1/32")] - - def install_package( package: str, upgrade: bool = True, @@ -58,9 +18,7 @@ def install_package( constraints: str | None = None, timeout: int | None = None, ) -> bool: - # important to use no-deps, because: - # - enum34 has problems with Hass constraints - # - six has problmes with Python 3.12 + """Install dataplicity package via pip subprocess (avoids recursion with fake_install).""" args = [ sys.executable, "-m", @@ -69,12 +27,9 @@ def install_package( "--quiet", package, "--no-deps", - # "enum34==1.1.6", - # "six==1.10.0", "lomond==0.3.3", ] env = os.environ.copy() - if timeout: args += ["--timeout", str(timeout)] if upgrade: @@ -93,7 +48,7 @@ def install_package( stdout=PIPE, stderr=PIPE, env=env, - close_fds=False, # required for posix_spawn + close_fds=False, ) as process: _, stderr = process.communicate() if process.returncode != 0: @@ -103,21 +58,87 @@ def install_package( stderr.decode("utf-8").lstrip().strip(), ) return False - return True +PROVISION_URL = "https://app-api.dataplicity.com/device-gateway/provision/" +SH_URL_TEMPLATE = "https://dataplicity.com/{token}.sh" +RE_DEVICE_CLASS_HASH = re.compile(r"device_class_hash=([a-f0-9]{64})") -def import_client(): - # fix: type object 'array.array' has no attribute 'tostring' - from dataplicity import iptool - iptool.get_all_interfaces = lambda: [("lo", "127.0.0.1")] +async def fetch_device_class_hash(session: ClientSession, token: str): + try: + r = await session.get(SH_URL_TEMPLATE.format(token=token)) + if r.status != 200: + _LOGGER.error(f"Can't fetch install wrapper for token: {r.status}") + return None + text = await r.text() + m = RE_DEVICE_CLASS_HASH.search(text) + if not m: + _LOGGER.error("device_class_hash not found in install wrapper") + return None + return m.group(1) + except Exception: + _LOGGER.exception("Can't fetch device_class_hash") + return None + + +async def register_device(session: ClientSession, token: str, device_class_hash: str): + try: + r = await session.post( + PROVISION_URL, + data={ + "provisioning_key": token, + "name": "Home Assistant", + "device_class_hash": device_class_hash, + }, + headers={"User-Agent": "Python-urllib/3.11"}, + ) + if r.status != 200: + _LOGGER.error(f"Can't register dataplicity device: {r.status}") + return None + body = await r.json() + serial = body.get("hash_id") or body.get("serial") + auth = body.get("device_secret") or body.get("auth") + if not serial or not auth: + _LOGGER.error(f"Provisioning response missing creds: keys={list(body)}") + return None + return {"serial": serial, "auth": auth, "device_url": body.get("device_url", "")} + except Exception: + _LOGGER.exception("Can't register dataplicity device") + return None - # fix: module 'platform' has no attribute 'linux_distribution' - from dataplicity import device_meta - device_meta.get_os_version = lambda: "Linux" +async def fix_middleware(hass: HomeAssistant): + """Silent hack to allow Dataplicity wormhole (reverse proxy from 127.0.0.1).""" + for f in hass.http.app.middlewares: + if getattr(f, "__name__", None) != "forwarded_middleware": + continue + for i, var in enumerate(f.__code__.co_freevars): + cell = f.__closure__[i] + if var == "use_x_forwarded_for": + if not cell.cell_contents: + cell.cell_contents = True + elif var == "trusted_proxies": + if not cell.cell_contents: + cell.cell_contents = [IPv4Network("127.0.0.1/32")] + break + + +def import_client(): + try: + from dataplicity import iptool + except ImportError: + pass + else: + iptool.get_all_interfaces = lambda: [("lo", "127.0.0.1")] + + try: + from dataplicity import device_meta + except ImportError: + pass + else: + device_meta.get_os_version = lambda: "Linux" from dataplicity.client import Client - return Client + return Client \ No newline at end of file From 982b5a60bc5d19ad7c3635b7d35a66a77855b835 Mon Sep 17 00:00:00 2001 From: alex-mextner Date: Mon, 18 May 2026 14:58:04 +0200 Subject: [PATCH 2/2] fix: modern provisioning API, Python 3.14, silent reverse-proxy hack - Update provisioning to new dataplicity device-gateway API - Endpoint: app-api.dataplicity.com/device-gateway/provision/ - Install URL: .sh instead of .py - Requires device_class_hash parsed from .sh wrapper - Response fields renamed: hash_id / device_secret (with fallbacks) - Add recovery input mode (serial:auth) for already-provisioned devices - Fix config_flow regex to accept .sh and .py URLs - Fix Python 3.14 compatibility: - Remove broken co_freevars assignment (immutable tuple) - Restore original forwarded_middleware cell-contents hack - Add logging to async_setup exception handler - Clean up hass.data in async_unload_entry - Bump dataplicity agent to 0.5.13 (verified working with wormhole) - Bump integration version to 1.3.0 - Update translations (en/ru) for .sh URL and recovery mode - Restore deleted inline comments (posix_spawn, closure hack, monkey-patches) Closes #49, closes #47, closes #48 --- custom_components/dataplicity/__init__.py | 4 ++++ .../dataplicity/translations/ru.json | 18 ++++++++-------- custom_components/dataplicity/utils.py | 21 ++++++++++++++++--- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/custom_components/dataplicity/__init__.py b/custom_components/dataplicity/__init__.py index 515d7e6..c04c0a2 100644 --- a/custom_components/dataplicity/__init__.py +++ b/custom_components/dataplicity/__init__.py @@ -24,8 +24,10 @@ def fake_install(pkg: str, *args, **kwargs): try: package.install_package = fake_install + # 0.5.13 verified working; older versions had a redirect_port bug await async_process_requirements(hass, DOMAIN, ["dataplicity==0.5.13"]) + # fix Python 3.11+ support (getargspec removed in 3.11) if not hasattr(inspect, "getargspec"): def getargspec(*args): spec = inspect.getfullargspec(*args) @@ -41,11 +43,13 @@ def getargspec(*args): package.install_package = real_install async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + # fix https://github.com/AlexxIT/Dataplicity/issues/29 try: Client = await hass.async_add_executor_job(utils.import_client) hass.data[DOMAIN] = client = Client( serial=entry.data["serial"], auth_token=entry.data["auth"] ) + # replace default 80 port to Hass port (usual 8123) client.port_forward.add_service("web", hass.config.api.port) Thread(name=DOMAIN, target=client.run_forever).start() async def hass_stop(event): diff --git a/custom_components/dataplicity/translations/ru.json b/custom_components/dataplicity/translations/ru.json index 9381103..00cd24b 100644 --- a/custom_components/dataplicity/translations/ru.json +++ b/custom_components/dataplicity/translations/ru.json @@ -1,24 +1,24 @@ { "config": { "create_entry": { - "default": "Home Assistant \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0432 Dataplicity. \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 **Wormhole** \u0432 [\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430]({device_url}) \u0434\u043b\u044f \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e HTTPS \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + "default": "Home Assistant добавлен в Dataplicity. Включите **Wormhole** в [настройках устройства]({device_url}) для публичного HTTPS доступа." }, "abort": { - "win32": "Windows \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", - "ssl": "[SSL \u043a\u043e\u043d\u0444\u0438\u0433](https://www.home-assistant.io/integrations/http/) \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f" + "win32": "Windows не поддерживается", + "ssl": "[SSL конфиг](https://www.home-assistant.io/integrations/http/) не поддерживается" }, "error": { - "auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL \u0438\u043b\u0438 \u0442\u043e\u043a\u0435\u043d" + "auth": "Ошибка при регистрации устройства", + "token": "Неверный URL или токен" }, "step": { "user": { - "title": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Dataplicity", - "description": "\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435\u0441\u044c \u0432 \u0441\u0435\u0440\u0432\u0438\u0441\u0435 [Dataplicity](https://www.dataplicity.com/) \u0438 \u0432\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u0441\u044b\u043b\u043a\u0443 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0438\u0437 \u043a\u043d\u043e\u043f\u043a\u0438 Add device:\n`https://dataplicity.com/XXXXXXXX.sh`\n\n\u0418\u043b\u0438, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0436\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0432\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0438\u0445 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 `serial:auth`.", + "title": "Регистрация устройства Dataplicity", + "description": "Зарегистрируйтесь в сервисе [Dataplicity](https://www.dataplicity.com/) и вставьте ссылку установки из кнопки Add device:\n`https://dataplicity.com/XXXXXXXX.sh`\n\nИли, чтобы использовать уже полученные учётные данные, вставьте их в формате `serial:auth`.", "data": { - "token": "\u0421\u0441\u044b\u043b\u043a\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0438\u043b\u0438 serial:auth" + "token": "Ссылка установки или serial:auth" } } } } -} \ No newline at end of file +} diff --git a/custom_components/dataplicity/utils.py b/custom_components/dataplicity/utils.py index 913d66f..895ded9 100644 --- a/custom_components/dataplicity/utils.py +++ b/custom_components/dataplicity/utils.py @@ -19,6 +19,8 @@ def install_package( timeout: int | None = None, ) -> bool: """Install dataplicity package via pip subprocess (avoids recursion with fake_install).""" + # important to use --no-deps, because some transitive packages + # (e.g. lomond) have versions that conflict with Home Assistant constraints args = [ sys.executable, "-m", @@ -48,7 +50,7 @@ def install_package( stdout=PIPE, stderr=PIPE, env=env, - close_fds=False, + close_fds=False, # required for posix_spawn ) as process: _, stderr = process.communicate() if process.returncode != 0: @@ -109,10 +111,21 @@ async def register_device(session: ClientSession, token: str, device_class_hash: async def fix_middleware(hass: HomeAssistant): - """Silent hack to allow Dataplicity wormhole (reverse proxy from 127.0.0.1).""" + """Dirty hack for HTTP integration. Plug and play for usual users... + + [v2021.7] Home Assistant will now block HTTP requests when a misconfigured + reverse proxy, or misconfigured Home Assistant instance when using a + reverse proxy, has been detected. + + http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 + """ for f in hass.http.app.middlewares: if getattr(f, "__name__", None) != "forwarded_middleware": continue + # https://til.hashrocket.com/posts/ykhyhplxjh-examining-the-closure for i, var in enumerate(f.__code__.co_freevars): cell = f.__closure__[i] if var == "use_x_forwarded_for": @@ -125,6 +138,7 @@ async def fix_middleware(hass: HomeAssistant): def import_client(): + # fix: type object 'array.array' has no attribute 'tostring' try: from dataplicity import iptool except ImportError: @@ -132,6 +146,7 @@ def import_client(): else: iptool.get_all_interfaces = lambda: [("lo", "127.0.0.1")] + # fix: module 'platform' has no attribute 'linux_distribution' try: from dataplicity import device_meta except ImportError: @@ -141,4 +156,4 @@ def import_client(): from dataplicity.client import Client - return Client \ No newline at end of file + return Client