197 files
This commit is contained in:
@@ -13,9 +13,11 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from ..const import (
|
||||
BATTERY_FLEET_EXCLUDED,
|
||||
BATTERY_FLEET_OBJECT_FLAG,
|
||||
BATTERY_FLEET_TASK_FLAG,
|
||||
CONF_GROUPS,
|
||||
CONF_OBJECT,
|
||||
CONF_TASKS,
|
||||
DEFAULT_TASK_PRIORITY,
|
||||
DEFAULT_WARNING_DAYS,
|
||||
DOMAIN,
|
||||
GLOBAL_UNIQUE_ID,
|
||||
@@ -149,7 +151,7 @@ def _build_task_summary(
|
||||
# auto-created "buy" reminder, the owning part marker ({part_id}).
|
||||
"consumes_parts": task_data.get("consumes_parts"),
|
||||
"part_ref": task_data.get("part_ref"),
|
||||
"priority": task_data.get("priority", "normal"),
|
||||
"priority": task_data.get("priority") or DEFAULT_TASK_PRIORITY,
|
||||
# v2.10.0 archive: archived_at is the persisted timestamp (None = active);
|
||||
# `archived` is the convenience bool the frontend filters on; reason is
|
||||
# manual | auto | object. All read from the persisted (static) task dict.
|
||||
@@ -187,7 +189,7 @@ def _build_task_summary(
|
||||
"trigger_config": trigger_config,
|
||||
# Battery Fleet: marks the single aggregate task so the detail view
|
||||
# renders the battery section instead of the generic trigger card.
|
||||
"battery_fleet_task": task_data.get("battery_fleet_task", False),
|
||||
"battery_fleet_task": task_data.get(BATTERY_FLEET_TASK_FLAG, False),
|
||||
"trigger_entity_info": trigger_entity_info,
|
||||
"trigger_entity_infos": trigger_entity_infos,
|
||||
"checklist": task_data.get("checklist", []),
|
||||
@@ -534,6 +536,7 @@ def async_register_commands(hass: HomeAssistant) -> None:
|
||||
ws_battery_fleet_overview,
|
||||
ws_battery_fleet_set_excluded,
|
||||
ws_battery_fleet_set_included,
|
||||
ws_battery_fleet_set_track_self_charging,
|
||||
ws_battery_fleet_setup,
|
||||
ws_battery_fleet_status,
|
||||
)
|
||||
@@ -689,6 +692,7 @@ def async_register_commands(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_battery_fleet_mark_replaced)
|
||||
websocket_api.async_register_command(hass, ws_battery_fleet_set_excluded)
|
||||
websocket_api.async_register_command(hass, ws_battery_fleet_set_included)
|
||||
websocket_api.async_register_command(hass, ws_battery_fleet_set_track_self_charging)
|
||||
websocket_api.async_register_command(hass, ws_discover_integration_setups)
|
||||
websocket_api.async_register_command(hass, ws_adopt_integration_setups)
|
||||
websocket_api.async_register_command(hass, ws_list_saved_views)
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..helpers.battery_fleet import (
|
||||
async_compute_overview,
|
||||
async_level_history,
|
||||
fleet_excluded_entities,
|
||||
fleet_track_self_charging,
|
||||
has_batteries,
|
||||
has_battery_notes,
|
||||
read_batteries,
|
||||
@@ -28,6 +29,8 @@ from ..helpers.battery_fleet_setup import (
|
||||
find_fleet_entry,
|
||||
fleet_task_trigger_ok,
|
||||
set_battery_excluded,
|
||||
set_battery_included,
|
||||
set_track_self_charging,
|
||||
)
|
||||
from ..helpers.permissions import require_write
|
||||
|
||||
@@ -65,6 +68,8 @@ async def ws_battery_fleet_overview(hass: HomeAssistant, connection: websocket_a
|
||||
"needs_now": dict(ov.needs_now),
|
||||
"needs_soon": dict(ov.needs_soon),
|
||||
"types": ov.types,
|
||||
# #135 follow-up: the roster's track-self-charging toggle state.
|
||||
"track_self_charging": fleet_track_self_charging(hass),
|
||||
# Manually excluded batteries (issue #107) — names enriched where
|
||||
# the entity still exists, so the restore list stays readable.
|
||||
"excluded": [
|
||||
@@ -167,14 +172,30 @@ async def ws_battery_fleet_set_included(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Manually ADD a battery the discovery heuristics miss (#135)."""
|
||||
from ..helpers.battery_fleet_setup import set_battery_included
|
||||
|
||||
if not set_battery_included(hass, msg["entity_id"], msg["included"]):
|
||||
connection.send_error(msg["id"], "not_configured", "Battery Fleet is not set up")
|
||||
return
|
||||
connection.send_result(msg["id"], {"success": True})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): f"{DOMAIN}/battery_fleet/set_track_self_charging",
|
||||
vol.Required("enabled"): bool,
|
||||
}
|
||||
)
|
||||
@require_write
|
||||
@websocket_api.async_response
|
||||
async def ws_battery_fleet_set_track_self_charging(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Fleet-wide opt-in: keep self-charging devices in the roster (#135)."""
|
||||
if not set_track_self_charging(hass, msg["enabled"]):
|
||||
connection.send_error(msg["id"], "not_configured", "Battery Fleet is not set up")
|
||||
return
|
||||
connection.send_result(msg["id"], {"success": True})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): f"{DOMAIN}/battery_fleet/mark_replaced",
|
||||
|
||||
@@ -49,6 +49,10 @@ _SELECTION_SCHEMA = vol.Schema(
|
||||
# Responsible HA user for the created task (the adopt dialog offers one
|
||||
# picker applied to every selection). Wins over a stashed value.
|
||||
vol.Optional("responsible_user_id"): vol.Any(vol.All(str, vol.Length(max=MAX_ID_LENGTH)), None),
|
||||
# #136: minutes the problem state must HOLD before the task triggers
|
||||
# (one dialog field applied to every selection). 0/omitted = trigger
|
||||
# on the first flicker, the pre-#136 behaviour.
|
||||
vol.Optional("for_minutes"): vol.Any(vol.All(int, vol.Range(min=0, max=1440)), None),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +118,7 @@ async def ws_adopt_problem_sensors(
|
||||
errors.append({"entity_id": entity_id, "reason": "target object not found"})
|
||||
continue
|
||||
|
||||
task = build_problem_task(entity_id, sel["name"])
|
||||
task = build_problem_task(entity_id, sel["name"], for_minutes=sel.get("for_minutes") or 0)
|
||||
task_data = {
|
||||
"id": uuid4().hex,
|
||||
"object_id": entry.data.get(CONF_OBJECT, {}).get("id", ""),
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from ..const import (
|
||||
BATTERY_FLEET_TASK_FLAG,
|
||||
CONF_OBJECT,
|
||||
CONF_TASKS,
|
||||
DEFAULT_WARNING_DAYS,
|
||||
@@ -33,6 +34,8 @@ from ..const import (
|
||||
MAX_LABELS,
|
||||
MAX_META_LENGTH,
|
||||
MAX_NAME_LENGTH,
|
||||
MAX_NFC_TAG_LENGTH,
|
||||
MAX_READING_UNIT_LENGTH,
|
||||
MAX_TEXT_LENGTH,
|
||||
MAX_TYPE_LENGTH,
|
||||
MAX_URL_LENGTH,
|
||||
@@ -112,8 +115,9 @@ TASK_UPDATE_FIELD_MAP = {
|
||||
}
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
# Hoisted as a module constant so the schema/field-map parity tripwire
|
||||
# (tests/test_task_schema_parity.py, drift audit 2026-08) can introspect it.
|
||||
_TASK_CREATE_SCHEMA: dict[Any, Any] = {
|
||||
vol.Required("type"): "maintenance_supporter/task/create",
|
||||
vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
vol.Required("name"): vol.All(str, vol.Length(min=1, max=MAX_NAME_LENGTH)),
|
||||
@@ -146,9 +150,9 @@ TASK_UPDATE_FIELD_MAP = {
|
||||
vol.Optional("rotation_strategy"): vol.Any(vol.In(ROTATION_STRATEGY_VALUES), None),
|
||||
vol.Optional("entity_slug"): vol.Any(vol.All(str, vol.Length(max=MAX_ENTITY_SLUG_LENGTH)), None),
|
||||
vol.Optional("custom_icon"): vol.Any(vol.All(str, vol.Length(max=MAX_ICON_LENGTH)), None),
|
||||
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=256)), None),
|
||||
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=MAX_NFC_TAG_LENGTH)), None),
|
||||
# v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...).
|
||||
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=32)), None),
|
||||
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=MAX_READING_UNIT_LENGTH)), None),
|
||||
# Spare parts consumed on completion: [{part_id, quantity}].
|
||||
vol.Optional("consumes_parts"): vol.Any(list, None),
|
||||
vol.Optional("priority"): vol.In(TASK_PRIORITIES),
|
||||
@@ -173,7 +177,9 @@ TASK_UPDATE_FIELD_MAP = {
|
||||
vol.Optional("enabled", default=True): bool,
|
||||
vol.Optional("dry_run", default=False): bool,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(_TASK_CREATE_SCHEMA)
|
||||
@require_write
|
||||
@websocket_api.async_response
|
||||
async def ws_create_task(
|
||||
@@ -377,8 +383,9 @@ async def ws_create_task(
|
||||
connection.send_result(msg["id"], result)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
# Hoisted as a module constant so the schema/field-map parity tripwire
|
||||
# (tests/test_task_schema_parity.py, drift audit 2026-08) can introspect it.
|
||||
_TASK_UPDATE_SCHEMA: dict[Any, Any] = {
|
||||
vol.Required("type"): "maintenance_supporter/task/update",
|
||||
vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
@@ -410,9 +417,9 @@ async def ws_create_task(
|
||||
vol.Optional("rotation_strategy"): vol.Any(vol.In(ROTATION_STRATEGY_VALUES), None),
|
||||
vol.Optional("entity_slug"): vol.Any(vol.All(str, vol.Length(max=MAX_ENTITY_SLUG_LENGTH)), None),
|
||||
vol.Optional("custom_icon"): vol.Any(vol.All(str, vol.Length(max=MAX_ICON_LENGTH)), None),
|
||||
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=256)), None),
|
||||
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=MAX_NFC_TAG_LENGTH)), None),
|
||||
# v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...).
|
||||
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=32)), None),
|
||||
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=MAX_READING_UNIT_LENGTH)), None),
|
||||
# Spare parts consumed on completion: [{part_id, quantity}].
|
||||
vol.Optional("consumes_parts"): vol.Any(list, None),
|
||||
vol.Optional("priority"): vol.In(TASK_PRIORITIES),
|
||||
@@ -430,7 +437,9 @@ async def ws_create_task(
|
||||
vol.Optional("on_complete_action"): vol.Any(dict, None),
|
||||
vol.Optional("quick_complete_defaults"): vol.Any(dict, None),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(_TASK_UPDATE_SCHEMA)
|
||||
@require_write
|
||||
@websocket_api.async_response
|
||||
async def ws_update_task(
|
||||
@@ -464,7 +473,7 @@ async def ws_update_task(
|
||||
# null can only be a client (possibly stale/cached bundle) failing to
|
||||
# round-trip a trigger it did not hydrate. Ignore the null; every other
|
||||
# field in the message still applies.
|
||||
if task.get("battery_fleet_task") and "trigger_config" in msg and msg["trigger_config"] is None:
|
||||
if task.get(BATTERY_FLEET_TASK_FLAG) and "trigger_config" in msg and msg["trigger_config"] is None:
|
||||
del msg["trigger_config"]
|
||||
|
||||
# Validate trigger_config if provided
|
||||
|
||||
@@ -14,6 +14,7 @@ from ..const import (
|
||||
CONF_OBJECT,
|
||||
CONF_OBJECT_NAME,
|
||||
CONF_TASKS,
|
||||
UNAVAILABLE_STATES,
|
||||
TriggerType,
|
||||
)
|
||||
from . import (
|
||||
@@ -158,7 +159,7 @@ def _validate_trigger_config(
|
||||
state = hass.states.get(eid)
|
||||
if state is None:
|
||||
warnings.append(f"Entity {eid} does not exist (yet)")
|
||||
elif state.state in ("unavailable", "unknown"):
|
||||
elif state.state in UNAVAILABLE_STATES:
|
||||
warnings.append(f"Entity {eid} is currently '{state.state}'")
|
||||
# Ensure entity_id is set for backwards compat
|
||||
if not trigger_config.get("entity_id"):
|
||||
|
||||
Reference in New Issue
Block a user