Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d72cdcf
Add from_json to BaseModel to reduce duplicated code
mj23000 Sep 15, 2025
ae03d15
Fix Websocket Error model by not requiring translation keys
mj23000 Sep 15, 2025
3954660
Add method to get non-user flows in progress
mj23000 Sep 15, 2025
65e39cc
Add disable/enable config entry methods
mj23000 Sep 15, 2025
d79ff4a
Add ignore flow method
mj23000 Sep 15, 2025
6443a29
Add method to get a filtered list of config entries
mj23000 Sep 15, 2025
90a14ca
Add get entry subentry method
mj23000 Sep 15, 2025
6156007
Add delete subentry method
mj23000 Sep 15, 2025
9292ae2
Fix typing
mj23000 Sep 22, 2025
fce15b6
Add Websocket endpoints to docstrings
mj23000 Sep 24, 2025
9836466
Add config_entries subscribe method
mj23000 Sep 25, 2025
e5101b3
Add testing
mj23000 Sep 25, 2025
c8ec47f
Merge branch 'dev' into add_websocket_config_entries
GrandMoff100 Sep 30, 2025
bfced2f
Add from_json to BaseModel to reduce duplicated code
mj23000 Sep 15, 2025
6ceac43
Add method to get non-user flows in progress
mj23000 Sep 15, 2025
8e4ce7a
Add disable/enable config entry methods
mj23000 Sep 15, 2025
2899499
Add ignore flow method
mj23000 Sep 15, 2025
245d892
Add method to get a filtered list of config entries
mj23000 Sep 15, 2025
0c375a0
Add get entry subentry method
mj23000 Sep 15, 2025
d161c74
Add delete subentry method
mj23000 Sep 15, 2025
46148a4
Fix typing
mj23000 Sep 22, 2025
475991e
Add Websocket endpoints to docstrings
mj23000 Sep 24, 2025
723a9d9
Add config_entries subscribe method
mj23000 Sep 25, 2025
171509f
Add testing
mj23000 Sep 25, 2025
d5a73d4
add missing testing.
adamlogan73 Mar 16, 2026
e9d5372
Merge remote-tracking branch 'mj23000/add_websocket_config_entries' i…
adamlogan73 Mar 16, 2026
15f62c9
update ruff commands in github cicd pipeline
adamlogan73 Mar 16, 2026
3812d36
Update Python build_from version and fix websocket client fixture type
GrandMoff100 Mar 20, 2026
84c0ec6
Fix test coverage for logbook and entity history tests
adamlogan73 Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ jobs:
run: |
pip install poetry
poetry install --with styling
- name: Run Ruff
run: poetry run ruff homeassistant_api
- name: Run Ruff format
run: poetry run ruff format homeassistant_api
- name: Run Ruff linting
run: poetry run ruff check homeassistant_api
- name: Run MyPy
run: poetry run mypy homeassistant_api --show-error-codes

Expand Down
8 changes: 5 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ repos:
hooks:
- id: trailing-whitespace
rev: "v4.1.0"
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.209'
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.15.6'
hooks:
- id: ruff
- id: ruff-format
- id: ruff-check
- id: ruff-format
2 changes: 1 addition & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
build:
context: .
args:
BUILD_FROM: "python:3.9"
BUILD_FROM: "python:3.13"
image: homeassistant-tests:latest
volumes:
- ./volumes/coverage:/app/coverage:rw
Expand Down
28 changes: 28 additions & 0 deletions homeassistant_api/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
"""The Model objects for the entire library."""

from .base import BaseModel
from .config_entries import (
ConfigEntry,
ConfigEntryChange,
ConfigEntryDisabler,
ConfigEntryEvent,
ConfigEntryState,
ConfigFlowContext,
ConfigSubEntry,
DisableEnableResult,
DiscoveryKey,
FlowContext,
FlowResult,
FlowResultType,
IntegrationTypes,
)
from .domains import Domain, Service, ServiceField
from .entity import Entity, Group
from .events import Event
Expand All @@ -21,4 +36,17 @@
"History",
"LogbookEntry",
"State",
"DisableEnableResult",
"DiscoveryKey",
"FlowContext",
"ConfigFlowContext",
"FlowResult",
"FlowResultType",
"IntegrationTypes",
"ConfigEntryDisabler",
"ConfigEntryState",
"ConfigEntry",
"ConfigSubEntry",
"ConfigEntryChange",
"ConfigEntryEvent",
)
11 changes: 10 additions & 1 deletion homeassistant_api/models/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Module for Global Base Model Configuration inheritance."""

from datetime import datetime
from typing import Annotated
from typing import Annotated, Any, Union

from pydantic import BaseModel as PydanticBaseModel
from pydantic import ConfigDict, PlainSerializer
from typing_extensions import Self

from homeassistant_api.utils import JSONType

__all__ = (
"BaseModel",
Expand All @@ -25,3 +28,9 @@ class BaseModel(PydanticBaseModel):
validate_assignment=True,
protected_namespaces=(),
)

# TODO: Any being accepted is not ideal. Narrow it down.
@classmethod
def from_json(cls, json: Union[dict[str, JSONType], Any, None]) -> Self:
"""Constructs Self model from json data"""
return cls.model_validate(json)
158 changes: 158 additions & 0 deletions homeassistant_api/models/config_entries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""File for models used in responses from config entries."""

