mirror of
https://github.com/tykeal/homeassistant-rental-control.git
synced 2026-11-06 05:17:23 +00:00
Refactor(coordinator): Split ghost reconstruction
_ghost_from_mapping interleaved defensive reads of corrupt persisted fields, the physical-name fence, the stay-date fence and the Reservation construction in one function. Extract _dict_field and _str_field for the corrupt-tolerant reads and _ghost_stay for the slot-name and stay-date fence. Each fence still runs in the same order, logs the same message and returns None on the same inputs, so ghost reservations are unchanged. Refs #816 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
This commit is contained in:
1 parent
517af0bdbc
commit
1efab15543
1 file changed
+58
-36
@@ -5,6 +5,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import tzinfo
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -104,16 +105,10 @@ def _ghost_from_mapping(
|
||||
ctx: ReservationBuildContext,
|
||||
) -> _Reservation | None:
|
||||
"""Return a ghost reservation from a persisted mapping, or None when fenced."""
|
||||
identity = mapping.get("identity", {})
|
||||
if not isinstance(identity, dict):
|
||||
identity = {}
|
||||
slot_name_raw = identity.get("slot_name", "")
|
||||
slot_name = slot_name_raw if isinstance(slot_name_raw, str) else ""
|
||||
summary_raw = identity.get("summary", "")
|
||||
summary = summary_raw if isinstance(summary_raw, str) else ""
|
||||
last_actual = mapping.get("last_observed_actual", {})
|
||||
if not isinstance(last_actual, dict):
|
||||
last_actual = {}
|
||||
identity = _dict_field(mapping, "identity")
|
||||
slot_name = _str_field(identity, "slot_name")
|
||||
summary = _str_field(identity, "summary")
|
||||
last_actual = _dict_field(mapping, "last_observed_actual")
|
||||
actual_name = last_actual.get("name_state")
|
||||
if _ghost_physical_name_mismatch(
|
||||
actual_name, slot_name, prefix, observed_mapping_keys, key, ctx
|
||||
@@ -128,33 +123,10 @@ def _ghost_from_mapping(
|
||||
new_mc,
|
||||
)
|
||||
return None
|
||||
start_raw = last_actual.get("start_state")
|
||||
end_raw = last_actual.get("end_state")
|
||||
if not slot_name or start_raw is None or end_raw is None:
|
||||
_LOGGER.debug(
|
||||
"Ghost reservation %s: missing slot_name or dates; slot remains "
|
||||
"fenced with missing_count=%d",
|
||||
key,
|
||||
new_mc,
|
||||
)
|
||||
return None
|
||||
|
||||
start_dt = _coerce_persisted_datetime(start_raw)
|
||||
end_dt = _coerce_persisted_datetime(end_raw)
|
||||
if start_dt is not None and start_dt.tzinfo is None:
|
||||
start_dt = start_dt.replace(tzinfo=ctx.timezone)
|
||||
if end_dt is not None and end_dt.tzinfo is None:
|
||||
end_dt = end_dt.replace(tzinfo=ctx.timezone)
|
||||
if start_dt is None or end_dt is None or start_dt >= end_dt:
|
||||
_LOGGER.debug(
|
||||
"Ghost reservation %s: invalid dates (start=%s end=%s); "
|
||||
"slot remains fenced with missing_count=%d",
|
||||
key,
|
||||
start_raw,
|
||||
end_raw,
|
||||
new_mc,
|
||||
)
|
||||
stay = _ghost_stay(key, slot_name, last_actual, new_mc, ctx.timezone)
|
||||
if stay is None:
|
||||
return None
|
||||
start_dt, end_dt = stay
|
||||
|
||||
try:
|
||||
ghost = _Reservation(
|
||||
@@ -186,6 +158,56 @@ def _ghost_from_mapping(
|
||||
return ghost
|
||||
|
||||
|
||||
def _dict_field(container: dict[str, Any], name: str) -> dict[str, Any]:
|
||||
"""Return a persisted nested mapping, or an empty one when corrupt."""
|
||||
value = container.get(name, {})
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _str_field(container: dict[str, Any], name: str) -> str:
|
||||
"""Return a persisted string field, or an empty string when corrupt."""
|
||||
value = container.get(name, "")
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _ghost_stay(
|
||||
key: str,
|
||||
slot_name: str,
|
||||
last_actual: dict[str, Any],
|
||||
new_mc: int,
|
||||
timezone: tzinfo,
|
||||
) -> tuple[datetime, datetime] | None:
|
||||
"""Return a ghost's last observed stay, or None when it stays fenced."""
|
||||
start_raw = last_actual.get("start_state")
|
||||
end_raw = last_actual.get("end_state")
|
||||
if not slot_name or start_raw is None or end_raw is None:
|
||||
_LOGGER.debug(
|
||||
"Ghost reservation %s: missing slot_name or dates; slot remains "
|
||||
"fenced with missing_count=%d",
|
||||
key,
|
||||
new_mc,
|
||||
)
|
||||
return None
|
||||
|
||||
start_dt = _coerce_persisted_datetime(start_raw)
|
||||
end_dt = _coerce_persisted_datetime(end_raw)
|
||||
if start_dt is not None and start_dt.tzinfo is None:
|
||||
start_dt = start_dt.replace(tzinfo=timezone)
|
||||
if end_dt is not None and end_dt.tzinfo is None:
|
||||
end_dt = end_dt.replace(tzinfo=timezone)
|
||||
if start_dt is None or end_dt is None or start_dt >= end_dt:
|
||||
_LOGGER.debug(
|
||||
"Ghost reservation %s: invalid dates (start=%s end=%s); "
|
||||
"slot remains fenced with missing_count=%d",
|
||||
key,
|
||||
start_raw,
|
||||
end_raw,
|
||||
new_mc,
|
||||
)
|
||||
return None
|
||||
return start_dt, end_dt
|
||||
|
||||
|
||||
def _ghost_physical_name_mismatch(
|
||||
actual_name: Any,
|
||||
slot_name: str,
|
||||
|
||||
Reference in new issue
Block a user