Coverage for src/bluetooth_sig/gatt/characteristics/cooking_common.py: 98%
85 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"""Shared helpers and structs for Cooking/Cookware characteristics."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from ..constants import SIZE_UINT8, SIZE_UINT16
10from .cooking_temperature import CookingTemperatureCharacteristic
11from .cooking_zone_perceived_power import CookingZonePerceivedPowerCharacteristic
12from .humidity import HumidityCharacteristic
13from .kitchen_appliance_airflow import KitchenApplianceAirflowCharacteristic
14from .percentage_8 import Percentage8Characteristic
15from .power import PowerCharacteristic
16from .utils import DataParser
18COOKING_TEMPERATURE = CookingTemperatureCharacteristic()
19COOKING_ZONE_PERCEIVED_POWER = CookingZonePerceivedPowerCharacteristic()
20HUMIDITY = HumidityCharacteristic()
21KITCHEN_APPLIANCE_AIRFLOW = KitchenApplianceAirflowCharacteristic()
22PERCENTAGE_8 = Percentage8Characteristic()
23POWER = PowerCharacteristic()
26class CookingConditionsFlags(IntFlag):
27 """Bit flags for Cooking Conditions payload presence."""
29 POWER_LEVEL_PRESENT = 1 << 0
30 TEMPERATURE_PRESENT = 1 << 1
31 HUMIDITY_PRESENT = 1 << 2
32 BLOWER_FAN_SPEED_PRESENT = 1 << 3
33 MANUFACTURER_DATA_PRESENT = 1 << 4
36class CookingConditionsData(msgspec.Struct, frozen=True, kw_only=True):
37 """Shared parsed data model for Cooking Conditions characteristics."""
39 flags: CookingConditionsFlags
40 power_level: float | None = None
41 temperature: float | None = None
42 humidity: float | None = None
43 blower_fan_speed: float | None = None
44 manufacturer_specific_data: bytes | None = None
47def parse_cooking_conditions(data: bytearray) -> CookingConditionsData:
48 """Parse a Cooking Conditions-compatible byte payload."""
49 flags = CookingConditionsFlags(DataParser.parse_int16(data, 0, signed=False))
50 validate_flags(flags, CookingConditionsFlags, "Cooking Conditions Flags")
51 offset = 2
53 power_level = None
54 if flags & CookingConditionsFlags.POWER_LEVEL_PRESENT:
55 power_level = COOKING_ZONE_PERCEIVED_POWER.parse_value(bytearray(data[offset : offset + SIZE_UINT16]))
56 offset += SIZE_UINT16
58 temperature = None
59 if flags & CookingConditionsFlags.TEMPERATURE_PRESENT:
60 temperature = COOKING_TEMPERATURE.parse_value(bytearray(data[offset : offset + SIZE_UINT16]))
61 offset += SIZE_UINT16
63 humidity = None
64 if flags & CookingConditionsFlags.HUMIDITY_PRESENT:
65 humidity = HUMIDITY.parse_value(bytearray(data[offset : offset + SIZE_UINT16]))
66 offset += SIZE_UINT16
68 blower_fan_speed = None
69 if flags & CookingConditionsFlags.BLOWER_FAN_SPEED_PRESENT:
70 blower_fan_speed = PERCENTAGE_8.parse_value(bytearray(data[offset : offset + SIZE_UINT8]))
71 offset += SIZE_UINT8
73 manufacturer_data = None
74 if flags & CookingConditionsFlags.MANUFACTURER_DATA_PRESENT:
75 manufacturer_data = bytes(data[offset:])
77 return CookingConditionsData(
78 flags=flags,
79 power_level=power_level,
80 temperature=temperature,
81 humidity=humidity,
82 blower_fan_speed=blower_fan_speed,
83 manufacturer_specific_data=manufacturer_data,
84 )
87def encode_cooking_conditions(value: CookingConditionsData) -> bytearray:
88 """Encode shared Cooking Conditions data model into bytes."""
89 validate_flags(value.flags, CookingConditionsFlags, "Cooking Conditions Flags")
90 result = bytearray()
91 result.extend(DataParser.encode_int16(int(value.flags), signed=False))
93 if value.flags & CookingConditionsFlags.POWER_LEVEL_PRESENT:
94 if value.power_level is None:
95 raise ValueError("power_level is required when POWER_LEVEL_PRESENT is set")
96 result.extend(COOKING_ZONE_PERCEIVED_POWER.build_value(value.power_level))
98 if value.flags & CookingConditionsFlags.TEMPERATURE_PRESENT:
99 if value.temperature is None:
100 raise ValueError("temperature is required when TEMPERATURE_PRESENT is set")
101 result.extend(COOKING_TEMPERATURE.build_value(value.temperature))
103 if value.flags & CookingConditionsFlags.HUMIDITY_PRESENT:
104 if value.humidity is None:
105 raise ValueError("humidity is required when HUMIDITY_PRESENT is set")
106 result.extend(HUMIDITY.build_value(value.humidity))
108 if value.flags & CookingConditionsFlags.BLOWER_FAN_SPEED_PRESENT:
109 if value.blower_fan_speed is None:
110 raise ValueError("blower_fan_speed is required when BLOWER_FAN_SPEED_PRESENT is set")
111 result.extend(PERCENTAGE_8.build_value(value.blower_fan_speed))
113 if value.flags & CookingConditionsFlags.MANUFACTURER_DATA_PRESENT:
114 if value.manufacturer_specific_data is None:
115 raise ValueError("manufacturer_specific_data is required when MANUFACTURER_DATA_PRESENT is set")
116 result.extend(value.manufacturer_specific_data)
118 return result
121def validate_flags(flags: IntFlag, flag_type: type[IntFlag], field_name: str) -> None:
122 """Reject RFU bits for manually parsed IntFlag fields."""
123 valid_mask = 0
124 for member in flag_type:
125 valid_mask |= int(member)
126 if int(flags) & ~valid_mask:
127 raise ValueError(f"{field_name} contains reserved bits")