Coverage for tests/tests/test_apis_hvac.py: 0%

54 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2026-01-05 21:46 +0100

1#!/usr/bin/env python3 

2"""RAMSES RF - Test the Command.put_*, Command.set_* APIs.""" 

3 

4from collections.abc import Callable 

5from datetime import datetime as dt 

6from typing import Any 

7 

8from ramses_tx.command import CODE_API_MAP, Command 

9from ramses_tx.message import Message 

10from ramses_tx.packet import Packet 

11 

12 

13def _test_api(api: Callable, packets: dict[str]) -> None: # NOTE: incl. addr_set check 

14 """Test a verb|code pair that has a Command constructor, src and dst.""" 

15 

16 for pkt_line, kwargs in packets.items(): 

17 pkt = _create_pkt_from_frame(pkt_line) 

18 

19 msg = Message(pkt) 

20 

21 _test_api_from_kwargs(api, pkt, **kwargs) 

22 _test_api_from_msg(api, msg) 

23 

24 

25def _test_api_one( 

26 api: Callable, packets: dict[str] 

27) -> None: # NOTE: incl. addr_set check 

28 """Test a verb|code pair that has a Command constructor and src, but no dst.""" 

29 

30 for pkt_line, kwargs in packets.items(): 

31 pkt = _create_pkt_from_frame(pkt_line) 

32 

33 msg = Message(pkt) 

34 

35 _test_api_one_from_kwargs(api, pkt, **kwargs) 

36 _test_api_one_from_msg(api, msg) 

37 

38 

39def _create_pkt_from_frame(pkt_line: str) -> Packet: 

40 """Create a pkt from a pkt_line and assert their frames match.""" 

41 

42 pkt = Packet.from_port(dt.now(), pkt_line) 

43 assert str(pkt) == pkt_line[4:] 

44 return pkt 

45 

46 

47def _test_api_from_msg(api: Callable, msg: Message) -> Command: 

48 """Create a cmd from a msg with a src_id, and assert they're equal 

49 (*also* asserts payload).""" 

50 

51 cmd: Command = api( 

52 msg.dst.id, 

53 src_id=msg.src.id, 

54 **{k: v for k, v in msg.payload.items() if k[:1] != "_"}, 

55 ) 

56 

57 assert cmd == msg._pkt # must have exact same addr set 

58 

59 return cmd 

60 

61 

62def _test_api_one_from_msg(api: Callable, msg: Message) -> Command: 

63 """Create a cmd from a msg and assert they're equal (*also* asserts payload).""" 

64 

65 cmd: Command = api( 

66 msg.dst.id, 

67 **{k: v for k, v in msg.payload.items()}, # if k[:1] != "_"}, 

68 # requirement turned off as it skips required item like _unknown_fan_info_flags 

69 ) 

70 

71 assert cmd == msg._pkt # must have exact same addr set 

72 

73 return cmd 

74 

75 

76def _test_api_from_kwargs(api: Callable, pkt: Packet, **kwargs: Any) -> None: 

77 """ 

78 Test comparing a created packet to an expected result. 

79 

80 :param api: Command lookup by Verb|Code 

81 :param pkt: expected result to match 

82 :param kwargs: arguments for the Command 

83 """ 

84 cmd = api(HRU, src_id=REM, **kwargs) 

85 

86 assert str(cmd) == str(pkt) 

87 

88 

89def _test_api_one_from_kwargs(api: Callable, pkt: Packet, **kwargs: Any) -> None: 

90 cmd = api(HRU, **kwargs) 

91 

92 assert str(cmd) == str(pkt) 

93 

94 

95def test_set() -> None: 

96 for test_pkts in (SET_22F1_KWARGS, SET_22F7_KWARGS): 

97 pkt = list(test_pkts)[0] 

98 api = CODE_API_MAP[f"{pkt[4:6]}|{pkt[41:45]}"] 

99 _test_api(api, test_pkts) 

100 

101 

102HRU = "32:155617" # also used as a FAN 

103REM = "37:171871" 

104NUL = "--:------" 

105 

106SET_22F1_KWARGS = { 

107 f"000 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": None}, 

108 # 

109 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": 0}, 

110 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0001": {"fan_mode": 1}, 

111 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0002": {"fan_mode": 2}, 

112 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0003": {"fan_mode": 3}, 

113 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0004": {"fan_mode": 4}, 

114 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0005": {"fan_mode": 5}, 

115 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0006": {"fan_mode": 6}, 

116 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0007": {"fan_mode": 7}, 

117 # 

118 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": "00"}, 

119 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0001": {"fan_mode": "01"}, 

120 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0002": {"fan_mode": "02"}, 

121 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0003": {"fan_mode": "03"}, 

122 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0004": {"fan_mode": "04"}, 

123 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0005": {"fan_mode": "05"}, 

124 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0006": {"fan_mode": "06"}, 

125 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0007": {"fan_mode": "07"}, 

126 # 

127 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": "away"}, 

128 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0001": {"fan_mode": "low"}, 

129 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0002": {"fan_mode": "medium"}, 

130 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0003": {"fan_mode": "high"}, 

