Skip to content
Merged
29 changes: 21 additions & 8 deletions components/crow_alarm_panel/crow_alarm_panel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -523,11 +523,6 @@ void CrowAlarmPanel::loop() {
format_hex_pretty(data).c_str());
break;
}
if (data[3] >= 60) {
ESP_LOGW(TAG, "[%-*s] Current time has invalid seconds value %u [%02x.%s]", this->keypad_label_width_,
CONTROLLER_LABEL, data[3], type, format_hex_pretty(data).c_str());
break;
}
uint8_t day = data[4];
uint8_t month = data[5];
uint8_t year = data[6];
Expand All @@ -540,14 +535,20 @@ void CrowAlarmPanel::loop() {
// recovery astronomically unlikely, addressing the coincidental-valid-range risk noted in
// that doc. Validated against real HA log timestamps in the 2026-08-05 traces: the
// recovered date matched the true date/time exactly in every sample checked.
//
// This glitch is frequent (up to hundreds of occurrences/hour — see the 2026-08-19
// protocol_investigations.md update) and, per that update, an expected/benign property of
// the panel's own broadcast logic rather than a bus fault — and CURRENT_TIME isn't published
// to any entity, so none of this is user-actionable. Logged at DEBUG, same level as the
// successful decode below, to avoid spamming WARN/INFO on every occurrence.
bool recovered = false;
if ((data[4] % 2) == 0 && (data[5] % 2) == 0 && (data[6] % 2) == 0) {
uint8_t rec_day = data[4] / 2;
uint8_t rec_month = data[5] / 2;
uint8_t rec_year = data[6] / 2;
if (rec_day >= 1 && rec_day <= 31 && rec_month >= 1 && rec_month <= 12 &&
day_of_week_from_date(2000 + rec_year, rec_month, rec_day) == data[0]) {
ESP_LOGI(TAG,
ESP_LOGD(TAG,
"[%-*s] Current time: recovered doubled-bit glitch, using 20%02u-%02u-%02u [%02x.%s]",
this->keypad_label_width_, CONTROLLER_LABEL, rec_year, rec_month, rec_day, type,
format_hex_pretty(data).c_str());
Expand All @@ -558,12 +559,22 @@ void CrowAlarmPanel::loop() {
}
}
if (!recovered) {
ESP_LOGW(TAG, "[%-*s] Current time has invalid day/month value %u/%u [%02x.%s]",
ESP_LOGD(TAG, "[%-*s] Current time has invalid day/month value %u/%u [%02x.%s]",
this->keypad_label_width_, CONTROLLER_LABEL, data[4], data[5], type,
format_hex_pretty(data).c_str());
break;
}
}
// Checked after the day/month recovery attempt above (not before) so a bad seconds value
// doesn't discard an otherwise-recoverable day/month/year — see protocol_investigations.md,
// the 2026-08-10 CURRENT_TIME update, for two sessions where this order previously discarded
// cleanly-recoverable date data alongside an unrelated bad seconds byte.
if (data[3] >= 60) {
// Same known glitch family as above (frequent, benign, unpublished) — DEBUG, not WARN.
ESP_LOGD(TAG, "[%-*s] Current time has invalid seconds value %u [%02x.%s]", this->keypad_label_width_,
CONTROLLER_LABEL, data[3], type, format_hex_pretty(data).c_str());
break;
}
ESP_LOGD(TAG, "[%-*s] Controller time update: %s 20%02d-%02d-%02d %02d:%02d:%02d",
this->keypad_label_width_, CONTROLLER_LABEL, day_of_week, year, month, day, hour, minute, data[3]);
break;
Expand Down Expand Up @@ -958,12 +969,14 @@ void CrowAlarmPanel::loop() {
ESP_LOGW(TAG, "No ping for 60 s, re-sending registration announce");
this->registration_sent_ = false;
}
if (!this->registration_sent_ && now_ms >= this->registration_after_ms_ && this->is_bus_idle_()) {
if (!this->registration_sent_ && now_ms >= this->registration_after_ms_ && this->is_bus_idle_() &&
(now_ms - this->last_registration_announce_ms_) >= REGISTRATION_ANNOUNCE_MIN_INTERVAL_MS) {
CrowAlarmPanelKeypad keypad = this->find_keypad_(this->keypad_address_);
ESP_LOGW(TAG, "[%-*s] Sending registration announce", this->keypad_label_width_,
keypad_label(keypad, this->keypad_address_).c_str());
this->send_packet(KEYPAD_REGISTRATION, {0x00});
this->registration_sent_ = true;
this->last_registration_announce_ms_ = now_ms;
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions components/crow_alarm_panel/crow_alarm_panel.h
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@ class CrowAlarmPanel : public Component {

// Watchdog: track last time the controller polled us; re-register if silent for 60 s.
uint32_t last_ping_ms_{0};
// Minimum spacing between registration-announce resends triggered by the no-ping watchdog
// above. Without this, registration_sent_ flips false->true every loop() iteration while
// last_ping_ms_ stays stale (no KEYPAD_PING arriving to advance it), re-sending on every pass
// instead of backing off — observed firing 6x within ~1s in a real capture (2026-08-19).
static const uint32_t REGISTRATION_ANNOUNCE_MIN_INTERVAL_MS = 1000;
uint32_t last_registration_announce_ms_{0};

// Output-select state machine.
OutputSelectState output_select_state_{OutputSelectState::IDLE};
Expand Down
2 changes: 2 additions & 0 deletions components/crow_alarm_panel/docs/arm_disarm_state_machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ Compiles and passes `esphome compile` against `crow_alarm_panel_test.yaml`. **No

A single `disarm()` call resolved in 3.8s total across two automatic retries, with no user action in between — the retries land cleanly, the CMD-byte-change tolerance from the 2026-07-25 fix keeps working unmodified mid-retry, and there's no sign of any new interaction between the rapid consecutive retries and the controller. Confirms the fix works in practice, not just in code review.

**Further confirmed (2026-08-19, logs-33):** `esphome-aap-alarm-interface-logs-33.txt`, four arm/disarm cycles in a ~14-minute session (`14:06`–`14:20`). All four arms completed cleanly on the first attempt. Two of the four disarms (`14:08:36.196`, `14:20:00.756`) hit the ordinary `CODE_ENTER_PENDING` silence signature — terminal key acked (`CMD 0x07`), then genuine bus silence, 1s watchdog fires `Arm/disarm: timeout in state 4, retrying (1/5)`, full code+terminal sequence resent, succeeds immediately on the retry with no user action. Same shape as the logs-27 validation above, one retry each instead of two, still 100% eventual success. No new failure mechanism, no code change proposed.

## Notes

- ARM/STAY/DISARM sequences are simpler than OUTPUT because there's no ACK handshake
Expand Down
2 changes: 2 additions & 0 deletions components/crow_alarm_panel/docs/keypad_protocol_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ Output-select and arm/disarm sequences are now captured (`esphome-aap-keypad-mon
- During arming countdown, keypad receives `Command [14.06.AA.00.00.01.80]` before final armed-away state
- Protocol appears identical to IP keypad for output-select and code-entry command gating
- Address 0x06 confirmed
- **Digit batching, not per-keystroke bus timing** (`esphome-aap-alarm-interface-logs-32.txt`): during code entry, digits arrive as two near-simultaneous pairs (e.g. digits 1+2 within ~1ms of each other, then a ~150–200ms gap, then digits 3+4 within ~1ms, then another gap before ENTER) rather than the smoothly user-paced single-digit spacing IP/AAP keypads show. This is the RS232 interface's own "enter code" prompt UI batching digits behind a confirm step before relaying them to the bus — not a bus collision or double-transmit — and shouldn't be misread as a protocol anomaly if seen again.
- **Confirms multi-user-code support:** the code used by Control4 to disarm differs from the code the IP Keypad and ESPHome integration use (both redacted) — the panel accepts more than one independently-valid user code.

#### ARM_AWAY + DISARM with code (from logs-25)
```text
Expand Down
Loading