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.
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/PKG-INFO +48 -16
- homeassistant_api-5.0.0/README.md +78 -0
- homeassistant_api-5.0.0/homeassistant_api/__init__.py +48 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/client.py +17 -5
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/errors.py +12 -4
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/__init__.py +1 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/base.py +5 -3
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/domains.py +82 -26
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/history.py +1 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/logbook.py +1 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/states.py +15 -1
- homeassistant_api-5.0.0/homeassistant_api/models/websocket.py +97 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/rawasyncclient.py +45 -12
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/rawbaseclient.py +9 -39
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/rawclient.py +40 -9
- homeassistant_api-5.0.0/homeassistant_api/rawwebsocket.py +199 -0
- homeassistant_api-5.0.0/homeassistant_api/utils.py +32 -0
- homeassistant_api-5.0.0/homeassistant_api/websocket.py +378 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/pyproject.toml +4 -3
- homeassistant_api-4.2.2.post1/README.md +0 -47
- homeassistant_api-4.2.2.post1/homeassistant_api/__init__.py +0 -47
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/LICENSE +0 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/entity.py +0 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/events.py +0 -0
- {homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/processing.py +0 -0
- {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
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: HomeAssistant-API
|
|
3
|
-
Version:
|
|
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
|
+
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
|
[](https://codecov.io/gh/GrandMoff100/HomeAssistantAPI)
|
|
30
|
-
[](https://
|
|
31
|
+
[](https://pypistats.org/packages/homeassistant-api)
|
|
31
32
|

|
|
32
33
|
[](https://homeassistantapi.readthedocs.io/en/latest/?badge=latest)
|
|
33
34
|
[](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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
asyncio.run(main())
|
|
54
76
|
```
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
[](https://codecov.io/gh/GrandMoff100/HomeAssistantAPI)
|
|
4
|
+
[](https://pypistats.org/packages/homeassistant-api)
|
|
5
|
+

|
|
6
|
+
[](https://homeassistantapi.readthedocs.io/en/latest/?badge=latest)
|
|
7
|
+
[](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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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(
|
|
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."""
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
|
|
6
|
-
from pydantic import
|
|
7
|
-
|
|
6
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
7
|
+
from pydantic import ConfigDict, PlainSerializer
|
|
8
8
|
|
|
9
9
|
DatetimeIsoField = Annotated[
|
|
10
|
-
datetime,
|
|
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,
|
{homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/domains.py
RENAMED
|
@@ -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__(
|
|
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(
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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)
|
{homeassistant_api-4.2.2.post1 → homeassistant_api-5.0.0}/homeassistant_api/models/states.py
RENAMED
|
@@ -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]
|