diff --git a/custom_components/dataplicity/config_flow.py b/custom_components/dataplicity/config_flow.py index 99310f7..72c5c10 100644 --- a/custom_components/dataplicity/config_flow.py +++ b/custom_components/dataplicity/config_flow.py @@ -10,7 +10,9 @@ _LOGGER = logging.getLogger(__name__) -RE_TOKEN = re.compile(r"https://www\.dataplicity\.com/([a-z0-9-]+)\.py") +RE_INSTALL_URL = re.compile(r"dataplicity\.com/([A-Za-z0-9_-]+)") +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,17 +34,32 @@ 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 = data["token"].strip() + + if m := RE_RECOVERY.match(token): + serial, auth = m.group(1), m.group(2).strip() + return self.async_create_entry( + title="Dataplicity", + data={"auth": auth, "serial": serial}, + description_placeholders={"device_url": ""}, + ) + + # 2026.05 link format https://app-api.dataplicity.com/3-XXXXXXXX.py + if m := RE_INSTALL_URL.search(token): + token = m.group(1) + 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") session = async_get_clientsession(self.hass) - resp = await utils.register_device(session, token) - if resp: + + 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") + + if resp := await utils.register_device(session, token, device_class_hash): return self.async_create_entry( title="Dataplicity", data={"auth": resp["auth"], "serial": resp["serial"]}, diff --git a/custom_components/dataplicity/translations/en.json b/custom_components/dataplicity/translations/en.json index 163d9cd..059a3ca 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`dataplicity.com/XXXXXXXX...`\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..b405bef 100644 --- a/custom_components/dataplicity/translations/ru.json +++ b/custom_components/dataplicity/translations/ru.json @@ -14,9 +14,9 @@ "step": { "user": { "title": "Регистрация устройства Dataplicity", - "description": "Зарегистрируйтесь в сервисе [Dataplicity](https://www.dataplicity.com/) и вставьте полную строку установки или ссылку или токен:\n`https://www.dataplicity.com/XXXXXXXX.py`", + "description": "Зарегистрируйтесь в сервисе [Dataplicity](https://www.dataplicity.com/) и вставьте ссылку установки из кнопки Add device:\n`dataplicity.com/XXXXXXXX...`\n\nИли используйте ранее полученные учётные данные, в формате `serial:auth`.", "data": { - "token": "Ссылка или токен" + "token": "Ссылка установки или serial:auth" } } } diff --git a/custom_components/dataplicity/utils.py b/custom_components/dataplicity/utils.py index 862c71e..0ed019d 100644 --- a/custom_components/dataplicity/utils.py +++ b/custom_components/dataplicity/utils.py @@ -1,5 +1,6 @@ import logging import os +import re import sys from ipaddress import IPv4Network from subprocess import Popen, PIPE @@ -9,20 +10,54 @@ _LOGGER = logging.getLogger(__name__) +RE_DEVICE_CLASS_HASH = re.compile(r"device_class_hash=([a-f0-9]{64})") -async def register_device(session: ClientSession, token: str): + +async def fetch_device_class_hash(session: ClientSession, token: str): + try: + r = await session.get(f"https://dataplicity.com/{token}.sh") + if r.status != 200: + _LOGGER.error(f"Can't fetch install wrapper for token: {r.status}") + return None + + text = await r.text() + if m := RE_DEVICE_CLASS_HASH.search(text): + return m.group(1) + + _LOGGER.error("device_class_hash not found in install wrapper") + except Exception as e: + _LOGGER.error("Can't fetch device_class_hash", exc_info=e) + + return None + + +async def register_device(session: ClientSession, token: str, device_class_hash: str): try: r = await session.post( - "https://www.dataplicity.com/install/", - data={"name": "Home Assistant", "serial": "None", "token": token}, + "https://app-api.dataplicity.com/device-gateway/provision/", + 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 - return await r.json() - except: - _LOGGER.exception("Can't register dataplicity device") - return None + + data = await r.json() + serial = data.get("hash_id") or data.get("serial") + auth = data.get("device_secret") or data.get("auth") + if serial and auth: + device_url = data.get("device_url") or "https://www.dataplicity.com/" + return {"serial": serial, "auth": auth, "device_url": device_url} + + _LOGGER.error(f"Provisioning response missing creds: keys={list(data)}") + except Exception as e: + _LOGGER.error("Can't register dataplicity device", exc_info=e) + + return None async def fix_middleware(hass: HomeAssistant):