Coverage for src/bluetooth_sig/gatt/descriptors/cooking_sensor_info.py: 95%

63 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-28 01:26 +0000

1"""Cooking Sensor Info Descriptor implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6 

7import msgspec 

8 

9from bluetooth_sig.types.uuid import BluetoothUUID 

10 

11from ..characteristics.utils import DataParser 

12from ..constants import SIZE_UINT8, SIZE_UINT16, SIZE_UUID16 

13from .base import BaseDescriptor 

14 

15_AGGREGATE_OFFSET_SIZE = SIZE_UINT16 

16_SENSOR_INFO_MIN_LENGTH = SIZE_UUID16 + SIZE_UINT8 + SIZE_UINT8 + SIZE_UINT8 

17 

18 

19class CookingSensorLocationType(IntEnum): 

20 """Permitted Cooking Sensor Info location types from CWS Table 3.20.""" 

21 

22 VESSEL_SIDE = 0x01 

23 VESSEL_BOTTOM = 0x02 

24 GRILL_PLATE = 0x03 

25 LID = 0x04 

26 PROBE_FOOD_CORE = 0x05 

27 PROBE_AMBIENT = 0x06 

28 HANDLE = 0x07 

29 ELECTRONICS_BATTERY = 0x08 

30 OTHER_UNKNOWN = 0xFF 

31 

32 

33class CookingSensorLocation(msgspec.Struct, frozen=True, kw_only=True): 

34 """Typed Sensor Location field from the Cooking Sensor Info descriptor.""" 

35 

36 location_type: CookingSensorLocationType 

37 distance_mm: int | None = None 

38 

39 

40class CookingSensorInfoData(msgspec.Struct, frozen=True, kw_only=True): 

41 """Cooking Sensor Info descriptor data.""" 

42 

43 sensor_uuid: BluetoothUUID 

44 measurement_uncertainty: int 

45 location: CookingSensorLocation 

46 aggregate_offset: int | None = None 

47 

48 

49class CookingSensorInfoDescriptor(BaseDescriptor): 

50 """Cooking Sensor Info Descriptor (0x2916).""" 

51 

52 def _has_structured_data(self) -> bool: 

53 return True 

54 

55 def _get_data_format(self) -> str: 

56 return "struct" 

57 

58 def _parse_descriptor_value(self, data: bytes) -> CookingSensorInfoData: 

59 if len(data) < _SENSOR_INFO_MIN_LENGTH: 

60 raise ValueError( 

61 f"Cooking Sensor Info descriptor needs at least {_SENSOR_INFO_MIN_LENGTH} bytes, got {len(data)}" 

62 ) 

63 

64 sensor_uuid = BluetoothUUID(DataParser.parse_int16(data, 0, signed=False, endian="little")) 

65 measurement_uncertainty = DataParser.parse_int8(data, SIZE_UUID16, signed=False) 

66 location_type = DataParser.parse_int8(data, SIZE_UUID16 + SIZE_UINT8, signed=False) 

67 location_data_size = DataParser.parse_int8(data, SIZE_UUID16 + SIZE_UINT8 + SIZE_UINT8, signed=False) 

68 

69 location_data_start = _SENSOR_INFO_MIN_LENGTH 

70 location_data_end = location_data_start + location_data_size 

71 if len(data) < location_data_end: 

72 raise ValueError( 

73 "Cooking Sensor Info descriptor location data is shorter than the declared Location Data Size" 

74 ) 

75 location = _parse_location(location_type, data[location_data_start:location_data_end]) 

76 

77 remaining = len(data) - location_data_end 

78 if remaining not in {0, _AGGREGATE_OFFSET_SIZE}: 

79 raise ValueError("Cooking Sensor Info descriptor has invalid trailing aggregate offset length") 

80 

81 aggregate_offset = None 

82 if remaining == _AGGREGATE_OFFSET_SIZE: 

83 aggregate_offset = DataParser.parse_int16(data, location_data_end, signed=False, endian="little") 

84 

85 return CookingSensorInfoData( 

86 sensor_uuid=sensor_uuid, 

87 measurement_uncertainty=measurement_uncertainty, 

88 location=location, 

89 aggregate_offset=aggregate_offset, 

90 ) 

91 

92 

93def _parse_location(location_type_raw: int, location_data: bytes) -> CookingSensorLocation: 

94 """Parse the typed Sensor Location field from CWS Table 3.20.""" 

95 try: 

96 location_type = CookingSensorLocationType(location_type_raw) 

97 except ValueError as exc: 

98 raise ValueError("Cooking Sensor Info location type is RFU") from exc 

99 

100 if location_type in { 

101 CookingSensorLocationType.VESSEL_SIDE, 

102 CookingSensorLocationType.VESSEL_BOTTOM, 

103 CookingSensorLocationType.GRILL_PLATE, 

104 CookingSensorLocationType.LID, 

105 CookingSensorLocationType.PROBE_FOOD_CORE, 

106 CookingSensorLocationType.PROBE_AMBIENT, 

107 }: 

108 if len(location_data) != SIZE_UINT16: 

109 raise ValueError("Cooking Sensor Info location type requires 2 bytes of location data") 

110 return CookingSensorLocation( 

111 location_type=location_type, 

112 distance_mm=DataParser.parse_int16(location_data, 0, signed=False, endian="little"), 

113 ) 

114 

115 if len(location_data) != 0: 

116 raise ValueError("Cooking Sensor Info location type does not include location data") 

117 return CookingSensorLocation(location_type=location_type)