124 files
This commit is contained in:
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",
|
||||
|
||||
Reference in new issue
Block a user