Add Auto/Cool/Heat mode, 18 new sensors, and HA 2024.2+ compatibility fixes - #21
Open
laurensdehoorne wants to merge 57 commits into
Open
Add Auto/Cool/Heat mode, 18 new sensors, and HA 2024.2+ compatibility fixes#21laurensdehoorne wants to merge 57 commits into
laurensdehoorne wants to merge 57 commits into
Conversation
Error Logger undefined + code cleanup error is: Logger: custom_components.alsavopro.AlsavoPyCtrl Bron: custom_components/alsavopro/AlsavoPyCtrl.py:44 integratie: AlsavoPro (documentatie) Eerst voorgekomen: 13:04:41 (8 gebeurtenissen) Laatst gelogd: 13:59:26 Unable to update: name '_LOGGER' is not defined Unable to update: unpack requires a buffer of 164 bytes
temperature step by 1.0 degrees removed AUTO mode
Added logger to fix error "WARNING (MainThread) [custom_components.alsavopro.AlsavoPyCtrl] Update attempt 1 failed: name '_LOGGER' is not defined"
update version
- Fixed `NoneType object is not subscriptable` crash when pump is temporarily offline during auth challenge - Fixed `unpack requires a buffer of X bytes` error when receiving truncated UDP packets - Added 2-second delay between update retries so the pump has time to recover when briefly offline
This reverts commit 45567a3.
goev
pushed a commit
that referenced
this pull request
May 17, 2026
…status Fix connectivity binary sensor always showing "connected" after first successful update
AlsavoPyCtrl.py:41 — retry attempts now log at DEBUG instead of WARNING, so they won't flood the HA log during normal network hiccups AlsavoPyCtrl.py:44-45 — update() now raises ConnectionError after all retries fail, so the coordinator's OFFLINE_TOLERANCE (5 consecutive full failures = ~5 minutes) properly gates when HA marks the device unavailable udpclient.py:57 — UDP timeout demoted from ERROR to DEBUG
- Remove internal retry loops in AlsavoPyCtrl.update() and set_config().
The coordinator's 10s timeout was cancelling the 18+s internal retry
loop, so retries 3-10 were dead code. The coordinator's OFFLINE_TOLERANCE
is now the single retry layer. Bumped coordinator timeout to 15s.
- Make set_config raise on failure instead of silently logging an error,
so mode/temperature changes surface as errors in HA.
- Implement async_turn_on/async_turn_off (TURN_ON|TURN_OFF features were
declared but never implemented, which would NotImplementedError).
- Replace status-index-based min_temp/max_temp with hardcoded limits per
mode and dev_type (matches official Alsavo Pro Android app behaviour
reverse-engineered from APK 1.8). dev_type-aware hvac_modes filter:
SINGLE devices only get Heat; FIXCH/FREQCH don't get Auto.
- Preset modes (Silent/Smart/Powerful) now only exposed for
variable-frequency devices (dev_type 0 or 3), and the name list is
derived from POWER_MODE_MAP instead of duplicated hardcoded strings.
- Fix OptionsFlowHandler: previously showed a form but never persisted
user input, silently discarding password changes.
- unique_id now uses serial number only (was f"{name}-{serial_no}" where
name is mutable user input). Broken duplicate detection that used OR
across all fields (blocking legit second devices on same port) replaced
with HA's built-in _abort_if_unique_id_configured().
- Add asyncio.Lock around read-modify-write on config register 4
(mode/power/timer bits) to prevent races.
- Switch random.randint -> secrets.randbelow for auth client token.
- Remove legacy async_setup; use async_unload_platforms on unload.
- Lazy %s log formatting; remove dead async_update in climate entity;
remove unused exception classes in config_flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix practical bugs in protocol handling, climate entity, and config flow
- target_temperature: return None when operating_mode has no mapping, instead of falling through to config[0] which is unrelated data (would have shown a bogus value). - Remove no-op async_update in AlsavoProErrorSensor (same dead reassign pattern that was removed from climate.py). - Remove unused properties on AlsavoPro that were defined but never referenced anywhere: water_out_temperature, ambient_temperature, is_timer_on_enabled, water_pump_running_mode, electronic_valve_style, is_debug_mode, is_timer_off_enabled, manual_defrost. The last one checked config_sys2 bit 0 which has no documented meaning in the official app's SDK either. - Remove the ConnectionStatus Enum (unused) and lstConfigReqTime timestamp field (set but never read). - Remove __payloads list and __deviceInfo field on QueryResponse (never read outside the class). - Remove MAX_UPDATE_RETRIES and MAX_SET_CONFIG_RETRIES from const.py (no longer used after retry loops were removed). - Drop unused `from enum import Enum` import. - Use ConnectionError instead of bare Exception in query_all. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cleanup: remove dead code and fix target_temperature fallback
- Drop the cloud-relay configuration option (47.254.157.150:51192). That
endpoint is no longer reachable; the regional GalaxyWind dispatchers
appear to be retired in some regions. The integration has always
worked against the pump's LAN IP, so only document that.
- Add a Troubleshooting section explaining:
- "Offline in app, online in HA" is normal — the app uses the cloud,
we use direct UDP.
- Intermittent local timeouts are caused by the pump's WiFi module
looping on cloud retries (47.88.188.100, hardcoded in firmware as
EU/AU/BR fallback). Mitigation: firewall REJECT (not DROP) outbound
traffic from the pump to that IP and to *.ice.galaxywind.com.
docs: drop dead cloud config + add cloud-retry troubleshooting
Previously, every status poll and every config write called AlsavoSocketCom.connect(), which redoes the full UDP handshake (auth challenge → response → auth ack) before sending the actual request. That's 4 extra UDP roundtrips per minute and a much larger failure surface during slow periods — especially relevant for users whose pump WiFi module is looping on cloud-reconnect attempts and starving local UDP. The official Android app's native library shows the pump's protocol supports session reuse: get keeplive reset send keeplive timer_keeplive Drop bad packet: packet session id=%08x, but now is %08x So we now hold CSID/DSID across calls and only re-auth when the session goes stale. - connect() is idempotent: returns immediately if a session exists. - disconnect() drops session state for explicit reset. - A partial-handshake failure cleans up so we don't appear connected. - update() and set_config() go through _with_session_retry(), which invokes the operation once on the existing session; on any failure it tears the session down, re-auths, and retries the operation once. This matches the happy path of the official app (1 request, 1 reply per poll) while keeping the same worst-case behaviour we had before (full handshake + retry on failure). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Persist auth session across calls (reduce handshake overhead)
Raise ConfigEntryNotReady instead of propagating the raw exception so HA retries setup with exponential backoff rather than marking the entry as broken. Add a 2-second sleep before the session retry to give the pump time to clear any half-open state from the first failed handshake attempt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix startup failure when pump unreachable + session retry race
The official app suppresses incoming data-update events for 3 s after a user interaction, then re-reads settled device state. We replicate this by scheduling a second coordinator refresh 5 s after every set_* call, in addition to the existing immediate refresh. If two commands arrive in quick succession the pending follow-up is cancelled and the 5 s window resets, preventing back-to-back polls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Persist auth session, fix startup failure, add post-command follow-up poll
Two bugs: 1. schedule_followup_refresh called the TimerHandle as a function instead of calling .cancel() on it. This raised TypeError on any second command after the 5 s window had already fired. Also clear _followup_cancel when the timer fires so the next command starts from a clean state. 2. QueryResponse.unpack silently returned an all-zero object when the pump returned an unexpected packet (e.g. a stale ACK received by the query socket). Added QueryResponse.is_valid and raised in query_all so _with_session_retry re-auths and retries instead of storing zeros. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pcap showed every poll cycle does a full re-auth, confirming the pump's session timeout is shorter than 60 s. The persistent-session optimisation was therefore always hitting the retry path: 5 s UDP timeout + 2 s sleep before the inevitable re-auth on every single poll. Fix: remove the is_connected guard in connect() so each operation authenticates fresh. The _with_session_retry wrapper remains for genuine transient failures (packet loss during auth). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Live debug logs show the pump maintains sessions well beyond 60 s. At 13:34:36 the session was established; at 13:35:07 a set_config and immediate refresh both reused it (~10 ms each vs ~56 ms for auth). The "always re-auth" change was a wrong diagnosis — the persistent session is working correctly and should be kept. This reverts commit 57c6cba. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix follow-up timer bug, silent zeros, and session re-auth regression
The pump queues a write-ACK for each set_config command and delivers it to the next socket that contacts the session. Since set_config used send_packet (fire-and-forget, socket closed immediately), the ACK had nowhere to go and was held by the pump. The subsequent query_all opened a new socket; the pump sent the queued ACK there first, which EchoClientProtocol captured as the result, causing is_valid to fail and triggering a 2-second re-auth cycle on every command+refresh pair. Fix: switch set_config to send_and_rcv_packet so the ACK is consumed in-band. The return value is discarded; we rely on the follow-up query_all to verify the new state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drain pump write-ACK in set_config to prevent query_all collision
The pump only commits config writes when they arrive on a freshly authenticated session. After the persistent-session refactor, writes were going out on a reused CSID/DSID: the pump ACKed them but never applied the value. The baseline always did a full handshake before every write — restore that, but only for writes (reads still reuse the session across the 60 s poll). Also drop the immediate async_request_refresh() that ran right after every command. It queried the pump 1-14 ms after the write-ACK, before the pump had committed the new register value, and overwrote the coordinator cache with stale state — making the UI appear to "snap back" to the old value. The 5 s schedule_followup_refresh() matches the official app's 3 s UI-suppression window and is enough on its own. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-auth before every set_config; drop immediate post-command refresh
Removed changelog section detailing previous versions.
The pump invalidates the session immediately after a config write: the next query on the same CSID/DSID returns a truncated packet that fails parsing, and _with_session_retry recovers by sleeping 2 s + re-authing. The data ends up correct, but every command takes ~7 s end-to-end. Disconnect the session right after a successful write so the follow-up read does a fast ~50 ms fresh handshake instead. Brings command-to-UI latency back down to ~5 s, matching the schedule_followup_refresh delay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop session after each write to skip post-write retry penalty
…lowup on unload After today's session-handling work, four small leftovers: - AlsavoSocketCom.send_packet, AlsavoSocketCom.send, UDPClient.send and UDPClient.SimpleClientProtocol are no longer called. The only sender path left is send_and_rcv_packet → UDPClient.send_rcv. Delete them. - The comment on socket.set_config explained the old "drain ACK so the next socket on this session doesn't grab it" rationale; with the new disconnect-after-write logic the next call always opens a new session, so the rationale for send_and_rcv_packet is now just "confirm the write landed before the caller schedules a follow-up poll". Updated. - The _with_session_retry docstring claimed we "reuse sessions instead of re-authing every call" — true for reads, but writes now disconnect first. Clarified. - AlsavoProDataCoordinator schedules a one-shot follow-up refresh via loop.call_later(). If the config entry is unloaded during that 5 s window, the timer fires against a torn-down coordinator. Added a shutdown() that cancels the handle, called from async_unload_entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ession Cleanup: drop dead code, fix stale comments, cancel follow-up on unload
These registers (config idx 14/15) carry a hysteresis-style offset
that can legitimately be negative. The sensor was reading them as
raw unsigned 16-bit values, so Cold over showed as 65516 instead of
-20 (and Hot over only escaped by being positive).
Add a `signed` flag to AlsavoProSensor that switches to
get_signed_{status,config}_value when set, and expose the signed
getters on AlsavoPro. Marking Hot over signed too keeps the pair
consistent — same register family, same interpretation.
Conservative fix: keeps the raw integer scale (no °C / ÷10) since
the register's exact semantics aren't documented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show Hot/Cold over registers as signed integers
The official app's HtcHpParamActivity and HtcHpTimerActivity write to a
handful of config registers that this integration only read out as
sensors. Decoded them from the APK's ControlApi.java and added matching
HA entities for each.
New platforms:
- number.py — 5 NumberEntity instances with the app's exact min/max:
defrost in temp (idx 9, -30..0 °C, step 1)
defrost out temp (idx 10, 2..30 °C, step 1)
defrost in time (idx 12, 30..90 min)
defrost out time (idx 13, 1..12 min)
water compensation (idx 11, -9..9 °C, step 0.1)
- switch.py — 3 SwitchEntity instances for the bit flags in config
register 4:
timer on enabled (bit 2)
timer off enabled (bit 7)
pump continuous run (bit 3)
- time.py — 2 TimeEntity instances for the daily timer schedule:
timer on time (idx 33, encoded as hour<<8 | minute)
timer off time (idx 34, same encoding)
AlsavoPyCtrl gains a `_toggle_config4_bit` helper that takes the
existing `_config4_lock` so the new bit-flip writes don't race with
mode/power changes. Each new entity's set call schedules the existing
5 s follow-up refresh so HA sees the settled state without bouncing
the user through a re-auth cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose writeable settings: defrost, water comp, daily timer, pump run mode
Bump manifest to 1.1.0 and document everything that landed today: - New number/switch/time platforms (10 entities) - HVAC modes filtered per device type - Cold over signed fix - Protocol/session bugfixes (writes-not-applied, silent zeros, stale-packet retry, follow-up timer leak) - Persistent reads + per-write re-handshake Also adds a "Tuning for winter operation" section to the README with sensible defrost parameter starting points for NW-European climate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs: 1.1.0 changelog + README update
HA's device page splits entities into Controls / Sensors / Configuration / Diagnostic sections based on each entity's entity_category. Everything was landing in one flat "Sensors" list because no category was set. - Writeable settings (number/switch/time) -> EntityCategory.CONFIG - Diagnostic readouts (codes, firmware, manual settings, defrost params, pipe/IPM/exhaust temps, alarm registers, hot/cold over, timers, clock) -> EntityCategory.DIAGNOSTIC - Primary metrics stay uncategorised so they show at the top: water in/out, ambient, mode targets, fan speed, compressor current/frequency, error messages Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Group entities into HA device-page categories
Ports three ideas from the upstream goev fork, adapted to this fork's register layout and naming conventions: - binary_sensor.py with Frost protection / Connectivity / Alarm sensors. The frost-protection bit (PP07) is read from alarm register 50 bit 0x40 to match THIS firmware's layout — upstream reads register 49, which in our layout is "EE23: Compressor start failure". The connectivity sensor overrides available=True so it can report "off" when the pump drops offline. - AlsavoProEntity mixin in __init__.py providing DeviceInfo. Mixed into every entity class (sensor, climate, number, switch, time, binary sensor) so they all group under a single Alsavo Pro device card with manufacturer/model/serial and live HW/SW versions. Adding device_info doesn't change entity IDs or names, so existing dashboards and automations keep working. - AlsavoPro data handler gains serial_no, is_frost_protection, hardware_version and software_version accessors. Bumps version to 1.2.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y-sensors Add binary sensors + group all entities under one device
Document why this fork diverges from upstream goev: 10 writable settings vs 2, switch/time platforms, persistent read session, device-type-aware HVAC modes, signed Cold over, frost-protection from the correct alarm register, and the robustness fixes. Includes an honest note on the entity-naming tradeoff. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
climate.pyNew features:
HVACMode.AUTO— maps to the pump's internal auto mode (operating mode 2), withset_auto_modeaction andmdi:autorenewiconhvac_modenow falls back toHVACMode.OFFfor unknown operating modes instead of returningNoneBug fixes:
ClimateEntityFeature.TURN_ON | TURN_OFFtosupported_features— required in HA 2024.2+PLATFORM_SCHEMA,DataUpdateCoordinator,UpdateFailedsensor.pyNew sensors (18):
Register | Name | Type -- | -- | -- Status 24 | Compressor input temperature | Temperature Status 25 | EEV opening | Raw Status 33 | Compressor speed setting | Raw Status 54 | Device status code | Raw Status 55 | Heating max temperature | Temperature Status 56 | Cooling min temperature | Temperature Config 6 | Manual frequency setting | Raw Config 7 | Manual EEV setting | Raw Config 8 | Manual fan speed setting | Raw Config 9 | Defrost in temperature | Temperature Config 10 | Defrost out temperature | Temperature Config 11 | Water temperature calibration | Temperature Config 12 | Defrost in time | Minutes Config 13 | Defrost out time | Minutes Config 14 | Hot over | Raw Config 15 | Cold over | Raw Config 17 | Unknown config 17 | Raw Config 32 | Current time | Raw Config 33 | Timer on time | Raw Config 34 | Timer off time | RawBug fixes:
availableproperty toAlsavoProErrorSensor— entity now correctly reflects online/offline stateDataUpdateCoordinator,UpdateFailed