197 files
This commit is contained in:
1 parent
89d9f5e49b
commit
383e9ed04e
197 files changed
+4386
-1837
No files matched your search
@@ -31,7 +31,7 @@ import re
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, timedelta
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -322,33 +322,53 @@ def _level_of(state_val: str) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def fleet_excluded_entities(hass: HomeAssistant) -> set[str]:
|
||||
"""Manually excluded battery entity_ids, stored on the fleet object entry.
|
||||
def _fleet_object(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""The fleet object dict from its config entry, or ``{}`` when no fleet.
|
||||
|
||||
Inlined lookup (not via battery_fleet_setup.find_fleet_entry) to keep this
|
||||
module import-cycle-free — setup imports the aggregation, not vice versa.
|
||||
Inlined entry loop (not via battery_fleet_setup.find_fleet_entry) to keep
|
||||
this module import-cycle-free — setup imports the aggregation, not vice
|
||||
versa. The three getters below were three copies of this loop (drift
|
||||
audit 2026-08).
|
||||
"""
|
||||
from ..const import BATTERY_FLEET_EXCLUDED, BATTERY_FLEET_OBJECT_FLAG, CONF_OBJECT, DOMAIN
|
||||
from ..const import BATTERY_FLEET_OBJECT_FLAG, CONF_OBJECT, DOMAIN
|
||||
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
obj = entry.data.get(CONF_OBJECT, {})
|
||||
if obj.get(BATTERY_FLEET_OBJECT_FLAG):
|
||||
return set(obj.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
return set()
|
||||
return cast(dict[str, Any], obj)
|
||||
return {}
|
||||
|
||||
|
||||
def fleet_excluded_entities(hass: HomeAssistant) -> set[str]:
|
||||
"""Manually excluded battery entity_ids, stored on the fleet object entry."""
|
||||
from ..const import BATTERY_FLEET_EXCLUDED
|
||||
|
||||
return set(_fleet_object(hass).get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
|
||||
|
||||
def fleet_included_entities(hass: HomeAssistant) -> set[str]:
|
||||
"""Manually ADDED battery entity_ids (#135) — same storage pattern as
|
||||
the exclusions. An include bypasses the discovery heuristic and the
|
||||
self-charging filter in the native pass; Battery-Notes coverage and the
|
||||
exclusion list still win (no duplicate rows, exclusion stays king)."""
|
||||
from ..const import BATTERY_FLEET_INCLUDED, BATTERY_FLEET_OBJECT_FLAG, CONF_OBJECT, DOMAIN
|
||||
self-charging filter in BOTH passes (lurisin's Oura Ring carried a
|
||||
Battery Notes note, so the native-pass-only bypass never fired);
|
||||
Battery-Notes coverage and the exclusion list still win (no duplicate
|
||||
rows, exclusion stays king)."""
|
||||
from ..const import BATTERY_FLEET_INCLUDED
|
||||
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
obj = entry.data.get(CONF_OBJECT, {})
|
||||
if obj.get(BATTERY_FLEET_OBJECT_FLAG):
|
||||
return set(obj.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
return set()
|
||||
return set(_fleet_object(hass).get(BATTERY_FLEET_INCLUDED) or [])
|
||||
|
||||
|
||||
def fleet_track_self_charging(hass: HomeAssistant) -> bool:
|
||||
"""Whether the fleet keeps self-charging devices in the roster (#135).
|
||||
|
||||
Off (the default) preserves #107: vacuums/phones/rings never enter. On,
|
||||
they appear as rechargeables — typed "Rechargeable", labelled
|
||||
"— recharge", never counted into the shopping needs — so a low phone or
|
||||
smart ring can drive a "charge it" notification. Exclusions still win.
|
||||
"""
|
||||
from ..const import BATTERY_FLEET_TRACK_SELF_CHARGING
|
||||
|
||||
return bool(_fleet_object(hass).get(BATTERY_FLEET_TRACK_SELF_CHARGING))
|
||||
|
||||
|
||||
def _is_self_charging(hass: HomeAssistant, device_id: str | None) -> bool:
|
||||
@@ -418,6 +438,12 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
|
||||
for free via its retained ``battery_low`` attribute).
|
||||
* Manually excluded entity_ids (fleet detail → exclude) are dropped from
|
||||
BOTH passes.
|
||||
* Manual includes (#135) act DEVICE-wide: whichever of a device's battery
|
||||
entities the user picked, the self-charging filter is lifted for the
|
||||
whole device in both passes — so the richest source (a Battery Notes
|
||||
note) still wins the row. With ``fleet_track_self_charging`` on, the
|
||||
self-charging filter is off entirely and such devices surface as
|
||||
rechargeables (native rows typed "Rechargeable", never "Unknown").
|
||||
"""
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -426,6 +452,24 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
|
||||
dev_reg = dr.async_get(hass)
|
||||
excluded = fleet_excluded_entities(hass)
|
||||
included = fleet_included_entities(hass)
|
||||
track_self = fleet_track_self_charging(hass)
|
||||
# The include acts device-wide (see docstring) — resolve each included
|
||||
# entity to its device once.
|
||||
included_devices: set[str] = set()
|
||||
for _eid in included:
|
||||
_reg = ent_reg.async_get(_eid)
|
||||
if _reg and _reg.device_id:
|
||||
included_devices.add(_reg.device_id)
|
||||
# _is_self_charging walks the device's registry entries — cache per device
|
||||
# (a device's %-sensor and low-binary would otherwise both pay for it).
|
||||
_self_charging_cache: dict[str, bool] = {}
|
||||
|
||||
def _self_charging(dev_id: str | None) -> bool:
|
||||
if not dev_id:
|
||||
return False
|
||||
if dev_id not in _self_charging_cache:
|
||||
_self_charging_cache[dev_id] = _is_self_charging(hass, dev_id)
|
||||
return _self_charging_cache[dev_id]
|
||||
|
||||
out: list[Battery] = []
|
||||
covered_sources: set[str] = set()
|
||||
@@ -487,8 +531,10 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
|
||||
# sensor, so the state reads unknown forever — must SURVIVE when it
|
||||
# carries a replacement date: that date is all `_predicted_date`
|
||||
# needs, and dropping these hid 11 overdue batteries in a live fleet.
|
||||
# Offline AND not low AND no date = pure connectivity noise → drop.
|
||||
if not available and not low and last_replaced is None:
|
||||
# Offline AND not low AND no date = pure connectivity noise → drop —
|
||||
# unless the user explicitly included THIS entity (#135): a stated
|
||||
# battery stays visible even while it reads like noise.
|
||||
if not available and not low and last_replaced is None and state.entity_id not in included:
|
||||
continue
|
||||
# B3: only a KEPT note covers its source/device — a dropped dead note
|
||||
# must not suppress the native fallback for its own device (a device
|
||||
@@ -506,8 +552,17 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
|
||||
# #107 follow-up: the skip covers noted devices too (it covers
|
||||
# above for the same reason exclusion does). Battery Notes
|
||||
# auto-discovers vacuums/phones from its library, so a note is
|
||||
# not evidence anyone means to swap cells there.
|
||||
if _is_self_charging(hass, dev_id):
|
||||
# not evidence anyone means to swap cells there. A manual include
|
||||
# (this entity or a sibling on the same device, #135) or the
|
||||
# track-self-charging option lifts the skip — lurisin's Oura Ring
|
||||
# was noted AND self-charging, and the v2.61.0 bypass lived only
|
||||
# in the native pass, so including it did nothing.
|
||||
if (
|
||||
not track_self
|
||||
and state.entity_id not in included
|
||||
and dev_id not in included_devices
|
||||
and _self_charging(dev_id)
|
||||
):
|
||||
continue
|
||||
out.append(
|
||||
Battery(
|
||||
@@ -550,12 +605,28 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
|
||||
dev_id = reg.device_id if reg else None
|
||||
if dev_id and dev_id in covered_devices:
|
||||
continue
|
||||
if eid not in included and _is_self_charging(hass, dev_id): # #107
|
||||
self_charging = _self_charging(dev_id)
|
||||
if ( # #107; lifted by an include on the device (#135) or the option
|
||||
self_charging
|
||||
and not track_self
|
||||
and eid not in included
|
||||
and dev_id not in included_devices
|
||||
):
|
||||
continue
|
||||
key = dev_id or eid
|
||||
rec = native.setdefault(
|
||||
key,
|
||||
{"level_state": None, "low_state": None, "device_id": dev_id, "eid": eid, "name": None},
|
||||
{
|
||||
"level_state": None,
|
||||
"low_state": None,
|
||||
"device_id": dev_id,
|
||||
"eid": eid,
|
||||
"name": None,
|
||||
# A surfaced self-charging device is a RECHARGEABLE, not an
|
||||
# "Unknown" cell — the type drives the "— recharge" label
|
||||
# and keeps it out of the shopping needs.
|
||||
"self_charging": self_charging,
|
||||
},
|
||||
)
|
||||
friendly = state.attributes.get("friendly_name")
|
||||
if domain == "sensor":
|
||||
@@ -600,7 +671,7 @@ def read_batteries(hass: HomeAssistant) -> list[Battery]:
|
||||
Battery(
|
||||
entity_id=rec["eid"],
|
||||
device_name=name or rec["eid"],
|
||||
battery_type="Unknown",
|
||||
battery_type="Rechargeable" if rec.get("self_charging") else "Unknown",
|
||||
quantity=1,
|
||||
low=low,
|
||||
level=level,
|
||||
@@ -875,6 +946,7 @@ __all__ = [
|
||||
"discover_battery_types",
|
||||
"fleet_excluded_entities",
|
||||
"fleet_included_entities",
|
||||
"fleet_track_self_charging",
|
||||
"has_batteries",
|
||||
"has_battery_notes",
|
||||
"is_rechargeable_type",
|
||||
|
||||
@@ -10,6 +10,7 @@ threshold trigger. No per-battery task.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -22,6 +23,7 @@ from ..const import (
|
||||
BATTERY_FLEET_EXCLUDED,
|
||||
BATTERY_FLEET_INCLUDED,
|
||||
BATTERY_FLEET_OBJECT_FLAG,
|
||||
BATTERY_FLEET_TASK_FLAG,
|
||||
CONF_OBJECT,
|
||||
CONF_PARTS,
|
||||
CONF_TASKS,
|
||||
@@ -32,10 +34,16 @@ from .battery_fleet import _norm_type, discover_battery_types, lifetime_months,
|
||||
# The global aggregate sensor the fleet task triggers on (fixed entity_id).
|
||||
LOW_COUNT_ENTITY_ID = "sensor.maintenance_supporter_batteries_to_replace"
|
||||
|
||||
# Hygiene bound on the manual exclude/include lists (security review
|
||||
# 2026-08-21): both are write-gated, but nothing else limited their growth —
|
||||
# a scripted operator could bloat the fleet object's config entry without
|
||||
# bound. 2000 is far above any real fleet.
|
||||
FLEET_LIST_CAP = 2000
|
||||
|
||||
# Marker on the object + task so the panel renders the battery detail section
|
||||
# and setup is idempotent (never a second fleet).
|
||||
OBJECT_FLAG = BATTERY_FLEET_OBJECT_FLAG
|
||||
TASK_FLAG = "battery_fleet_task"
|
||||
TASK_FLAG = BATTERY_FLEET_TASK_FLAG
|
||||
|
||||
|
||||
def find_fleet_entry(hass: HomeAssistant) -> ConfigEntry | None:
|
||||
@@ -342,6 +350,21 @@ async def async_mark_replaced(hass: HomeAssistant, entity_ids: list[str] | None
|
||||
return {"marked": len(targets), "pressed": pressed, "consumed": consumed}
|
||||
|
||||
|
||||
def _mutate_fleet_object(hass: HomeAssistant, mutate: Callable[[dict[str, Any]], None]) -> bool:
|
||||
"""Find the fleet entry, apply ``mutate`` to a copy of its object dict,
|
||||
persist. Returns False when no fleet exists yet — the shared scaffolding
|
||||
the three setters below each hand-rolled (drift audit 2026-08)."""
|
||||
entry = find_fleet_entry(hass)
|
||||
if entry is None:
|
||||
return False
|
||||
new_data = dict(entry.data)
|
||||
obj = dict(new_data.get(CONF_OBJECT, {}))
|
||||
mutate(obj)
|
||||
new_data[CONF_OBJECT] = obj
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
return True
|
||||
|
||||
|
||||
def set_battery_excluded(hass: HomeAssistant, entity_id: str, excluded: bool) -> bool:
|
||||
"""Persist a manual exclude/include of one battery on the fleet object.
|
||||
|
||||
@@ -350,28 +373,25 @@ def set_battery_excluded(hass: HomeAssistant, entity_id: str, excluded: bool) ->
|
||||
``battery_fleet_excluded`` on the fleet object dict. Returns False when
|
||||
no fleet exists yet.
|
||||
"""
|
||||
entry = find_fleet_entry(hass)
|
||||
if entry is None:
|
||||
return False
|
||||
new_data = dict(entry.data)
|
||||
obj = dict(new_data.get(CONF_OBJECT, {}))
|
||||
current = set(obj.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
if excluded:
|
||||
# Symmetry (#135): hiding a manually-added battery removes the manual
|
||||
# include instead of stacking an exclusion on top of it.
|
||||
includes = set(obj.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
if entity_id in includes:
|
||||
includes.discard(entity_id)
|
||||
obj[BATTERY_FLEET_INCLUDED] = sorted(includes)
|
||||
else:
|
||||
current.add(entity_id)
|
||||
else:
|
||||
current.discard(entity_id)
|
||||
obj[BATTERY_FLEET_EXCLUDED] = sorted(current)
|
||||
new_data[CONF_OBJECT] = obj
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
return True
|
||||
|
||||
def _apply(obj: dict[str, Any]) -> None:
|
||||
current = set(obj.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
if excluded:
|
||||
# Symmetry (#135): hiding a manually-added battery removes the
|
||||
# manual include instead of stacking an exclusion on top of it.
|
||||
includes = set(obj.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
if entity_id in includes:
|
||||
includes.discard(entity_id)
|
||||
obj[BATTERY_FLEET_INCLUDED] = sorted(includes)
|
||||
else:
|
||||
if len(current) >= FLEET_LIST_CAP and entity_id not in current:
|
||||
raise HomeAssistantError(f"Battery fleet exclusion list is full ({FLEET_LIST_CAP})")
|
||||
current.add(entity_id)
|
||||
else:
|
||||
current.discard(entity_id)
|
||||
obj[BATTERY_FLEET_EXCLUDED] = sorted(current)
|
||||
|
||||
return _mutate_fleet_object(hass, _apply)
|
||||
|
||||
|
||||
def set_battery_included(hass: HomeAssistant, entity_id: str, included: bool) -> bool:
|
||||
@@ -382,24 +402,34 @@ def set_battery_included(hass: HomeAssistant, entity_id: str, included: bool) ->
|
||||
a previous exclusion (the user changed their mind). Returns False when no
|
||||
fleet exists yet.
|
||||
"""
|
||||
entry = find_fleet_entry(hass)
|
||||
if entry is None:
|
||||
return False
|
||||
new_data = dict(entry.data)
|
||||
obj = dict(new_data.get(CONF_OBJECT, {}))
|
||||
includes = set(obj.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
if included:
|
||||
includes.add(entity_id)
|
||||
excludes = set(obj.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
if entity_id in excludes:
|
||||
excludes.discard(entity_id)
|
||||
obj[BATTERY_FLEET_EXCLUDED] = sorted(excludes)
|
||||
else:
|
||||
includes.discard(entity_id)
|
||||
obj[BATTERY_FLEET_INCLUDED] = sorted(includes)
|
||||
new_data[CONF_OBJECT] = obj
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
return True
|
||||
|
||||
def _apply(obj: dict[str, Any]) -> None:
|
||||
includes = set(obj.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
if included:
|
||||
if len(includes) >= FLEET_LIST_CAP and entity_id not in includes:
|
||||
raise HomeAssistantError(f"Battery fleet include list is full ({FLEET_LIST_CAP})")
|
||||
includes.add(entity_id)
|
||||
excludes = set(obj.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
if entity_id in excludes:
|
||||
excludes.discard(entity_id)
|
||||
obj[BATTERY_FLEET_EXCLUDED] = sorted(excludes)
|
||||
else:
|
||||
includes.discard(entity_id)
|
||||
obj[BATTERY_FLEET_INCLUDED] = sorted(includes)
|
||||
|
||||
return _mutate_fleet_object(hass, _apply)
|
||||
|
||||
|
||||
def set_track_self_charging(hass: HomeAssistant, enabled: bool) -> bool:
|
||||
"""Persist the fleet-wide track-self-charging opt-in (#135 follow-up).
|
||||
|
||||
Stored on the fleet object like the exclude/include lists. Returns False
|
||||
when no fleet exists yet.
|
||||
"""
|
||||
from ..const import BATTERY_FLEET_TRACK_SELF_CHARGING
|
||||
|
||||
return _mutate_fleet_object(hass, lambda obj: obj.__setitem__(BATTERY_FLEET_TRACK_SELF_CHARGING, bool(enabled)))
|
||||
|
||||
|
||||
def find_fleet_task(entry: ConfigEntry) -> tuple[str, dict[str, Any]] | None:
|
||||
"""The flagged fleet task (id, data) on the fleet entry, or None."""
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.core import HomeAssistant
|
||||
from ..const import (
|
||||
CONF_OBJECT,
|
||||
CONF_TASKS,
|
||||
DEFAULT_TASK_PRIORITY,
|
||||
DEFAULT_WARNING_DAYS,
|
||||
MAX_CHECKLIST_ITEM_LENGTH,
|
||||
MAX_CHECKLIST_ITEMS,
|
||||
@@ -130,7 +131,7 @@ def export_objects_csv(hass: HomeAssistant, entry_ids: set[str] | None = None) -
|
||||
"custom_icon": _csv_safe(tdata.get("custom_icon", "")),
|
||||
"nfc_tag_id": _csv_safe(tdata.get("nfc_tag_id", "")),
|
||||
"responsible_user_id": _csv_safe(tdata.get("responsible_user_id", "")),
|
||||
"priority": tdata.get("priority") or "normal",
|
||||
"priority": tdata.get("priority") or DEFAULT_TASK_PRIORITY,
|
||||
# Labels ride one cell, "\n"-separated like the checklist.
|
||||
"labels": "\n".join(_csv_safe(lb) for lb in (tdata.get("labels") or []) if lb),
|
||||
"trigger_type": (tdata.get("trigger_config") or {}).get("type", ""),
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from collections.abc import Iterator
|
||||
from datetime import date, timedelta
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
|
||||
from .workday import is_business_day
|
||||
|
||||
@@ -27,6 +27,27 @@ def parse_iso_date(value: str | None) -> date | None:
|
||||
return None
|
||||
|
||||
|
||||
def parse_persisted_utc(value: str | None) -> datetime | None:
|
||||
"""Persisted ISO timestamp → TZ-aware ``datetime``; ``None`` when absent
|
||||
or malformed.
|
||||
|
||||
Live writes use ``dt_util.utcnow().isoformat()`` (TZ-aware), but older
|
||||
payloads may be naive — those are assumed UTC, because subtracting a naive
|
||||
datetime from ``dt_util.utcnow()`` raises ``TypeError``. One home for the
|
||||
block the threshold and state-change triggers each hand-rolled (and the
|
||||
runtime trigger FORGOT — a naive ``on_since`` crashed its elapsed math).
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=UTC)
|
||||
return parsed
|
||||
|
||||
|
||||
def _add_months(anchor: date, months: int) -> date:
|
||||
"""Advance ``anchor`` by ``months`` calendar months, clamping the day."""
|
||||
total = anchor.month - 1 + months
|
||||
|
||||
@@ -239,20 +239,28 @@ def pop_stashed_config(hass: HomeAssistant, entity_id: str) -> dict[str, Any] |
|
||||
return None
|
||||
|
||||
|
||||
def build_problem_task(entity_id: str, name: str) -> dict[str, Any]:
|
||||
def build_problem_task(entity_id: str, name: str, *, for_minutes: int = 0) -> dict[str, Any]:
|
||||
"""The task payload for an adopted problem sensor: manual schedule (no
|
||||
calendar), triggered while the problem is on, auto-completed on recovery."""
|
||||
calendar), triggered while the problem is on, auto-completed on recovery.
|
||||
|
||||
``for_minutes`` (#136) makes the problem state HOLD that long before the
|
||||
task triggers — the opt-in flicker filter for sensors that report problems
|
||||
for a few seconds at night. 0 keeps the trigger-on-first-flicker default.
|
||||
"""
|
||||
# A concise task title; the sensor's friendly name often already reads like
|
||||
# "Printer problem", so keep it as-is rather than double-prefixing.
|
||||
trigger_config: dict[str, Any] = {
|
||||
"type": "state_change",
|
||||
"entity_ids": [entity_id],
|
||||
"trigger_to_state": "on",
|
||||
"trigger_target_changes": 1,
|
||||
"auto_complete_on_recovery": True,
|
||||
}
|
||||
if for_minutes > 0:
|
||||
trigger_config["trigger_for_minutes"] = for_minutes
|
||||
return {
|
||||
"name": name,
|
||||
"task_type": "inspection",
|
||||
"schedule": {"kind": "manual"},
|
||||
"trigger_config": {
|
||||
"type": "state_change",
|
||||
"entity_ids": [entity_id],
|
||||
"trigger_to_state": "on",
|
||||
"trigger_target_changes": 1,
|
||||
"auto_complete_on_recovery": True,
|
||||
},
|
||||
"trigger_config": trigger_config,
|
||||
}
|
||||
@@ -31,6 +31,8 @@ from ..const import (
|
||||
MAX_INTERVAL_DAYS,
|
||||
MAX_META_LENGTH,
|
||||
MAX_NAME_LENGTH,
|
||||
MAX_NFC_TAG_LENGTH,
|
||||
MAX_READING_UNIT_LENGTH,
|
||||
MAX_SCHEDULE_TIME_LENGTH,
|
||||
MAX_TEXT_LENGTH,
|
||||
MAX_TYPE_LENGTH,
|
||||
@@ -50,7 +52,7 @@ _TASK_STR_LIMITS: dict[str, int] = {
|
||||
"notes": MAX_TEXT_LENGTH,
|
||||
"documentation_url": MAX_URL_LENGTH,
|
||||
"custom_icon": MAX_ICON_LENGTH,
|
||||
"nfc_tag_id": 256,
|
||||
"nfc_tag_id": MAX_NFC_TAG_LENGTH,
|
||||
"responsible_user_id": MAX_META_LENGTH,
|
||||
"entity_slug": MAX_ENTITY_SLUG_LENGTH,
|
||||
"created_at": MAX_DATE_LENGTH,
|
||||
@@ -61,7 +63,7 @@ _TASK_STR_LIMITS: dict[str, int] = {
|
||||
"archived_reason": MAX_META_LENGTH,
|
||||
"schedule_time": MAX_SCHEDULE_TIME_LENGTH,
|
||||
"priority": MAX_TYPE_LENGTH,
|
||||
"reading_unit": 32,
|
||||
"reading_unit": MAX_READING_UNIT_LENGTH,
|
||||
}
|
||||
|
||||
_OBJECT_STR_LIMITS: dict[str, int] = {
|
||||
|
||||
@@ -23,7 +23,7 @@ from uuid import uuid4
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from ..const import CONF_SAVED_FILTER_VIEWS, MAX_SAVED_VIEWS, MAX_VIEW_NAME_LENGTH, TaskPriority
|
||||
from ..const import CONF_SAVED_FILTER_VIEWS, DEFAULT_TASK_PRIORITY, MAX_SAVED_VIEWS, MAX_VIEW_NAME_LENGTH, TaskPriority
|
||||
from .global_options import get_global_options
|
||||
|
||||
# Closed value sets mirrored from the panel's filter controls. Anything outside
|
||||
@@ -98,7 +98,7 @@ def view_matches_task(filters: Mapping[str, Any], task: Mapping[str, Any]) -> bo
|
||||
# Tasks without an explicit priority are "normal" everywhere (the model
|
||||
# default) — the filter must see them the same way.
|
||||
priority = filters.get("priority")
|
||||
if priority and (task.get("priority") or "normal") != priority:
|
||||
if priority and (task.get("priority") or DEFAULT_TASK_PRIORITY) != priority:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from ..const import UNAVAILABLE_STATES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import State
|
||||
|
||||
@@ -72,7 +74,7 @@ def threshold_exceeds(
|
||||
def _numeric_entity_value(get_state: StateGetter, entity_id: str, attribute: str | None) -> float | None:
|
||||
"""Read a numeric value from an entity state/attribute (None when unusable)."""
|
||||
state = get_state(entity_id)
|
||||
if state is None or state.state in ("unavailable", "unknown"):
|
||||
if state is None or state.state in UNAVAILABLE_STATES:
|
||||
return None
|
||||
try:
|
||||
raw = state.attributes.get(attribute) if attribute else state.state
|
||||
|
||||
Reference in new issue
Block a user