Skip to content

Commit 1e78dc6

Browse files
committed
feat: Enable fetching activity & log diapers for babies
1 parent 3ee67a5 commit 1e78dc6

File tree

2 files changed

+154
-4
lines changed

2 files changed

+154
-4
lines changed

python_snoo/baby.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from python_snoo.containers import BabyData
1+
from datetime import datetime
2+
3+
from python_snoo.containers import Activity, BabyData, BreastfeedingActivity, DiaperActivity, DiaperTypes
24
from python_snoo.exceptions import SnooBabyError
35
from python_snoo.snoo import Snoo
46

@@ -8,6 +10,7 @@ def __init__(self, baby_id: str, snoo: Snoo):
810
self.baby_id = baby_id
911
self.snoo = snoo
1012
self.baby_url = f"https://api-us-east-1-prod.happiestbaby.com/us/me/v10/babies/{self.baby_id}"
13+
self.activity_base_url = "https://api-us-east-1-prod.happiestbaby.com/cs/me/v11"
1114

1215
@property
1316
def session(self):
@@ -21,3 +24,93 @@ async def get_status(self) -> BabyData:
2124
except Exception as ex:
2225
raise SnooBabyError from ex
2326
return BabyData.from_dict(resp)
27+
28+
async def get_activity_data(self, from_date: datetime, to_date: datetime) -> list[Activity]:
29+
"""Get activity data for this baby including feeding and diaper changes
30+
31+
Args:
32+
from_date: Start date for activity range
33+
to_date: End date for activity range
34+
35+
Returns:
36+
List of typed Activity objects (DiaperActivity or BreastfeedingActivity)
37+
"""
38+
hdrs = self.snoo.generate_snoo_auth_headers(self.snoo.tokens.aws_id)
39+
40+
url = f"{self.activity_base_url}/babies/{self.baby_id}/journals/grouped-tracking"
41+
42+
params = {
43+
"group": "activity",
44+
"fromDateTime": from_date.astimezone().isoformat(timespec="milliseconds"),
45+
"toDateTime": to_date.astimezone().isoformat(timespec="milliseconds"),
46+
}
47+
48+
try:
49+
r = await self.session.get(url, headers=hdrs, params=params)
50+
resp = await r.json()
51+
if r.status < 200 or r.status >= 300:
52+
raise SnooBabyError(f"Failed to get activity data: {r.status}: {resp}. Payload: {params}")
53+
54+
activities: list[Activity] = []
55+
if isinstance(resp, list):
56+
for activity in resp:
57+
activity_type = activity.get("type", "").lower()
58+
59+
if activity_type == "diaper":
60+
activities.append(DiaperActivity.from_dict(activity))
61+
elif activity_type == "breastfeeding":
62+
activities.append(BreastfeedingActivity.from_dict(activity))
63+
else:
64+
# Other activity types exist but aren't supported yet
65+
raise SnooBabyError(f"Unknown activity type: {activity_type}")
66+
else:
67+
raise SnooBabyError(f"Unexpected response format: {type(resp)}")
68+
69+
return activities
70+
71+
except Exception as ex:
72+
raise SnooBabyError from ex
73+
74+
async def log_diaper_change(
75+
self,
76+
diaper_types: list[DiaperTypes],
77+
note: str | None = None,
78+
start_time: datetime | None = None,
79+
) -> DiaperActivity:
80+
"""Log a diaper change for this baby
81+
82+
Args:
83+
diaper_types (list): List of diaper types. e.g. ['pee'], ['poo'], or ['pee', 'poo']
84+
note (str, optional): Optional note about the diaper change
85+
start_time (datetime, optional): Diaper change timestamp, doesn't allow length.
86+
Defaults to current local time if not provided.
87+
"""
88+
89+
if not start_time:
90+
start_time = datetime.now()
91+
92+
# Always include the timezone indicator in the ISO string - seems to be required by the API
93+
if start_time.tzinfo is None:
94+
start_time = start_time.astimezone()
95+
96+
hdrs = self.snoo.generate_snoo_auth_headers(self.snoo.tokens.aws_id)
97+
url = f"{self.activity_base_url}/journals"
98+
99+
payload = {
100+
"babyId": self.baby_id,
101+
"data": {"types": [dt.value for dt in diaper_types]},
102+
"type": "diaper",
103+
"startTime": start_time.isoformat(timespec="milliseconds"),
104+
}
105+
106+
if note:
107+
payload["note"] = note
108+
109+
try:
110+
r = await self.session.post(url, headers=hdrs, json=payload)
111+
resp = await r.json()
112+
if r.status < 200 or r.status >= 300:
113+
raise SnooBabyError(f"Failed to log diaper change: {r.status}: {resp}. Payload: {payload}")
114+
return DiaperActivity.from_dict(resp)
115+
except Exception as ex:
116+
raise SnooBabyError from ex

python_snoo/containers.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
import datetime
33
from enum import StrEnum
4-
from typing import Any
4+
from typing import Any, Union
55

66
from mashumaro.mixins.json import DataClassJSONMixin
77

@@ -47,6 +47,13 @@ class SnooEvents(StrEnum):
4747
RESTART = "restart"
4848

4949

50+
class DiaperTypes(StrEnum):
51+
"""Diaper change types, matching what the Happiest Baby app uses"""
52+
53+
WET = "pee"
54+
DIRTY = "poo"
55+
56+
5057
@dataclasses.dataclass
5158
class AuthorizationInfo:
5259
snoo: str
@@ -140,8 +147,58 @@ class BabyData(DataClassJSONMixin):
140147
disabledLimiter: bool
141148
expectedBirthDate: str
142149
pictures: list
143-
preemie: Any # Not sure what datatype this is yet
144150
settings: BabySettings
145-
sex: Any # Not sure what datatype this is yet
151+
sex: str
152+
preemie: Any | None = None # Not sure what datatype this is yet & may not be supplied - boolean?
146153
startedUsingSnooAt: str | None = None
147154
updatedAt: str | None = None
155+
156+
157+
@dataclasses.dataclass
158+
class DiaperData(DataClassJSONMixin):
159+
"""Data for diaper change activities"""
160+
161+
types: list[DiaperTypes]
162+
163+
def __post_init__(self):
164+
if not self.types:
165+
raise ValueError("DiaperData.types cannot be empty or None")
166+
167+
self.types = [DiaperTypes(dt) for dt in self.types]
168+
169+
170+
@dataclasses.dataclass
171+
class BreastfeedingData(DataClassJSONMixin):
172+
lastUsedBreast: str
173+
totalDuration: int
174+
left: dict | None = None
175+
right: dict | None = None
176+
177+
178+
@dataclasses.dataclass
179+
class DiaperActivity(DataClassJSONMixin):
180+
id: str
181+
type: str
182+
startTime: str
183+
babyId: str
184+
userId: str
185+
data: DiaperData
186+
createdAt: str
187+
updatedAt: str
188+
note: str | None = None
189+
190+
191+
@dataclasses.dataclass
192+
class BreastfeedingActivity(DataClassJSONMixin):
193+
id: str
194+
type: str
195+
startTime: str
196+
endTime: str
197+
babyId: str
198+
userId: str
199+
data: BreastfeedingData
200+
createdAt: str
201+
updatedAt: str
202+
203+
204+
Activity = Union[DiaperActivity, BreastfeedingActivity]

0 commit comments

Comments
 (0)