Run:python -m pytest tests -q ·
Current status: 92 test cases, all passing.
The robust-ack functionality described below was implemented on 2026-10-04;
the test-first cases that were red during design are now green.
Passes now — all 92 cases are green in the current build
Live evidence base. These descriptions were written against measurements from
six real Vento Expert fans on 192.168.29.0/24 (4× unit type 3 A50-1/A85-1/A100-1 W V.2,
2× type 4 Duo A30-1 W V.2, firmware 0.4), using the Blauberg Smart Home connection guide
(B133-4-1EN-02) as the protocol source of truth. Key measured facts:
FUNC 0x02 writes never get a response; FUNC 0x03 writes get a 0x06 echo ack whose values equal the applied state (clamping proven: write 30 → echo 40).
Negative acks use the 0xFD special command (e.g. fd 90) in reads and writes alike.
~3% UDP loss measured (2 silent responses in ~70 round trips) → retries are mandatory.
Wrong password, unknown FUNC, corrupted checksum and device-off are all silent — indistinguishable.
A 0x03 write to a write-only param (RESET_ALARMS, 128) returns a 0x06 packet with an empty data section.
Capability-driven writes. Params marked RW-capable (28 of 51) may only be
written via FUNC 0x03 (verified write). Plain FUNC 0x02 writes are rejected at packet-build
time, before any UDP traffic. Write-only params (5: FILTER_RESET, RESET_ALARMS, FACTORY_RESET,
WIFI_APPLY, WIFI_DISCARD) keep FUNC 0x02. Read-only params (18) reject every write.
Verified write. 0x03 + echo comparison; returns the applied values.
Idempotent writes retry up to 3 attempts on timeout, then raise
VentoAckError (with attempts). Invert-sentinel writes
(value 2 on params defining “2 – Invert”, e.g. POWER) get a single attempt
so a lost ack can never double-toggle.
Correlation. Acks are tied to requests by the header device ID and the
transport source address; mismatches raise VentoAckMismatchError / are ignored.
Negative acks.VentoUnsupportedParamError gains an
applied mapping so mixed acks (device applied SPEED, rejected 0x90) do not
discard the applied subset.
New: tests/test_ack_protocol.py
Protocol-layer contract for the robust-ack functionality. 19 cases, all green.
TestEmptyAckParsing
Basis: live probe — a 0x03 write to W-only param 128 returned 0x06 with an empty data section; the current parser raises “Response packet missing payload data”.
Test case
Description
Expected result
Status
test_empty_response_ack_returns_empty_dict
Parses a valid 0x06 response packet (correct header, checksum) whose data section is empty (0 bytes).
parse_response() returns {} without raising.
Passes now
test_empty_response_ack_with_matching_expected_id
Same empty-data 0x06 packet, parsed with expected_device_id equal to the packet's device ID.
Returns {}; the correlation check accepts a matching ID.
Passes now
TestMixedAckAppliedValues
Basis: live mixed ack data=02 01 fd 90 — SPEED applied, 0x90 rejected; today the applied value is discarded when the error raises.
Ack from a foreign device parsed withoutexpected_device_id (backward compatibility).
Returns the parsed mapping; no correlation check is performed.
Passes now
TestWriteCapabilityValidation
Encodes the write policy: FUNC 0x02 is reserved for W-only params; FUNC 0x03 accepts any writable param; read-only and unknown params are rejected by both write paths before any packet is built.
Test case
Description
Expected result
Status
test_build_write_rejects_rw_param
build_write() (FUNC 0x02) with RW-capable param POWER.
Raises VentoCapabilityError (subclass of VentoProtocolError, so existing invalid-input tests remain coherent).
Passes now
test_build_write_allows_w_only_param
build_write() with W-only param FILTER_RESET.
Builds a packet whose FUNC byte is 0x02.
Passes now
test_build_write_rejects_read_only_param
build_write() with read-only param FIRMWARE_VERSION.
Raises VentoCapabilityError.
Passes now
test_build_write_allows_unknown_param_for_probing
build_write() with raw unlisted param 0x90 — the library does not know the param's capabilities, so the controller's 0xFD negative ack is the source of truth.
Builds a packet whose FUNC byte is 0x02.
Passes now
test_build_write_resp_allows_rw_param
build_write_resp() (FUNC 0x03) with RW param SPEED.
Builds a packet whose FUNC byte is 0x03.
Passes now
test_build_write_resp_allows_w_only_param
build_write_resp() with W-only param RESET_ALARMS.
Builds a packet whose FUNC byte is 0x03.
Passes now
test_build_write_resp_rejects_read_only_param
build_write_resp() with read-only param UNIT_TYPE.
Scripted mixed ack 02 01 fd 90 for a write of SPEED + 0x90.
Raises VentoUnsupportedParamError whose .applied == {Param.SPEED: b"\x01"} — the caller learns what took effect.
Passes now
test_wrong_device_ack_raises_mismatch
Scripted ack with a foreign device ID in the header.
Raises VentoAckMismatchError; exactly 1 send (anomalous acks are not retried).
Passes now
TestScheduleDaySelectors (sync)
Pins the datasheet rule: day byte 0 (all days), 8 (Mon–Fri), 9 (Sat–Sun) are write-only selectors; reads accept only 1–7. This asymmetry is intended behaviour, not a bug.
Test case
Description
Expected result
Status
test_schedule_write_accepts_day_zero
set_schedule_period(0, 1, 1, 8, 0).
Accepted without VentoValueError.
Passes now
test_schedule_write_accepts_day_nine
set_schedule_period(9, 1, 1, 8, 0).
Accepted without VentoValueError.
Passes now
test_schedule_read_rejects_day_zero
get_schedule_period(0, 1).
Raises VentoValueError (read selector range is 1–7).
Passes now
TestAsyncVerifiedWrite
Async mirror of the core sync cases using a fake async transport. async client must not lag the sync client.
Test case
Description
Expected result
Status
test_async_verified_write_returns_applied_echo
AsyncVentoClient.write_params({SPEED: 2}) with a scripted echo ack.
Returns {Param.SPEED: b"\x02"}; packet FUNC byte is 0x03.
Passes now
test_async_verified_write_retries_on_timeout
Two timeouts, then a good ack.
Succeeds on 3rd attempt; 3 sends total.
Passes now
test_async_w_only_write_fire_and_forget
write_params({FACTORY_RESET: 1}) — W-only param.
Returns {}; one send_only; no send_recv.
Passes now
test_async_invert_single_attempt
write_params({POWER: 2}) with a timing-out transport.
Exactly 1 send; VentoAckError.attempts == 1.
Passes now
New: tests/test_ack_transport.py
Transport-layer source-address filtering so a stray datagram from another host on the LAN
cannot be mistaken for the device's ack. 6 cases.
TestSyncSourceFiltering
Test case
Description
Expected result
Status
test_sync_send_recv_ignores_foreign_source
Mocked socket first receives a datagram from 10.9.9.9, then the real reply from the target host 192.0.2.10.
send_recv() returns the target host's datagram; the stray one is discarded.
0xFE size command for a 2-byte battery voltage param.
Decodes 5000 mV.
test_page_change
0xFF page switch to page 0x03, then a 2-byte NIGHT_TIMER value.
Value decodes to minutes=30, hours=8.
TestDecoders
Test case
Description
Expected result
test_decode_ip
4-byte IP decode.
"192.168.1.50".
test_encode_ip
IP string encode.
bytes([192,168,1,50]).
test_firmware
6-byte firmware block decode.
major=1, minor=5, day=14, month=3, year=2024.
test_machine_hours
4-byte machine hours decode.
minutes=30, hours=5, days=10.
test_rtc_time
3-byte RTC time decode.
seconds=45, minutes=30, hours=14.
test_rtc_calendar
4-byte RTC calendar decode.
year=2024, month=6.
test_schedule
6-byte schedule period decode.
speed=2, end_hours=8, end_minutes=30.
test_filter_countdown
3-byte filter countdown decode.
minutes=0, hours=12, days=90.
test_bad_ip_size
IP decode with a 2-byte value.
Raises (VentoProtocolError).
TestValidation (client input ranges)
Test case
Description
Expected result
test_bad_speed
set_speed(5) — only 1–3 valid.
Raises VentoValueError.
test_bad_humidity_threshold
set_humidity_threshold(100) — range 40–80.
Raises VentoValueError.
test_bad_boost_delay
set_boost_delay(61) — range 0–60.
Raises VentoValueError.
test_long_password
change_password("TOOLONGPWD") — max 8 chars.
Raises VentoValueError.
test_manual_speed_boundaries
set_manual_speed(0) and set_manual_speed(255) are the valid boundaries; the transport is mocked to return a verified-write echo ack (SPEED and MANUAL_SPEED).
Both accepted (no raise), applied echo parsed.
Existing: tests/test_protocol_invalid.py
Malformed-packet rejections. All cases pass today and must stay green.
Test case
Description
Expected result
test_invalid_packet_rejections[truncated]
4-byte fragment (too short for any parsing).
Raises VentoProtocolError.
test_invalid_packet_rejections[no-header]
Packet without the 0xFD 0xFD start bytes.
Raises VentoProtocolError.
test_invalid_packet_rejections[bad-checksum]
Full-length packet whose checksum does not match.
Raises VentoChecksumError.
test_parse_response_rejects_truncated_packet
Packet cut off before the data section and checksum bytes begin (no checksum to verify).