Coverage for src / bluetooth_sig / gatt / characteristics / chromaticity_in_cct_and_duv_values.py: 90%

39 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Chromaticity in CCT and Duv Values characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ...types.gatt_enums import CharacteristicRole 

8from ..constants import SINT16_MAX, SINT16_MIN, UINT16_MAX 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13# Duv resolution: M=1, d=-5, b=0 → 0.00001 

14_DUV_RESOLUTION = 1e-5 

15 

16 

17class ChromaticityInCCTAndDuvData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

18 """Data class for Chromaticity in CCT and Duv Values. 

19 

20 Combines Correlated Color Temperature (Kelvin) with Chromatic 

21 Distance from Planckian (unitless Duv). 

22 """ 

23 

24 correlated_color_temperature: int # Kelvin (uint16, raw) 

25 chromaticity_distance_from_planckian: float # Unitless Duv (sint16, scaled) 

26 

27 def __post_init__(self) -> None: 

28 """Validate CCT and Duv data.""" 

29 if not 0 <= self.correlated_color_temperature <= UINT16_MAX: 

30 raise ValueError(f"CCT {self.correlated_color_temperature} K is outside valid range (0 to {UINT16_MAX})") 

31 duv_min = SINT16_MIN * _DUV_RESOLUTION 

32 duv_max = SINT16_MAX * _DUV_RESOLUTION 

33 if not duv_min <= self.chromaticity_distance_from_planckian <= duv_max: 

34 raise ValueError( 

35 f"Duv {self.chromaticity_distance_from_planckian} is outside valid range ({duv_min} to {duv_max})" 

36 ) 

37 

38 

39class ChromaticityInCCTAndDuvValuesCharacteristic(BaseCharacteristic[ChromaticityInCCTAndDuvData]): 

40 """Chromaticity in CCT and Duv Values characteristic (0x2AE5). 

41 

42 org.bluetooth.characteristic.chromaticity_in_cct_and_duv_values 

43 

44 Combines Correlated Color Temperature and Chromatic Distance from 

45 Planckian into a single composite characteristic. 

46 

47 Field 1: CCT — uint16, raw Kelvin (references Correlated Color Temperature). 

48 Field 2: Duv — sint16, M=1 d=-5 b=0 (references Chromatic Distance From Planckian). 

49 """ 

50 

51 _manual_role = CharacteristicRole.MEASUREMENT 

52 

53 # Validation attributes 

54 expected_length: int = 4 # uint16 + sint16 

55 min_length: int = 4 

56 

57 def _decode_value( 

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

59 ) -> ChromaticityInCCTAndDuvData: 

60 """Parse CCT and Duv values. 

61 

62 Args: 

63 data: Raw bytes from the characteristic read. 

64 ctx: Optional CharacteristicContext (may be None). 

65 validate: Whether to validate ranges (default True). 

66 

67 Returns: 

68 ChromaticityInCCTAndDuvData with CCT and Duv fields. 

69 

70 """ 

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

72 duv_raw = DataParser.parse_int16(data, 2, signed=True) 

73 

74 return ChromaticityInCCTAndDuvData( 

75 correlated_color_temperature=cct_raw, 

76 chromaticity_distance_from_planckian=duv_raw * _DUV_RESOLUTION, 

77 ) 

78 

79 def _encode_value(self, data: ChromaticityInCCTAndDuvData) -> bytearray: 

80 """Encode CCT and Duv values to bytes. 

81 

82 Args: 

83 data: ChromaticityInCCTAndDuvData instance. 

84 

85 Returns: 

86 Encoded bytes (uint16 + sint16, little-endian). 

87 

88 """ 

89 if not isinstance(data, ChromaticityInCCTAndDuvData): 

90 raise TypeError(f"Expected ChromaticityInCCTAndDuvData, got {type(data).__name__}") 

91 

92 cct_raw = data.correlated_color_temperature 

93 duv_raw = round(data.chromaticity_distance_from_planckian / _DUV_RESOLUTION) 

94 

95 if not 0 <= cct_raw <= UINT16_MAX: 

96 raise ValueError(f"CCT raw value {cct_raw} exceeds uint16 range") 

97 if not SINT16_MIN <= duv_raw <= SINT16_MAX: 

98 raise ValueError(f"Duv raw value {duv_raw} exceeds sint16 range") 

99 

100 result = bytearray() 

101 result.extend(DataParser.encode_int16(cct_raw, signed=False)) 

102 result.extend(DataParser.encode_int16(duv_raw, signed=True)) 

103 return result