Coverage for src/bluetooth_sig/gatt/characteristics/cooking_zone_capabilities.py: 98%

85 statements  

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

1"""Cooking Zone Capabilities characteristic (0x2C29).""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum, IntFlag 

6 

7import msgspec 

8 

9from ..constants import SIZE_UINT8, SIZE_UINT16, SIZE_UINT24 

10from ..context import CharacteristicContext 

11from .base import BaseCharacteristic 

12from .cooking_common import ( 

13 COOKING_TEMPERATURE, 

14 COOKING_ZONE_PERCEIVED_POWER, 

15 KITCHEN_APPLIANCE_AIRFLOW, 

16 POWER, 

17 validate_flags, 

18) 

19from .utils import DataParser 

20 

21 

22class CookingZoneCapabilitiesFlags(IntFlag): 

23 """Bit flags for supported zone capability fields.""" 

24 

25 AGGREGATION_SUPPORTED = 1 << 0 

26 POWER_CONTROL_SUPPORTED = 1 << 1 

27 TEMPERATURE_CONTROL_SUPPORTED = 1 << 2 

28 HUMIDITY_CONTROL_SUPPORTED = 1 << 3 

29 BLOWER_FAN_AIRFLOW_SUPPORTED = 1 << 4 

30 MANUFACTURER_SPECIFIC_CONTROL_SUPPORTED = 1 << 5 

31 

32 

33class PowerTechnology(IntEnum): 

34 """Power technology for the cooking zone.""" 

35 

36 UNKNOWN_OR_OTHER = 0x00 

37 HEATING_INDUCTION = 0x01 

38 HEATING_GAS = 0x02 

39 HEATING_RADIANT = 0x03 

40 COOLING_REFRIGERATION = 0x04 

41 COOLING_FREEZER = 0x05 

42 MANUFACTURER_SPECIFIC = 0xFF 

43 

44 

45class CookingZoneCapabilitiesData(msgspec.Struct, frozen=True, kw_only=True): 

46 """Decoded Cooking Zone Capabilities payload.""" 

47 

48 flags: CookingZoneCapabilitiesFlags 

49 power_technology: PowerTechnology 

50 number_of_cooking_steps: int 

51 nominal_power: float | None = None 

52 boost_level_percent: int | None = None 

53 minimum_available_power: float | None = None 

54 maximum_temperature: float | None = None 

55 minimum_temperature: float | None = None 

56 maximum_blower_fan_airflow: float | None = None 

57 

58 

59class CookingZoneCapabilitiesCharacteristic(BaseCharacteristic[CookingZoneCapabilitiesData]): 

60 """Cooking Zone Capabilities characteristic (0x2C29). 

61 

62 org.bluetooth.characteristic.cooking_zone_capabilities 

63 """ 

64 

65 min_length = 5 

66 allow_variable_length = True 

67 

68 def _decode_value( 

69 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

70 ) -> CookingZoneCapabilitiesData: 

71 flags = CookingZoneCapabilitiesFlags(DataParser.parse_int16(data, 0, signed=False)) 

72 validate_flags(flags, CookingZoneCapabilitiesFlags, "Cooking Zone Capabilities Flags") 

73 power_technology = PowerTechnology(DataParser.parse_int8(data, 2, signed=False)) 

74 number_of_cooking_steps = DataParser.parse_int16(data, 3, signed=False) 

75 

76 offset = 5 

77 nominal_power = None 

78 boost_level_percent = None 

79 minimum_available_power = None 

80 if flags & CookingZoneCapabilitiesFlags.POWER_CONTROL_SUPPORTED: 

81 nominal_power = POWER.parse_value(bytearray(data[offset : offset + SIZE_UINT24])) 

82 offset += SIZE_UINT24 

83 boost_level_percent = DataParser.parse_int8(data, offset, signed=False) 

84 offset += SIZE_UINT8 

85 minimum_available_power = COOKING_ZONE_PERCEIVED_POWER.parse_value( 

86 bytearray(data[offset : offset + SIZE_UINT16]) 

87 ) 

88 offset += SIZE_UINT16 

89 

90 maximum_temperature = None 

91 minimum_temperature = None 

92 if flags & CookingZoneCapabilitiesFlags.TEMPERATURE_CONTROL_SUPPORTED: 

93 maximum_temperature = COOKING_TEMPERATURE.parse_value(bytearray(data[offset : offset + SIZE_UINT16])) 

94 offset += SIZE_UINT16 

95 minimum_temperature = COOKING_TEMPERATURE.parse_value(bytearray(data[offset : offset + SIZE_UINT16])) 

96 offset += SIZE_UINT16 

97 

98 maximum_blower_fan_airflow = None 

99 if flags & CookingZoneCapabilitiesFlags.BLOWER_FAN_AIRFLOW_SUPPORTED: 

100 maximum_blower_fan_airflow = KITCHEN_APPLIANCE_AIRFLOW.parse_value( 

101 bytearray(data[offset : offset + SIZE_UINT16]) 

102 ) 

103 

104 return CookingZoneCapabilitiesData( 

105 flags=flags, 

106 power_technology=power_technology, 

107 number_of_cooking_steps=number_of_cooking_steps, 

108 nominal_power=nominal_power, 

109 boost_level_percent=boost_level_percent, 

110 minimum_available_power=minimum_available_power, 

111 maximum_temperature=maximum_temperature, 

112 minimum_temperature=minimum_temperature, 

113 maximum_blower_fan_airflow=maximum_blower_fan_airflow, 

114 ) 

115 

116 def _encode_value(self, data: CookingZoneCapabilitiesData) -> bytearray: 

117 validate_flags(data.flags, CookingZoneCapabilitiesFlags, "Cooking Zone Capabilities Flags") 

118 result = bytearray() 

119 result.extend(DataParser.encode_int16(int(data.flags), signed=False)) 

120 result.extend(DataParser.encode_int8(data.power_technology, signed=False)) 

121 result.extend(DataParser.encode_int16(data.number_of_cooking_steps, signed=False)) 

122 

123 if data.flags & CookingZoneCapabilitiesFlags.POWER_CONTROL_SUPPORTED: 

124 if data.nominal_power is None or data.boost_level_percent is None or data.minimum_available_power is None: 

125 raise ValueError("power control fields are required when POWER_CONTROL_SUPPORTED is set") 

126 result.extend(POWER.build_value(data.nominal_power)) 

127 result.extend(DataParser.encode_int8(data.boost_level_percent, signed=False)) 

128 result.extend(COOKING_ZONE_PERCEIVED_POWER.build_value(data.minimum_available_power)) 

129 

130 if data.flags & CookingZoneCapabilitiesFlags.TEMPERATURE_CONTROL_SUPPORTED: 

131 if data.maximum_temperature is None or data.minimum_temperature is None: 

132 raise ValueError("temperature fields are required when TEMPERATURE_CONTROL_SUPPORTED is set") 

133 result.extend(COOKING_TEMPERATURE.build_value(data.maximum_temperature)) 

134 result.extend(COOKING_TEMPERATURE.build_value(data.minimum_temperature)) 

135 

136 if data.flags & CookingZoneCapabilitiesFlags.BLOWER_FAN_AIRFLOW_SUPPORTED: 

137 if data.maximum_blower_fan_airflow is None: 

138 raise ValueError("maximum_blower_fan_airflow is required when BLOWER_FAN_AIRFLOW_SUPPORTED is set") 

139 result.extend(KITCHEN_APPLIANCE_AIRFLOW.build_value(data.maximum_blower_fan_airflow)) 

140 

141 return result