Skip to content

Add Grohe Sense Guard integration for ESPHome - #81

Draft
patricknitsch wants to merge 14 commits into
mainfrom
claude/elegant-fermi-pgY1L
Draft

Add Grohe Sense Guard integration for ESPHome#81
patricknitsch wants to merge 14 commits into
mainfrom
claude/elegant-fermi-pgY1L

Conversation

@patricknitsch

Copy link
Copy Markdown
Owner

Summary

This PR adds a complete ESPHome integration for the Grohe Sense Guard smart water valve, enabling monitoring and control of the device via UART communication.

Key Changes

  • Protocol Implementation (grohe_protocol.h): Defines the Grohe proprietary UART protocol with frame structure, message types (INFO, STATUS, CONFIG, HEARTBEAT, WATER_DATA), and helper functions for parsing incoming frames and building outgoing commands.

  • Core Component (grohe_sense_guard.h/cpp): Implements the main GroheSenseGuard class that:

    • Manages UART communication with byte-level state machine for frame reception
    • Parses and dispatches different message types (info, status, config)
    • Provides command API for valve control (open/close), snooze management, and sprinkler configuration
    • Publishes sensor data and raw frame diagnostics
  • Sensor Integrations:

    • binary_sensor.py: Exposes valve state, snooze status, pressure test state, and sprinkler schedule (7 days)
    • sensor.py: Exposes sprinkler start/stop times in minutes
    • text_sensor.py: Exposes firmware version, raw frame hex dumps, and unknown frame diagnostics
  • Example Configuration (grohe_tap.yaml): Complete working example showing UART wiring, sensor setup, and button controls for valve/snooze/sprinkler operations.

Notable Implementation Details

  • Frame Reception: Implements timeout-based frame flushing (50ms) and buffer overflow protection (200 byte max)
  • Sequence Tracking: Maintains device address and sequence numbers for bidirectional communication
  • Config Caching: Stores last known config payload to enable partial writes (e.g., changing only sprinkler days)
  • Diagnostic Logging: Supports VERBOSE logging level for protocol analysis and frame capture
  • Callback System: Allows registration of frame callbacks for custom handling or MQTT publishing
  • Phase 2 Support: Commands work reliably when Grohe cloud is blocked; includes safety notes in configuration

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS

claude added 2 commits June 2, 2026 20:10
Implements a UART tap component that decodes the M-Bus-like protocol
between the Grohe Sense Guard MCU and its WT-8266-S1 WiFi module.

Protocol reverse-engineered from Saleae logic analyzer capture:
- Frame: FE×4 preamble + 68 [6-byte addr] 68 + CI + payload + CS + 16
- MSG_INFO (0x01): device firmware/serial at startup
- MSG_STATUS (0x05): valve state, snooze active, pressure test flag
- MSG_CONFIG (0x04): sprinkler days (Mon-Sun), start/stop times (minutes)
- MSG_HEARTBEAT (0x06): periodic keep-alive

Phase 1 (listen only): ESP32 RX on MCU TX line → all sensors readable.
Phase 2 (inject): ESP32 TX on MCU RX line + Grohe cloud blocked → full
local control of valve, snooze, sprinkler mode.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
Every successfully parsed frame is now published as a hex string to the
'last_raw_frame' text sensor in Home Assistant, making it easy to spot
new message types without needing a logic analyzer.

Unknown frame types (new messages from Grohe firmware updates) are
additionally published to 'last_unknown_frame' with a warning log.

An on_frame_callback() API allows optional MQTT publishing of every
frame by type (grohe/frame/0x04 etc.) for offline protocol analysis.

Logger level VERBOSE enables raw frame output on serial for debugging.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
@patricknitsch
patricknitsch marked this pull request as draft June 3, 2026 05:31
claude added 12 commits June 3, 2026 19:38
- handle_status_: use flags bit 1 (0x02) as primary snooze indicator
  (live data shows flags=0x22 when snooze active, not just payload byte)