131 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0004": {"fan_mode": "auto"}, 

132 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0005": {"fan_mode": "auto_alt"}, 

133 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0006": {"fan_mode": "boost"}, 

134 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0007": {"fan_mode": "off"}, 

135} 

136 

137 

138SET_22F7_KWARGS = { 

139 f"000 W --- {REM} {HRU} {NUL} 22F7 002 00FF": {}, # shouldn't be OK 

140 # 

141 f"001 W --- {REM} {HRU} {NUL} 22F7 002 00FF": { 

142 "bypass_position": None 

143 }, # is auto? 

144 f"001 W --- {REM} {HRU} {NUL} 22F7 002 0000": {"bypass_position": 0.0}, 

145 # 001 W --- {REM} {HRU} {NUL} 22F7 002 0064": {"bypass_position": 0.5}, 

146 f"001 W --- {REM} {HRU} {NUL} 22F7 002 00C8": {"bypass_position": 1.0}, 

147 f"002 W --- {REM} {HRU} {NUL} 22F7 002 00FF": { 

148 "bypass_mode": "auto" 

149 }, # is auto, or None? 

150 f"002 W --- {REM} {HRU} {NUL} 22F7 002 0000": {"bypass_mode": "off"}, 

151 f"002 W --- {REM} {HRU} {NUL} 22F7 002 00C8": {"bypass_mode": "on"}, 

152} 

153 

154 

155# new tests 

156def test_get() -> None: 

157 for test_pkts in (GET_12A0_KWARGS, GET_1298_KWARGS, GET_31DA_KWARGS): 

158 pkt = list(test_pkts)[0] 

159 api = CODE_API_MAP[f"{pkt[4:6]}|{pkt[41:45]}"] 

160 _test_api_one(api, test_pkts) 

161 

162 

163GET_12A0_KWARGS = { 

164 f"000 I --- {HRU} {NUL} {HRU} 12A0 002 00EF": { 

165 "indoor_humidity": None 

166 }, # shouldn't be OK 

167 # 

168 f"082 I --- {HRU} {NUL} {HRU} 12A0 002 0037": {"indoor_humidity": 0.55}, 

169} 

170 

171GET_1298_KWARGS = { 

172 f"064 I --- {HRU} {NUL} {HRU} 1298 003 000322": {"co2_level": 802}, 

173} 

174 

175GET_31DA_KWARGS = { 

176 # this is a composite payload, containing many keys 

177 # 31DA packet from values in ramses_tx/command.py#get_hvac_fan_31da 

178 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00EF007FFF343308980898088A0882F800001514140000EFEF05F50613": { 

179 "hvac_id": "00", 

180 "bypass_position": 0.000, 

181 "air_quality": None, 

182 "co2_level": None, 

183 "indoor_humidity": 0.52, 

184 "outdoor_humidity": 0.51, 

185 "exhaust_temp": 22.0, 

186 "supply_temp": 22.0, 

187 "indoor_temp": 21.86, 

188 "outdoor_temp": 21.78, 

189 "speed_capabilities": ["off", "low_med_high", "timer", "boost", "auto"], 

190 "fan_info": "away", 

191 "_unknown_fan_info_flags": [0, 0, 0], 

192 "exhaust_fan_speed": 0.1, 

193 "supply_fan_speed": 0.1, 

194 "remaining_mins": 0, 

195 "post_heat": None, 

196 "pre_heat": None, 

197 "supply_flow": 15.25, 

198 "exhaust_flow": 15.55, 

199 }, 

200 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00C84004B2EFEF7FFF7FFF7FFF7FFFF808EF831F000000EFEF7FFF7FFF": { 

201 "hvac_id": "00", 

202 "co2_level": 1202, 

203 "air_quality": 1.0, 

204 "air_quality_basis": "rel_humidity", 

205 "indoor_humidity": None, 

206 "outdoor_humidity": None, 

207 "exhaust_temp": None, 

208 "supply_temp": None, 

209 "indoor_temp": None, 

210 "outdoor_temp": None, 

211 "speed_capabilities": [ 

212 "off", 

213 "low_med_high", 

214 "timer", 

215 "boost", 

216 "auto", 

217 "auto_night", 

218 ], 

219 "bypass_position": None, 

220 "fan_info": "speed 3, high", 

221 "_unknown_fan_info_flags": [1, 0, 0], 

222 "exhaust_fan_speed": 0.155, 

223 "supply_fan_speed": 0.0, 

224 "remaining_mins": 0, 

225 "post_heat": None, 

226 "pre_heat": None, 

227 "supply_flow": None, 

228 "exhaust_flow": None, 

229 }, 

230 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00EF007FFF30EF7FFF7FFF7FFF7FFFF808EF83C7000000EFEF7FFF7FFF": { 

231 "hvac_id": "00", 

232 "speed_capabilities": [ 

233 "off", 

234 "low_med_high", 

235 "timer", 

236 "boost", 

237 "auto", 

238 "auto_night", 

239 ], 

240 "fan_info": "speed 3, high", 

241 "_unknown_fan_info_flags": [1, 0, 0], 

