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.
Files changed (51) hide show
  1. {ucapi-0.4.0 → ucapi-0.5.0}/CHANGELOG.md +14 -0
  2. {ucapi-0.4.0/ucapi.egg-info → ucapi-0.5.0}/PKG-INFO +18 -12
  3. {ucapi-0.4.0 → ucapi-0.5.0}/README.md +14 -8
  4. ucapi-0.5.0/docs/setup.md +43 -0
  5. {ucapi-0.4.0 → ucapi-0.5.0}/examples/README.md +7 -1
  6. {ucapi-0.4.0 → ucapi-0.5.0}/examples/hello_integration.py +3 -2
  7. {ucapi-0.4.0 → ucapi-0.5.0}/examples/remote.py +3 -2
  8. {ucapi-0.4.0 → ucapi-0.5.0}/examples/setup_flow.py +4 -3
  9. ucapi-0.5.0/examples/voice.json +18 -0
  10. ucapi-0.5.0/examples/voice.py +186 -0
  11. {ucapi-0.4.0 → ucapi-0.5.0}/pyproject.toml +20 -6
  12. {ucapi-0.4.0 → ucapi-0.5.0}/requirements.txt +1 -0
  13. ucapi-0.5.0/scripts/compile_protos.py +145 -0
  14. {ucapi-0.4.0 → ucapi-0.5.0}/setup.cfg +1 -0
  15. {ucapi-0.4.0 → ucapi-0.5.0}/test-requirements.txt +2 -1
  16. ucapi-0.5.0/tests/test_voice_assistant.py +71 -0
  17. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/__init__.py +10 -1
  18. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/_version.py +3 -3
  19. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/api.py +404 -19
  20. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/api_definitions.py +97 -4
  21. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/button.py +2 -2
  22. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/climate.py +6 -6
  23. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/cover.py +6 -6
  24. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/entities.py +2 -3
  25. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/entity.py +51 -12
  26. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/light.py +6 -6
  27. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/media_player.py +6 -6
  28. ucapi-0.5.0/ucapi/proto/__init__.py +7 -0
  29. ucapi-0.5.0/ucapi/proto/ucr_integration_voice.proto +83 -0
  30. ucapi-0.5.0/ucapi/proto/ucr_integration_voice_pb2.py +49 -0
  31. ucapi-0.5.0/ucapi/proto/ucr_integration_voice_pb2.pyi +77 -0
  32. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/remote.py +4 -4
  33. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/sensor.py +4 -4
  34. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/switch.py +6 -6
  35. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi/ui.py +1 -1
  36. ucapi-0.5.0/ucapi/voice_assistant.py +330 -0
  37. ucapi-0.5.0/ucapi/voice_stream.py +210 -0
  38. {ucapi-0.4.0 → ucapi-0.5.0/ucapi.egg-info}/PKG-INFO +18 -12
  39. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/SOURCES.txt +11 -1
  40. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/requires.txt +2 -1
  41. ucapi-0.4.0/docs/setup.md +0 -18
  42. {ucapi-0.4.0 → ucapi-0.5.0}/CONTRIBUTING.md +0 -0
  43. {ucapi-0.4.0 → ucapi-0.5.0}/LICENSE +0 -0
  44. {ucapi-0.4.0 → ucapi-0.5.0}/docs/code_guidelines.md +0 -0
  45. {ucapi-0.4.0 → ucapi-0.5.0}/examples/hello_integration.json +0 -0
  46. {ucapi-0.4.0 → ucapi-0.5.0}/examples/remote.json +0 -0
  47. {ucapi-0.4.0 → ucapi-0.5.0}/examples/remote_ui_page.json +0 -0
  48. {ucapi-0.4.0 → ucapi-0.5.0}/examples/setup_flow.json +0 -0
  49. {ucapi-0.4.0 → ucapi-0.5.0}/tests/test_api.py +0 -0
  50. {ucapi-0.4.0 → ucapi-0.5.0}/ucapi.egg-info/dependency_links.txt +0 -0
  51. {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.4.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.10
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
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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
- It's an alpha release (in our eyes). Breaking changes are to be expected and missing features will be continuously added.
44
- Based on our [Node.js integration library](https://github.com/unfoldedcircle/integration-node-library).
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
- ❗️**Attention:**
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 based authentication
56
+ - Token-based authentication
55
57
 
56
58
  Requirements:
57
- - Python 3.10 or newer
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 a minimal
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
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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
- It's an alpha release (in our eyes). Breaking changes are to be expected and missing features will be continuously added.
10
- Based on our [Node.js integration library](https://github.com/unfoldedcircle/integration-node-library).
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
- ❗️**Attention:**
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 based authentication
22
+ - Token-based authentication
21
23
 
22
24
  Requirements:
23
- - Python 3.10 or newer
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 a minimal
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.get_event_loop()
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.get_event_loop()
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.get_event_loop()
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.10"
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.MASTER]
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
+ """
@@ -2,6 +2,7 @@
2
2
  # Waiting for: https://github.com/pypa/pip/issues/11440
3
3
  # Workaround: use a pre-commit hook with https://github.com/scikit-image/scikit-image/blob/main/tools/generate_requirements.py
4
4
 
5
+ protobuf~=6.33.2
5
6
  pyee>=9.0
6
7
  websockets>=14.0
7
8
  zeroconf>=0.120.0