Files
HomeAssistantVS/custom_components/ha_washdata/ws_schema.py
T

1031 lines
36 KiB
Python

# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs.
# Copyright (C) 2026 Lukas Bandura
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Type-safe contract for the WashData WebSocket API (Group H1).
This module is the **single source of truth** for the shape of every
``ha_washdata/*`` WebSocket command: its request parameters (:data:`WS_COMMANDS`)
and its response payload (:data:`WS_RESPONSE_TYPES`, one ``TypedDict`` per
command). It is deliberately dependency-free — it imports nothing from Home
Assistant and nothing from :mod:`ws_api` — so it is safe to import from tooling
(``devtools/generate_ws_types.py``), from tests, and from ``ws_api`` itself
without creating an import cycle.
Nothing here runs in the hot path. ``ws_api._send_result`` consults the
registry only when the debug contract flag is on; otherwise this module is
imported once and never touched again.
**Keeping the contract in sync:** ``tests/test_ws_contract.py`` asserts that the
set of commands registered in :mod:`ws_api` matches the keys of
:data:`WS_COMMANDS` exactly, so adding, removing, or renaming a WS command must
be mirrored here or the suite fails.
Convention notes:
- ``TypedDict`` totality is used to express required-vs-optional keys: a class
with ``total=True`` (the default) means every key is always present in the
response; ``total=False`` means the response has conditional keys. (Because
this file uses ``from __future__ import annotations``, per-field
``Required``/``NotRequired`` markers cannot be introspected reliably, so
per-class totality is the mechanism the debug validator reads via
``__required_keys__`` / ``__optional_keys__``.)
- A handful of commands splat an upstream summary dict into their response and
therefore have an open-ended key set; those are listed in
:data:`WS_OPEN_RESPONSES` so the validator does not flag their extra keys.
"""
from __future__ import annotations
from typing import Any, TypedDict
# ─── Shared / trivial responses ────────────────────────────────────────────────
class SuccessResponse(TypedDict):
"""The common ``{"success": bool}`` acknowledgement."""
success: bool
class OkResponse(TypedDict):
"""The ``{"ok": bool}`` acknowledgement used by the cycle-control commands."""
ok: bool
# ─── Devices ────────────────────────────────────────────────────────────────────
class DeviceInfo(TypedDict):
"""One entry in :class:`GetDevicesResponse` — live state for a device."""
entry_id: str
perm: str
title: str
detector_state: str
sub_state: str | None
current_program: str | None
time_remaining_s: float | None
total_duration_s: float | None
current_power_w: float | None
cycle_progress_pct: float | None
suggestions_count: int
feedback_count: int
recording: bool
is_user_paused: bool
manual_program: bool
options: dict[str, Any]
class GetDevicesResponse(TypedDict):
devices: list[DeviceInfo]
class GetDeviceCyclesResponse(TypedDict):
entry_id: str
cycles: list[dict[str, Any]]
reference_cycles: list[dict[str, Any]]
total: int
has_more: bool
# ─── Settings ──────────────────────────────────────────────────────────────────
class GetOptionsResponse(TypedDict):
options: dict[str, Any]
class GetSettingsChangelogResponse(TypedDict):
changelog: list[dict[str, Any]]
# ─── Profiles ──────────────────────────────────────────────────────────────────
class GetProfilesResponse(TypedDict):
profiles: list[dict[str, Any]]
profile_health: dict[str, Any]
profile_trends: dict[str, Any]
coverage_gaps: dict[str, Any]
profile_advisories: list[dict[str, Any]]
class CreateProfileResponse(TypedDict):
success: bool
name: str
class ProfileGroupInfo(TypedDict):
name: str
members: list[str]
cohesion: float
cohesive: bool
class GetProfileGroupsResponse(TypedDict):
groups: list[ProfileGroupInfo]
min_cohesion: float
suggestions: list[dict[str, Any]]
class GetProfilePhasesResponse(TypedDict):
phases: list[dict[str, Any]]
# ─── Maintenance log ───────────────────────────────────────────────────────────
class GetMaintenanceLogResponse(TypedDict):
log: list[dict[str, Any]]
due: Any
event_types: list[str]
reminders: dict[str, Any]
class AddMaintenanceEventResponse(TypedDict):
success: bool
event: dict[str, Any]
# ─── Phase catalog ─────────────────────────────────────────────────────────────
class GetPhaseCatalogResponse(TypedDict):
phases: list[dict[str, Any]]
device_type: str | None
# ─── Recording ─────────────────────────────────────────────────────────────────
class GetRecordingStateResponse(TypedDict, total=False):
"""``state`` is always present; the remaining keys depend on the state."""
state: str
duration_s: int
sample_count: int
start_time: str | None
end_time: str | None
# ─── Feedbacks ─────────────────────────────────────────────────────────────────
class GetFeedbacksResponse(TypedDict):
feedbacks: list[dict[str, Any]]
class DismissAllFeedbacksResponse(TypedDict):
success: bool
dismissed: int
# ─── Diagnostics ───────────────────────────────────────────────────────────────
class GetDiagnosticsResponse(TypedDict):
stats: dict[str, Any]
class ClearDebugDataResponse(TypedDict):
success: bool
count: int
class ExportConfigResponse(TypedDict):
json_data: str
class GetExportInventoryResponse(TypedDict):
manifest: dict[str, Any]
class AnalyzeImportResponse(TypedDict):
manifest: dict[str, Any]
class ImportConfigSelectiveResponse(TypedDict):
success: bool
summary: dict[str, Any]
# ─── Shared constants ──────────────────────────────────────────────────────────
class GetConstantsResponse(TypedDict):
device_types: list[dict[str, Any]]
state_colors: dict[str, Any]
ml_lab_enabled: bool
ml_suggestions_enabled: bool
ml_training_available: bool
PROFILE_MIN_WARMUP_CYCLES: Any
store_online_available: bool
store_online_enabled: bool
store_web_origin: str
# ─── Suggestions ───────────────────────────────────────────────────────────────
class GetSuggestionsResponse(TypedDict):
suggestions: list[dict[str, Any]]
class ApplySuggestionsResponse(TypedDict):
success: bool
applied: list[str]
class RunSuggestionAnalysisResponse(TypedDict, total=False):
"""``success`` plus whatever the analysis pass reports (e.g. ``count``)."""
success: bool
count: int
# ─── Cycle curve / interactive editing ─────────────────────────────────────────
class GetCyclePowerDataResponse(TypedDict, total=False):
"""``cycle_id`` / ``samples`` / ``full_duration_s`` are always present; the
metadata keys are present only when the cycle is found."""
cycle_id: str
samples: list[list[float]]
full_duration_s: float
start_time: str | None
end_time: str | None
duration: float | None
profile_name: str | None
status: str | None
energy_kwh: float | None
artifacts: list[dict[str, Any]]
restart_gaps: list[Any]
class AnalyzeSplitResponse(TypedDict):
segments: list[list[float]]
split_offsets: list[float]
samples: list[list[float]]
full_duration_s: float
# ─── Profile envelope / member cycles ──────────────────────────────────────────
class ProfileEnvelope(TypedDict):
avg: list[list[float]]
min: list[list[float]]
max: list[list[float]]
target_duration: float | None
avg_energy: float | None
duration_std_dev: float | None
cycle_count: int
class GetProfileEnvelopeResponse(TypedDict):
envelope: ProfileEnvelope | None
class GetProfileCyclesResponse(TypedDict):
cycles: list[dict[str, Any]]
# ─── Panel config + RBAC ───────────────────────────────────────────────────────
class GetPanelConfigResponse(TypedDict, total=False):
"""``panel`` / ``is_admin`` / ``user`` / ``prefs`` are always present;
``rbac`` and ``users`` are added for admins only."""
panel: dict[str, Any]
is_admin: bool
user: dict[str, Any]
prefs: dict[str, Any]
rbac: dict[str, Any]
users: list[dict[str, Any]]
# ─── Live match debug ──────────────────────────────────────────────────────────
class GetMatchDebugResponse(TypedDict):
confidence: float | None
ambiguous: bool
candidates: list[dict[str, Any]]
# ─── Live power history ────────────────────────────────────────────────────────
class GetPowerHistoryResponse(TypedDict, total=False):
"""Base keys are always present; ``cycle_start_iso`` only while a cycle runs."""
cycle_active: bool
cycle_elapsed_s: float
live: list[list[float]]
raw: list[list[float]]
restart_gaps: list[Any]
cycle_start_iso: str
# ─── Logs ──────────────────────────────────────────────────────────────────────
class GetLogsResponse(TypedDict):
logs: list[dict[str, Any]]
# ─── ML Lab shadow-mode comparison ─────────────────────────────────────────────
class GetMlComparisonResponse(TypedDict, total=False):
"""Two shapes: a disabled report (``enabled=False`` + ``error``) and an
enabled report; both carry ``cycles`` + ``settings_comparison``."""
enabled: bool
error: str
cycles: list[dict[str, Any]]
settings_comparison: dict[str, Any]
cycle_count: int
evaluated_count: int
model_source: dict[str, Any]
profile_stats: dict[str, Any]
ml_suggestions_enabled: bool
# ─── On-device ML training ─────────────────────────────────────────────────────
class GetMlTrainingStatusResponse(TypedDict):
available: bool
enabled: bool
running: bool
last_trained: str | None
cycle_count: int
min_cycles: int
interval_days: int
hour: int
on_device_models: dict[str, Any]
matching: dict[str, Any]
# ─── Playground (F3) ───────────────────────────────────────────────────────────
class RunPlaygroundCycleDetailResponse(TypedDict, total=False):
cycle_id: Any
label: str | None
duration_s: float | None
config_summary: dict[str, Any]
series: list[dict[str, Any]]
events: list[dict[str, Any]]
alerts: list[dict[str, Any]]
outcome: dict[str, Any]
error: str
class RunPlaygroundHistoryResponse(TypedDict, total=False):
rows: list[dict[str, Any]]
summary: dict[str, Any]
baseline_rows: list[dict[str, Any]]
baseline_summary: dict[str, Any]
diff: dict[str, list[str]]
class RunPlaygroundSweepResponse(TypedDict, total=False):
param: str
objective: str
points: list[dict[str, Any]]
current_value: Any
best_value: Any
best_metric: float | None
param_x: str
param_y: str
x_values: list[float]
y_values: list[float]
grid: list[list[Any]]
best: dict[str, Any]
current: dict[str, Any]
error: str
class DtwStage2Scores(TypedDict):
correlation: float
mae_score: float
score: float
class DtwScores(TypedDict):
l1_score: float
ddtw_score: float
ensemble_score: float
blend_weight: float
blended_score: float
class DtwStage4Scores(TypedDict):
duration_agreement: float
energy_agreement: float
final_score: float
class GetDtwDebugResponse(TypedDict):
cycle_id: Any
profile_name: str
grid_n: int
cycle_duration_s: float
profile_duration_s: float
cycle_trace: list[list[float]]
profile_trace: list[list[float]]
stage2: DtwStage2Scores
dtw: DtwScores
stage4: DtwStage4Scores
warp_path: list[list[int]]
class TaskSnapshot(TypedDict, total=False):
id: str
entry_id: str
kind: str
label: str
state: str
done: int
total: int
progress: float | None
eta_s: float | None
started_at: float
updated_at: float
finished_at: float | None
error: str | None
has_result: bool
result: Any
class ListTasksResponse(TypedDict):
tasks: list[TaskSnapshot]
class CancelTaskResponse(TypedDict):
cancelled: bool
class StartTaskResponse(TypedDict):
task_id: str
class SubscribeTasksResponse(TypedDict, total=False):
"""Empty ack for the ``subscribe_tasks`` subscription; the live data arrives
as ``{"type": "task", "task": TaskSnapshot}`` event messages, not in this
result."""
# ─── Response-type registry ────────────────────────────────────────────────────
#: Map ``<command>`` (the string after ``ha_washdata/``) -> its response TypedDict.
#: Commands whose response is the trivial ``{"success": True}`` share
#: :class:`SuccessResponse`; the cycle-control commands share :class:`OkResponse`.
class StoreStatusResponse(TypedDict, total=False):
"""Community-store status / identity (no refresh token)."""
enabled: bool
connected: bool
uid: str | None
name: str | None
brand: str | None
model: str | None
disabled: bool
class StoreItemsResponse(TypedDict, total=False):
"""A list of store docs (devices / profiles / cycles)."""
items: list
disabled: bool
class StoreImportResponse(TypedDict, total=False):
"""Result of importing a reference cycle."""
profile: str
cycle_id: str
error: str
disabled: bool
class StoreUploadResponse(TypedDict, total=False):
"""Result of sharing (uploading) a local cycle."""
store_cycle_id: str
error: str
detail: str | None
disabled: bool
class StoreSimpleResponse(TypedDict, total=False):
"""Connect/disconnect acknowledgement + identity/error markers."""
connected: bool
uid: str | None
name: str | None
brand: str | None
model: str | None
error: str
disabled: bool
class StoreQualityResponse(TypedDict, total=False):
"""Device 5-star quality summary (count + average)."""
avg: float | None
count: int
disabled: bool
class StoreConfirmResponse(TypedDict, total=False):
"""Result of confirming a device (drives community auto-approval)."""
confirmed: bool
confirmCount: int
status: str | None
error: str
disabled: bool
class StoreOnlineResponse(TypedDict, total=False):
"""Result of toggling integration-wide online features."""
enabled: bool
ok: bool
error: str
disabled: bool
class StorePrefsResponse(TypedDict, total=False):
"""Result of setting integration-wide community-store preferences."""
prefs: dict[str, Any]
error: str
disabled: bool
class StoreDeviceProfilesResponse(TypedDict, total=False):
"""Resolved store deviceId + its profiles (for the Share dialog picker)."""
device_id: str
items: list
disabled: bool
class StoreUploadDeviceResponse(TypedDict, total=False):
"""Result of sharing a whole-device bundle (multi-profile, multi-cycle)."""
ok: bool
cycle_ids: list
errors: list
error: str
detail: str | None
disabled: bool
class StoreDownloadDeviceResponse(TypedDict, total=False):
"""Result of adopting a whole-device bundle into local reference cycles."""
profiles_adopted: int
cycles_imported: int
phases_applied: int
settings_applied: int
error: str
disabled: bool
class GetShareableCyclesResponse(TypedDict, total=False):
"""Recorded/golden reference cycles eligible to share (share-device tree source)
plus the programs that carry a local phase map."""
items: list
phase_programs: list
class GetSetupStatusResponse(TypedDict, total=False):
"""Current adoption phase for the setup guidance card."""
phase: str
message_key: str
message_params: dict
cta_label_key: str
cta_action: str
secondary_label_key: str | None
secondary_action: str | None
skippable: bool
dismissible: bool
step_key: str | None
WS_RESPONSE_TYPES: dict[str, type] = {
"get_devices": GetDevicesResponse,
"get_device_cycles": GetDeviceCyclesResponse,
"get_options": GetOptionsResponse,
"set_options": SuccessResponse,
"get_settings_changelog": GetSettingsChangelogResponse,
"get_setup_status": GetSetupStatusResponse,
"get_profiles": GetProfilesResponse,
"create_profile": CreateProfileResponse,
"rename_profile": SuccessResponse,
"delete_profile": SuccessResponse,
"get_profile_groups": GetProfileGroupsResponse,
"save_profile_group": SuccessResponse,
"rename_profile_group": SuccessResponse,
"delete_profile_group": SuccessResponse,
"rebuild_envelopes": StartTaskResponse,
"get_profile_phases": GetProfilePhasesResponse,
"set_profile_phases": SuccessResponse,
"get_maintenance_log": GetMaintenanceLogResponse,
"add_maintenance_event": AddMaintenanceEventResponse,
"delete_maintenance_event": SuccessResponse,
"label_cycle": SuccessResponse,
"delete_cycle": SuccessResponse,
"auto_label_cycles": SuccessResponse,
"get_phase_catalog": GetPhaseCatalogResponse,
"create_phase": SuccessResponse,
"update_phase": SuccessResponse,
"delete_phase": SuccessResponse,
"get_recording_state": GetRecordingStateResponse,
"start_recording": SuccessResponse,
"stop_recording": SuccessResponse,
"process_recording": SuccessResponse,
"discard_recording": SuccessResponse,
"get_feedbacks": GetFeedbacksResponse,
"resolve_feedback": SuccessResponse,
"dismiss_all_feedbacks": DismissAllFeedbacksResponse,
"get_diagnostics": GetDiagnosticsResponse,
"reprocess_history": StartTaskResponse,
"clear_debug_data": ClearDebugDataResponse,
"wipe_history": SuccessResponse,
"export_config": ExportConfigResponse,
"import_config": SuccessResponse,
"get_export_inventory": GetExportInventoryResponse,
"analyze_import": AnalyzeImportResponse,
"export_config_selective": ExportConfigResponse,
"import_config_selective": ImportConfigSelectiveResponse,
"get_constants": GetConstantsResponse,
"get_suggestions": GetSuggestionsResponse,
"apply_suggestions": ApplySuggestionsResponse,
"clear_suggestions": SuccessResponse,
"run_suggestion_analysis": RunSuggestionAnalysisResponse,
"get_cycle_power_data": GetCyclePowerDataResponse,
"trim_cycle": StartTaskResponse,
"analyze_split": AnalyzeSplitResponse,
"apply_split": StartTaskResponse,
"apply_merge": StartTaskResponse,
"get_profile_envelope": GetProfileEnvelopeResponse,
"get_profile_cycles": GetProfileCyclesResponse,
"get_panel_config": GetPanelConfigResponse,
"set_panel_config": SuccessResponse,
"set_user_prefs": SuccessResponse,
"get_match_debug": GetMatchDebugResponse,
"set_program": SuccessResponse,
"get_power_history": GetPowerHistoryResponse,
"get_logs": GetLogsResponse,
"get_ml_comparison": GetMlComparisonResponse,
"get_ml_training_status": GetMlTrainingStatusResponse,
"trigger_ml_training": StartTaskResponse,
"revert_matching_config": SuccessResponse,
"revert_ml_models": SuccessResponse,
"set_ml_review": SuccessResponse,
"pause_cycle": OkResponse,
"resume_cycle": OkResponse,
"terminate_cycle": OkResponse,
"run_playground_cycle_detail": RunPlaygroundCycleDetailResponse,
"run_playground_history": RunPlaygroundHistoryResponse,
"run_playground_sweep": RunPlaygroundSweepResponse,
"get_dtw_debug": GetDtwDebugResponse,
"list_tasks": ListTasksResponse,
"subscribe_tasks": SubscribeTasksResponse,
"cancel_task": CancelTaskResponse,
"get_task_result": TaskSnapshot,
"start_playground_history": StartTaskResponse,
"start_playground_sweep": StartTaskResponse,
"start_playground_cycle_detail": StartTaskResponse,
"store_status": StoreStatusResponse,
"store_connect": StoreSimpleResponse,
"store_disconnect": StoreSimpleResponse,
"store_search_devices": StoreItemsResponse,
"store_get_profiles": StoreItemsResponse,
"store_get_cycles": StoreItemsResponse,
"store_import_cycle": StoreImportResponse,
"store_upload_cycle": StoreUploadResponse,
"store_list_brands": StoreItemsResponse,
"store_get_device_quality": StoreQualityResponse,
"store_confirm_device": StoreConfirmResponse,
"store_rate_device": StoreOnlineResponse,
"store_set_online": StoreOnlineResponse,
"store_set_prefs": StorePrefsResponse,
"store_get_device_profiles": StoreDeviceProfilesResponse,
"store_upload_device": StoreUploadDeviceResponse,
"store_download_device": StoreDownloadDeviceResponse,
"get_shareable_cycles": GetShareableCyclesResponse,
}
#: Commands whose response splats an upstream summary dict and therefore has an
#: open-ended top-level key set. The debug validator still checks required keys
#: for these but does not flag extra keys.
WS_OPEN_RESPONSES: frozenset[str] = frozenset({
"run_suggestion_analysis",
# Playground what-if responses carry nested/variant shapes (incl. an error
# variant); skip strict extra-key validation.
"run_playground_cycle_detail",
"run_playground_history",
"run_playground_sweep",
# Task snapshot splats a variant key set (result present only when finished).
"get_task_result",
})
# ─── Request-parameter contract ────────────────────────────────────────────────
def _p(name: str, type_: str, required: bool = True, **extra: Any) -> dict[str, Any]:
"""Build one request-parameter descriptor.
``type_`` is a small vocabulary of type names mirroring the voluptuous
schema: ``str``, ``int``, ``float``, ``bool``, ``dict``, ``list``,
``list[str]``, ``list[float]``, and the nullable ``str|null`` / ``float|null``.
``extra`` may carry an ``enum`` list of allowed string values.
"""
param: dict[str, Any] = {"name": name, "required": required, "type": type_}
param.update(extra)
return param
# entry_id is required by nearly every command; a tiny helper keeps it terse.
def _entry() -> dict[str, Any]:
return _p("entry_id", "str", True)
#: Map ``<command>`` -> ``{"params": [<param descriptor>, ...]}``. Hand-maintained
#: to mirror the voluptuous ``@websocket_command`` schemas in :mod:`ws_api`
#: (the implicit ``type`` discriminator is omitted). This is the source of truth
#: ``devtools/generate_ws_types.py`` reads to emit TS types + Markdown docs.
WS_COMMANDS: dict[str, dict] = {
"get_devices": {"params": []},
"get_device_cycles": {"params": [
_entry(),
_p("limit", "int", False),
_p("offset", "int", False),
]},
"get_options": {"params": [_entry()]},
"set_options": {"params": [_entry(), _p("options", "dict")]},
"get_settings_changelog": {"params": [_entry()]},
"get_setup_status": {"params": [_entry()]},
"get_profiles": {"params": [_entry()]},
"create_profile": {"params": [
_entry(),
_p("name", "str"),
_p("reference_cycle", "str|null", False),
_p("manual_duration_min", "float|null", False),
]},
"rename_profile": {"params": [
_entry(),
_p("profile_name", "str"),
_p("new_name", "str"),
_p("manual_duration_min", "float|null", False),
]},
"delete_profile": {"params": [
_entry(),
_p("profile_name", "str"),
_p("unlabel_cycles", "bool", False),
]},
"get_profile_groups": {"params": [_entry()]},
"save_profile_group": {"params": [
_entry(),
_p("name", "str"),
_p("members", "list[str]"),
]},
"rename_profile_group": {"params": [
_entry(),
_p("name", "str"),
_p("new_name", "str"),
]},
"delete_profile_group": {"params": [_entry(), _p("name", "str")]},
"rebuild_envelopes": {"params": [_entry()]},
"get_profile_phases": {"params": [_entry(), _p("profile_name", "str")]},
"set_profile_phases": {"params": [
_entry(),
_p("profile_name", "str"),
_p("phases", "list"),
]},
"get_maintenance_log": {"params": [_entry()]},
"add_maintenance_event": {"params": [
_entry(),
_p("event_type", "str"),
_p("date", "str|null", False),
_p("notes", "str", False),
]},
"delete_maintenance_event": {"params": [_entry(), _p("event_id", "str")]},
"label_cycle": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("profile_name", "str|null", False),
_p("new_profile_name", "str|null", False),
]},
"delete_cycle": {"params": [_entry(), _p("cycle_id", "str")]},
"auto_label_cycles": {"params": [
_entry(),
_p("confidence_threshold", "float", False),
]},
"get_phase_catalog": {"params": [_entry(), _p("device_type", "str|null", False)]},
"create_phase": {"params": [
_entry(),
_p("device_type", "str"),
_p("name", "str"),
_p("description", "str", False),
]},
"update_phase": {"params": [
_entry(),
_p("phase_id", "str"),
_p("new_name", "str"),
_p("description", "str", False),
]},
"delete_phase": {"params": [_entry(), _p("phase_id", "str")]},
"get_recording_state": {"params": [_entry()]},
"start_recording": {"params": [_entry()]},
"stop_recording": {"params": [_entry()]},
"process_recording": {"params": [
_entry(),
_p("profile_name", "str"),
_p("save_mode", "str", enum=["new_profile", "existing_profile"]),
_p("head_trim", "float", False),
_p("tail_trim", "float", False),
]},
"discard_recording": {"params": [_entry()]},
"get_feedbacks": {"params": [_entry()]},
"resolve_feedback": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("action", "str", enum=["confirm", "correct", "ignore", "delete"]),
_p("corrected_profile", "str|null", False),
_p("corrected_duration_min", "float|null", False),
]},
"dismiss_all_feedbacks": {"params": [_entry()]},
"get_diagnostics": {"params": [_entry()]},
"reprocess_history": {"params": [_entry()]},
"clear_debug_data": {"params": [_entry()]},
"wipe_history": {"params": [_entry()]},
"export_config": {"params": [_entry()]},
"import_config": {"params": [_entry(), _p("json_data", "str")]},
"get_export_inventory": {"params": [_entry()]},
"analyze_import": {"params": [_entry(), _p("json_data", "str")]},
"export_config_selective": {"params": [_entry(), _p("selection", "dict")]},
"import_config_selective": {"params": [
_entry(),
_p("json_data", "str"),
_p("selection", "dict"),
_p("mode", "str", False, enum=["merge", "replace"]),
_p("conflict_resolutions", "dict", False),
_p("cycle_destination", "str", False, enum=["reference", "real_history"]),
_p("apply_settings", "bool", False),
]},
"get_constants": {"params": []},
"get_suggestions": {"params": [_entry()]},
"apply_suggestions": {"params": [_entry(), _p("keys", "list[str]")]},
"clear_suggestions": {"params": [_entry()]},
"run_suggestion_analysis": {"params": [_entry()]},
"get_cycle_power_data": {"params": [_entry(), _p("cycle_id", "str")]},
"trim_cycle": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("start_s", "float"),
_p("end_s", "float"),
]},
"analyze_split": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("gap_seconds", "int", False),
]},
"apply_split": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("split_offsets", "list[float]"),
_p("segment_profiles", "list", False),
]},
"apply_merge": {"params": [
_entry(),
_p("cycle_ids", "list[str]"),
_p("target_profile", "str|null", False),
_p("new_profile_name", "str|null", False),
]},
"get_profile_envelope": {"params": [_entry(), _p("profile_name", "str")]},
"get_profile_cycles": {"params": [
_entry(),
_p("profile_name", "str"),
_p("limit", "int", False),
]},
"get_panel_config": {"params": []},
"set_panel_config": {"params": [
_p("panel", "dict", False),
_p("rbac", "dict", False),
]},
"set_user_prefs": {"params": [_p("prefs", "dict")]},
"get_match_debug": {"params": [_entry()]},
"set_program": {"params": [_entry(), _p("program", "str|null")]},
"get_power_history": {"params": [_entry(), _p("with_raw", "bool", False)]},
"get_logs": {"params": [
_p("level", "str|null", False),
_p("limit", "int", False),
]},
"get_ml_comparison": {"params": [_entry()]},
"get_ml_training_status": {"params": [_entry()]},
"trigger_ml_training": {"params": [_entry()]},
"revert_matching_config": {"params": [_entry()]},
"revert_ml_models": {"params": [_entry()]},
"set_ml_review": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("quality", "str", False, enum=["", "bad", "good", "unusable"]),
_p("golden", "bool", False),
_p("tags", "list[str]", False),
_p("notes", "str", False),
]},
"pause_cycle": {"params": [_entry()]},
"resume_cycle": {"params": [_entry()]},
"terminate_cycle": {"params": [_entry()]},
"run_playground_cycle_detail": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("settings_override", "dict", False),
]},
"run_playground_history": {"params": [
_entry(),
_p("cycle_ids", "list[str]", False),
_p("settings_override", "dict", False),
_p("concurrency", "int", False),
]},
"run_playground_sweep": {"params": [
_entry(),
_p("param", "str"),
_p("values", "list[float]"),
_p("objective", "str"),
_p("cycle_ids", "list[str]", False),
_p("concurrency", "int", False),
_p("param_y", "str", False),
_p("values_y", "list[float]", False),
]},
"get_dtw_debug": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("profile_name", "str|null", False),
]},
"list_tasks": {"params": [_p("entry_id", "str|null", False)]},
"subscribe_tasks": {"params": [_p("entry_id", "str|null", False)]},
"cancel_task": {"params": [_p("task_id", "str")]},
"get_task_result": {"params": [_p("task_id", "str")]},
"start_playground_history": {"params": [
_entry(),
_p("cycle_ids", "list[str]", False),
_p("settings_override", "dict", False),
]},
"start_playground_sweep": {"params": [
_entry(),
_p("param", "str"),
_p("values", "list[float]"),
_p("objective", "str"),
_p("param_y", "str|null", False),
_p("values_y", "list[float]", False),
]},
"start_playground_cycle_detail": {"params": [
_entry(),
_p("cycle_id", "str"),
_p("settings_override", "dict", False),
_p("stress_tail", "bool", False),
_p("stress_idle_w", "float|null", False),
]},
# Community store (online features)
"store_status": {"params": [_entry()]},
"store_connect": {"params": [
_entry(), _p("refresh_token", "str"), _p("uid", "str"), _p("name", "str|null", False),
]},
"store_disconnect": {"params": [_entry()]},
"store_search_devices": {"params": [
_entry(), _p("query", "str|null", False), _p("appliance_type", "str|null", False),
_p("model_query", "str|null", False), _p("include_pending", "bool", False),
]},
"store_list_brands": {"params": [
_entry(), _p("query", "str|null", False), _p("include_pending", "bool", False),
]},
"store_get_profiles": {"params": [_entry(), _p("device_id", "str")]},
"store_get_cycles": {"params": [_entry(), _p("profile_id", "str")]},
"store_get_device_quality": {"params": [_entry(), _p("device_id", "str")]},
"store_get_device_profiles": {"params": [_entry(), _p("brand", "str"), _p("model", "str"), _p("appliance_type", "str")]},
"store_confirm_device": {"params": [_entry(), _p("device_id", "str")]},
"store_rate_device": {"params": [_entry(), _p("device_id", "str"), _p("rating", "int")]},
"store_set_online": {"params": [_entry(), _p("enabled", "bool")]},
"store_set_prefs": {"params": [_entry(), _p("prefs", "dict")]},
"store_import_cycle": {"params": [
_entry(), _p("cycle_id", "str"),
_p("target_profile", "str|null", False), _p("new_profile_name", "str|null", False),
]},
"store_upload_cycle": {"params": [
_entry(), _p("local_cycle_id", "str"), _p("program", "str"), _p("description", "str|null", False),
]},
"store_upload_device": {"params": [_entry(), _p("items", "list"), _p("include_phases", "list", False), _p("include_settings", "bool", False)]},
"store_download_device": {"params": [_entry(), _p("device_id", "str"), _p("include_settings", "bool", False)]},
"get_shareable_cycles": {"params": [_entry()]},
}
#: The domain prefix every command name carries on the wire.
WS_PREFIX = "ha_washdata"
def full_command_name(command: str) -> str:
"""``"get_devices"`` -> ``"ha_washdata/get_devices"``."""
return f"{WS_PREFIX}/{command}"
__all__ = [
"WS_COMMANDS",
"WS_RESPONSE_TYPES",
"WS_OPEN_RESPONSES",
"WS_PREFIX",
"full_command_name",
]