import asyncio
from enum import Enum
from typing import Any, Container, Dict, Optional, Tuple, Union

from .base import BaseModel


class FlowResultType(Enum):
"""Result type for a data entry flow."""

FORM = "form"
CREATE_ENTRY = "create_entry"
ABORT = "abort"
EXTERNAL_STEP = "external"
EXTERNAL_STEP_DONE = "external_done"
SHOW_PROGRESS = "progress"
SHOW_PROGRESS_DONE = "progress_done"
MENU = "menu"


class DiscoveryKey(BaseModel):
"""Serializable discovery key."""

domain: str
key: Union[str, Tuple[str, ...]]
version: int


class FlowContext(BaseModel):
"""Base flow context"""

show_advanced_options: Union[bool, None] = None
source: str


class ConfigFlowContext(FlowContext):
"""Context for config flow."""

alternative_domain: Optional[str] = None
configuration_url: Optional[str] = None
confirm_only: Optional[bool] = None
discovery_key: DiscoveryKey
entry_id: Optional[str] = None
title_placeholders: Optional[Dict[str, str]] = None
unique_id: Optional[str] = None


class FlowResult(BaseModel):
"""Base flow result ."""

context: ConfigFlowContext
data_schema: Optional[Any] = None
data: Optional[Dict[str, Any]] = None
description_placeholders: Optional[Dict[str, str]] = None
description: Optional[str] = None
errors: Optional[Dict[str, str]] = None
extra: Optional[str] = None
flow_id: str
handler: str
last_step: Optional[bool] = None
menu_options: Optional[Container[str]] = None
preview: Optional[str] = None
progress_action: Optional[str] = None
progress_task: Optional[asyncio.Task[Any]] = None
reason: Optional[str] = None
required: Optional[bool] = None
result: Optional[Any] = None
step_id: Optional[str] = None
title: Optional[str] = None
translation_domain: Optional[str] = None
type: Optional[FlowResultType] = None
url: Optional[str] = None


class DisableEnableResult(BaseModel):
"""Result from a disable/enable config entry call."""

require_restart: bool


class IntegrationTypes(Enum):
"""Types of integrations."""

ENTITY = "entity"
DEVICE = "device"
HARDWARE = "hardware"
HELPER = "helper"
HUB = "hub"
SERVICE = "service"
SYSTEM = "system"
VIRTUAL = "virtual"


class ConfigEntryState(str, Enum):
"""Config entry state."""

LOADED = "loaded"
SETUP_ERROR = "setup_error"
MIGRATION_ERROR = "migration_error"
SETUP_RETRY = "setup_retry"
NOT_LOADED = "not_loaded"
FAILED_UNLOAD = "failed_unload"
SETUP_IN_PROGRESS = "setup_in_progress"
UNLOAD_IN_PROGRESS = "unload_in_progress"


class ConfigEntryDisabler(Enum):
"""What disabled a config entry."""

USER = "user"


class ConfigEntry(BaseModel):
"""A configuration entry. This is the model that Home Assistant returns, but not what is used internally."""

created_at: float
entry_id: str
domain: str
modified_at: float
title: str
source: str
state: ConfigEntryState
supports_options: bool
supports_remove_device: bool
supports_unload: bool
supports_reconfigure: bool
supported_subentry_types: Dict[str, Dict[str, bool]]
pref_disable_new_entities: bool
pref_disable_polling: bool
disabled_by: Optional[ConfigEntryDisabler]
reason: Optional[str]
error_reason_translation_key: Optional[str]
error_reason_translation_placeholders: Optional[Dict[str, Any]]
num_subentries: int


