mirror of
https://github.com/tahnok/colmi_r02_client.git
synced 2026-02-06 10:47:28 +00:00
Merge branch 'sync-sport-details'
This commit is contained in:
commit
9e08383f63
@ -105,7 +105,7 @@ async def set_time(client: Client, when: datetime | None) -> None:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if when is None:
|
if when is None:
|
||||||
when = datetime.now(tz=timezone.utc)
|
when = date_utils.now()
|
||||||
async with client:
|
async with client:
|
||||||
await client.set_time(when)
|
await client.set_time(when)
|
||||||
click.echo("Time set successfully")
|
click.echo("Time set successfully")
|
||||||
@ -258,16 +258,22 @@ async def sync(client: Client, db_path: Path | None, start: datetime | None, end
|
|||||||
with db.get_db_session(db_path) as session:
|
with db.get_db_session(db_path) as session:
|
||||||
if start is None:
|
if start is None:
|
||||||
start = db.get_last_sync(session, client.address)
|
start = db.get_last_sync(session, client.address)
|
||||||
|
else:
|
||||||
|
start = date_utils.naive_to_aware(start)
|
||||||
|
|
||||||
if start is None:
|
if start is None:
|
||||||
start = date_utils.now() - timedelta(days=7)
|
start = date_utils.now() - timedelta(days=7)
|
||||||
|
|
||||||
if end is None:
|
if end is None:
|
||||||
end = date_utils.now()
|
end = date_utils.now()
|
||||||
|
else:
|
||||||
|
end = date_utils.naive_to_aware(end)
|
||||||
|
|
||||||
click.echo(f"Syncing from {start} to {end}")
|
click.echo(f"Syncing from {start} to {end}")
|
||||||
|
|
||||||
async with client:
|
async with client:
|
||||||
fd = await client.get_full_data(start, end)
|
fd = await client.get_full_data(start, end)
|
||||||
db.sync(session, fd)
|
db.full_sync(session, fd)
|
||||||
when = datetime.now(tz=timezone.utc)
|
when = datetime.now(tz=timezone.utc)
|
||||||
click.echo("Ignore unexpect packet")
|
click.echo("Ignore unexpect packet")
|
||||||
await client.set_time(when)
|
await client.set_time(when)
|
||||||
|
|||||||
@ -37,6 +37,7 @@ def log_packet(packet: bytearray) -> None:
|
|||||||
class FullData:
|
class FullData:
|
||||||
address: str
|
address: str
|
||||||
heart_rates: list[hr.HeartRateLog | hr.NoData]
|
heart_rates: list[hr.HeartRateLog | hr.NoData]
|
||||||
|
sport_details: list[list[steps.SportDetail] | steps.NoData]
|
||||||
|
|
||||||
|
|
||||||
COMMAND_HANDLERS: dict[int, Callable[[bytearray], Any]] = {
|
COMMAND_HANDLERS: dict[int, Callable[[bytearray], Any]] = {
|
||||||
@ -249,9 +250,10 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
Fetches all data from the ring between start and end. Useful for syncing.
|
Fetches all data from the ring between start and end. Useful for syncing.
|
||||||
"""
|
"""
|
||||||
|
heart_rate_logs = []
|
||||||
logs = []
|
sport_detail_logs = []
|
||||||
for d in date_utils.dates_between(start, end):
|
for d in date_utils.dates_between(start, end):
|
||||||
logs.append(await self.get_heart_rate_log(d))
|
heart_rate_logs.append(await self.get_heart_rate_log(d))
|
||||||
|
sport_detail_logs.append(await self.get_steps(d))
|
||||||
|
|
||||||
return FullData(self.address, logs)
|
return FullData(self.address, heart_rates=heart_rate_logs, sport_details=sport_detail_logs)
|
||||||
|
|||||||
@ -39,3 +39,10 @@ def minutes_so_far(dt: datetime) -> int:
|
|||||||
def is_today(ts: datetime) -> bool:
|
def is_today(ts: datetime) -> bool:
|
||||||
n = now()
|
n = now()
|
||||||
return bool(ts.year == n.year and ts.month == n.month and ts.day == n.day)
|
return bool(ts.year == n.year and ts.month == n.month and ts.day == n.day)
|
||||||
|
|
||||||
|
|
||||||
|
def naive_to_aware(dt: datetime) -> datetime:
|
||||||
|
if dt.tzinfo is not None:
|
||||||
|
raise ValueError("already tz aware datetime")
|
||||||
|
|
||||||
|
return dt.replace(tzinfo=timezone.utc)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session, rela
|
|||||||
from sqlalchemy import select, UniqueConstraint, ForeignKey, create_engine, event, func, types
|
from sqlalchemy import select, UniqueConstraint, ForeignKey, create_engine, event, func, types
|
||||||
from sqlalchemy.engine import Engine, Dialect
|
from sqlalchemy.engine import Engine, Dialect
|
||||||
|
|
||||||
from colmi_r02_client import hr
|
from colmi_r02_client import hr, steps
|
||||||
from colmi_r02_client.client import FullData
|
from colmi_r02_client.client import FullData
|
||||||
from colmi_r02_client.date_utils import start_of_day, end_of_day
|
from colmi_r02_client.date_utils import start_of_day, end_of_day
|
||||||
|
|
||||||
@ -61,6 +61,7 @@ class Ring(Base):
|
|||||||
ring_id: Mapped[int] = mapped_column(primary_key=True)
|
ring_id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
address: Mapped[str]
|
address: Mapped[str]
|
||||||
heart_rates: Mapped[list["HeartRate"]] = relationship(back_populates="ring")
|
heart_rates: Mapped[list["HeartRate"]] = relationship(back_populates="ring")
|
||||||
|
sport_details: Mapped[list["SportDetail"]] = relationship(back_populates="ring")
|
||||||
syncs: Mapped[list["Sync"]] = relationship(back_populates="ring")
|
syncs: Mapped[list["Sync"]] = relationship(back_populates="ring")
|
||||||
|
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ class Sync(Base):
|
|||||||
comment: Mapped[str | None]
|
comment: Mapped[str | None]
|
||||||
ring: Mapped["Ring"] = relationship(back_populates="syncs")
|
ring: Mapped["Ring"] = relationship(back_populates="syncs")
|
||||||
heart_rates: Mapped[list["HeartRate"]] = relationship(back_populates="sync")
|
heart_rates: Mapped[list["HeartRate"]] = relationship(back_populates="sync")
|
||||||
|
sport_details: Mapped[list["SportDetail"]] = relationship(back_populates="sync")
|
||||||
|
|
||||||
|
|
||||||
class HeartRate(Base):
|
class HeartRate(Base):
|
||||||
@ -86,6 +88,20 @@ class HeartRate(Base):
|
|||||||
sync: Mapped["Sync"] = relationship(back_populates="heart_rates")
|
sync: Mapped["Sync"] = relationship(back_populates="heart_rates")
|
||||||
|
|
||||||
|
|
||||||
|
class SportDetail(Base):
|
||||||
|
__tablename__ = "sport_details"
|
||||||
|
__table_args__ = (UniqueConstraint("ring_id", "timestamp"),)
|
||||||
|
sport_detail_id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
calories: Mapped[int]
|
||||||
|
steps: Mapped[int]
|
||||||
|
distance: Mapped[int]
|
||||||
|
timestamp = mapped_column(DateTimeInUTC(timezone=True), nullable=False)
|
||||||
|
ring_id = mapped_column(ForeignKey("rings.ring_id"), nullable=False)
|
||||||
|
ring: Mapped["Ring"] = relationship(back_populates="sport_details")
|
||||||
|
sync_id = mapped_column(ForeignKey("syncs.sync_id"), nullable=False)
|
||||||
|
sync: Mapped["Sync"] = relationship(back_populates="sport_details")
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(Engine, "connect")
|
@event.listens_for(Engine, "connect")
|
||||||
def set_sqlite_pragma(dbapi_connection: Any, _connection_record: Any) -> None:
|
def set_sqlite_pragma(dbapi_connection: Any, _connection_record: Any) -> None:
|
||||||
"""Enable actual foreign key checks in sqlite on every connection to the database"""
|
"""Enable actual foreign key checks in sqlite on every connection to the database"""
|
||||||
@ -124,17 +140,23 @@ def create_or_find_ring(session: Session, address: str) -> Ring:
|
|||||||
return ring
|
return ring
|
||||||
|
|
||||||
|
|
||||||
def sync(session: Session, data: FullData) -> None:
|
def full_sync(session: Session, data: FullData) -> None:
|
||||||
"""
|
"""
|
||||||
TODO:
|
TODO:
|
||||||
- grab battery
|
- grab battery
|
||||||
- grab steps
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ring = create_or_find_ring(session, data.address)
|
ring = create_or_find_ring(session, data.address)
|
||||||
sync = Sync(ring=ring, timestamp=datetime.now(tz=timezone.utc))
|
sync = Sync(ring=ring, timestamp=datetime.now(tz=timezone.utc))
|
||||||
session.add(sync)
|
session.add(sync)
|
||||||
|
|
||||||
|
_add_heart_rate(sync, ring, data, session)
|
||||||
|
_add_sport_details(sync, ring, data, session)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _add_heart_rate(sync: Sync, ring: Ring, data: FullData, session: Session) -> None:
|
||||||
|
logger.info(f"Adding {len(data.heart_rates)} days of heart rates")
|
||||||
for log in data.heart_rates:
|
for log in data.heart_rates:
|
||||||
if isinstance(log, hr.NoData):
|
if isinstance(log, hr.NoData):
|
||||||
logger.info("No heart rate data for date")
|
logger.info("No heart rate data for date")
|
||||||
@ -159,7 +181,44 @@ def sync(session: Session, data: FullData) -> None:
|
|||||||
h = HeartRate(reading=reading, timestamp=timestamp, ring=ring, sync=sync)
|
h = HeartRate(reading=reading, timestamp=timestamp, ring=ring, sync=sync)
|
||||||
session.add(h)
|
session.add(h)
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
def _add_sport_details(sync: Sync, ring: Ring, data: FullData, session: Session) -> None:
|
||||||
|
logger.info(f"Adding {len(data.sport_details)} days of sport details")
|
||||||
|
sport_detail_logs: list[steps.SportDetail] = []
|
||||||
|
for slog in data.sport_details:
|
||||||
|
if isinstance(slog, steps.NoData):
|
||||||
|
logger.info("No step data for date")
|
||||||
|
else:
|
||||||
|
sport_detail_logs.extend(slog)
|
||||||
|
if len(sport_detail_logs) == 0:
|
||||||
|
return
|
||||||
|
start = min([sport_detail.timestamp for sport_detail in sport_detail_logs])
|
||||||
|
end = max([sport_detail.timestamp for sport_detail in sport_detail_logs])
|
||||||
|
|
||||||
|
existing_sport_logs = {}
|
||||||
|
for sport_detail_record in session.scalars(
|
||||||
|
select(SportDetail)
|
||||||
|
.where(SportDetail.timestamp >= start_of_day(start))
|
||||||
|
.where(SportDetail.timestamp <= end_of_day(end))
|
||||||
|
):
|
||||||
|
existing_sport_logs[sport_detail_record.timestamp] = sport_detail_record
|
||||||
|
|
||||||
|
for sport_detail in sport_detail_logs:
|
||||||
|
if x := existing_sport_logs.get(sport_detail.timestamp):
|
||||||
|
x.calories = sport_detail.calories
|
||||||
|
x.steps = sport_detail.steps
|
||||||
|
x.distance = sport_detail.distance
|
||||||
|
session.add(x)
|
||||||
|
else:
|
||||||
|
s = SportDetail(
|
||||||
|
calories=sport_detail.calories,
|
||||||
|
steps=sport_detail.steps,
|
||||||
|
distance=sport_detail.distance,
|
||||||
|
timestamp=sport_detail.timestamp,
|
||||||
|
ring=ring,
|
||||||
|
sync=sync,
|
||||||
|
)
|
||||||
|
session.add(s)
|
||||||
|
|
||||||
|
|
||||||
def get_last_sync(session: Session, ring_address: str) -> datetime | None:
|
def get_last_sync(session: Session, ring_address: str) -> datetime | None:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from colmi_r02_client.packet import make_packet
|
from colmi_r02_client.packet import make_packet
|
||||||
|
|
||||||
@ -27,12 +28,23 @@ class SportDetail:
|
|||||||
month: int
|
month: int
|
||||||
day: int
|
day: int
|
||||||
time_index: int
|
time_index: int
|
||||||
"""I'm not sure about this one yet"""
|
"""time_index represents 15 minutes intevals within a day"""
|
||||||
calories: int
|
calories: int
|
||||||
steps: int
|
steps: int
|
||||||
distance: int
|
distance: int
|
||||||
"""Distance in meters"""
|
"""Distance in meters"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self) -> datetime:
|
||||||
|
return datetime(
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
hour=self.time_index // 4,
|
||||||
|
minute=self.time_index % 4 * 15,
|
||||||
|
tzinfo=timezone.utc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoData:
|
class NoData:
|
||||||
"""Returned when there's no heart rate data"""
|
"""Returned when there's no heart rate data"""
|
||||||
|
|||||||
@ -76,3 +76,6 @@ select = [
|
|||||||
"YTT", # flake8-2020
|
"YTT", # flake8-2020
|
||||||
"RUF", # Ruff-specific rules
|
"RUF", # Ruff-specific rules
|
||||||
]
|
]
|
||||||
|
ignore = [
|
||||||
|
"SIM108",
|
||||||
|
]
|
||||||
|
|||||||
@ -24,4 +24,18 @@ CREATE TABLE heart_rates (
|
|||||||
UNIQUE (ring_id, timestamp),
|
UNIQUE (ring_id, timestamp),
|
||||||
FOREIGN KEY(ring_id) REFERENCES rings (ring_id),
|
FOREIGN KEY(ring_id) REFERENCES rings (ring_id),
|
||||||
FOREIGN KEY(sync_id) REFERENCES syncs (sync_id)
|
FOREIGN KEY(sync_id) REFERENCES syncs (sync_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE sport_details (
|
||||||
|
sport_detail_id INTEGER NOT NULL,
|
||||||
|
calories INTEGER NOT NULL,
|
||||||
|
steps INTEGER NOT NULL,
|
||||||
|
distance INTEGER NOT NULL,
|
||||||
|
timestamp DATETIME NOT NULL,
|
||||||
|
ring_id INTEGER NOT NULL,
|
||||||
|
sync_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (sport_detail_id),
|
||||||
|
UNIQUE (ring_id, timestamp),
|
||||||
|
FOREIGN KEY(ring_id) REFERENCES rings (ring_id),
|
||||||
|
FOREIGN KEY(sync_id) REFERENCES syncs (sync_id)
|
||||||
)
|
)
|
||||||
@ -9,19 +9,30 @@ from sqlalchemy import text, select, func, Dialect
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from colmi_r02_client.client import FullData
|
from colmi_r02_client.client import FullData
|
||||||
from colmi_r02_client import hr
|
from colmi_r02_client import hr, steps
|
||||||
from colmi_r02_client.db import (
|
from colmi_r02_client.db import (
|
||||||
get_db_session,
|
get_db_session,
|
||||||
create_or_find_ring,
|
create_or_find_ring,
|
||||||
sync,
|
full_sync,
|
||||||
Ring,
|
Ring,
|
||||||
HeartRate,
|
HeartRate,
|
||||||
|
SportDetail,
|
||||||
Sync,
|
Sync,
|
||||||
get_last_sync,
|
get_last_sync,
|
||||||
DateTimeInUTC,
|
DateTimeInUTC,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="address")
|
||||||
|
def get_address() -> str:
|
||||||
|
return "fake"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="empty_full_data")
|
||||||
|
def get_empty_full_data(address) -> FullData:
|
||||||
|
return FullData(address=address, heart_rates=[], sport_details=[])
|
||||||
|
|
||||||
|
|
||||||
def test_get_db_session_memory():
|
def test_get_db_session_memory():
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
assert session.scalars(text("SELECT 1")).one() == 1
|
assert session.scalars(text("SELECT 1")).one() == 1
|
||||||
@ -44,6 +55,7 @@ def test_get_db_tables_exist():
|
|||||||
"rings",
|
"rings",
|
||||||
"syncs",
|
"syncs",
|
||||||
"heart_rates",
|
"heart_rates",
|
||||||
|
"sport_details",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -86,32 +98,25 @@ def test_ring_sync_id_required_for_heart_rate():
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def test_sync_creates_ring():
|
def test_sync_creates_ring(address, empty_full_data):
|
||||||
address = "fake"
|
|
||||||
fd = FullData(address=address, heart_rates=[])
|
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
sync(session, fd)
|
full_sync(session, empty_full_data)
|
||||||
|
|
||||||
ring = session.scalars(select(Ring)).one()
|
ring = session.scalars(select(Ring)).one()
|
||||||
assert address == ring.address
|
assert address == ring.address
|
||||||
|
|
||||||
|
|
||||||
def test_sync_uses_existing_ring():
|
def test_sync_uses_existing_ring(address, empty_full_data):
|
||||||
address = "fake"
|
|
||||||
fd = FullData(address=address, heart_rates=[])
|
|
||||||
|
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
create_or_find_ring(session, address)
|
create_or_find_ring(session, address)
|
||||||
sync(session, fd)
|
full_sync(session, empty_full_data)
|
||||||
|
|
||||||
assert session.scalars(func.count(Ring.ring_id)).one() == 1
|
assert session.scalars(func.count(Ring.ring_id)).one() == 1
|
||||||
|
|
||||||
|
|
||||||
def test_sync_creates_sync():
|
def test_sync_creates_sync(address, empty_full_data):
|
||||||
address = "fake"
|
|
||||||
fd = FullData(address=address, heart_rates=[])
|
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
sync(session, fd)
|
full_sync(session, empty_full_data)
|
||||||
|
|
||||||
sync_obj = session.scalars(select(Sync)).one()
|
sync_obj = session.scalars(select(Sync)).one()
|
||||||
|
|
||||||
@ -127,9 +132,9 @@ def test_sync_writes_heart_rates():
|
|||||||
index=295,
|
index=295,
|
||||||
range=5,
|
range=5,
|
||||||
)
|
)
|
||||||
fd = FullData(address=address, heart_rates=[hrl])
|
fd = FullData(address=address, heart_rates=[hrl], sport_details=[])
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
sync(session, fd)
|
full_sync(session, fd)
|
||||||
|
|
||||||
ring = session.scalars(select(Ring)).one()
|
ring = session.scalars(select(Ring)).one()
|
||||||
logs = session.scalars(select(HeartRate)).all()
|
logs = session.scalars(select(HeartRate)).all()
|
||||||
@ -152,9 +157,9 @@ def test_sync_writes_heart_rates_only_non_zero_heart_rates():
|
|||||||
index=295,
|
index=295,
|
||||||
range=5,
|
range=5,
|
||||||
)
|
)
|
||||||
fd = FullData(address=address, heart_rates=[hrl])
|
fd = FullData(address=address, heart_rates=[hrl], sport_details=[])
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
sync(session, fd)
|
full_sync(session, fd)
|
||||||
|
|
||||||
logs = session.scalars(select(HeartRate)).all()
|
logs = session.scalars(select(HeartRate)).all()
|
||||||
|
|
||||||
@ -170,7 +175,7 @@ def test_sync_writes_heart_rates_once():
|
|||||||
index=295,
|
index=295,
|
||||||
range=5,
|
range=5,
|
||||||
)
|
)
|
||||||
fd_1 = FullData(address=address, heart_rates=[hrl_1])
|
fd_1 = FullData(address=address, heart_rates=[hrl_1], sport_details=[])
|
||||||
|
|
||||||
hrl_2 = hr.HeartRateLog(
|
hrl_2 = hr.HeartRateLog(
|
||||||
heart_rates=[80] * 288,
|
heart_rates=[80] * 288,
|
||||||
@ -179,10 +184,10 @@ def test_sync_writes_heart_rates_once():
|
|||||||
index=295,
|
index=295,
|
||||||
range=5,
|
range=5,
|
||||||
)
|
)
|
||||||
fd_2 = FullData(address=address, heart_rates=[hrl_2])
|
fd_2 = FullData(address=address, heart_rates=[hrl_2], sport_details=[])
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
sync(session, fd_1)
|
full_sync(session, fd_1)
|
||||||
sync(session, fd_2)
|
full_sync(session, fd_2)
|
||||||
|
|
||||||
logs = session.scalars(select(HeartRate)).all()
|
logs = session.scalars(select(HeartRate)).all()
|
||||||
|
|
||||||
@ -198,7 +203,7 @@ def test_sync_handles_inconsistent_data(caplog):
|
|||||||
index=295,
|
index=295,
|
||||||
range=5,
|
range=5,
|
||||||
)
|
)
|
||||||
fd_1 = FullData(address=address, heart_rates=[hrl_1])
|
fd_1 = FullData(address=address, heart_rates=[hrl_1], sport_details=[])
|
||||||
|
|
||||||
hrl_2 = hr.HeartRateLog(
|
hrl_2 = hr.HeartRateLog(
|
||||||
heart_rates=[90] * 288,
|
heart_rates=[90] * 288,
|
||||||
@ -207,10 +212,10 @@ def test_sync_handles_inconsistent_data(caplog):
|
|||||||
index=295,
|
index=295,
|
||||||
range=5,
|
range=5,
|
||||||
)
|
)
|
||||||
fd_2 = FullData(address=address, heart_rates=[hrl_2])
|
fd_2 = FullData(address=address, heart_rates=[hrl_2], sport_details=[])
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
sync(session, fd_1)
|
full_sync(session, fd_1)
|
||||||
sync(session, fd_2)
|
full_sync(session, fd_2)
|
||||||
|
|
||||||
logs = session.scalars(select(HeartRate)).all()
|
logs = session.scalars(select(HeartRate)).all()
|
||||||
|
|
||||||
@ -219,6 +224,42 @@ def test_sync_handles_inconsistent_data(caplog):
|
|||||||
assert "Inconsistent data detected! 2024-11-11 00:00:00+00:00 is 80 in db but got 90 from ring" in caplog.text
|
assert "Inconsistent data detected! 2024-11-11 00:00:00+00:00 is 80 in db but got 90 from ring" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_sync_writes_sport_details():
|
||||||
|
address = "fake"
|
||||||
|
sd = steps.SportDetail(
|
||||||
|
year=2025,
|
||||||
|
month=1,
|
||||||
|
day=1,
|
||||||
|
time_index=0,
|
||||||
|
calories=4200,
|
||||||
|
steps=6969,
|
||||||
|
distance=1234,
|
||||||
|
)
|
||||||
|
fd = FullData(address=address, heart_rates=[], sport_details=[[sd]])
|
||||||
|
with get_db_session() as session:
|
||||||
|
full_sync(session, fd)
|
||||||
|
|
||||||
|
ring = session.scalars(select(Ring)).one()
|
||||||
|
sport_details = session.scalars(select(SportDetail)).all()
|
||||||
|
sync_obj = session.scalars(select(Sync)).one()
|
||||||
|
|
||||||
|
assert len(sport_details) == 1
|
||||||
|
assert sport_details[0].ring_id == ring.ring_id
|
||||||
|
assert sport_details[0].timestamp == datetime(2025, 1, 1, 0, 0, tzinfo=timezone.utc)
|
||||||
|
assert sport_details[0].sync_id == sync_obj.sync_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_sync_no_sport_details():
|
||||||
|
address = "fake"
|
||||||
|
fd = FullData(address=address, heart_rates=[], sport_details=[steps.NoData(), steps.NoData()])
|
||||||
|
with get_db_session() as session:
|
||||||
|
full_sync(session, fd)
|
||||||
|
|
||||||
|
sport_details = session.scalars(select(SportDetail)).all()
|
||||||
|
|
||||||
|
assert len(sport_details) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_get_last_sync_never():
|
def test_get_last_sync_never():
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
ring = Ring(address="foo")
|
ring = Ring(address="foo")
|
||||||
@ -276,6 +317,7 @@ def test_datetime_in_utc_process_bind_none():
|
|||||||
assert dtiu.process_bind_param(None, dialect) is None
|
assert dtiu.process_bind_param(None, dialect) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
@given(st.datetimes())
|
@given(st.datetimes())
|
||||||
def test_datetime_in_utc_process_bind_no_tz(ts: datetime):
|
def test_datetime_in_utc_process_bind_no_tz(ts: datetime):
|
||||||
dtiu = DateTimeInUTC()
|
dtiu = DateTimeInUTC()
|
||||||
@ -285,6 +327,7 @@ def test_datetime_in_utc_process_bind_no_tz(ts: datetime):
|
|||||||
dtiu.process_bind_param(ts, dialect)
|
dtiu.process_bind_param(ts, dialect)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
@given(st.datetimes(timezones=st.timezones()))
|
@given(st.datetimes(timezones=st.timezones()))
|
||||||
def test_datetime_in_utc_process_bind_tz(ts: datetime):
|
def test_datetime_in_utc_process_bind_tz(ts: datetime):
|
||||||
dtiu = DateTimeInUTC()
|
dtiu = DateTimeInUTC()
|
||||||
@ -304,6 +347,7 @@ def test_datetime_in_utc_process_result_none():
|
|||||||
assert dtiu.process_result_value(None, dialect) is None
|
assert dtiu.process_result_value(None, dialect) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
@given(st.datetimes())
|
@given(st.datetimes())
|
||||||
def test_datetime_in_utc_process_result_no_tz(ts: datetime):
|
def test_datetime_in_utc_process_result_no_tz(ts: datetime):
|
||||||
dtiu = DateTimeInUTC()
|
dtiu = DateTimeInUTC()
|
||||||
@ -315,6 +359,7 @@ def test_datetime_in_utc_process_result_no_tz(ts: datetime):
|
|||||||
assert result.tzinfo == timezone.utc
|
assert result.tzinfo == timezone.utc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
@given(st.datetimes(timezones=st.timezones()))
|
@given(st.datetimes(timezones=st.timezones()))
|
||||||
def test_datetime_in_utc_process_tz(ts: datetime):
|
def test_datetime_in_utc_process_tz(ts: datetime):
|
||||||
dtiu = DateTimeInUTC()
|
dtiu = DateTimeInUTC()
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from colmi_r02_client.steps import SportDetailParser, SportDetail, NoData
|
from colmi_r02_client.steps import SportDetailParser, SportDetail, NoData
|
||||||
|
|
||||||
|
|
||||||
@ -86,3 +88,31 @@ def test_no_data_parse():
|
|||||||
actual = sdp.parse(resp)
|
actual = sdp.parse(resp)
|
||||||
|
|
||||||
assert isinstance(actual, NoData)
|
assert isinstance(actual, NoData)
|
||||||
|
|
||||||
|
|
||||||
|
def test_timestamp_midnight():
|
||||||
|
sd = SportDetail(
|
||||||
|
year=2025,
|
||||||
|
month=1,
|
||||||
|
day=1,
|
||||||
|
time_index=0,
|
||||||
|
calories=0,
|
||||||
|
distance=0,
|
||||||
|
steps=0,
|
||||||
|
)
|
||||||
|
ts = datetime(2025, 1, 1, 0, 0, tzinfo=timezone.utc)
|
||||||
|
assert sd.timestamp == ts
|
||||||
|
|
||||||
|
|
||||||
|
def test_timestamp_one_more():
|
||||||
|
sd = SportDetail(
|
||||||
|
year=2025,
|
||||||
|
month=1,
|
||||||
|
day=1,
|
||||||
|
time_index=95,
|
||||||
|
calories=0,
|
||||||
|
distance=0,
|
||||||
|
steps=0,
|
||||||
|
)
|
||||||
|
ts = datetime(2025, 1, 1, 23, 45, tzinfo=timezone.utc)
|
||||||
|
assert sd.timestamp == ts
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user