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,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()
|
||||
Reference in new issue
Block a user