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
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
1"""Cooking Zone Capabilities characteristic (0x2C29)."""
3from __future__ import annotations
5from enum import IntEnum, IntFlag
7import msgspec
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
22class CookingZoneCapabilitiesFlags(IntFlag):
23 """Bit flags for supported zone capability fields."""
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
33class PowerTechnology(IntEnum):
34 """Power technology for the cooking zone."""
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
45class CookingZoneCapabilitiesData(msgspec.Struct, frozen=True, kw_only=True):
46 """Decoded Cooking Zone Capabilities payload."""
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
59class CookingZoneCapabilitiesCharacteristic(BaseCharacteristic[CookingZoneCapabilitiesData]):
60 """Cooking Zone Capabilities characteristic (0x2C29).
62 org.bluetooth.characteristic.cooking_zone_capabilities
63 """
65 min_length = 5
66 allow_variable_length = True
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)
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
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
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 )
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 )
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))
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))
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))
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))
141 return result