HomeAssistant-API 4.2.2.post1__tar.gz → 5.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/PKG-INFO +48 -16
  2. homeassistant_api-5.0.0/README.md +78 -0
  3. homeassistant_api-5.0.0/homeassistant_api/__init__.py +48 -0
  4. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/client.py +17 -5
  5. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/errors.py +12 -4
  6. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/__init__.py +1 -0
  7. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/base.py +5 -3
  8. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/domains.py +82 -26
  9. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/history.py +1 -0
  10. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/logbook.py +1 -0
  11. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/states.py +15 -1
  12. homeassistant_api-5.0.0/homeassistant_api/models/websocket.py +97 -0
  13. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/rawasyncclient.py +45 -12
  14. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/rawbaseclient.py +9 -39
  15. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/rawclient.py +40 -9
  16. homeassistant_api-5.0.0/homeassistant_api/rawwebsocket.py +199 -0
  17. homeassistant_api-5.0.0/homeassistant_api/utils.py +32 -0
  18. homeassistant_api-5.0.0/homeassistant_api/websocket.py +378 -0
  19. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/pyproject.toml +4 -3
  20. homeassistant_api-4.2.2.post1/README.md +0 -47
  21. homeassistant_api-4.2.2.post1/homeassistant_api/__init__.py +0 -47
  22. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/LICENSE +0 -0
  23. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/entity.py +0 -0
  24. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/events.py +0 -0
  25. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/processing.py +0 -0
  26. {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/py.typed +0 -0
@@ -1,61 +1,93 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: HomeAssistant-API
3
- Version: 4.2.2.post1
3
+ Version: 5.0.0
4
4
  Summary: Python Wrapper for Homeassistant's REST API
5
- Home-page: https://github.com/GrandMoff100/HomeAssistantAPI
6
5
  License: GPL-3.0-or-later
7
6
  Author: GrandMoff100
8
7
  Author-email: minecraftcrusher100@gmail.com
9
- Requires-Python: >=3.8,<4.0
8
+ Requires-Python: >=3.9,<4.0
10
9
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
10
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.8
13
11
  Classifier: Programming Language :: Python :: 3.9
14
12
  Classifier: Programming Language :: Python :: 3.10
15
13
  Classifier: Programming Language :: Python :: 3.11
16
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
17
16
  Requires-Dist: aiohttp (>=3.8.1,<4.0.0)
18
17
  Requires-Dist: aiohttp-client-cache (>=0.6.1)
19
- Requires-Dist: pydantic (>=2.0)
18
+ Requires-Dist: pydantic (>=2.0,<2.9)
20
19
  Requires-Dist: requests (>=2.27.1,<3.0.0)
21
20
  Requires-Dist: requests-cache (>=0.9.2,<0.10.0)
22
21
  Requires-Dist: simplejson (>=3.17.6,<4.0.0)
22
+ Requires-Dist: websockets (>=14.1,<15.0)
23
23
  Project-URL: Documentation, https://homeassistantapi.readthedocs.io
24
+ Project-URL: Homepage, https://github.com/GrandMoff100/HomeAssistantAPI
24
25
  Project-URL: Repository, https://github.com/GrandMoff100/HomeAssistantAPI
25
26
  Description-Content-Type: text/markdown
26
27
 
27
28
  # HomeassistantAPI
28
29
 
29
30
  [![Code Coverage](https://img.shields.io/codecov/c/github/GrandMoff100/HomeAssistantAPI/dev?style=for-the-badge&token=SJFC3HX5R1)](https://codecov.io/gh/GrandMoff100/HomeAssistantAPI)
30
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/HomeAssistant-API?style=for-the-badge)](https://pypi.org/project/homeassistant_api)
31
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/HomeAssistant-API?style=for-the-badge)](https://pypistats.org/packages/homeassistant-api)
31
32
  ![GitHub commits since latest release (by date including pre-releases)](https://img.shields.io/github/commits-since/GrandMoff100/HomeassistantAPI/latest/dev?include_prereleases&style=for-the-badge)
32
33
  [![Read the Docs (version)](https://img.shields.io/readthedocs/homeassistantapi?style=for-the-badge)](https://homeassistantapi.readthedocs.io/en/latest/?badge=latest)
33
34
  [![GitHub release (latest by date)](https://img.shields.io/github/v/release/GrandMoff100/HomeassistantAPI?style=for-the-badge)](https://github.com/GrandMoff100/HomeassistantAPI/releases)
34
35
 
35
36
  <a href="https://home-assistant.io">
36
- <img src="https://github.com/GrandMoff100/HomeAssistantAPI/blob/7edb4e6298d37bda19c08b807613c6d351788491/docs/images/homeassistant-logo.png?raw=true" width="60%">
37
+ <img src="https://github.com/GrandMoff100/HomeAssistantAPI/blob/7edb4e6298d37bda19c08b807613c6d351788491/docs/images/homeassistant-logo.png?raw=true" width="80%">
37
38
  </a>
38
39
 
39
- ## Python wrapper for Homeassistant's [REST API](https://developers.home-assistant.io/docs/api/rest/)
40
+ ## Python wrapper for Homeassistant's [Websocket API](https://developers.home-assistant.io/docs/api/websocket/) and [REST API](https://developers.home-assistant.io/docs/api/rest/)
40
41
 
41
- Here is a quick example.
42
+ > Note: As of [this comment](https://github.com/home-assistant/architecture/discussions/1074#discussioncomment-9196867) the REST API is not getting any new features or endpoints.
43
+ > However, it is not going to be deprecated according to [this comment](https://github.com/home-assistant/developers.home-assistant/pull/2150#pullrequestreview-2017433583)
44
+ > But it is recommended to use the Websocket API for new integrations.
45
+
46
+ ### REST API Examples
42
47
 
43
48
  ```py
44
49
  from homeassistant_api import Client
45
50
 
46
51
  with Client(
47
- '<API Server URL>',
52
+ '<API Server URL>', # i.e. 'http://homeassistant.local:8123/api/'
48
53
  '<Your Long Lived Access-Token>'
49
54
  ) as client:
55
+ light = client.trigger_service('light', 'turn_on', entity_id="light.living_room")
56
+ ```
57
+
58
+ All the methods also support async/await!
59
+ Just prefix the method with `async_` and pass the `use_async=True` argument to the `Client` constructor.
60
+ Then you can use the methods as coroutines
61
+ (i.e. `await light.async_turn_on(...)`).
62
+
63
+ ```py
64
+ import asyncio
65
+ from homeassistant_api import Client
50
66
 
51
- light = client.get_domain("light")
67
+ async def main():
68
+ with Client(
69
+ '<REST API Server URL>', # i.e. 'http://homeassistant.local:8123/api/'
70
+ '<Your Long Lived Access-Token>',
71
+ use_async=True
72
+ ) as client:
73
+ light = await client.async_trigger_service('light', 'turn_on', entity_id="light.living_room")
52
74
 
53
- light.turn_on(entity_id="light.living_room_lamp")
75
+ asyncio.run(main())
54
76
  ```
55
77
 
56
- All the methods also support async!
57
- Just prefix the method with `async_`
58
- (i.e. `await light.async_turn_on(...)`).
78
+ ### Websocket API Example
79
+
80
+ ```py
81
+ from homeassistant_api import WebsocketClient
82
+
83
+ with WebsocketClient(
84
+ '<WS API Server URL>', # i.e. 'ws://homeassistant.local:8123/api/websocket'
85
+ '<Your Long Lived Access-Token>'
86
+ ) as ws_client:
87
+ light = ws_client.trigger_service('light', 'turn_on', entity_id="light.living_room")
88
+ ```
89
+
90
+ > Note: The Websocket API is not yet supported in async/await mode.
59
91
 
60
92
  ## Documentation
61
93
 
@@ -0,0 +1,78 @@
1
+ # HomeassistantAPI
2
+
3
+ [![Code Coverage](https://img.shields.io/codecov/c/github/GrandMoff100/HomeAssistantAPI/dev?style=for-the-badge&token=SJFC3HX5R1)](https://codecov.io/gh/GrandMoff100/HomeAssistantAPI)
4
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/HomeAssistant-API?style=for-the-badge)](https://pypistats.org/packages/homeassistant-api)
5
+ ![GitHub commits since latest release (by date including pre-releases)](https://img.shields.io/github/commits-since/GrandMoff100/HomeassistantAPI/latest/dev?include_prereleases&style=for-the-badge)
6
+ [![Read the Docs (version)](https://img.shields.io/readthedocs/homeassistantapi?style=for-the-badge)](https://homeassistantapi.readthedocs.io/en/latest/?badge=latest)
7
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/GrandMoff100/HomeassistantAPI?style=for-the-badge)](https://github.com/GrandMoff100/HomeassistantAPI/releases)
8
+
9
+ <a href="https://home-assistant.io">
10
+ <img src="https://github.com/GrandMoff100/HomeAssistantAPI/blob/7edb4e6298d37bda19c08b807613c6d351788491/docs/images/homeassistant-logo.png?raw=true" width="80%">
11
+ </a>
12
+
13
+ ## Python wrapper for Homeassistant's [Websocket API](https://developers.home-assistant.io/docs/api/websocket/) and [REST API](https://developers.home-assistant.io/docs/api/rest/)
14
+
15
+ > Note: As of [this comment](https://github.com/home-assistant/architecture/discussions/1074#discussioncomment-9196867) the REST API is not getting any new features or endpoints.
16
+ > However, it is not going to be deprecated according to [this comment](https://github.com/home-assistant/developers.home-assistant/pull/2150#pullrequestreview-2017433583)
17
+ > But it is recommended to use the Websocket API for new integrations.
18
+
19
+ ### REST API Examples
20
+
21
+ ```py
22
+ from homeassistant_api import Client
23
+
24
+ with Client(
25
+ '<API Server URL>', # i.e. 'http://homeassistant.local:8123/api/'
26
+ '<Your Long Lived Access-Token>'
27
+ ) as client:
28
+ light = client.trigger_service('light', 'turn_on', entity_id="light.living_room")
29
+ ```
30
+
31
+ All the methods also support async/await!
32
+ Just prefix the method with `async_` and pass the `use_async=True` argument to the `Client` constructor.
33
+ Then you can use the methods as coroutines
34
+ (i.e. `await light.async_turn_on(...)`).
35
+
36
+ ```py
37
+ import asyncio
38
+ from homeassistant_api import Client
39
+
40
+ async def main():
41
+ with Client(
42
+ '<REST API Server URL>', # i.e. 'http://homeassistant.local:8123/api/'
43
+ '<Your Long Lived Access-Token>',
44
+ use_async=True
45
+ ) as client:
46
+ light = await client.async_trigger_service('light', 'turn_on', entity_id="light.living_room")
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ ### Websocket API Example
52
+
53
+ ```py
54
+ from homeassistant_api import WebsocketClient
55
+
56
+ with WebsocketClient(
57
+ '<WS API Server URL>', # i.e. 'ws://homeassistant.local:8123/api/websocket'
58
+ '<Your Long Lived Access-Token>'
59
+ ) as ws_client:
60
+ light = ws_client.trigger_service('light', 'turn_on', entity_id="light.living_room")
61
+ ```
62
+
63
+ > Note: The Websocket API is not yet supported in async/await mode.
64
+
65
+ ## Documentation
66
+
67
+ All documentation, API reference, contribution guidelines and pretty much everything else
68
+ you'd want to know is on our readthedocs site [here](https://homeassistantapi.readthedocs.io)
69
+
70
+ If there is something missing, open an issue and let us know! Thanks!
71
+
72
+ Go make some cool stuff! Maybe come back and tell us about it in a
73
+ [discussion](https://github.com/GrandMoff100/HomeAssistantAPI/discussions)?
74
+ We'd love to hear about how you use our library!!
75
+
76
+ ## License
77
+
78
+ This project is under the GNU GPLv3 license, as defined by the Free Software Foundation.
@@ -0,0 +1,48 @@
1
+ """Interact with your Homeassistant Instance remotely."""
2
+
3
+ __all__ = (
4
+ "Client",
5
+ "State",
6
+ "Context",
7
+ "Domain",
8
+ "Service",
9
+ "Group",
10
+ "Entity",
11
+ "History",
12
+ "Event",
13
+ "LogbookEntry",
14
+ "WebsocketClient",
15
+ "AuthInvalid",
16
+ "AuthOk",
17
+ "AuthRequired",
18
+ "ResultResponse",
19
+ "ErrorResponse",
20
+ "PingResponse",
21
+ "EventResponse",
22
+ )
23
+
24
+ from .client import Client
25
+ from .models.domains import Domain, Service
26
+ from .models.entity import Entity, Group
27
+ from .models.events import Event
28
+ from .models.history import History
29
+ from .models.logbook import LogbookEntry
30
+ from .models.states import Context, State
31
+ from .models.websocket import (
32
+ AuthInvalid,
33
+ AuthOk,
34
+ AuthRequired,
35
+ ErrorResponse,
36
+ EventResponse,
37
+ PingResponse,
38
+ ResultResponse,
39
+ )
40
+ from .websocket import WebsocketClient
41
+
42
+ Domain.model_rebuild()
43
+ Entity.model_rebuild()
44
+ Event.model_rebuild()
45
+ Group.model_rebuild()
46
+ History.model_rebuild()
47
+ Service.model_rebuild()
48
+ State.model_rebuild()
@@ -1,5 +1,7 @@
1
1
  """Module containing the primary Client class."""
2
+
2
3
  import logging
4
+ import urllib.parse as urlparse
3
5
  from typing import Any
4
6
 
5
7
  from .rawasyncclient import RawAsyncClient
@@ -21,12 +23,22 @@ class Client(RawClient, RawAsyncClient):
21
23
 
22
24
  def __init__(
23
25
  self,
24
- *args: Any,
26
+ api_url: str,
27
+ token: str,
25
28
  use_async: bool = False,
26
29
  verify_ssl: bool = True,
27
- **kwargs: Any
30
+ **kwargs: Any,
28
31
  ) -> None:
29
- if use_async:
30
- RawAsyncClient.__init__(self, *args, verify_ssl=verify_ssl, **kwargs)
32
+ parsed = urlparse.urlparse(api_url)
33
+
34
+ if parsed.scheme in {"http", "https"}:
35
+ if use_async:
36
+ RawAsyncClient.__init__(
37
+ self, api_url, token, verify_ssl=verify_ssl, **kwargs
38
+ )
39
+ else:
40
+ RawClient.__init__(
41
+ self, api_url, token, verify_ssl=verify_ssl, **kwargs
42
+ )
31
43
  else:
32
- RawClient.__init__(self, *args, verify_ssl=verify_ssl, **kwargs)
44
+ raise ValueError(f"Unknown scheme {parsed.scheme} in {api_url}")
@@ -1,9 +1,9 @@
1
1
  """Module for custom error classes"""
2
2
 
3
- from typing import Union
3
+ from typing import Optional, Union
4
4
 
5
5
 
6
- class HomeassistantAPIError(BaseException):
6
+ class HomeassistantAPIError(Exception):
7
7
  """Base class for custom errors"""
8
8
 
9
9
 
@@ -55,8 +55,8 @@ class InternalServerError(HomeassistantAPIError):
55
55
  class UnauthorizedError(HomeassistantAPIError):
56
56
  """Error raised when an invalid token in used to authenticate with homeassistant."""
57
57
 
58
- def __init__(self) -> None:
59
- super().__init__("Invalid authentication token")
58
+ def __init__(self, message: Optional[str] = None) -> None:
59
+ super().__init__(message or "Invalid authentication token")
60
60
 
61
61
 
62
62
  class EndpointNotFoundError(HomeassistantAPIError):
@@ -84,3 +84,11 @@ class UnexpectedStatusCodeError(ResponseError):
84
84
 
85
85
  def __init__(self, status_code: int) -> None:
86
86
  super().__init__(f"Response has unexpected status code: {status_code!r}")
87
+
88
+
89
+ class WebsocketError(HomeassistantAPIError):
90
+ """Error raised when an issue occurs with the websocket connection."""
91
+
92
+
93
+ class ReceivingError(WebsocketError):
94
+ """Error raised when an issue occurs when receiving a message from the websocket server."""
@@ -1,4 +1,5 @@
1
1
  """The Model objects for the entire library."""
2
+
2
3
  from .base import BaseModel
3
4
  from .domains import Domain, Service, ServiceField
4
5
  from .entity import Entity, Group
@@ -3,16 +3,18 @@
3
3
  from datetime import datetime
4
4
  from typing import Annotated
5
5
 
6
- from pydantic import ConfigDict, BaseModel as PydanticBaseModel, PlainSerializer
7
-
6
+ from pydantic import BaseModel as PydanticBaseModel
7
+ from pydantic import ConfigDict, PlainSerializer
8
8
 
9
9
  DatetimeIsoField = Annotated[
10
- datetime, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='json')
10
+ datetime,
11
+ PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used="json"),
11
12
  ]
12
13
 
13
14
 
14
15
  class BaseModel(PydanticBaseModel):
15
16
  """Base model that all Library Models inherit from."""
17
+
16
18
  model_config = ConfigDict(
17
19
  arbitrary_types_allowed=True,
18
20
  validate_assignment=True,
@@ -1,27 +1,35 @@
1
1
  """File for Service and Domain data models"""
2
+
2
3
  import gc
3
4
  import inspect
4
5
  from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Tuple, Union, cast
5
6
 
6
7
  from pydantic import Field
7
8
 
9
+ from homeassistant_api.errors import RequestError
10
+
8
11
  from .base import BaseModel
9
12
  from .states import State
10
13
 
11
14
  if TYPE_CHECKING:
12
- from homeassistant_api import Client
15
+ from homeassistant_api import Client, WebsocketClient
13
16
 
14
17
 
15
18
  class Domain(BaseModel):
16
19
  """Model representing the domain that services belong to."""
17
20
 
18
- def __init__(self, *args, _client: Optional["Client"] = None, **kwargs) -> None:
21
+ def __init__(
22
+ self,
23
+ *args,
24
+ _client: Optional[Union["Client", "WebsocketClient"]] = None,
25
+ **kwargs,
26
+ ) -> None:
19
27
  super().__init__(*args, **kwargs)
20
28
  if _client is None:
21
29
  raise ValueError("No client passed.")
22
30
  object.__setattr__(self, "_client", _client)
23
31
 
24
- _client: "Client"
32
+ _client: Union["Client", "WebsocketClient"]
25
33
  domain_id: str = Field(
26
34
  ...,
27
35
  description="The name of the domain that services belong to. "
@@ -33,12 +41,12 @@ class Domain(BaseModel):
33
41
  )
34
42
 
35
43
  @classmethod
36
- def from_json(cls, json: Dict[str, Any], client: "Client") -> "Domain":
44
+ def from_json(
45
+ cls, json: Dict[str, Any], client: Union["Client", "WebsocketClient"]
46
+ ) -> "Domain":
37
47
  """Constructs Domain and Service models from json data."""
38
48
  if "domain" not in json or "services" not in json:
39
- raise ValueError(
40
- "Missing services or attribute attribute in json argument."
41
- )
49
+ raise ValueError("Missing services or domain attribute in json argument.")
42
50
  domain = cls(domain_id=cast(str, json.get("domain")), _client=client)
43
51
  services = json.get("services")
44
52
  assert isinstance(services, dict)
@@ -67,7 +75,13 @@ class Domain(BaseModel):
67
75
  """Allows services accessible as attributes"""
68
76
  if attr in self.services:
69
77
  return self.get_service(attr)
70
- return super().__getattribute__(attr)
78
+ try:
79
+ return super().__getattribute__(attr)
80
+ except AttributeError as err:
81
+ try:
82
+ return object.__getattribute__(self, attr)
83
+ except AttributeError as e:
84
+ raise e from err
71
85
 
72
86
 
73
87
  class ServiceField(BaseModel):
@@ -89,33 +103,75 @@ class Service(BaseModel):
89
103
  description: Optional[str] = None
90
104
  fields: Optional[Dict[str, ServiceField]] = None
91
105
 
92
- def trigger(self, **service_data) -> Tuple[State, ...]:
106
+ def trigger(self, entity_id: Optional[str] = None, **service_data) -> Union[
107
+ Tuple[State, ...],
108
+ Tuple[Tuple[State, ...], Dict[str, Any]],
109
+ dict[str, Any],
110
+ None,
111
+ ]:
93
112
  """Triggers the service associated with this object."""
94
- return self.domain._client.trigger_service(
95
- self.domain.domain_id,
96
- self.service_id,
97
- **service_data,
98
- )
113
+ if entity_id is not None:
114
+ service_data["entity_id"] = entity_id
115
+ try:
116
+ return self.domain._client.trigger_service_with_response(
117
+ self.domain.domain_id,
118
+ self.service_id,
119
+ **service_data,
120
+ )
121
+ except RequestError:
122
+ return self.domain._client.trigger_service(
123
+ self.domain.domain_id,
124
+ self.service_id,
125
+ **service_data,
126
+ )
99
127
 
100
- async def async_trigger(self, **service_data) -> Tuple[State, ...]:
128
+ async def async_trigger(
129
+ self, entity_id: Optional[str] = None, **service_data
130
+ ) -> Union[Tuple[State, ...], Tuple[Tuple[State, ...], Dict[str, Any]]]:
101
131
  """Triggers the service associated with this object."""
102
- return await self.domain._client.async_trigger_service(
103
- self.domain.domain_id,
104
- self.service_id,
105
- **service_data,
106
- )
132
+ if entity_id is not None:
133
+ service_data["entity_id"] = entity_id
107
134
 
108
- def __call__(
109
- self, **service_data
110
- ) -> Union[Tuple[State, ...], Coroutine[Any, Any, Tuple[State, ...]]]:
111
- """Triggers the service associated with this object."""
135
+ from homeassistant_api import WebsocketClient # prevent circular import
136
+
137
+ if isinstance(self.domain._client, WebsocketClient):
138
+ raise NotImplementedError(
139
+ "WebsocketClient does not support async/await syntax."
140
+ )
141
+ try:
142
+ return await self.domain._client.async_trigger_service_with_response(
143
+ self.domain.domain_id,
144
+ self.service_id,
145
+ **service_data,
146
+ )
147
+ except RequestError:
148
+ return await self.domain._client.async_trigger_service(
149
+ self.domain.domain_id,
150
+ self.service_id,
151
+ **service_data,
152
+ )
153
+
154
+ def __call__(self, entity_id: Optional[str] = None, **service_data) -> Union[
155
+ Union[
156
+ Tuple[State, ...],
157
+ Tuple[Tuple[State, ...], Dict[str, Any]],
158
+ dict[str, Any],
159
+ None,
160
+ ],
161
+ Coroutine[
162
+ Any, Any, Union[Tuple[State, ...], Tuple[Tuple[State, ...], Dict[str, Any]]]
163
+ ],
164
+ ]:
165
+ """
166
+ Triggers the service associated with this object.
167
+ """
112
168
  assert (frame := inspect.currentframe()) is not None
113
169
  assert (parent_frame := frame.f_back) is not None
114
170
  try:
115
171
  if inspect.iscoroutinefunction(
116
172
  caller := gc.get_referrers(parent_frame.f_code)[0]
117
173
  ) or inspect.iscoroutine(caller):
118
- return self.async_trigger(**service_data)
174
+ return self.async_trigger(entity_id=entity_id, **service_data)
119
175
  except IndexError: # pragma: no cover
120
176
  pass
121
- return self.trigger(**service_data)
177
+ return self.trigger(entity_id=entity_id, **service_data)
@@ -1,4 +1,5 @@
1
1
  """Module for the History model."""
2
+
2
3
  from typing import Tuple
3
4
 
4
5
  from pydantic import Field
@@ -1,4 +1,5 @@
1
1
  """Module for the Logbook Entry model."""
2
+
2
3
  from typing import Optional
3
4
 
4
5
  from pydantic import Field
@@ -1,4 +1,5 @@
1
1
  """Module for the Entity State model."""
2
+
2
3
  from datetime import datetime
3
4
  from typing import Any, Dict, Optional
4
5
 
@@ -11,9 +12,22 @@ class Context(BaseModel):
11
12
  """Model for entity state contexts."""
12
13
 
13
14
  id: str = Field(
14
- max_length=128,
15
+ max_length=128, # arbitrary limit
15
16
  description="Unique string identifying the context.",
16
17
  )
18
+ parent_id: Optional[str] = Field(
19
+ max_length=128,
20
+ description="Unique string identifying the parent context.",
21
+ )
22
+ user_id: Optional[str] = Field(
23
+ max_length=128,
24
+ description="Unique string identifying the user.",
25
+ )
26
+
27
+ @classmethod
28
+ def from_json(cls, json: Dict[str, Any]) -> "Context":
29
+ """Constructs Context model from json data"""
30
+ return cls.model_validate(json)
17
31
 
18
32
 
19
33
  class State(BaseModel):
@@ -0,0 +1,97 @@
1
+ """A module defining the responses we expect from the websocket API."""
2
+
3
+ from typing import Any, Literal, Optional, Union
4
+
5
+ from .base import BaseModel
6
+ from .states import Context, DatetimeIsoField
7
+
8
+ __all__ = (
9
+ "AuthRequired",
10
+ "AuthOk",
11
+ "AuthInvalid",
12
+ "PingResponse",
13
+ "ErrorResponse",
14
+ "ResultResponse",
15
+ "EventResponse",
16
+ )
17
+
18
+
19
+ class AuthRequired(BaseModel):
20
+ type: Literal["auth_required"]
21
+ ha_version: str
22
+
23
+
24
+ class AuthOk(BaseModel):
25
+ type: Literal["auth_ok"]
26
+ ha_version: str
27
+
28
+
29
+ class AuthInvalid(BaseModel):
30
+ type: Literal["auth_invalid"]
31
+ message: str
32
+
33
+
34
+ class PingResponse(BaseModel):
35
+ """Ping websocket response model."""
36
+
37
+ id: int
38
+ type: Literal["pong"]
39
+ start: int # added by the client, nanoseconds
40
+ end: Optional[int] = None # added by the client, nanoseconds
41
+
42
+
43
+ class Error(BaseModel):
44
+ code: str
45
+ message: str
46
+
47
+
48
+ class ErrorResponse(BaseModel):
49
+ """Error websocket response model."""
50
+
51
+ id: int
52
+ success: Literal[False]
53
+ type: Literal["result"]
54
+ error: Error
55
+
56
+
57
+ class ResultResponse(BaseModel):
58
+ """Result websocket response model."""
59
+
60
+ id: int
61
+ success: Literal[True]
62
+ type: Literal["result"]
63
+ result: Optional[Any]
64
+
65
+
66
+ class FiredEvent(BaseModel):
67
+ """A model to parse the `event` key of fired event websocket responses."""
68
+
69
+ event_type: str
70
+ data: dict[str, Any]
71
+
72
+ origin: Literal["LOCAL", "REMOTE"]
73
+ # REMOTE if another API client or webhook fired the event
74
+ # LOCAL if Home Assistant (or the auth token we used) fired the event
75
+
76
+ time_fired: DatetimeIsoField # datetime.datetime
77
+ context: Optional[Context]
78
+
79
+
80
+ class TemplateEvent(BaseModel):
81
+ result: str
82
+ listeners: dict[str, Any]
83
+
84
+
85
+ class FiredTrigger(BaseModel):
86
+ """A model to parse the `trigger` key of fired event websocket responses."""
87
+
88
+ context: Optional[Context]
89
+ variables: dict[str, Any]
90
+
91
+
92
+ class EventResponse(BaseModel):
93
+ """A model to parse the response of a fired event websocket response."""
94
+
95
+ id: int
96
+ type: Literal["event"]
97
+ event: Union[FiredEvent, FiredTrigger, TemplateEvent]