class ConfigSubEntry(BaseModel):
"""A configuration sub-entry. This is the model that Home Assistant returns, but not what is used internally."""

subentry_id: str
subentry_type: str
title: str
unique_id: Optional[str]


class ConfigEntryChange(str, Enum):
"""What was changed in a config entry."""

ADDED = "added"
REMOVED = "removed"
UPDATED = "updated"


class ConfigEntryEvent(BaseModel):
type: Optional[ConfigEntryChange]
entry: ConfigEntry
24 changes: 18 additions & 6 deletions homeassistant_api/models/domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)

from pydantic import Field
from typing_extensions import Self, override

from homeassistant_api.errors import RequestError
from homeassistant_api.utils import JSONType
Expand Down Expand Up @@ -55,7 +56,14 @@ def __init__(
)

@classmethod
def from_json(
@override
def from_json(cls, json: Union[dict[str, JSONType], Any, None], **kwargs) -> Self:
raise ValueError(
f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`"
)

@classmethod
def from_json_with_client(
cls, json: Dict[str, JSONType], client: Union["Client", "WebsocketClient"]
) -> "Domain":
"""Constructs Domain and Service models from json data."""
Expand Down Expand Up @@ -375,9 +383,9 @@ class ServiceFieldSelectorObject(BaseModel):
class ServiceFieldSelectorQRCode(BaseModel):
data: str
scale: Optional[Union[int, float]] = None
error_correction_level: Optional[ServiceFieldSelectorQRCodeErrorCorrectionLevel] = (
None
)
error_correction_level: Optional[
ServiceFieldSelectorQRCodeErrorCorrectionLevel
] = None
center_image: Optional[str] = None


Expand Down Expand Up @@ -582,7 +590,9 @@ class Service(BaseModel):
target: Optional[ServiceFieldSelectorTarget] = None
response: Optional[ServiceResponse] = None

def trigger(self, **service_data) -> Union[
def trigger(
self, **service_data
) -> Union[
Tuple[State, ...],
Tuple[Tuple[State, ...], dict[str, JSONType]],
dict[str, JSONType],
Expand Down Expand Up @@ -625,7 +635,9 @@ async def async_trigger(
**service_data,
)

def __call__(self, **service_data) -> Union[
def __call__(
self, **service_data
) -> Union[
Union[
Tuple[State, ...],
Tuple[Tuple[State, ...], dict[str, JSONType]],
Expand Down
16 changes: 13 additions & 3 deletions homeassistant_api/models/events.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Event Model File"""

from typing import TYPE_CHECKING, Optional

from typing import TYPE_CHECKING, Any, Optional, Union
from typing_extensions import Self
from pydantic import Field
from typing_extensions import override

from homeassistant_api.utils import JSONType

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

@classmethod
def from_json(cls, json: dict[str, JSONType], client: "Client") -> "Event":
@override
def from_json(cls, json: Union[dict[str, JSONType], Any, None], **kwargs) -> Self:
raise ValueError(
f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`"
)

@classmethod
def from_json_with_client(
cls, json: dict[str, JSONType], client: "Client"
) -> "Event":
"""Constructs Event model from json data"""
return cls(**json, _client=client)
10 changes: 0 additions & 10 deletions homeassistant_api/models/states.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ class Context(BaseModel):
description="Unique string identifying the user.",
)

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


class State(BaseModel):
"""A model representing a state of an entity."""
Expand All @@ -61,8 +56,3 @@ class State(BaseModel):
context: Optional[Context] = Field(
None, description="Provides information about the context of the state."
)

@classmethod
def from_json(cls, json: dict[str, JSONType]) -> "State":
"""Constructs State model from json data"""
return cls.model_validate(json)
5 changes: 3 additions & 2 deletions homeassistant_api/models/websocket.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""A module defining the responses we expect from the websocket API."""

from typing import Any, Literal, Optional, Union
from typing import Any, List, Literal, Optional, Union

from homeassistant_api.utils import JSONType

from .base import BaseModel, DatetimeIsoField
from .config_entries import ConfigEntryEvent
from .states import Context

__all__ = (
Expand Down Expand Up @@ -99,4 +100,4 @@ class EventResponse(BaseModel):

id: int
type: Literal["event"]
event: Union[FiredEvent, FiredTrigger, TemplateEvent]
event: Union[FiredEvent, FiredTrigger, TemplateEvent, List[ConfigEntryEvent]]
Loading
Loading