Skip to content

Commit 67ac95c

Browse files
mj23000GrandMoff100adamlogan73
authored
Add Websocket "config entries" methods (#213)
* Add from_json to BaseModel to reduce duplicated code * Fix Websocket Error model by not requiring translation keys * Add method to get non-user flows in progress * Add disable/enable config entry methods * Add ignore flow method * Add method to get a filtered list of config entries * Add get entry subentry method * Add delete subentry method * Fix typing * Add Websocket endpoints to docstrings * Add config_entries subscribe method * Add testing * Add from_json to BaseModel to reduce duplicated code * Add method to get non-user flows in progress * Add disable/enable config entry methods * Add ignore flow method * Add method to get a filtered list of config entries * Add get entry subentry method * Add delete subentry method * Fix typing * Add Websocket endpoints to docstrings * Add config_entries subscribe method * Add testing * add missing testing. Some updates to dependencies and conftest were also required. Updated ruff version did formatting slightly different in a few files. * update ruff commands in github cicd pipeline * Update Python build_from version and fix websocket client fixture type * Fix test coverage for logbook and entity history tests Use sun.red_sun entity for logbook tests (sun.sun has no logbook entries). Replace for/else raise pattern with direct assertions in history tests. --------- Co-authored-by: Nathan Larsen <minecraftcrusher100@gmail.com> Co-authored-by: Adam Logan <adamlogan73@gmail.com> Co-authored-by: Nate <nlarsen23.student@gmail.com>
1 parent c451858 commit 67ac95c

24 files changed

Lines changed: 1977 additions & 1060 deletions

.github/workflows/test-suite.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ jobs:
3232
run: |
3333
pip install poetry
3434
poetry install --with styling
35-
- name: Run Ruff
36-
run: poetry run ruff homeassistant_api
35+
- name: Run Ruff format
36+
run: poetry run ruff format homeassistant_api
37+
- name: Run Ruff linting
38+
run: poetry run ruff check homeassistant_api
3739
- name: Run MyPy
3840
run: poetry run mypy homeassistant_api --show-error-codes
3941

.pre-commit-config.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ repos:
1010
hooks:
1111
- id: trailing-whitespace
1212
rev: "v4.1.0"
13-
- repo: https://github.com/charliermarsh/ruff-pre-commit
14-
rev: 'v0.0.209'
13+
- repo: https://github.com/astral-sh/ruff-pre-commit
14+
rev: 'v0.15.6'
1515
hooks:
16-
- id: ruff
16+
- id: ruff-format
17+
- id: ruff-check
18+
- id: ruff-format

compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
build:
1010
context: .
1111
args:
12-
BUILD_FROM: "python:3.9"
12+
BUILD_FROM: "python:3.13"
1313
image: homeassistant-tests:latest
1414
volumes:
1515
- ./volumes/coverage:/app/coverage:rw

homeassistant_api/models/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
"""The Model objects for the entire library."""
22

33
from .base import BaseModel
4+
from .config_entries import (
5+
ConfigEntry,
6+
ConfigEntryChange,
7+
ConfigEntryDisabler,
8+
ConfigEntryEvent,
9+
ConfigEntryState,
10+
ConfigFlowContext,
11+
ConfigSubEntry,
12+
DisableEnableResult,
13+
DiscoveryKey,
14+
FlowContext,
15+
FlowResult,
16+
FlowResultType,
17+
IntegrationTypes,
18+
)
419
from .domains import Domain, Service, ServiceField
520
from .entity import Entity, Group
621
from .events import Event
@@ -21,4 +36,17 @@
2136
"History",
2237
"LogbookEntry",
2338
"State",
39+
"DisableEnableResult",
40+
"DiscoveryKey",
41+
"FlowContext",
42+
"ConfigFlowContext",
43+
"FlowResult",
44+
"FlowResultType",
45+
"IntegrationTypes",
46+
"ConfigEntryDisabler",
47+
"ConfigEntryState",
48+
"ConfigEntry",
49+
"ConfigSubEntry",
50+
"ConfigEntryChange",
51+
"ConfigEntryEvent",
2452
)

homeassistant_api/models/base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Module for Global Base Model Configuration inheritance."""
22

33
from datetime import datetime
4-
from typing import Annotated
4+
from typing import Annotated, Any, Union
55

66
from pydantic import BaseModel as PydanticBaseModel
77
from pydantic import ConfigDict, PlainSerializer
8+
from typing_extensions import Self
9+
10+
from homeassistant_api.utils import JSONType
811

912
__all__ = (
1013
"BaseModel",
@@ -25,3 +28,9 @@ class BaseModel(PydanticBaseModel):
2528
validate_assignment=True,
2629
protected_namespaces=(),
2730
)
31+
32+
# TODO: Any being accepted is not ideal. Narrow it down.
33+
@classmethod
34+
def from_json(cls, json: Union[dict[str, JSONType], Any, None]) -> Self:
35+
"""Constructs Self model from json data"""
36+
return cls.model_validate(json)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""File for models used in responses from config entries."""
2+
3+
import asyncio
4+
from enum import Enum
5+
from typing import Any, Container, Dict, Optional, Tuple, Union
6+
7+
from .base import BaseModel
8+
9+
10+
class FlowResultType(Enum):
11+
"""Result type for a data entry flow."""
12+
13+
FORM = "form"
14+
CREATE_ENTRY = "create_entry"
15+
ABORT = "abort"
16+
EXTERNAL_STEP = "external"
17+
EXTERNAL_STEP_DONE = "external_done"
18+
SHOW_PROGRESS = "progress"
19+
SHOW_PROGRESS_DONE = "progress_done"
20+
MENU = "menu"
21+
22+
23+
class DiscoveryKey(BaseModel):
24+
"""Serializable discovery key."""
25+
26+
domain: str
27+
key: Union[str, Tuple[str, ...]]
28+
version: int
29+
30+
31+
class FlowContext(BaseModel):
32+
"""Base flow context"""
33+
34+
show_advanced_options: Union[bool, None] = None
35+
source: str
36+
37+
38+
class ConfigFlowContext(FlowContext):
39+
"""Context for config flow."""
40+
41+
alternative_domain: Optional[str] = None
42+
configuration_url: Optional[str] = None
43+
confirm_only: Optional[bool] = None
44+
discovery_key: DiscoveryKey
45+
entry_id: Optional[str] = None
46+
title_placeholders: Optional[Dict[str, str]] = None
47+
unique_id: Optional[str] = None
48+
49+
50+
class FlowResult(BaseModel):
51+
"""Base flow result ."""
52+
53+
context: ConfigFlowContext
54+
data_schema: Optional[Any] = None
55+
data: Optional[Dict[str, Any]] = None
56+
description_placeholders: Optional[Dict[str, str]] = None
57+
description: Optional[str] = None
58+
errors: Optional[Dict[str, str]] = None
59+
extra: Optional[str] = None
60+
flow_id: str
61+
handler: str
62+
last_step: Optional[bool] = None
63+
menu_options: Optional[Container[str]] = None
64+
preview: Optional[str] = None
65+
progress_action: Optional[str] = None
66+
progress_task: Optional[asyncio.Task[Any]] = None
67+
reason: Optional[str] = None
68+
required: Optional[bool] = None
69+
result: Optional[Any] = None
70+
step_id: Optional[str] = None
71+
title: Optional[str] = None
72+
translation_domain: Optional[str] = None
73+
type: Optional[FlowResultType] = None
74+
url: Optional[str] = None
75+
76+
77+
class DisableEnableResult(BaseModel):
78+
"""Result from a disable/enable config entry call."""
79+
80+
require_restart: bool
81+
82+
83+
class IntegrationTypes(Enum):
84+
"""Types of integrations."""
85+
86+
ENTITY = "entity"
87+
DEVICE = "device"
88+
HARDWARE = "hardware"
89+
HELPER = "helper"
90+
HUB = "hub"
91+
SERVICE = "service"
92+
SYSTEM = "system"
93+
VIRTUAL = "virtual"
94+
95+
96+
class ConfigEntryState(str, Enum):
97+
"""Config entry state."""
98+
99+
LOADED = "loaded"
100+
SETUP_ERROR = "setup_error"
101+
MIGRATION_ERROR = "migration_error"
102+
SETUP_RETRY = "setup_retry"
103+
NOT_LOADED = "not_loaded"
104+
FAILED_UNLOAD = "failed_unload"
105+
SETUP_IN_PROGRESS = "setup_in_progress"
106+
UNLOAD_IN_PROGRESS = "unload_in_progress"
107+
108+
109+
class ConfigEntryDisabler(Enum):
110+
"""What disabled a config entry."""
111+
112+
USER = "user"
113+
114+
115+
class ConfigEntry(BaseModel):
116+
"""A configuration entry. This is the model that Home Assistant returns, but not what is used internally."""
117+
118+
created_at: float
119+
entry_id: str
120+
domain: str
121+
modified_at: float
122+
title: str
123+
source: str
124+
state: ConfigEntryState
125+
supports_options: bool
126+
supports_remove_device: bool
127+
supports_unload: bool
128+
supports_reconfigure: bool
129+
supported_subentry_types: Dict[str, Dict[str, bool]]
130+
pref_disable_new_entities: bool
131+
pref_disable_polling: bool
132+
disabled_by: Optional[ConfigEntryDisabler]
133+
reason: Optional[str]
134+
error_reason_translation_key: Optional[str]
135+
error_reason_translation_placeholders: Optional[Dict[str, Any]]
136+
num_subentries: int
137+
138+
139+
class ConfigSubEntry(BaseModel):
140+
"""A configuration sub-entry. This is the model that Home Assistant returns, but not what is used internally."""
141+
142+
subentry_id: str
143+
subentry_type: str
144+
title: str
145+
unique_id: Optional[str]
146+
147+
148+
class ConfigEntryChange(str, Enum):
149+
"""What was changed in a config entry."""
150+
151+
ADDED = "added"
152+
REMOVED = "removed"
153+
UPDATED = "updated"
154+
155+
156+
class ConfigEntryEvent(BaseModel):
157+
type: Optional[ConfigEntryChange]
158+
entry: ConfigEntry

homeassistant_api/models/domains.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919

2020
from pydantic import Field
21+
from typing_extensions import Self, override
2122

2223
from homeassistant_api.errors import RequestError
2324
from homeassistant_api.utils import JSONType
@@ -55,7 +56,14 @@ def __init__(
5556
)
5657

5758
@classmethod
58-
def from_json(
59+
@override
60+
def from_json(cls, json: Union[dict[str, JSONType], Any, None], **kwargs) -> Self:
61+
raise ValueError(
62+
f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`"
63+
)
64+
65+
@classmethod
66+
def from_json_with_client(
5967
cls, json: Dict[str, JSONType], client: Union["Client", "WebsocketClient"]
6068
) -> "Domain":
6169
"""Constructs Domain and Service models from json data."""
@@ -375,9 +383,9 @@ class ServiceFieldSelectorObject(BaseModel):
375383
class ServiceFieldSelectorQRCode(BaseModel):
376384
data: str
377385
scale: Optional[Union[int, float]] = None
378-
error_correction_level: Optional[ServiceFieldSelectorQRCodeErrorCorrectionLevel] = (
379-
None
380-
)
386+
error_correction_level: Optional[
387+
ServiceFieldSelectorQRCodeErrorCorrectionLevel
388+
] = None
381389
center_image: Optional[str] = None
382390

383391

@@ -582,7 +590,9 @@ class Service(BaseModel):
582590
target: Optional[ServiceFieldSelectorTarget] = None
583591
response: Optional[ServiceResponse] = None
584592

585-
def trigger(self, **service_data) -> Union[
593+
def trigger(
594+
self, **service_data
595+
) -> Union[
586596
Tuple[State, ...],
587597
Tuple[Tuple[State, ...], dict[str, JSONType]],
588598
dict[str, JSONType],
@@ -625,7 +635,9 @@ async def async_trigger(
625635
**service_data,
626636
)
627637

628-
def __call__(self, **service_data) -> Union[
638+
def __call__(
639+
self, **service_data
640+
) -> Union[
629641
Union[
630642
Tuple[State, ...],
631643
Tuple[Tuple[State, ...], dict[str, JSONType]],

homeassistant_api/models/events.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Event Model File"""
22

3-
from typing import TYPE_CHECKING, Optional
4-
3+
from typing import TYPE_CHECKING, Any, Optional, Union
4+
from typing_extensions import Self
55
from pydantic import Field
6+
from typing_extensions import override
67

78
from homeassistant_api.utils import JSONType
89

@@ -40,6 +41,15 @@ async def async_fire(self, **event_data) -> str:
4041
return await self._client.async_fire_event(self.event, **event_data)
4142

4243
@classmethod
43-
def from_json(cls, json: dict[str, JSONType], client: "Client") -> "Event":
44+
@override
45+
def from_json(cls, json: Union[dict[str, JSONType], Any, None], **kwargs) -> Self:
46+
raise ValueError(
47+
f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`"
48+
)
49+
50+
@classmethod
51+
def from_json_with_client(
52+
cls, json: dict[str, JSONType], client: "Client"
53+
) -> "Event":
4454
"""Constructs Event model from json data"""
4555
return cls(**json, _client=client)

homeassistant_api/models/states.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ class Context(BaseModel):
3030
description="Unique string identifying the user.",
3131
)
3232

33-
@classmethod
34-
def from_json(cls, json: dict[str, JSONType]) -> "Context":
35-
"""Constructs Context model from json data"""
36-
return cls.model_validate(json)
37-
3833

3934
class State(BaseModel):
4035
"""A model representing a state of an entity."""
@@ -61,8 +56,3 @@ class State(BaseModel):
6156
context: Optional[Context] = Field(
6257
None, description="Provides information about the context of the state."
6358
)
64-
65-
@classmethod
66-
def from_json(cls, json: dict[str, JSONType]) -> "State":
67-
"""Constructs State model from json data"""
68-
return cls.model_validate(json)

homeassistant_api/models/websocket.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""A module defining the responses we expect from the websocket API."""
22

3-
from typing import Any, Literal, Optional, Union
3+
from typing import Any, List, Literal, Optional, Union
44

55
from homeassistant_api.utils import JSONType
66

77
from .base import BaseModel, DatetimeIsoField
8+
from .config_entries import ConfigEntryEvent
89
from .states import Context
910

1011
__all__ = (
@@ -99,4 +100,4 @@ class EventResponse(BaseModel):
99100

100101
id: int
101102
type: Literal["event"]
102-
event: Union[FiredEvent, FiredTrigger, TemplateEvent]
103+
event: Union[FiredEvent, FiredTrigger, TemplateEvent, List[ConfigEntryEvent]]

0 commit comments

Comments
 (0)