ucapi 0.4.0__tar.gz → 0.5.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.
- {ucapi-0.4.0 → ucapi-0.5.0}/CHANGELOG.md +14 -0
- {ucapi-0.4.0/ucapi.egg-info → ucapi-0.5.0}/PKG-INFO +18 -12
- {ucapi-0.4.0 → ucapi-0.5.0}/README.md +14 -8
- ucapi-0.5.0/docs/setup.md +43 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/README.md +7 -1
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/hello_integration.py +3 -2
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/remote.py +3 -2
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/setup_flow.py +4 -3
- ucapi-0.5.0/examples/voice.json +18 -0
- ucapi-0.5.0/examples/voice.py +186 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/pyproject.toml +20 -6
- {ucapi-0.4.0 → ucapi-0.5.0}/requirements.txt +1 -0
- ucapi-0.5.0/scripts/compile_protos.py +145 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/setup.cfg +1 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/test-requirements.txt +2 -1
- ucapi-0.5.0/tests/test_voice_assistant.py +71 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/__init__.py +10 -1
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/_version.py +3 -3
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/api.py +404 -19
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/api_definitions.py +97 -4
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/button.py +2 -2
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/climate.py +6 -6
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/cover.py +6 -6
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/entities.py +2 -3
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/entity.py +51 -12
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/light.py +6 -6
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/media_player.py +6 -6
- ucapi-0.5.0/ucapi/proto/__init__.py +7 -0
- ucapi-0.5.0/ucapi/proto/ucr_integration_voice.proto +83 -0
- ucapi-0.5.0/ucapi/proto/ucr_integration_voice_pb2.py +49 -0
- ucapi-0.5.0/ucapi/proto/ucr_integration_voice_pb2.pyi +77 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/remote.py +4 -4
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/sensor.py +4 -4
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/switch.py +6 -6
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/ui.py +1 -1
- ucapi-0.5.0/ucapi/voice_assistant.py +330 -0
- ucapi-0.5.0/ucapi/voice_stream.py +210 -0
- {ucapi-0.4.0 → ucapi-0.5.0/ucapi.egg-info}/PKG-INFO +18 -12
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/SOURCES.txt +11 -1
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/requires.txt +2 -1
- ucapi-0.4.0/docs/setup.md +0 -18
- {ucapi-0.4.0 → ucapi-0.5.0}/CONTRIBUTING.md +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/LICENSE +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/docs/code_guidelines.md +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/hello_integration.json +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/remote.json +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/remote_ui_page.json +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/examples/setup_flow.json +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/tests/test_api.py +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/dependency_links.txt +0 -0
- {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/top_level.txt +0 -0
|
@@ -11,6 +11,20 @@ _Changes in the next release_
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## v0.5.0 - 2025-12-17
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
- Enhance entity command handler with WS client connection parameter in `CommandHandler` callback and `Entity.command`
|
|
17
|
+
method to allow clients to send back event messages ([#38](https://github.com/unfoldedcircle/integration-python-library/pull/38)).
|
|
18
|
+
- The implementation is currently backward-compatible but will be removed in a future release.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- New voice-assistant entity with voice-stream session handling ([#38](https://github.com/unfoldedcircle/integration-python-library/pull/38)).
|
|
22
|
+
- This requires firmware 2.8.2 or newer to work correctly.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Remove logging in Entities.get method if entity doesn't exist. This could lead to excessive logging in some integrations ([#38](https://github.com/unfoldedcircle/integration-python-library/pull/38)).
|
|
26
|
+
- Prepare for Python 3.12 and 3.13: replace `asyncio.get_event_loop()` calls in the examples with `asyncio.new_event_loop()` ([#39](https://github.com/unfoldedcircle/integration-python-library/pull/39)).
|
|
27
|
+
|
|
14
28
|
## v0.4.0 - 2025-11-24
|
|
15
29
|
### Breaking Changes
|
|
16
30
|
- A WebSocket disconnection no longer emits the `DISCONNECT` event, but the new `CLIENT_DISCONNECTED` event ([#35](https://github.com/unfoldedcircle/integration-python-library/pull/35)).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python wrapper for the Unfolded Circle Integration API
|
|
5
5
|
Author-email: Unfolded Circle ApS <hello@unfoldedcircle.com>
|
|
6
6
|
License: MPL-2.0
|
|
@@ -16,16 +16,16 @@ Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries
|
|
18
18
|
Classifier: Topic :: Home Automation
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
22
21
|
Description-Content-Type: text/markdown; charset=UTF-8
|
|
23
22
|
License-File: LICENSE
|
|
23
|
+
Requires-Dist: protobuf~=6.33.2
|
|
24
24
|
Requires-Dist: pyee>=9.0
|
|
25
25
|
Requires-Dist: websockets>=14.0
|
|
26
26
|
Requires-Dist: zeroconf>=0.120.0
|
|
27
27
|
Provides-Extra: testing
|
|
28
|
-
Requires-Dist: pylint; extra == "testing"
|
|
28
|
+
Requires-Dist: pylint==4.0.4; extra == "testing"
|
|
29
29
|
Requires-Dist: flake8-docstrings; extra == "testing"
|
|
30
30
|
Requires-Dist: flake8; extra == "testing"
|
|
31
31
|
Requires-Dist: black; extra == "testing"
|
|
@@ -38,12 +38,14 @@ Dynamic: license-file
|
|
|
38
38
|
[](https://github.com/psf/black)
|
|
39
39
|
|
|
40
40
|
This library simplifies writing Python-based integrations for the [Unfolded Circle Remote devices](https://www.unfoldedcircle.com/)
|
|
41
|
-
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api)
|
|
41
|
+
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api)
|
|
42
|
+
and supporting the [available entities](https://unfoldedcircle.github.io/core-api/entities/).
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
> [!NOTE]
|
|
45
|
+
> Please note that this library is more of a convenience Python wrapper for the WebSocket Integreation-API than a
|
|
46
|
+
> full-featured SDK. It is based on our [Node.js integration library](https://github.com/unfoldedcircle/integration-node-library).
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
> [!IMPORTANT]
|
|
47
49
|
> This is our first Python project, and we don't see ourselves as Python professionals.
|
|
48
50
|
> Therefore, the library is most likely not yet that Pythonic!
|
|
49
51
|
> We are still learning and value your feedback on how to improve it :-)
|
|
@@ -51,10 +53,15 @@ Based on our [Node.js integration library](https://github.com/unfoldedcircle/int
|
|
|
51
53
|
Not yet supported:
|
|
52
54
|
|
|
53
55
|
- Secure WebSocket
|
|
54
|
-
- Token
|
|
56
|
+
- Token-based authentication
|
|
55
57
|
|
|
56
58
|
Requirements:
|
|
57
|
-
- Python 3.
|
|
59
|
+
- Python 3.11 or newer
|
|
60
|
+
|
|
61
|
+
Integrations using this library:
|
|
62
|
+
- [Android TV integration](https://github.com/unfoldedcircle/integration-androidtv)
|
|
63
|
+
- [Apple TV integration](https://github.com/unfoldedcircle/integration-appletv)
|
|
64
|
+
- [Denon AVR integration](https://github.com/unfoldedcircle/integration-denonavr)
|
|
58
65
|
|
|
59
66
|
## Installation
|
|
60
67
|
|
|
@@ -63,8 +70,7 @@ Use pip:
|
|
|
63
70
|
pip3 install ucapi
|
|
64
71
|
```
|
|
65
72
|
|
|
66
|
-
See [examples directory](https://github.com/aitatoi/integration-python-library/blob/main/examples) for
|
|
67
|
-
integration driver example. More examples will be published.
|
|
73
|
+
See [examples directory](https://github.com/aitatoi/integration-python-library/blob/main/examples) for some minimal integration driver examples.
|
|
68
74
|
|
|
69
75
|
### Environment Variables
|
|
70
76
|
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
[](https://github.com/psf/black)
|
|
5
5
|
|
|
6
6
|
This library simplifies writing Python-based integrations for the [Unfolded Circle Remote devices](https://www.unfoldedcircle.com/)
|
|
7
|
-
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api)
|
|
7
|
+
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api)
|
|
8
|
+
and supporting the [available entities](https://unfoldedcircle.github.io/core-api/entities/).
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
> [!NOTE]
|
|
11
|
+
> Please note that this library is more of a convenience Python wrapper for the WebSocket Integreation-API than a
|
|
12
|
+
> full-featured SDK. It is based on our [Node.js integration library](https://github.com/unfoldedcircle/integration-node-library).
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
> [!IMPORTANT]
|
|
13
15
|
> This is our first Python project, and we don't see ourselves as Python professionals.
|
|
14
16
|
> Therefore, the library is most likely not yet that Pythonic!
|
|
15
17
|
> We are still learning and value your feedback on how to improve it :-)
|
|
@@ -17,10 +19,15 @@ Based on our [Node.js integration library](https://github.com/unfoldedcircle/int
|
|
|
17
19
|
Not yet supported:
|
|
18
20
|
|
|
19
21
|
- Secure WebSocket
|
|
20
|
-
- Token
|
|
22
|
+
- Token-based authentication
|
|
21
23
|
|
|
22
24
|
Requirements:
|
|
23
|
-
- Python 3.
|
|
25
|
+
- Python 3.11 or newer
|
|
26
|
+
|
|
27
|
+
Integrations using this library:
|
|
28
|
+
- [Android TV integration](https://github.com/unfoldedcircle/integration-androidtv)
|
|
29
|
+
- [Apple TV integration](https://github.com/unfoldedcircle/integration-appletv)
|
|
30
|
+
- [Denon AVR integration](https://github.com/unfoldedcircle/integration-denonavr)
|
|
24
31
|
|
|
25
32
|
## Installation
|
|
26
33
|
|
|
@@ -29,8 +36,7 @@ Use pip:
|
|
|
29
36
|
pip3 install ucapi
|
|
30
37
|
```
|
|
31
38
|
|
|
32
|
-
See [examples directory](https://github.com/aitatoi/integration-python-library/blob/main/examples) for
|
|
33
|
-
integration driver example. More examples will be published.
|
|
39
|
+
See [examples directory](https://github.com/aitatoi/integration-python-library/blob/main/examples) for some minimal integration driver examples.
|
|
34
40
|
|
|
35
41
|
### Environment Variables
|
|
36
42
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Development Setup
|
|
2
|
+
|
|
3
|
+
This library requires Python 3.10 or newer.
|
|
4
|
+
|
|
5
|
+
Install build tools:
|
|
6
|
+
```shell
|
|
7
|
+
pip3 install build setuptools setuptools_scm
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Build:
|
|
11
|
+
```shell
|
|
12
|
+
python -m build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Local installation:
|
|
16
|
+
```shell
|
|
17
|
+
pip3 install --force-reinstall dist/ucapi-$VERSION-py3-none-any.whl
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Protobuf
|
|
21
|
+
|
|
22
|
+
1. Optional (recommended): install the Python plugin toolchain for consistent results:
|
|
23
|
+
```bash
|
|
24
|
+
python3 -m pip install --upgrade grpcio-tools protobuf
|
|
25
|
+
```
|
|
26
|
+
2. From the project root, run:
|
|
27
|
+
```bash
|
|
28
|
+
python3 scripts/compile_protos.py
|
|
29
|
+
```
|
|
30
|
+
- This will generate `ucapi/proto/ucr_integration_voice_pb2.py` (and `.pyi` if supported).
|
|
31
|
+
3. Add and commit the generated files to Git:
|
|
32
|
+
```bash
|
|
33
|
+
git add ucapi/proto/ucr_integration_voice_pb2.py ucapi/proto/ucr_integration_voice_pb2.pyi || true
|
|
34
|
+
git commit -m "Generate protobuf Python modules for voice integration"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
- The library does not re-generate at build time; we ship the generated code with the package.
|
|
39
|
+
- If you prefer using system `protoc`, ensure it’s on `PATH`; the script will fall back to it automatically.
|
|
40
|
+
- Imports at runtime (if/when needed) will look like:
|
|
41
|
+
```python
|
|
42
|
+
from ucapi.proto import ucr_integration_voice_pb2 as voice_pb2
|
|
43
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# API wrapper examples
|
|
2
2
|
|
|
3
|
-
This directory contains a few examples on how to use the Remote Two Integration-API wrapper.
|
|
3
|
+
This directory contains a few examples on how to use the Remote Two/3 Integration-API wrapper.
|
|
4
4
|
|
|
5
5
|
Each example uses a driver metadata definition file. It's a json file named after the example.
|
|
6
6
|
The most important fields are:
|
|
@@ -39,3 +39,9 @@ and are not yet available as typed Python objects.
|
|
|
39
39
|
|
|
40
40
|
See `Setting` object definition and the referenced SettingTypeNumber, SettingTypeText, SettingTypeTextArea,
|
|
41
41
|
SettingTypePassword, SettingTypeCheckbox, SettingTypeDropdown, SettingTypeLabel.
|
|
42
|
+
|
|
43
|
+
## voice
|
|
44
|
+
|
|
45
|
+
The [voice ](voice.py) example shows how to use the voice-assistant entity and receiving a microphone audio stream.
|
|
46
|
+
|
|
47
|
+
Firmware version 2.8.2 or higher is required.
|
|
@@ -6,12 +6,12 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import ucapi
|
|
8
8
|
|
|
9
|
-
loop = asyncio.
|
|
9
|
+
loop = asyncio.new_event_loop()
|
|
10
10
|
api = ucapi.IntegrationAPI(loop)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
async def cmd_handler(
|
|
14
|
-
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None
|
|
14
|
+
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None, websocket: Any
|
|
15
15
|
) -> ucapi.StatusCodes:
|
|
16
16
|
"""
|
|
17
17
|
Push button command handler.
|
|
@@ -21,6 +21,7 @@ async def cmd_handler(
|
|
|
21
21
|
:param entity: button entity
|
|
22
22
|
:param cmd_id: command
|
|
23
23
|
:param _params: optional command parameters
|
|
24
|
+
:param websocket: optional client connection for sending directed events
|
|
24
25
|
:return: status of the command
|
|
25
26
|
"""
|
|
26
27
|
print(f"Got {entity.id} command request: {cmd_id}")
|
|
@@ -20,7 +20,7 @@ from ucapi.ui import (
|
|
|
20
20
|
create_ui_text,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
loop = asyncio.
|
|
23
|
+
loop = asyncio.new_event_loop()
|
|
24
24
|
api = ucapi.IntegrationAPI(loop)
|
|
25
25
|
|
|
26
26
|
# Simple commands which are supported by this example remote-entity
|
|
@@ -46,7 +46,7 @@ supported_commands = [
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
async def cmd_handler(
|
|
49
|
-
entity: ucapi.Remote, cmd_id: str, params: dict[str, Any] | None
|
|
49
|
+
entity: ucapi.Remote, cmd_id: str, params: dict[str, Any] | None, websocket: Any
|
|
50
50
|
) -> ucapi.StatusCodes:
|
|
51
51
|
"""
|
|
52
52
|
Remote command handler.
|
|
@@ -56,6 +56,7 @@ async def cmd_handler(
|
|
|
56
56
|
:param entity: remote entity
|
|
57
57
|
:param cmd_id: command
|
|
58
58
|
:param params: optional command parameters
|
|
59
|
+
:param websocket: optional client connection for sending directed events
|
|
59
60
|
:return: status of the command
|
|
60
61
|
"""
|
|
61
62
|
print(f"Got {entity.id} command request: {cmd_id}")
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import ucapi
|
|
8
8
|
|
|
9
|
-
loop = asyncio.
|
|
9
|
+
loop = asyncio.new_event_loop()
|
|
10
10
|
api = ucapi.IntegrationAPI(loop)
|
|
11
11
|
|
|
12
12
|
|
|
@@ -38,7 +38,7 @@ async def handle_driver_setup(
|
|
|
38
38
|
"""
|
|
39
39
|
Start driver setup.
|
|
40
40
|
|
|
41
|
-
Initiated by Remote Two to set up the driver.
|
|
41
|
+
Initiated by Remote Two/3 to set up the driver.
|
|
42
42
|
|
|
43
43
|
:param msg: value(s) of input fields in the first setup screen.
|
|
44
44
|
:return: the setup action on how to continue
|
|
@@ -157,7 +157,7 @@ async def handle_user_data_response(msg: ucapi.UserDataResponse) -> ucapi.SetupA
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
async def cmd_handler(
|
|
160
|
-
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None
|
|
160
|
+
entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None, websocket: Any
|
|
161
161
|
) -> ucapi.StatusCodes:
|
|
162
162
|
"""
|
|
163
163
|
Push button command handler.
|
|
@@ -167,6 +167,7 @@ async def cmd_handler(
|
|
|
167
167
|
:param entity: button entity
|
|
168
168
|
:param cmd_id: command
|
|
169
169
|
:param _params: optional command parameters
|
|
170
|
+
:param websocket: optional client connection for sending directed events
|
|
170
171
|
:return: status of the command
|
|
171
172
|
"""
|
|
172
173
|
print(f"Got {entity.id} command request: {cmd_id}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"driver_id": "voice_test",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"min_core_api": "0.20.0",
|
|
5
|
+
"name": { "en": "Voice test" },
|
|
6
|
+
"icon": "uc:integration",
|
|
7
|
+
"description": {
|
|
8
|
+
"en": "Minimal Python integration driver example for voice commands."
|
|
9
|
+
},
|
|
10
|
+
"port": 9084,
|
|
11
|
+
"developer": {
|
|
12
|
+
"name": "Unfolded Circle ApS",
|
|
13
|
+
"email": "hello@unfoldedcircle.com",
|
|
14
|
+
"url": "https://www.unfoldedcircle.com"
|
|
15
|
+
},
|
|
16
|
+
"home_page": "https://www.unfoldedcircle.com",
|
|
17
|
+
"release_date": "2025-12-11"
|
|
18
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Simple voice assistant entity integration example.
|
|
3
|
+
|
|
4
|
+
Requires firmware 2.8.2 or newer.
|
|
5
|
+
|
|
6
|
+
See the [Android TV integration](https://github.com/unfoldedcircle/integration-androidtv)
|
|
7
|
+
for a full implementation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from asyncio import sleep
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import ucapi
|
|
16
|
+
from ucapi import (
|
|
17
|
+
AssistantError,
|
|
18
|
+
AssistantErrorCode,
|
|
19
|
+
AssistantEvent,
|
|
20
|
+
AssistantEventType,
|
|
21
|
+
VoiceAssistant,
|
|
22
|
+
)
|
|
23
|
+
from ucapi.api_definitions import AssistantSttResponse, AssistantTextResponse
|
|
24
|
+
from ucapi.voice_assistant import Attributes as VAAttr
|
|
25
|
+
from ucapi.voice_assistant import (
|
|
26
|
+
AudioConfiguration,
|
|
27
|
+
)
|
|
28
|
+
from ucapi.voice_assistant import Commands as VACommands
|
|
29
|
+
from ucapi.voice_assistant import Features as VAFeatures
|
|
30
|
+
from ucapi.voice_assistant import (
|
|
31
|
+
SampleFormat,
|
|
32
|
+
VoiceAssistantEntityOptions,
|
|
33
|
+
)
|
|
34
|
+
from ucapi.voice_stream import VoiceEndReason, VoiceSession, VoiceSessionClosed
|
|
35
|
+
|
|
36
|
+
loop = asyncio.new_event_loop()
|
|
37
|
+
api = ucapi.IntegrationAPI(loop)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@api.listens_to(ucapi.Events.CONNECT)
|
|
41
|
+
async def on_connect() -> None:
|
|
42
|
+
# When the remote connects, we just set the device state. We are ready all the time!
|
|
43
|
+
await api.set_device_state(ucapi.DeviceStates.CONNECTED)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@api.listens_to(ucapi.Events.SUBSCRIBE_ENTITIES)
|
|
47
|
+
async def on_subscribe_entities(entity_ids: list[str]) -> None:
|
|
48
|
+
for entity_id in entity_ids:
|
|
49
|
+
api.configured_entities.update_attributes(entity_id, {VAAttr.STATE: "ON"})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def on_voice_cmd(
|
|
53
|
+
entity: ucapi.VoiceAssistant,
|
|
54
|
+
cmd_id: str,
|
|
55
|
+
params: dict[str, Any] | None,
|
|
56
|
+
websocket: Any,
|
|
57
|
+
) -> ucapi.StatusCodes:
|
|
58
|
+
"""
|
|
59
|
+
Voice assistant command handler.
|
|
60
|
+
|
|
61
|
+
Called by the integration-API if a command is sent to a configured voice_assistant-entity.
|
|
62
|
+
|
|
63
|
+
:param entity: Voice assistant entity
|
|
64
|
+
:param cmd_id: command
|
|
65
|
+
:param params: optional command parameters
|
|
66
|
+
:param websocket: optional client connection for sending directed events
|
|
67
|
+
:return: status of the command
|
|
68
|
+
"""
|
|
69
|
+
print(f"Got {entity.id} command request: {cmd_id} {params}")
|
|
70
|
+
if params is None:
|
|
71
|
+
return ucapi.StatusCodes.BAD_REQUEST
|
|
72
|
+
|
|
73
|
+
session_id = params.get("session_id", 0)
|
|
74
|
+
if session_id <= 0:
|
|
75
|
+
return ucapi.StatusCodes.BAD_REQUEST
|
|
76
|
+
|
|
77
|
+
if cmd_id == VACommands.VOICE_START:
|
|
78
|
+
asyncio.create_task(start_voice(websocket, session_id))
|
|
79
|
+
# Acknowledge start; binary audio will arrive on the WS binary channel
|
|
80
|
+
return ucapi.StatusCodes.OK
|
|
81
|
+
return ucapi.StatusCodes.NOT_IMPLEMENTED
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def start_voice(websocket: Any, session_id: int):
|
|
85
|
+
# Here we'd set up the voice communication to the target device / system
|
|
86
|
+
await sleep(0.5)
|
|
87
|
+
|
|
88
|
+
# Once ready, send the READY event to the remote to start the voice stream.
|
|
89
|
+
# Otherwise, AssistantEventType.ERROR should be sent instead.
|
|
90
|
+
ready_evt = AssistantEvent(
|
|
91
|
+
type=AssistantEventType.READY,
|
|
92
|
+
entity_id=entity.id,
|
|
93
|
+
session_id=session_id,
|
|
94
|
+
)
|
|
95
|
+
await api.send_assistant_event(websocket, ready_evt)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def on_voice_session(session: VoiceSession):
|
|
99
|
+
print(
|
|
100
|
+
f"Voice stream started: session={session.session_id}, "
|
|
101
|
+
f"{session.config.channels}ch @ {session.config.sample_rate} Hz {session.config.sample_format}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Note: a real driver should check if the session_id matches the one from the voice_start command
|
|
105
|
+
|
|
106
|
+
total = 0
|
|
107
|
+
try:
|
|
108
|
+
async for frame in session: # frame is bytes
|
|
109
|
+
total += len(frame)
|
|
110
|
+
# feed frame into your voice assistant / LLM here
|
|
111
|
+
print(f"Got {len(frame)} bytes of audio data")
|
|
112
|
+
print(f"Voice stream ended: session={session.session_id}, bytes={total}")
|
|
113
|
+
|
|
114
|
+
event = AssistantEvent(
|
|
115
|
+
type=AssistantEventType.STT_RESPONSE,
|
|
116
|
+
entity_id=session.entity_id,
|
|
117
|
+
session_id=session.session_id,
|
|
118
|
+
data=AssistantSttResponse(
|
|
119
|
+
text="I'm just a demo and I don't know what you said."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
await session.send_event(event)
|
|
123
|
+
|
|
124
|
+
await sleep(1)
|
|
125
|
+
event = AssistantEvent(
|
|
126
|
+
type=AssistantEventType.TEXT_RESPONSE,
|
|
127
|
+
entity_id=session.entity_id,
|
|
128
|
+
session_id=session.session_id,
|
|
129
|
+
data=AssistantTextResponse(
|
|
130
|
+
success=True, text=f"You have sent {total} bytes of audio data"
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
await session.send_event(event)
|
|
134
|
+
|
|
135
|
+
await sleep(1)
|
|
136
|
+
except VoiceSessionClosed as ex:
|
|
137
|
+
print(
|
|
138
|
+
f"Voice stream session {session.session_id} closed (bytes={total})! Reason: {ex.reason}, exception: {ex.error}"
|
|
139
|
+
)
|
|
140
|
+
if ex.reason == VoiceEndReason.REMOTE:
|
|
141
|
+
return # Remote disconnected
|
|
142
|
+
event = AssistantEvent(
|
|
143
|
+
type=AssistantEventType.ERROR,
|
|
144
|
+
entity_id=session.entity_id,
|
|
145
|
+
session_id=session.session_id,
|
|
146
|
+
data=AssistantError(
|
|
147
|
+
code=(
|
|
148
|
+
AssistantErrorCode.TIMEOUT
|
|
149
|
+
if ex.reason == VoiceEndReason.TIMEOUT
|
|
150
|
+
else AssistantErrorCode.UNEXPECTED_ERROR
|
|
151
|
+
),
|
|
152
|
+
message=f"Reason: {ex.reason}, exception: {ex.error}",
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
await session.send_event(event)
|
|
156
|
+
|
|
157
|
+
# final event
|
|
158
|
+
event = AssistantEvent(
|
|
159
|
+
type=AssistantEventType.FINISHED,
|
|
160
|
+
entity_id=session.entity_id,
|
|
161
|
+
session_id=session.session_id,
|
|
162
|
+
)
|
|
163
|
+
await session.send_event(event)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
logging.basicConfig()
|
|
168
|
+
|
|
169
|
+
entity = VoiceAssistant(
|
|
170
|
+
identifier="va_main",
|
|
171
|
+
name={"en": "Demo Voice Assistant"},
|
|
172
|
+
features=[VAFeatures.TRANSCRIPTION, VAFeatures.RESPONSE_TEXT],
|
|
173
|
+
attributes={VAAttr.STATE.value: "ON"},
|
|
174
|
+
options=VoiceAssistantEntityOptions(
|
|
175
|
+
audio_cfg=AudioConfiguration(
|
|
176
|
+
channels=1, sample_rate=16000, sample_format=SampleFormat.I16
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
cmd_handler=on_voice_cmd,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
api.available_entities.add(entity)
|
|
183
|
+
api.set_voice_stream_handler(on_voice_session)
|
|
184
|
+
|
|
185
|
+
loop.run_until_complete(api.init("voice.json"))
|
|
186
|
+
loop.run_forever()
|
|
@@ -16,11 +16,11 @@ classifiers = [
|
|
|
16
16
|
"Operating System :: OS Independent",
|
|
17
17
|
"Topic :: Software Development :: Libraries",
|
|
18
18
|
"Topic :: Home Automation",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
19
|
"Programming Language :: Python :: 3.11",
|
|
21
20
|
]
|
|
22
|
-
requires-python = ">=3.
|
|
21
|
+
requires-python = ">=3.11"
|
|
23
22
|
dependencies = [
|
|
23
|
+
"protobuf~=6.33.2",
|
|
24
24
|
"pyee>=9.0",
|
|
25
25
|
"websockets>=14.0",
|
|
26
26
|
"zeroconf>=0.120.0",
|
|
@@ -40,7 +40,7 @@ content-type = "text/markdown; charset=UTF-8"
|
|
|
40
40
|
|
|
41
41
|
[project.optional-dependencies]
|
|
42
42
|
testing = [
|
|
43
|
-
"pylint",
|
|
43
|
+
"pylint==4.0.4",
|
|
44
44
|
"flake8-docstrings",
|
|
45
45
|
"flake8",
|
|
46
46
|
"black",
|
|
@@ -48,7 +48,7 @@ testing = [
|
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
[tool.setuptools]
|
|
51
|
-
packages = ["ucapi"]
|
|
51
|
+
packages = ["ucapi", "ucapi.proto"]
|
|
52
52
|
platforms = ["any"]
|
|
53
53
|
license-files = ["LICENSE"]
|
|
54
54
|
|
|
@@ -57,6 +57,7 @@ write_to = "ucapi/_version.py"
|
|
|
57
57
|
|
|
58
58
|
[tool.isort]
|
|
59
59
|
profile = "black"
|
|
60
|
+
skip_glob = ["ucapi/proto/*_pb2.py", "ucapi/proto/*_pb2.pyi"]
|
|
60
61
|
|
|
61
62
|
[tool.pylint.exceptions]
|
|
62
63
|
overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
|
|
@@ -64,10 +65,13 @@ overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
|
|
|
64
65
|
[tool.pylint.format]
|
|
65
66
|
max-line-length = "88"
|
|
66
67
|
|
|
67
|
-
[tool.pylint.
|
|
68
|
+
[tool.pylint.main]
|
|
68
69
|
ignore-paths = [
|
|
69
70
|
# ignore generated file
|
|
70
|
-
"ucapi/_version.py"
|
|
71
|
+
"ucapi/_version.py",
|
|
72
|
+
# ignore generated protobuf modules (Python + type stubs and optional gRPC stubs)
|
|
73
|
+
"ucapi/proto/.*_pb2.py",
|
|
74
|
+
"ucapi/proto/.*_pb2.pyi",
|
|
71
75
|
]
|
|
72
76
|
|
|
73
77
|
[tool.pylint."messages control"]
|
|
@@ -93,3 +97,13 @@ disable = [
|
|
|
93
97
|
# This flag controls whether inconsistent-quotes generates a warning when the
|
|
94
98
|
# character used as a quote delimiter is used inconsistently within a module.
|
|
95
99
|
check-quote-consistency = true
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
[tool.black]
|
|
103
|
+
line-length = 88
|
|
104
|
+
# Exclude generated protobuf modules, even if passed explicitly to Black
|
|
105
|
+
force-exclude = """
|
|
106
|
+
(
|
|
107
|
+
proto/.+_pb2(?:_grpc)?\\.pyi?$ # *_pb2.py, *_pb2.pyi, *_pb2_grpc.py
|
|
108
|
+
)
|
|
109
|
+
"""
|