Skip to content

Add Auto/Cool/Heat mode, 18 new sensors, and HA 2024.2+ compatibility fixes - #21

Open
laurensdehoorne wants to merge 57 commits into
goev:mainfrom
laurensdehoorne:main
Open

Add Auto/Cool/Heat mode, 18 new sensors, and HA 2024.2+ compatibility fixes#21
laurensdehoorne wants to merge 57 commits into
goev:mainfrom
laurensdehoorne:main

Conversation

@laurensdehoorne

Copy link
Copy Markdown

climate.py

New features:

  • Added HVACMode.AUTO — maps to the pump's internal auto mode (operating mode 2), with set_auto_mode action and mdi:autorenew icon
  • hvac_mode now falls back to HVACMode.OFF for unknown operating modes instead of returning None

Bug fixes:

  • Added ClimateEntityFeature.TURN_ON | TURN_OFF to supported_features — required in HA 2024.2+
  • Removed unused imports: PLATFORM_SCHEMA, DataUpdateCoordinator, UpdateFailed

sensor.py

New 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 | Raw

Bug fixes:

  • Added available property to AlsavoProErrorSensor — entity now correctly reflects online/offline state
  • Removed unused imports: DataUpdateCoordinator, UpdateFailed

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
- Fixed `set_config` recursive retry replaced with iterative loop to prevent stack overflow and stale `_online` state
- Fixed `is_online` now correctly reflects live connection state instead of stale data presence
- Fixed `Payload.get_value` off-by-one bounds check
 Added Auto HVAC mode (maps to pump's internal auto mode)
- Added 18 new sensors: compressor input temp, EEV opening, compressor speed, device status code, heating max/cooling min temps, manual settings, defrost config, timer config, and more
goev pushed a commit that referenced this pull request May 17, 2026
…status

Fix connectivity binary sensor always showing "connected" after first successful update
laurensdehoorne and others added 9 commits May 17, 2026 09:46
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)
laurensdehoorne and others added 28 commits May 17, 2026 13:09
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant