"""WebSocket commands for the Battery Fleet. ``battery_fleet/overview`` (read) returns the live aggregated view — low now, grouped shopping needs, forecast — for the fleet task's detail. ``setup`` (admin write) creates the fleet object + type-parts + the single task. """ from __future__ import annotations from typing import Any import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant from ..const import DOMAIN, MAX_ENTITY_ID_LENGTH from ..helpers.battery_fleet import compute_overview, fleet_excluded_entities, has_batteries, has_battery_notes from ..helpers.battery_fleet_setup import ( async_mark_replaced, async_setup_battery_fleet, find_fleet_entry, fleet_task_trigger_ok, set_battery_excluded, ) from ..helpers.permissions import require_write @websocket_api.websocket_command({vol.Required("type"): f"{DOMAIN}/battery_fleet/overview"}) @websocket_api.async_response async def ws_battery_fleet_overview(hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]) -> None: """Return the live fleet overview (low now, needs grouped, forecast).""" ov = compute_overview(hass) fleet = find_fleet_entry(hass) connection.send_result( msg["id"], { "available": has_batteries(hass), "has_battery_notes": has_battery_notes(hass), "configured": fleet is not None, # False when the fleet task was deleted or its trigger was wiped # (issue #106) — the detail section offers a one-click repair, # which re-runs the idempotent setup. "task_ok": fleet is not None and fleet_task_trigger_ok(fleet), "entry_id": fleet.entry_id if fleet else None, "total": ov.total, "low": ov.low, "soon": ov.soon, "needs_now": dict(ov.needs_now), "needs_soon": dict(ov.needs_soon), "types": ov.types, # Manually excluded batteries (issue #107) — names enriched where # the entity still exists, so the restore list stays readable. "excluded": [ { "entity_id": eid, "device_name": ( (st := hass.states.get(eid)) is not None and (st.attributes.get("device_name") or st.attributes.get("friendly_name")) ) or eid, } for eid in sorted(fleet_excluded_entities(hass)) ], }, ) @websocket_api.websocket_command( { vol.Required("type"): f"{DOMAIN}/battery_fleet/setup", # The caller's UI language (same contract as the template WS) — # the created object/task/part names are localized through _T. vol.Optional("language"): vol.All(str, vol.Length(max=10)), } ) @require_write @websocket_api.async_response async def ws_battery_fleet_setup(hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]) -> None: """Create (or reconcile) the Battery Fleet object + type-parts + task.""" if not has_batteries(hass): connection.send_error(msg["id"], "not_available", "No battery devices found") return result = await async_setup_battery_fleet(hass, language=msg.get("language")) connection.send_result(msg["id"], result) @websocket_api.websocket_command( { vol.Required("type"): f"{DOMAIN}/battery_fleet/set_excluded", vol.Required("entity_id"): vol.All(str, vol.Length(max=MAX_ENTITY_ID_LENGTH)), vol.Required("excluded"): bool, } ) @require_write @websocket_api.async_response async def ws_battery_fleet_set_excluded( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Manually exclude a battery from the fleet (or take it back in) — #107.""" if not set_battery_excluded(hass, msg["entity_id"], msg["excluded"]): 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", # battery_plus entity_ids to mark; omit to mark ALL currently low. vol.Optional("entity_ids"): [vol.All(str, vol.Length(max=MAX_ENTITY_ID_LENGTH))], } ) @require_write @websocket_api.async_response async def ws_battery_fleet_mark_replaced( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Mark batteries replaced (press their button + consume the type-parts).""" result = await async_mark_replaced(hass, msg.get("entity_ids")) connection.send_result(msg["id"], result)