Add Baby Tracker client and Home Assistant integration
Python client for the Baby Tracker sync server, reverse engineered from the Android app (4.53), and a HACS-installable Home Assistant integration built on it. Each baby gets a log calendar and summary sensors, and actions add entries that sync back to the group. Reading has been run against the real server. Uploading and running inside real Home Assistant have not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
6466f002e0
51 files changed
+5199
No files matched your search
@@ -0,0 +1,88 @@
|
||||
"""Config flow for Baby Tracker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Mapping
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.selector import TextSelector, TextSelectorConfig, TextSelectorType
|
||||
|
||||
from .babytracker_api import AccountStateError, AuthenticationError, BabyTrackerClient, BabyTrackerError
|
||||
from .compat import vol
|
||||
from .const import CONF_DEVICE_UUID, DEVICE_NAME, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD))
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): TextSelector(TextSelectorConfig(type=TextSelectorType.EMAIL)),
|
||||
vol.Required(CONF_PASSWORD): _PASSWORD_SELECTOR,
|
||||
}
|
||||
)
|
||||
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): _PASSWORD_SELECTOR})
|
||||
|
||||
|
||||
class BabyTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
async def _async_login(self, email: str, password: str, device_uuid: str) -> tuple[int | None, dict[str, str]]:
|
||||
client = BabyTrackerClient(
|
||||
email, password, device_uuid=device_uuid, device_name=DEVICE_NAME, device_os_info=DEVICE_NAME
|
||||
)
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(client.login), {}
|
||||
except AuthenticationError:
|
||||
return None, {"base": "invalid_auth"}
|
||||
except AccountStateError:
|
||||
return None, {"base": "account_state"}
|
||||
except (BabyTrackerError, requests.RequestException):
|
||||
return None, {"base": "cannot_connect"}
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error logging in")
|
||||
return None, {"base": "unknown"}
|
||||
|
||||
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
# This ID stays with the entry for good. The server treats each new
|
||||
# value as a new device on the account.
|
||||
device_uuid = str(uuid.uuid4())
|
||||
account_id, errors = await self._async_login(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD], device_uuid
|
||||
)
|
||||
if not errors:
|
||||
await self.async_set_unique_id(str(account_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL],
|
||||
data={**user_input, CONF_DEVICE_UUID: device_uuid},
|
||||
)
|
||||
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA, errors=errors)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> ConfigFlowResult:
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
errors: dict[str, str] = {}
|
||||
entry = self._get_reauth_entry()
|
||||
if user_input is not None:
|
||||
account_id, errors = await self._async_login(
|
||||
entry.data[CONF_EMAIL], user_input[CONF_PASSWORD], entry.data[CONF_DEVICE_UUID]
|
||||
)
|
||||
if not errors:
|
||||
await self.async_set_unique_id(str(account_id))
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(entry, data_updates=user_input)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=REAUTH_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={"email": entry.data[CONF_EMAIL]},
|
||||
)
|
||||
Reference in new issue
Block a user