diet-guard/diet_guard/_phone_weight.py
Krzysztof kuhy Rudnicki e761926df4
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Body tab: weight/body-fat logs, BMI, ideal weight, goal-based kcal target, workouts + Health Connect steps, week planner
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 09:03:03 +02:00

89 lines
3.2 KiB
Python

"""Carry the newest logged weight into the budget's stored weight.
The Body weight log (:mod:`diet_guard._body_store`) holds every weigh-in --
the phone's morning ones, ingested each tick by :mod:`diet_guard._sync_body`,
and any typed on either device. Its newest entry is what the protein target
here is derived from (``_budget_derived.protein_target_g``), so after each
successful ``sync`` the stored ``w`` is refreshed from it.
Only ``w`` moves. The kcal budget is never recomputed -- the Body tab's
calorie table is informational, and the budget stays the number the user
edits. ``t`` is refreshed with ``w`` because both ride the same edit
timestamp into the per-field last-write-wins merge (``sync_merge._budget``);
a new weight under an old ``t`` would lose to every other device on the next
merge.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from diet_guard._body_store import newest_weight
from diet_guard._budget import _now_local, read_raw_record, write_raw_record
if TYPE_CHECKING:
from collections.abc import Callable
__all__ = ["refresh_weight_from_log", "update_weight"]
# Below this the scale and the record agree; a rewrite would only churn ``t``.
_SAME_WEIGHT_KG = 0.05
def update_weight(kg: float) -> bool:
"""Set the budget record's stored weight to ``kg``; False when no budget.
Writes through the raw accessors rather than :func:`_budget.write_budget`
on purpose: that path stamps a budget change into the effective-from
history, and the budget has not changed.
"""
record = read_raw_record()
if record is None:
return False
record["w"] = round(float(kg), 1)
record["t"] = _now_local().isoformat(timespec="seconds")
write_raw_record(record)
return True
def _budget_edit_day(record: dict[str, object]) -> str:
"""The local ``yyyy-mm-dd`` of the record's last edit, or '' when unknown."""
return str(record.get("t", ""))[:10]
def refresh_weight_from_log(emit: Callable[[str], None]) -> None:
"""Apply the weight log's newest weigh-in to the stored weight, if newer.
Skipped, silently, when: the log is empty; no budget exists; the
weigh-in is not from a *later* day than the budget's last edit; or it
matches the stored weight within :data:`_SAME_WEIGHT_KG`.
The day comparison is strict. A weigh-in happens in the morning and a
manual budget edit at any time of day, and ``t`` carries only the day's
granularity worth trusting here, so on a shared day the budget edit is
taken as the more recent human action and kept. The cost is one day of
lag in that one case; the alternative overwrites an edit made hours
after the weigh-in.
Args:
emit: One-line output sink, given a line only when a write happened.
"""
newest = newest_weight()
if newest is None:
return
day, kg = newest
record = read_raw_record()
if record is None:
return
if day <= _budget_edit_day(record):
return
stored = record.get("w")
if (
isinstance(stored, (int, float))
and not isinstance(stored, bool)
and abs(float(stored) - kg) < _SAME_WEIGHT_KG
):
return
update_weight(kg)
emit(f"weight: {kg} kg from the weight log ({day}).")