291 files

This commit is contained in:
Home Assistant Version Control
2026-10-14 18:10:18 +00:00
parent c4e0178d8a
commit 280501d2e8
291 changed files with 32540 additions and 3280 deletions
@@ -46,6 +46,7 @@ from ..helpers.qr_generator import (
generate_qr_svg,
generate_qr_svg_data_uri,
)
from ..helpers.update_state import async_version_state
from ..helpers.ws_errors import send_translated_error
from ..websocket.tasks import _check_nfc_tag_duplicate, _validate_trigger_config
from . import ID_FIELD, _get_object_entries, _load_object_entry, _load_object_task, _merge_global_options
@@ -512,17 +513,17 @@ def _keep_fleet_part_id(is_fleet: bool, old_id: str) -> bool:
@websocket_api.websocket_command({vol.Required("type"): f"{DOMAIN}/version"})
@websocket_api.async_response
async def ws_version(hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]) -> None:
"""The installed integration version (manifest).
"""The running integration version, and the installed one while an update waits for a restart.
Roadmap guard 2 — stale-bundle handshake: the panel compares this against
the version esbuild stamped into its bundle and offers a reload when a
cached old frontend is talking to a newer backend (HA's service worker
updates stale-while-revalidate, so this happens routinely after updates).
Roadmap guard 2 — stale-bundle handshake: the panel compares `version`
against the version esbuild stamped into its bundle and offers a reload
when a cached old frontend is talking to a newer backend (HA's service
worker updates stale-while-revalidate, so this happens routinely after
updates). `restart_pending` (with `installed_version`, the manifest on
disk) says the files were updated but Home Assistant still runs the old
code — the panel then asks for a restart instead (helpers/update_state).
"""
from homeassistant.loader import async_get_integration
integration = await async_get_integration(hass, DOMAIN)
connection.send_result(msg["id"], {"version": integration.version})
connection.send_result(msg["id"], await async_version_state(hass))
@websocket_api.websocket_command(
@@ -95,11 +95,12 @@ async def ws_create_part(
stock = msg.get("stock")
if stock is not None:
from ..parts_runtime import async_change_part_stock, async_set_part_restoring
from ..parts_runtime import async_change_part_stock
await async_change_part_stock(hass, entry, part["id"], absolute=float(stock))
if part.get("reuse") and msg.get("restoring"):
await async_set_part_restoring(hass, entry, part["id"], float(msg["restoring"]))
pile = msg.get("restoring") if part.get("reuse") else None
await async_change_part_stock(
hass, entry, part["id"], absolute=float(stock), restoring=float(pile) if pile is not None else None
)
# Reload so the part's stock sensor appears (entities are created at setup).
await hass.config_entries.async_reload(entry.entry_id)
connection.send_result(msg["id"], {"part_id": part["id"]})
@@ -139,24 +140,27 @@ async def ws_update_part(
parts[part["id"]] = part
_persist_parts(hass, entry, parts)
from ..parts_runtime import async_change_part_stock, async_set_part_restoring, schedule_buy_task_reconcile
from ..parts_runtime import (
async_change_part_stock,
async_set_part_restoring,
async_untrack_part_stock,
schedule_buy_task_reconcile,
)
# #208: the waiting pile goes with the part's reuse — set when given, and
# gone when the part is no longer reusable.
# gone when the part is no longer reusable. Booked together with the
# stock: a first count creates the stock the pile lives next to.
pile: float | None = None
if not part.get("reuse") and existing.get("reuse"):
await async_set_part_restoring(hass, entry, part["id"], 0)
pile = 0.0
elif part.get("reuse") and msg.get("restoring") is not None:
await async_set_part_restoring(hass, entry, part["id"], float(msg["restoring"]))
if "stock" in msg:
stock = msg.get("stock")
if stock is None:
rd = _get_runtime_data(hass, entry.entry_id)
if rd and rd.store:
rd.store.set_part_stock(part["id"], None)
await rd.store.async_save()
schedule_buy_task_reconcile(hass, entry)
else:
await async_change_part_stock(hass, entry, part["id"], absolute=float(stock))
pile = float(msg["restoring"])
if "stock" in msg and msg.get("stock") is None:
await async_untrack_part_stock(hass, entry, part["id"])
elif "stock" in msg:
await async_change_part_stock(hass, entry, part["id"], absolute=float(msg["stock"]), restoring=pile)
elif pile is not None:
await async_set_part_restoring(hass, entry, part["id"], pile)
else:
# Threshold/opt-in edits can change the desired buy-task set.
schedule_buy_task_reconcile(hass, entry)
@@ -199,14 +199,19 @@ async def ws_adopt_problem_sensors(
# dialog's suggestion (the target object/parts may have changed).
from ..const import CONF_PARTS
from ..helpers.parts import sanitize_consumes_parts
from ..helpers.sanitize import sanitize_assignee_pool, seed_rotation_assignee
stashed = pop_stashed_config(hass, entity_id) or {}
for field in ("notes", "responsible_user_id", "priority", "labels"):
for field in ("notes", "responsible_user_id", "assignee_pool", "rotation_strategy", "priority", "labels"):
if stashed.get(field):
task_data[field] = stashed[field]
# An explicit dialog pick wins over the stashed responsible user.
if "assignee_pool" in task_data:
task_data["assignee_pool"] = sanitize_assignee_pool(task_data["assignee_pool"])
# An explicit dialog pick wins over the stashed responsible user;
# a rotation keeps someone of its pool on duty.
if sel.get("responsible_user_id"):
task_data["responsible_user_id"] = sel["responsible_user_id"]
seed_rotation_assignee(task_data)
# Link the suggested spare part — or, absent one, the stashed link —
# validated against the target object's parts (an unknown id is
# silently dropped, same as the task-CRUD path).
@@ -17,6 +17,7 @@ from ..helpers.aggregate import get_coordinator_data, get_runtime_data, object_n
from ..helpers.assignees import task_is_for
from ..helpers.entry_tasks import write_task
from ..helpers.permissions import require_write, user_may_write
from ..helpers.sanitize import seed_rotation_assignee
from . import (
ID_FIELD,
_build_task_summary,
@@ -110,6 +111,10 @@ async def ws_assign_user(
task.pop("responsible_user_id", None)
else:
task["responsible_user_id"] = user_id
# The rotation rule of task/update: a rotation keeps someone of its pool
# on duty (unassigning, or a user outside the pool, picks its first).
seed_rotation_assignee(task)
user_id = task.get("responsible_user_id")
# Patch only this task's key onto a fresh read — never write back a map
# snapshot from before an await.