242 "air_quality": None, 

243 "co2_level": None, 

244 "indoor_humidity": 0.48, 

245 "outdoor_humidity": None, 

246 "exhaust_temp": None, 

247 "supply_temp": None, 

248 "indoor_temp": None, 

249 "outdoor_temp": None, 

250 "bypass_position": None, 

251 "exhaust_fan_speed": 0.995, 

252 "supply_fan_speed": 0.0, 

253 "remaining_mins": 0, 

254 "post_heat": None, 

255 "pre_heat": None, 

256 "supply_flow": None, 

257 "exhaust_flow": None, 

258 }, 

259 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00EF007FFFEFEF7FFF7FFF7FFF7FFFF000EF0162000000EFEF7FFF7FFF": { 

260 "hvac_id": "00", 

261 "outdoor_humidity": None, 

262 "outdoor_temp": None, 

263 "air_quality": None, 

264 "co2_level": None, 

265 "indoor_humidity": None, 

266 "exhaust_temp": None, 

267 "supply_temp": None, 

268 "indoor_temp": None, 

269 "speed_capabilities": ["off", "low_med_high", "timer", "boost"], 

270 "bypass_position": None, 

271 "fan_info": "speed 1, low", 

272 "_unknown_fan_info_flags": [0, 0, 0], 

273 "exhaust_fan_speed": 0.49, 

274 "supply_fan_speed": 0.0, 

275 "remaining_mins": 0, 

276 "post_heat": None, 

277 "pre_heat": None, 

278 "supply_flow": None, 

279 "exhaust_flow": None, 

280 }, 

281 f"... I --- {HRU} {NUL} {HRU} 31DA 029 21EF00020136EF7FFF7FFF7FFF7FFF0002EF18FFFF000000EF7FFF7FFF": { 

282 "hvac_id": "21", 

283 "speed_capabilities": ["post_heater"], 

284 "fan_info": "auto", 

285 "_unknown_fan_info_flags": [0, 0, 0], 

286 "air_quality": None, 

287 "co2_level": 513, 

288 "indoor_humidity": 0.54, 

289 "outdoor_humidity": None, 

290 "exhaust_temp": None, 

291 "supply_temp": None, 

292 "indoor_temp": None, 

293 "outdoor_temp": None, 

294 "bypass_position": None, 

295 "exhaust_fan_speed": None, 

296 "supply_fan_speed": None, 

297 "remaining_mins": 0, 

298 "post_heat": 0.0, 

299 "pre_heat": None, 

300 "supply_flow": None, 

301 "exhaust_flow": None, 

302 }, 

303 # messages with 30 byte payload 

304 f"... I --- {HRU} {NUL} {HRU} 31DA 030 00EF007FFF2CEF7FFF7FFF7FFF7FFFF800EF0128000000EFEF7FFF7FFF00": { 

305 "hvac_id": "00", 

306 "exhaust_temp": None, 

307 "air_quality": None, 

308 "co2_level": None, 

309 "indoor_humidity": 0.44, 

310 "outdoor_humidity": None, 

311 "supply_temp": None, 

312 "indoor_temp": None, 

313 "outdoor_temp": None, 

314 "speed_capabilities": ["off", "low_med_high", "timer", "boost", "auto"], 

315 "bypass_position": None, 

316 "fan_info": "speed 1, low", 

317 "_unknown_fan_info_flags": [0, 0, 0], 

318 "exhaust_fan_speed": 0.2, 

319 "supply_fan_speed": 0.0, 

320 "remaining_mins": 0, 

321 "post_heat": None, 

322 "pre_heat": None, 

323 "supply_flow": None, 

324 "exhaust_flow": None, 

325 "_extra": "00", 

326 }, 

327 f"... I --- {HRU} {NUL} {HRU} 31DA 030 21EF007FFF41EF080607D709480737F002AA0234400000005C7FFF7FFF00": { 

328 "hvac_id": "21", 

329 "exhaust_fan_speed": 0.26, 

330 "supply_fan_speed": 0.32, 

331 "supply_flow": None, 

332 "exhaust_flow": None, 

333 "air_quality": None, 

334 "co2_level": None, 

335 "indoor_humidity": 0.65, 

336 "outdoor_humidity": None, 

337 "exhaust_temp": 20.54, 

338 "supply_temp": 20.08, 

339 "indoor_temp": 23.76, 

340 "outdoor_temp": 18.47, 

341 "speed_capabilities": ["off", "low_med_high", "timer", "boost", "post_heater"], 

342 "bypass_position": 0.85, 

343 "fan_info": "speed 2, medium", 

344 "_unknown_fan_info_flags": [0, 0, 0], 

345 "remaining_mins": 0, 

346 "post_heat": 0.0, 

347 "pre_heat": 0.46, 

348 "_extra": "00", 

349 }, 

350} 

351 

352 

353# TODO Add tests to get states from 31DA 

354# (verifies SQLite refactoring) 

355# set up HVAC system first from messages 

356# 

357# Example: current_temperature(self) in ramses_cc.climate.py 

358# simulates requesting Climate self._device.indoor_temp from a system