Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions custom_components/dataplicity/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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"]},
Expand Down
4 changes: 2 additions & 2 deletions custom_components/dataplicity/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/dataplicity/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down
49 changes: 42 additions & 7 deletions custom_components/dataplicity/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re
import sys
from ipaddress import IPv4Network
from subprocess import Popen, PIPE
Expand All @@ -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):
Expand Down
Loading