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
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
1"""Cooking Sensor Info Descriptor implementation."""
3from __future__ import annotations
5from enum import IntEnum
7import msgspec
9from bluetooth_sig.types.uuid import BluetoothUUID
11from ..characteristics.utils import DataParser
12from ..constants import SIZE_UINT8, SIZE_UINT16, SIZE_UUID16
13from .base import BaseDescriptor
15_AGGREGATE_OFFSET_SIZE = SIZE_UINT16
16_SENSOR_INFO_MIN_LENGTH = SIZE_UUID16 + SIZE_UINT8 + SIZE_UINT8 + SIZE_UINT8
19class CookingSensorLocationType(IntEnum):
20 """Permitted Cooking Sensor Info location types from CWS Table 3.20."""
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
33class CookingSensorLocation(msgspec.Struct, frozen=True, kw_only=True):
34 """Typed Sensor Location field from the Cooking Sensor Info descriptor."""
36 location_type: CookingSensorLocationType
37 distance_mm: int | None = None
40class CookingSensorInfoData(msgspec.Struct, frozen=True, kw_only=True):
41 """Cooking Sensor Info descriptor data."""
43 sensor_uuid: BluetoothUUID
44 measurement_uncertainty: int
45 location: CookingSensorLocation
46 aggregate_offset: int | None = None
49class CookingSensorInfoDescriptor(BaseDescriptor):
50 """Cooking Sensor Info Descriptor (0x2916)."""
52 def _has_structured_data(self) -> bool:
53 return True
55 def _get_data_format(self) -> str:
56 return "struct"
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 )
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)
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])
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")
81 aggregate_offset = None
82 if remaining == _AGGREGATE_OFFSET_SIZE:
83 aggregate_offset = DataParser.parse_int16(data, location_data_end, signed=False, endian="little")
85 return CookingSensorInfoData(
86 sensor_uuid=sensor_uuid,
87 measurement_uncertainty=measurement_uncertainty,
88 location=location,
89 aggregate_offset=aggregate_offset,
90 )
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
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 )
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)