124 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-31 15:02:40 +00:00
1 parent 33de328a33
commit 9742f385a3
124 files changed
+13907 -1376

No files matched your search

@@ -445,6 +445,10 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
self-charging filter is off entirely and such devices surface as
rechargeables (native rows typed "Rechargeable", never "Unknown").
"""
# #146: the fleet-wide low floor is a household setting (default 20 %).
from .global_options import get_battery_low_percent
floor = float(get_battery_low_percent(hass))
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
@@ -525,7 +529,7 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
# was "healthy" here while the same level counted low in the
# native pass. A HIGHER Battery Notes threshold (e.g. 30 %)
# still wins through battery_low.
low = bool(attrs.get("battery_low")) or (level is not None and level <= NATIVE_LOW_PERCENT)
low = bool(attrs.get("battery_low")) or (level is not None and level <= floor)
last_replaced = _parse_last_replaced(attrs.get("battery_last_replaced"))
# B1 (roadmap 2026-07-22 audit): a forecast-only note — no level
# sensor, so the state reads unknown forever — must SURVIVE when it
@@ -575,7 +579,7 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
last_replaced=last_replaced,
available=available,
source="battery_notes",
low_threshold=_note_low_threshold(attrs),
low_threshold=_note_low_threshold(attrs, floor),
)
)
@@ -648,7 +652,7 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
if low_state is not None:
low = low_available and str(low_state).lower() in ("on", "true", "1")
else:
low = level is not None and level <= NATIVE_LOW_PERCENT
low = level is not None and level <= floor
if available:
# Remember the last real reading — the retention path below needs
# it once the entity goes unavailable.
@@ -683,6 +687,61 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
return out
def battery_notes_summary(hass: HomeAssistant) -> dict[str, Any] | None:
"""What Battery Notes currently reports, for the Settings hint (#146).
Derived purely from the ``battery_low_threshold`` attribute the
``battery_plus`` entities expose — no reach into Battery Notes' own
configuration. The most common value is their default; devices whose
value differs are overrides, named (highest first, capped at 5 so a
large install cannot spam the settings page) and linked via their
device id when the registry knows one. ``None`` when Battery Notes is
not present.
"""
from collections import Counter
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
per_device: dict[str, dict[str, Any]] = {}
for domain in ("sensor", "binary_sensor"):
for state in hass.states.async_all(domain):
raw = state.attributes.get("battery_low_threshold")
if not isinstance(raw, (int, float)) or isinstance(raw, bool):
continue
reg = ent_reg.async_get(state.entity_id)
device_id = reg.device_id if reg else None
name = state.attributes.get("device_name")
if device_id and (device := dev_reg.async_get(device_id)):
name = device.name_by_user or device.name or name
key = device_id or state.entity_id
row = per_device.get(key)
threshold = float(raw)
if row is None or threshold > row["threshold"]:
per_device[key] = {
"name": str(name or state.entity_id),
"device_id": device_id,
"threshold": threshold,
}
if not per_device:
return None
counts = Counter(row["threshold"] for row in per_device.values())
default = counts.most_common(1)[0][0]
overrides = sorted(
(row for row in per_device.values() if row["threshold"] != default),
key=lambda r: (-r["threshold"], r["name"].lower()),
)
return {
"default": default,
"devices": len(per_device),
"overrides": overrides[:5],
"more": max(0, len(overrides) - 5),
}
def has_battery_notes(hass: HomeAssistant) -> bool:
"""Whether the Battery Notes integration is present (any battery_plus).
@@ -851,12 +910,16 @@ def _detect_unrecorded_jump(
return None
def _note_low_threshold(attrs: dict[str, Any]) -> float:
"""The Battery-Notes-configured threshold OR the fleet floor — the higher."""
def _note_low_threshold(attrs: dict[str, Any], floor: float = float(NATIVE_LOW_PERCENT)) -> float:
"""The Battery-Notes-configured threshold OR the fleet floor — the higher.
``floor`` is the household's battery_low_percent (#146); the module
constant only remains as the fallback default.
"""
raw = attrs.get("battery_low_threshold")
if isinstance(raw, (int, float)):
return float(max(raw, NATIVE_LOW_PERCENT))
return float(NATIVE_LOW_PERCENT)
return float(max(raw, floor))
return float(floor)
async def async_level_history(hass: HomeAssistant, batteries: list[Battery]) -> dict[str, dict[str, Any]]:
@@ -941,6 +1004,7 @@ __all__ = [
"async_compute_overview",
"async_level_history",
"async_trend_predictions",
"battery_notes_summary",
"build_overview",
"compute_overview",
"discover_battery_types",