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

+81
View File
@@ -0,0 +1,81 @@
"""Builds a seed database using table definitions taken from the app (4.53)."""
import sqlite3
import zipfile
from pathlib import Path
_ID = '"ID" TEXT PRIMARY KEY NOT NULL UNIQUE, "Timestamp" REAL DEFAULT 0'
_ACTIVITY = f'{_ID}, "Time" REAL NOT NULL, "Note" TEXT, "HasPicture" INTEGER'
SCHEMA = f"""
CREATE TABLE "Baby" ({_ID}, "Name" TEXT NOT NULL UNIQUE, "DOB" REAL, "DueDay" REAL, "Gender" INTEGER, "Picture" TEXT);
CREATE TABLE "MergedTransaction" ("DeviceID" TEXT PRIMARY KEY NOT NULL UNIQUE, "SyncID" INTEGER);
CREATE TABLE "Diaper" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "Status" INTEGER NOT NULL, "PeeColor" INTEGER DEFAULT 5,
"PooColor" INTEGER DEFAULT 5, "Amount" INTEGER DEFAULT 5, "Texture" INTEGER DEFAULT 5, "Flag" INTEGER DEFAULT 0);
CREATE TABLE "Sleep" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "Duration" INTEGER DEFAULT 0, "LocationID" TEXT, "DescID" TEXT);
CREATE TABLE "Formula" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "DescID" TEXT, "Amount" REAL NOT NULL, "IsEnglishScale" INTEGER NOT NULL);
CREATE TABLE "Pumped" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "DescID" TEXT, "Amount" REAL NOT NULL, "IsEnglishScale" INTEGER NOT NULL, "PumpID" TEXT);
CREATE TABLE "Nursing" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "DescID" TEXT, "FinishSide" INTEGER, "LeftDuration" INTEGER DEFAULT 0,
"RightDuration" INTEGER DEFAULT 0, "BothDuration" INTEGER DEFAULT 0);
CREATE TABLE "Temperature" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "Temp" REAL NOT NULL, "IsEnglishScale" INTEGER NOT NULL);
CREATE TABLE "Growth" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "Weight" REAL DEFAULT 0, "Length" REAL DEFAULT 0, "Head" READ DEFAULT 0,
"IsEnglishLengthScale" INTEGER DEFAULT 1, "IsEnglishWeightScale" INTEGER DEFAULT 1);
CREATE TABLE "Medicine" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "MedID" TEXT NOT NULL, "Amount" REAL);
CREATE TABLE "MedicineSelection" ({_ID}, "Name" TEXT NOT NULL UNIQUE, "Description" TEXT, "IsPrescription" INTEGER,
"AmountPerTime" REAL, "Unit" TEXT, "Interval" INTEGER);
CREATE TABLE "Pump" ({_ACTIVITY}, "FinishSide" INTEGER, "Sides" INTEGER, "Amount" REAL, "IsEnglishScale" INTEGER, "Label" TEXT, "PumpedID" TEXT);
CREATE TABLE "Sick" ({_ACTIVITY}, "BabyID" TEXT NOT NULL, "Symptom" TEXT);
"""
BABY_ID = "baby-1"
def build_seed_db(path: Path, now: float) -> Path:
"""Write a database with one baby, Sam, whose entries are relative to ``now``."""
connection = sqlite3.connect(path)
connection.executescript(SCHEMA)
connection.execute(
'INSERT INTO "Baby" VALUES (?, ?, ?, ?, ?, ?, ?)', (BABY_ID, now - 86400, "Sam", now - 90 * 86400, 0, 1, None)
)
connection.execute('INSERT INTO "MergedTransaction" VALUES (?, ?)', ("phone", 3))
hour = 3600
rows = {
'"Diaper" ("ID","Timestamp","Time","Note","BabyID","Status","Amount")': [
("d-old", now, now - 40 * hour, "", BABY_ID, 0, 2),
("d-dry", now, now - 2 * hour, "", BABY_ID, 0, 0),
("d-new", now, now - 60, "messy", BABY_ID, 1, 3),
],
'"Sleep" ("ID","Timestamp","Time","BabyID","Duration")': [("s-1", now, now - 3 * hour, BABY_ID, 45)],
'"Formula" ("ID","Timestamp","Time","BabyID","Amount","IsEnglishScale")': [
("f-oz", now, now - 120, BABY_ID, 4, 1),
("f-ml", now, now - 180, BABY_ID, 100, 0),
],
'"Nursing" ("ID","Timestamp","Time","BabyID","FinishSide","LeftDuration","RightDuration")': [
("n-1", now, now - 5 * hour, BABY_ID, 2, 5, 7)
],
'"Temperature" ("ID","Timestamp","Time","BabyID","Temp","IsEnglishScale")': [
("t-1", now, now - 6 * hour, BABY_ID, 98.6, 1)
],
'"Growth" ("ID","Timestamp","Time","BabyID","Weight","Length","Head","IsEnglishLengthScale","IsEnglishWeightScale")': [
("g-old", now, now - 50 * hour, BABY_ID, 5.5, 60, 40, 0, 0),
("g-new", now, now - 7 * hour, BABY_ID, 13.2277, 0, 0, 1, 1),
],
'"MedicineSelection" ("ID","Timestamp","Name")': [("med-1", now, "Vitamin D")],
'"Medicine" ("ID","Timestamp","Time","BabyID","MedID","Amount")': [("m-1", now, now - 8 * hour, BABY_ID, "med-1", 1)],
'"Pump" ("ID","Timestamp","Time","Amount","IsEnglishScale")': [("p-1", now, now - 9 * hour, 80, 0)],
'"Sick" ("ID","Timestamp","Time","BabyID","Symptom")': [("k-1", now, now - 10 * hour, BABY_ID, "cough")],
}
for target, values in rows.items():
marks = ",".join("?" * len(values[0]))
connection.executemany(f"INSERT INTO {target} VALUES ({marks})", values)
connection.commit()
connection.close()
return path
def build_seed_zip(folder: Path, now: float) -> bytes:
db = build_seed_db(folder / "fixture.db", now)
zip_path = folder / "fixture.zip"
with zipfile.ZipFile(zip_path, "w") as archive:
archive.write(db, "EasyLog.db")
return zip_path.read_bytes()