Coverage for src / bluetooth_sig / gatt / characteristics / templates / domain.py: 72%

65 statements  

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

1"""Domain-specific templates that compose scaled templates. 

2 

3Covers TemperatureTemplate, ConcentrationTemplate, PressureTemplate. 

4""" 

5 

6from __future__ import annotations 

7 

8from ...context import CharacteristicContext 

9from ..utils.extractors import RawExtractor 

10from ..utils.translators import ValueTranslator 

11from .base import _RESOLUTION_HUNDREDTH, _RESOLUTION_INTEGER, _RESOLUTION_TENTH, CodingTemplate 

12from .scaled import ScaledSint16Template, ScaledUint16Template, ScaledUint32Template 

13 

14 

15class TemperatureTemplate(CodingTemplate[float]): 

16 """Template for standard Bluetooth SIG temperature format (sint16, 0.01°C resolution).""" 

17 

18 def __init__(self) -> None: 

19 """Initialize with standard temperature resolution.""" 

20 self._scaled_template = ScaledSint16Template.from_letter_method(1, -2, 0) 

21 

22 @property 

23 def data_size(self) -> int: 

24 """Size: 2 bytes.""" 

25 return 2 

26 

27 @property 

28 def extractor(self) -> RawExtractor: 

29 """Get extractor from underlying scaled template.""" 

30 return self._scaled_template.extractor 

31 

32 @property 

33 def translator(self) -> ValueTranslator[float]: 

34 """Return the linear translator from the underlying scaled template.""" 

35 return self._scaled_template.translator 

36 

37 def decode_value( 

38 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

39 ) -> float: 

40 """Parse temperature in 0.01°C resolution.""" 

41 return self._scaled_template.decode_value(data, offset, ctx, validate=validate) # pylint: disable=protected-access 

42 

43 def encode_value(self, value: float, *, validate: bool = True) -> bytearray: 

44 """Encode temperature to bytes.""" 

45 return self._scaled_template.encode_value(value, validate=validate) # pylint: disable=protected-access 

46 

47 

48class ConcentrationTemplate(CodingTemplate[float]): 

49 """Template for concentration measurements with configurable resolution. 

50 

51 Used for environmental sensors like CO2, VOC, particulate matter, etc. 

52 """ 

53 

54 def __init__(self, resolution: float = 1.0) -> None: 

55 """Initialize with resolution. 

56 

57 Args: 

58 resolution: Measurement resolution (e.g., 1.0 for integer ppm, 0.1 for 0.1 ppm) 

59 

60 """ 

61 # Convert resolution to M, d, b parameters when it fits the pattern 

62 # resolution = M * 10^d, so we find M and d such that M * 10^d = resolution 

63 if resolution == _RESOLUTION_INTEGER: 

64 # resolution = 1 * 10^0 # noqa: ERA001 

65 self._scaled_template = ScaledUint16Template.from_letter_method(M=1, d=0, b=0) 

66 elif resolution == _RESOLUTION_TENTH: 

67 # resolution = 1 * 10^-1 # noqa: ERA001 

68 self._scaled_template = ScaledUint16Template.from_letter_method(M=1, d=-1, b=0) 

69 elif resolution == _RESOLUTION_HUNDREDTH: 

70 # resolution = 1 * 10^-2 # noqa: ERA001 

71 self._scaled_template = ScaledUint16Template.from_letter_method(M=1, d=-2, b=0) 

72 else: 

73 # Fallback to scale_factor for resolutions that don't fit M * 10^d pattern 

74 self._scaled_template = ScaledUint16Template(scale_factor=resolution) 

75 

76 @classmethod 

77 def from_letter_method(cls, M: int, d: int, b: int = 0) -> ConcentrationTemplate: # noqa: N803 

78 """Create instance using Bluetooth SIG M, d, b parameters. 

79 

80 Args: 

81 M: Multiplier factor 

82 d: Decimal exponent (10^d) 

83 b: Offset to add to raw value before scaling 

84 

85 Returns: 

86 ConcentrationTemplate instance 

87 

88 """ 

89 instance = cls.__new__(cls) 

90 instance._scaled_template = ScaledUint16Template.from_letter_method(M=M, d=d, b=b) 

91 return instance 

92 

93 @property 

94 def data_size(self) -> int: 

95 """Size: 2 bytes.""" 

96 return 2 

97 

98 @property 

99 def extractor(self) -> RawExtractor: 

100 """Get extractor from underlying scaled template.""" 

101 return self._scaled_template.extractor 

102 

103 @property 

104 def translator(self) -> ValueTranslator[float]: 

105 """Return the linear translator from the underlying scaled template.""" 

106 return self._scaled_template.translator 

107 

108 def decode_value( 

109 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

110 ) -> float: 

111 """Parse concentration with resolution.""" 

112 return self._scaled_template.decode_value(data, offset, ctx, validate=validate) # pylint: disable=protected-access 

113 

114 def encode_value(self, value: float, *, validate: bool = True) -> bytearray: 

115 """Encode concentration value to bytes.""" 

116 return self._scaled_template.encode_value(value, validate=validate) # pylint: disable=protected-access 

117 

118 

119class PressureTemplate(CodingTemplate[float]): 

120 """Template for pressure measurements (uint32, 0.1 Pa resolution).""" 

121 

122 def __init__(self) -> None: 

123 """Initialize with standard pressure resolution (0.1 Pa).""" 

124 self._scaled_template = ScaledUint32Template(scale_factor=0.1) 

125 

126 @property 

127 def data_size(self) -> int: 

128 """Size: 4 bytes.""" 

129 return 4 

130 

131 @property 

132 def extractor(self) -> RawExtractor: 

133 """Get extractor from underlying scaled template.""" 

134 return self._scaled_template.extractor 

135 

136 @property 

137 def translator(self) -> ValueTranslator[float]: 

138 """Return the linear translator from the underlying scaled template.""" 

139 return self._scaled_template.translator 

140 

141 def decode_value( 

142 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

143 ) -> float: 

144 """Parse pressure in 0.1 Pa resolution (returns Pa).""" 

145 return self._scaled_template.decode_value(data, offset, ctx, validate=validate) # pylint: disable=protected-access 

146 

147 def encode_value(self, value: float, *, validate: bool = True) -> bytearray: 

148 """Encode pressure to bytes.""" 

149 return self._scaled_template.encode_value(value, validate=validate) # pylint: disable=protected-access