- handle_water_data_: decode type 0x03 counter frames and log counter byte
- verify_checksum_: log 4 CS candidates on every received frame so we can
  identify the correct algorithm once CH1 capture is available
- send_frame_: log computed CS in TX path for cross-reference
- set_sprinkler: guard against empty/short last_config_payload_ with
  clear error message instead of silently sending 00:00-00:00

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
Empirically verified from two captured frames (type=0x03 and type=0x04):
actual_cs == (sum of bytes from second 0x68 to last data byte) - 2

Previous implementation summed from addr[0] (Candidate C) which was wrong
and caused MCU to reject all injected commands. CS debug logging now only
warns on mismatch (VERBOSE otherwise) to keep logs clean.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
Previous hardcoded payload had wrong bytes at position [6] (0x07 vs
observed 0x01) and didn't preserve snooze/other fields.

Now caches the last received STATUS payload (same pattern as CONFIG) and
uses it as the write template, changing only the target field. This
ensures the MCU receives a valid packet structure it recognizes.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
Observed payload[75,76] = 05 9F for stop time.
Little-endian read: (0x9F<<8)|0x05 = 40709 (wrong, shows 678:29)
Big-endian read:    (0x05<<8)|0x9F = 1439  (correct, 23:59)

Fix both read (handle_config_) and write (set_sprinkler) paths.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
From captured app commands (seq=22..25):
- flags=0x20 for all write commands, not 0x60 as assumed
- payload[6]=0x07 in all app write frames (STATUS + CONFIG)
- MCU broadcasts have payload[5]=0x01, payload[6]=0x00 — different!

FLAG_WRITE constant changed to 0x20. build_status_cmd_ and set_sprinkler
now explicitly set payload[5]=0x00, payload[6]=0x07 to match app format.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
Instead of aborting with a warning, populate last_status_payload_ with
a safe default (16 bytes, valve=0, snooze=0, payload[6]=0x07) matching
the observed app command format. Avoids "No STATUS cached" on first boot.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
- handle_status_: flags bit 0 (0x21) = short MCU ACK frame (7 bytes),
  log at DEBUG and return — stops "Status packet too short" warning
- request_status(): sends type=0x03 poll frame to trigger MCU to send
  current STATUS+CONFIG; exposed as diagnostic button in HA
- grohe_tap.yaml: add "Status abrufen" button

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
build_frame was adding '00 00 seq' before data AND data already contained
'00 00 seq' as its first 3 bytes (from build_status_cmd_ and set_sprinkler).
This sent 3 extra bytes per command, making the payload unrecognisable to MCU.

Also fix length formula: data.size()+10 → data.size()+5, matching observed
app frames (STATUS 16-byte payload → L=21, CONFIG 85-byte → L=90).

STATUS commands now produce 39 bytes (was 42), matching app format.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
Without WiFi, the ESP8266 is silent and the MCU stops sending STATUS/CONFIG
updates. The ESP32 now sends a type=0x03 poll frame every 30s (matching the
format the MCU itself uses) to trigger MCU status broadcasts.

Also start tx_seq_ at 0x01 (matching observed app frame sequence counters).

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
type=0x03 poll only triggers a type=0x03 response, not STATUS/CONFIG.
Switching to a minimal STATUS read request (type=0x05, flags=0x20,
7-byte zero payload) to ask the MCU for its current state directly.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
The MCU sends periodic type=0x03 counter frames. The ESP8266 likely
responds to these to remain the active controller. Without responses,
the MCU ACKs commands but doesn't execute them.

Now handle_water_data_ echoes the same frame back immediately when a
heartbeat is received. This should cause the MCU to treat the ESP32
as the active controller and execute subsequent valve/snooze commands.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
App always sends payload[12]=0x01 in ALL control commands (valve open,
valve close, both with snooze ON). Our default was 0x00. This byte
appears to be a required 'active controller' flag - without it the MCU
ACKs the frame but does not execute the command.

https://claude.ai/code/session_014ddy1uWXvRhtvSD9oUf5RS
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.

2 participants