Handle set_time_packet response by ignoring it

There is an implementation included for parsing the response, which is
some weird capabilities response but it's wrong
This commit is contained in:
Wesley Ellis 2024-09-03 21:33:41 -04:00
parent 41c3e8b9b9
commit 974982db72
4 changed files with 118 additions and 8 deletions

View File

@ -50,6 +50,7 @@ COMMAND_HANDLERS: dict[int, Callable[[bytearray], Any]] = {
real_time_heart_rate.CMD_STOP_HEART_RATE: empty_parse,
steps.CMD_GET_STEP_SOMEDAY: steps.SportDetailParser().parse,
heart_rate.CMD_READ_HEART_RATE: heart_rate.HeartRateLogParser().parse,
set_time.CMD_SET_TIME: empty_parse,
}

View File

@ -2,7 +2,7 @@
A python client for connecting to the Colmi R02 Smart ring
"""
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
import logging
import time
@ -76,14 +76,20 @@ async def get_heart_rate_log(client: Client, target: datetime) -> None:
@cli_client.command()
@click.option(
"--target",
"--when",
type=click.DateTime(),
required=True,
help="The date you want logs for",
required=False,
help="The date and time you want to set the ring to",
)
@click.pass_obj
async def set_time(client: Client, target: datetime) -> None:
await client.set_time(target)
async def set_time(client: Client, when: datetime | None) -> None:
"""
Set the time on the ring, required if you want to be able to interpret any of the logged data
"""
if when is None:
when = datetime.now(tz=timezone.utc)
await client.set_time(when)
DEVICE_NAME_PREFIXES = [

View File

@ -1,12 +1,26 @@
from datetime import datetime
"""
The smart ring has it's own internal clock that is used to determine what time a given heart rate or step took
place for accurate counting.
We always set the time in UTC.
"""
from datetime import datetime, timezone
import logging
from colmi_r02_client.packet import make_packet
logger = logging.getLogger(__name__)
CMD_SET_TIME = 1
def set_time_packet(target: datetime) -> bytearray:
if target.tzinfo != timezone.utc:
logger.info("Converting target time to utc")
target = target.astimezone(tz=timezone.utc)
assert target.year >= 2000
data = bytearray(7)
data[0] = byte_to_bcd(target.year % 2000)
data[1] = byte_to_bcd(target.month)
@ -25,3 +39,55 @@ def byte_to_bcd(b: int) -> int:
tens = b // 10
ones = b % 10
return (tens << 4) | ones
def parse_set_time_packet(packet: bytearray) -> dict[str, bool | int]:
"""
Parse the response to the set time packet which is some kind of capability response.
It seems useless. It does correctly say avatar is not supported and that heart rate is supported.
But it also says there's wechat support and it supports 20 contacts.
I think this is safe to swallow and ignore.
"""
assert packet[0] == CMD_SET_TIME
bArr = packet[1:]
data: dict[str, bool | int] = {}
data["mSupportTemperature"] = bArr[0] == 1
data["mSupportPlate"] = bArr[1] == 1
data["mSupportMenstruation"] = True
data["mSupportCustomWallpaper"] = (bArr[3] & 1) != 0
data["mSupportBloodOxygen"] = (bArr[3] & 2) != 0
data["mSupportBloodPressure"] = (bArr[3] & 4) != 0
data["mSupportFeature"] = (bArr[3] & 8) != 0
data["mSupportOneKeyCheck"] = (bArr[3] & 16) != 0
data["mSupportWeather"] = (bArr[3] & 32) != 0
data["mSupportWeChat"] = (bArr[3] & 64) == 0
data["mSupportAvatar"] = (bArr[3] & 128) != 0
# data["#width"] = ByteUtil.bytesToInt(Arrays.copyOfRange(bArr, 4, 6))
# data["#height"] = ByteUtil.bytesToInt(Arrays.copyOfRange(bArr, 6, 8))
data["mNewSleepProtocol"] = bArr[8] == 1
data["mMaxWatchFace"] = bArr[9]
data["mSupportContact"] = (bArr[10] & 1) != 0
data["mSupportLyrics"] = (bArr[10] & 2) != 0
data["mSupportAlbum"] = (bArr[10] & 4) != 0
data["mSupportGPS"] = (bArr[10] & 8) != 0
data["mSupportJieLiMusic"] = (bArr[10] & 16) != 0
data["mSupportManualHeart"] = (bArr[11] & 1) != 0
data["mSupportECard"] = (bArr[11] & 2) != 0
data["mSupportLocation"] = (bArr[11] & 4) != 0
data["mMusicSupport"] = (bArr[11] & 16) != 0
data["rtkMcu"] = (bArr[11] & 32) != 0
data["mEbookSupport"] = (bArr[11] & 64) != 0
data["mSupportBloodSugar"] = (bArr[11] & 128) != 0
if bArr[12] == 0:
data["mMaxContacts"] = 20
else:
data["mMaxContacts"] = bArr[12] * 10
data["bpSettingSupport"] = (bArr[13] & 2) != 0
data["mSupport4G"] = (bArr[13] & 4) != 0
data["mSupportNavPicture"] = (bArr[13] & 8) != 0
data["mSupportPressure"] = (bArr[13] & 16) != 0
data["mSupportHrv"] = (bArr[13] & 32) != 0
return data

View File

@ -1,4 +1,6 @@
from colmi_r02_client.set_time import byte_to_bcd
from datetime import datetime, timezone, timedelta
from colmi_r02_client.set_time import byte_to_bcd, set_time_packet, CMD_SET_TIME, parse_set_time_packet
import pytest
@ -12,3 +14,38 @@ def test_byte_to_bcd(normal, bcd):
def test_byte_to_bcd_bad(bad):
with pytest.raises(AssertionError):
byte_to_bcd(bad)
def test_set_time_packet():
ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
expected = bytearray(b"\x01$\x01\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00(")
actual = set_time_packet(ts)
assert actual == expected
assert actual[0] == CMD_SET_TIME
def test_set_time_1999():
ts = datetime(1999, 1, 1, 0, 0, 0)
with pytest.raises(AssertionError):
set_time_packet(ts)
def test_set_time_with_timezone():
ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone(timedelta(hours=-4)))
expected = bytearray(b"\x01$\x01\x01\x04\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00,")
actual = set_time_packet(ts)
assert actual == expected
def test_parse_set_time_response():
packet = bytearray(b'\x01\x00\x01\x00"\x00\x00\x00\x00\x01\x000\x01\x00\x10f')
capabilities = parse_set_time_packet(packet)
assert capabilities["mSupportManualHeart"]
assert not capabilities["mSupportBloodSugar"]