Coverage for src / bluetooth_sig / gatt / characteristics / cgm_session_start_time.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""CGM Session Start Time characteristic implementation. 

2 

3Implements the CGM Session Start Time characteristic (0x2AAA). 

4 

5Structure (from GSS YAML): 

6 Session Start Time (7 bytes) -- DateTime struct (year+month+day+h+m+s) 

7 Time Zone (1 byte, uint8) -- offset from UTC in 15-minute increments 

8 DST Offset (1 byte, uint8) -- DST adjustment code 

9 E2E-CRC (2 bytes, uint16, optional) 

10 

11References: 

12 Bluetooth SIG Continuous Glucose Monitoring Service 

13 org.bluetooth.characteristic.cgm_session_start_time (GSS YAML) 

14""" 

15 

16from __future__ import annotations 

17 

18from datetime import datetime 

19 

20import msgspec 

21 

22from ..context import CharacteristicContext 

23from .base import BaseCharacteristic 

24from .dst_offset import DSTOffset 

25from .utils import DataParser 

26 

27 

28class CGMSessionStartTimeData(msgspec.Struct, frozen=True, kw_only=True): 

29 """Parsed data from CGM Session Start Time characteristic. 

30 

31 Attributes: 

32 start_time: Session start date and time. 

33 time_zone: Time zone offset from UTC in 15-minute increments. 

34 dst_offset: DST adjustment code. 

35 e2e_crc: E2E-CRC value. None if absent. 

36 

37 """ 

38 

39 start_time: datetime 

40 time_zone: int 

41 dst_offset: DSTOffset 

42 e2e_crc: int | None = None 

43 

44 

45class CGMSessionStartTimeCharacteristic(BaseCharacteristic[CGMSessionStartTimeData]): 

46 """CGM Session Start Time characteristic (0x2AAA). 

47 

48 Reports the session start time, time zone, and DST offset 

49 for a CGM session. 

50 """ 

51 

52 expected_type = CGMSessionStartTimeData 

53 min_length: int = 9 # datetime(7) + timezone(1) + dst(1) 

54 allow_variable_length: bool = True # optional E2E-CRC 

55 

56 def _decode_value( 

57 self, 

58 data: bytearray, 

59 ctx: CharacteristicContext | None = None, 

60 *, 

61 validate: bool = True, 

62 ) -> CGMSessionStartTimeData: 

63 """Parse CGM Session Start Time from raw BLE bytes. 

64 

65 Args: 

66 data: Raw bytearray from BLE characteristic (9 or 11 bytes). 

67 ctx: Optional context (unused). 

68 validate: Whether to validate ranges. 

69 

70 Returns: 

71 CGMSessionStartTimeData with parsed date/time and timezone info. 

72 

73 """ 

74 year = DataParser.parse_int16(data, 0, signed=False) 

75 month = data[2] 

76 day = data[3] 

77 hour = data[4] 

78 minute = data[5] 

79 second = data[6] 

80 start_time = datetime(year=year, month=month, day=day, hour=hour, minute=minute, second=second) 

81 

82 time_zone = data[7] 

83 dst_offset = DSTOffset(data[8]) 

84 

85 _min_length_with_crc = 11 

86 e2e_crc: int | None = None 

87 if len(data) >= _min_length_with_crc: 

88 e2e_crc = DataParser.parse_int16(data, 9, signed=False) 

89 

90 return CGMSessionStartTimeData( 

91 start_time=start_time, 

92 time_zone=time_zone, 

93 dst_offset=dst_offset, 

94 e2e_crc=e2e_crc, 

95 ) 

96 

97 def _encode_value(self, data: CGMSessionStartTimeData) -> bytearray: 

98 """Encode CGMSessionStartTimeData back to BLE bytes. 

99 

100 Args: 

101 data: CGMSessionStartTimeData instance. 

102 

103 Returns: 

104 Encoded bytearray (9 or 11 bytes). 

105 

106 """ 

107 result = bytearray() 

108 result.extend(DataParser.encode_int16(data.start_time.year, signed=False)) 

109 result.append(data.start_time.month) 

110 result.append(data.start_time.day) 

111 result.append(data.start_time.hour) 

112 result.append(data.start_time.minute) 

113 result.append(data.start_time.second) 

114 result.append(data.time_zone) 

115 result.append(int(data.dst_offset)) 

116 

117 if data.e2e_crc is not None: 

118 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False)) 

119 

120 return result