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:
arneandClaude Opus 5.5 committed 2026-11-04 10:04:39 +00:00
commit 6466f002e0
51 files changed
+5199

No files matched your search

Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,24 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from ...exceptions import HomeAssistantError
@dataclass
class CalendarEvent:
start: datetime
end: datetime
summary: str
description: Optional[str] = None
uid: Optional[str] = None
def __post_init__(self):
if self.start.tzinfo is None or self.end.tzinfo is None:
raise HomeAssistantError("Expected all values to have a timezone")
if self.end <= self.start:
raise HomeAssistantError("Expected end time to be after start time")
class CalendarEntity:
pass
@@ -0,0 +1,30 @@
from dataclasses import dataclass
from enum import Enum
from typing import Any, Optional
class SensorDeviceClass(str, Enum):
DISTANCE = "distance"
DURATION = "duration"
TEMPERATURE = "temperature"
TIMESTAMP = "timestamp"
VOLUME = "volume"
WEIGHT = "weight"
class SensorStateClass(str, Enum):
MEASUREMENT = "measurement"
TOTAL_INCREASING = "total_increasing"
@dataclass(frozen=True, kw_only=True)
class SensorEntityDescription:
key: str
device_class: Optional[SensorDeviceClass] = None
state_class: Optional[SensorStateClass] = None
native_unit_of_measurement: Optional[Any] = None
suggested_display_precision: Optional[int] = None
class SensorEntity:
pass
@@ -0,0 +1,63 @@
from enum import Enum
from typing import Generic, TypeVar
from .data_entry_flow import AbortFlow
_T = TypeVar("_T")
ConfigFlowResult = dict
class ConfigEntryState(Enum):
LOADED = "loaded"
NOT_LOADED = "not_loaded"
class ConfigEntry(Generic[_T]):
def __init__(self, data, unique_id, title="entry"):
self.data = dict(data)
self.unique_id = unique_id
self.title = title
self.state = ConfigEntryState.NOT_LOADED
self.runtime_data = None
self.unloads = []
def async_on_unload(self, func):
self.unloads.append(func)
class ConfigFlow:
def __init_subclass__(cls, domain=None, **kwargs):
super().__init_subclass__(**kwargs)
cls.domain = domain
def __init__(self):
self.hass = None
self.unique_id = None
self.existing_unique_ids = set()
self.reauth_entry = None
self.updated = None
def async_show_form(self, step_id, data_schema=None, errors=None, description_placeholders=None):
return {"type": "form", "step_id": step_id, "errors": errors or {}, "schema": data_schema,
"description_placeholders": description_placeholders}
def async_create_entry(self, title, data):
return {"type": "create_entry", "title": title, "data": data}
async def async_set_unique_id(self, unique_id):
self.unique_id = unique_id
def _abort_if_unique_id_configured(self):
if self.unique_id in self.existing_unique_ids:
raise AbortFlow("already_configured")
def _abort_if_unique_id_mismatch(self, reason="unique_id_mismatch"):
if self.reauth_entry.unique_id != self.unique_id:
raise AbortFlow(reason)
def _get_reauth_entry(self):
return self.reauth_entry
def async_update_reload_and_abort(self, entry, data_updates=None):
entry.data.update(data_updates or {})
return {"type": "abort", "reason": "reauth_successful"}
+29
View File
@@ -0,0 +1,29 @@
from enum import Enum
CONF_EMAIL = "email"
CONF_PASSWORD = "password"
class Platform(str, Enum):
CALENDAR = "calendar"
SENSOR = "sensor"
class UnitOfLength(str, Enum):
CENTIMETERS = "cm"
class UnitOfMass(str, Enum):
KILOGRAMS = "kg"
class UnitOfTemperature(str, Enum):
CELSIUS = "°C"
class UnitOfTime(str, Enum):
MINUTES = "min"
class UnitOfVolume(str, Enum):
MILLILITERS = "mL"
+50
View File
@@ -0,0 +1,50 @@
import asyncio
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional
ServiceResponse = Optional[dict]
def callback(func):
return func
class SupportsResponse(Enum):
NONE = "none"
OPTIONAL = "optional"
ONLY = "only"
@dataclass
class ServiceCall:
data: dict = field(default_factory=dict)
return_response: bool = False
class _Services:
def __init__(self):
self.registered = {}
def async_register(self, domain, service, handler, schema=None, supports_response=SupportsResponse.NONE):
self.registered[(domain, service)] = (handler, schema, supports_response)
async def async_call(self, domain, service, data=None, return_response=False):
handler, schema, _ = self.registered[(domain, service)]
validated = schema(dict(data or {})) if schema else dict(data or {})
return await handler(ServiceCall(validated, return_response))
class _Config:
def __init__(self, units):
self.units = units
class HomeAssistant:
def __init__(self, units):
self.config = _Config(units)
self.services = _Services()
self.config_entries = None
async def async_add_executor_job(self, target, *args):
return await asyncio.get_running_loop().run_in_executor(None, target, *args)
@@ -0,0 +1,4 @@
class AbortFlow(Exception):
def __init__(self, reason):
super().__init__(reason)
self.reason = reason
@@ -0,0 +1,18 @@
class HomeAssistantError(Exception):
def __init__(self, *args, translation_domain=None, translation_key=None, translation_placeholders=None):
super().__init__(*args)
self.translation_domain = translation_domain
self.translation_key = translation_key
self.translation_placeholders = translation_placeholders
class ServiceValidationError(HomeAssistantError):
pass
class ConfigEntryAuthFailed(HomeAssistantError):
pass
class ConfigEntryNotReady(HomeAssistantError):
pass
Whitespace-only changes.
@@ -0,0 +1,22 @@
from datetime import datetime as datetime_sys
import voluptuous
def string(value):
if value is None:
raise voluptuous.Invalid("string value is None")
return str(value)
def datetime(value):
if isinstance(value, datetime_sys):
return value
try:
return datetime_sys.fromisoformat(value)
except (TypeError, ValueError) as err:
raise voluptuous.Invalid("invalid datetime") from err
def config_entry_only_config_schema(domain):
return lambda config: config
@@ -0,0 +1,30 @@
from dataclasses import dataclass
from enum import Enum
class DeviceEntryType(Enum):
SERVICE = "service"
def DeviceInfo(**kwargs):
return kwargs
@dataclass
class DeviceEntry:
identifiers: set
class DeviceRegistry:
def __init__(self):
self.devices = {}
def async_get(self, device_id):
return self.devices.get(device_id)
REGISTRY = DeviceRegistry()
def async_get(hass):
return REGISTRY
@@ -0,0 +1,18 @@
from enum import Enum
class TextSelectorType(str, Enum):
EMAIL = "email"
PASSWORD = "password"
def TextSelectorConfig(**kwargs):
return kwargs
class TextSelector:
def __init__(self, config=None):
self.config = config
def __call__(self, value):
return str(value)
@@ -0,0 +1 @@
ConfigType = dict
@@ -0,0 +1,60 @@
from typing import Generic, TypeVar
from ..exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError
_T = TypeVar("_T")
class UpdateFailed(HomeAssistantError):
pass
class DataUpdateCoordinator(Generic[_T]):
def __init__(self, hass, logger, *, config_entry=None, name=None, update_interval=None):
self.hass = hass
self.logger = logger
self.config_entry = config_entry
self.name = name
self.update_interval = update_interval
self.data = None
self.last_update_success = True
self.last_exception = None
self._listeners = []
def async_add_listener(self, listener):
self._listeners.append(listener)
return lambda: self._listeners.remove(listener)
def _notify(self):
for listener in list(self._listeners):
listener()
async def async_refresh(self):
try:
self.data = await self._async_update_data()
except UpdateFailed as err:
self.last_update_success = False
self.last_exception = err
else:
self.last_update_success = True
self._notify()
async def async_config_entry_first_refresh(self):
try:
self.data = await self._async_update_data()
except UpdateFailed as err:
raise ConfigEntryNotReady(str(err)) from err
def async_set_updated_data(self, data):
self.data = data
self.last_update_success = True
self._notify()
class CoordinatorEntity(Generic[_T]):
def __init__(self, coordinator):
self.coordinator = coordinator
@property
def available(self):
return self.coordinator.last_update_success
Whitespace-only changes.
+21
View File
@@ -0,0 +1,21 @@
from datetime import datetime, timezone
DEFAULT_TIME_ZONE = timezone.utc
def utcnow():
return datetime.now(timezone.utc)
def as_utc(value):
if value.tzinfo is None:
value = value.replace(tzinfo=DEFAULT_TIME_ZONE)
return value.astimezone(timezone.utc)
def as_local(value):
return value.astimezone(DEFAULT_TIME_ZONE)
def start_of_local_day():
return utcnow().astimezone(DEFAULT_TIME_ZONE).replace(hour=0, minute=0, second=0, microsecond=0)
@@ -0,0 +1,6 @@
class UnitSystem:
pass
METRIC_SYSTEM = UnitSystem()
US_CUSTOMARY_SYSTEM = UnitSystem()