ucapi 0.1.7__tar.gz → 0.3.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 (39) hide show
  1. {ucapi-0.1.7 → ucapi-0.3.0}/CHANGELOG.md +11 -0
  2. {ucapi-0.1.7 → ucapi-0.3.0}/CONTRIBUTING.md +8 -4
  3. {ucapi-0.1.7/ucapi.egg-info → ucapi-0.3.0}/PKG-INFO +6 -5
  4. {ucapi-0.1.7 → ucapi-0.3.0}/README.md +1 -1
  5. {ucapi-0.1.7 → ucapi-0.3.0}/examples/README.md +6 -0
  6. ucapi-0.3.0/examples/remote.json +18 -0
  7. ucapi-0.3.0/examples/remote.py +214 -0
  8. ucapi-0.3.0/examples/remote_ui_page.json +65 -0
  9. {ucapi-0.1.7 → ucapi-0.3.0}/pyproject.toml +2 -2
  10. {ucapi-0.1.7 → ucapi-0.3.0}/requirements.txt +1 -1
  11. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/__init__.py +1 -0
  12. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/_version.py +9 -4
  13. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/api.py +50 -18
  14. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/button.py +2 -4
  15. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/climate.py +2 -1
  16. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/cover.py +2 -1
  17. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/entity.py +4 -2
  18. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/light.py +2 -1
  19. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/media_player.py +3 -1
  20. ucapi-0.3.0/ucapi/remote.py +168 -0
  21. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/sensor.py +2 -1
  22. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/switch.py +2 -1
  23. ucapi-0.3.0/ucapi/ui.py +191 -0
  24. {ucapi-0.1.7 → ucapi-0.3.0/ucapi.egg-info}/PKG-INFO +6 -5
  25. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi.egg-info/SOURCES.txt +5 -0
  26. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi.egg-info/requires.txt +1 -1
  27. {ucapi-0.1.7 → ucapi-0.3.0}/LICENSE +0 -0
  28. {ucapi-0.1.7 → ucapi-0.3.0}/docs/code_guidelines.md +0 -0
  29. {ucapi-0.1.7 → ucapi-0.3.0}/docs/setup.md +0 -0
  30. {ucapi-0.1.7 → ucapi-0.3.0}/examples/hello_integration.json +0 -0
  31. {ucapi-0.1.7 → ucapi-0.3.0}/examples/hello_integration.py +0 -0
  32. {ucapi-0.1.7 → ucapi-0.3.0}/examples/setup_flow.json +0 -0
  33. {ucapi-0.1.7 → ucapi-0.3.0}/examples/setup_flow.py +0 -0
  34. {ucapi-0.1.7 → ucapi-0.3.0}/setup.cfg +0 -0
  35. {ucapi-0.1.7 → ucapi-0.3.0}/test-requirements.txt +0 -0
  36. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/api_definitions.py +0 -0
  37. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi/entities.py +0 -0
  38. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi.egg-info/dependency_links.txt +0 -0
  39. {ucapi-0.1.7 → ucapi-0.3.0}/ucapi.egg-info/top_level.txt +0 -0
@@ -11,6 +11,17 @@ _Changes in the next release_
11
11
 
12
12
  ---
13
13
 
14
+ ## v0.3.0 - 2024-04-25
15
+ ### Added
16
+ - New media-player attribute MEDIA_POSITION_UPDATED_AT ([feature-and-bug-tracker#443](https://github.com/unfoldedcircle/feature-and-bug-tracker/issues/443)).
17
+ ### Changed
18
+ - Filter out base64 encoded media-player image fields in entity_states response log messages ([#22](https://github.com/unfoldedcircle/integration-python-library/issues/22)).
19
+ - Require websockets version v14 or newer.
20
+
21
+ ## v0.2.0 - 2024-04-28
22
+ ### Added
23
+ - New remote-entity type. Requires remote-core / Core Simulator version 0.43.0 or newer.
24
+
14
25
  ## v0.1.7 - 2024-03-13
15
26
  ### Changed
16
27
  - Filter out base64 encoded media-player image fields in log messages ([#17](https://github.com/unfoldedcircle/integration-python-library/issues/17)).
@@ -25,7 +25,7 @@ We love contributions from everyone.
25
25
  Either by opening a feature request describing your proposed changes before submitting code, or by contacting us on
26
26
  one of the other [feedback channels](#feedback-speech_balloon).
27
27
 
28
- Since this library is being used in integration drivers running on the embedded Remote Two device,
28
+ Since this library is being used in integration drivers running on the embedded UC Remote devices,
29
29
  we have to make sure it remains compatible with the embedded runtime environment and runs smoothly.
30
30
 
31
31
  Submitting pull requests for typos, formatting issues etc. are happily accepted and usually approved relatively quick.
@@ -36,7 +36,8 @@ With that out of the way, here's the process of creating a pull request and maki
36
36
 
37
37
  1. Fork the repo.
38
38
 
39
- 2. Make your changes or enhancements (preferably on a feature-branch).
39
+ 2. Make your changes or enhancements.
40
+ This should be done in a dedicated branch and not on the main branch to easily submit individual pull requests.
40
41
 
41
42
  Contributed code must be licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/),
42
43
  or a compatible license, if existing parts of other projects are reused (e.g. MIT licensed code).
@@ -53,13 +54,16 @@ With that out of the way, here's the process of creating a pull request and maki
53
54
 
54
55
  3. Make sure your changes follow the project's code style and the lints pass described in [Code Style](docs/code_guidelines.md).
55
56
 
56
- 4. Push to your fork.
57
+ 4. Push to your fork.
58
+ Do not include any project configuration changes in the `.idea` folder! If you are also using an IntelliJ product,
59
+ chances are that you're using a different IDE, version or other settings, which can cause issues.
60
+ For example, between IntelliJ Ultimate and PyCharm.
57
61
 
58
62
  5. Submit a pull request.
59
63
 
60
64
  At this point we will review the PR and give constructive feedback.
61
65
  This is a time for discussion and improvements, and making the necessary changes will be required before we can
62
- merge the contribution.
66
+ merge the contribution. Furthermore, all the automated checks must pass, otherwise the pull request will not be merged.
63
67
 
64
68
  ### Feedback :speech_balloon:
65
69
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: ucapi
3
- Version: 0.1.7
3
+ Version: 0.3.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
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://www.unfoldedcircle.com/
8
8
  Project-URL: Source Code, https://github.com/unfoldedcircle/integration-python-library
9
9
  Project-URL: Bug Reports, https://github.com/unfoldedcircle/integration-python-library/issues
10
10
  Project-URL: Discord, http://unfolded.chat/
11
- Project-URL: Forum, http://unfolded.community/
11
+ Project-URL: Forum, https://unfolded.community/
12
12
  Platform: any
13
13
  Classifier: Development Status :: 3 - Alpha
14
14
  Classifier: Intended Audience :: Developers
@@ -22,7 +22,7 @@ Requires-Python: >=3.10
22
22
  Description-Content-Type: text/markdown; charset=UTF-8
23
23
  License-File: LICENSE
24
24
  Requires-Dist: pyee>=9.0
25
- Requires-Dist: websockets>=11.0
25
+ Requires-Dist: websockets>=14.0
26
26
  Requires-Dist: zeroconf>=0.120.0
27
27
  Provides-Extra: testing
28
28
  Requires-Dist: pylint; extra == "testing"
@@ -30,13 +30,14 @@ Requires-Dist: flake8-docstrings; extra == "testing"
30
30
  Requires-Dist: flake8; extra == "testing"
31
31
  Requires-Dist: black; extra == "testing"
32
32
  Requires-Dist: isort; extra == "testing"
33
+ Dynamic: license-file
33
34
 
34
35
  # Python API wrapper for the UC Integration API
35
36
  [![PyPi](https://img.shields.io/pypi/v/ucapi.svg)](https://pypi.org/project/ucapi)
36
37
  [![License](https://img.shields.io/github/license/unfoldedcircle/integration-python-library.svg)](LICENSE)
37
38
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
38
39
 
39
- This library simplifies writing Python based integrations for the [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/)
40
+ This library simplifies writing Python-based integrations for the [Unfolded Circle Remote devices](https://www.unfoldedcircle.com/)
40
41
  by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api).
41
42
 
42
43
  It's an alpha release (in our eyes). Breaking changes are to be expected and missing features will be continuously added.
@@ -3,7 +3,7 @@
3
3
  [![License](https://img.shields.io/github/license/unfoldedcircle/integration-python-library.svg)](LICENSE)
4
4
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
5
5
 
6
- This library simplifies writing Python based integrations for the [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/)
6
+ This library simplifies writing Python-based integrations for the [Unfolded Circle Remote devices](https://www.unfoldedcircle.com/)
7
7
  by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api).
8
8
 
9
9
  It's an alpha release (in our eyes). Breaking changes are to be expected and missing features will be continuously added.
@@ -19,6 +19,12 @@ to start with an integration driver for the Remote Two.
19
19
 
20
20
  It defines a single push button with a callback handler. When pushed, it just prints a message in the console.
21
21
 
22
+ ## remote
23
+
24
+ The [remote.py](remote.py) example shows how to use the [remote-entity](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_remote.md).
25
+
26
+ It defines some simple commands, a custom button mapping and user interface pages for the available commands.
27
+
22
28
  ## setup_flow
23
29
 
24
30
  The [setup_flow](setup_flow.py) example shows how to define a dynamic setup flow for the driver setup.
@@ -0,0 +1,18 @@
1
+ {
2
+ "driver_id": "remote_test",
3
+ "version": "0.0.1",
4
+ "min_core_api": "0.20.0",
5
+ "name": { "en": "Remote test" },
6
+ "icon": "uc:integration",
7
+ "description": {
8
+ "en": "Minimal Python integration driver example with a remote entity."
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": "2024-04-08"
18
+ }
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ """Remote entity integration example. Bare minimum of an integration driver."""
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import sys
7
+ from typing import Any
8
+
9
+ import ucapi
10
+ from ucapi import remote
11
+ from ucapi.remote import *
12
+ from ucapi.remote import create_send_cmd, create_sequence_cmd
13
+ from ucapi.ui import (
14
+ Buttons,
15
+ DeviceButtonMapping,
16
+ Size,
17
+ UiPage,
18
+ create_btn_mapping,
19
+ create_ui_icon,
20
+ create_ui_text,
21
+ )
22
+
23
+ loop = asyncio.get_event_loop()
24
+ api = ucapi.IntegrationAPI(loop)
25
+
26
+ # Simple commands which are supported by this example remote-entity
27
+ supported_commands = [
28
+ "VOLUME_UP",
29
+ "VOLUME_DOWN",
30
+ "HOME",
31
+ "GUIDE",
32
+ "CONTEXT_MENU",
33
+ "CURSOR_UP",
34
+ "CURSOR_DOWN",
35
+ "CURSOR_LEFT",
36
+ "CURSOR_RIGHT",
37
+ "CURSOR_ENTER",
38
+ "MY_RECORDINGS",
39
+ "MY_APPS",
40
+ "REVERSE",
41
+ "PLAY",
42
+ "PAUSE",
43
+ "FORWARD",
44
+ "RECORD",
45
+ ]
46
+
47
+
48
+ async def cmd_handler(
49
+ entity: ucapi.Remote, cmd_id: str, params: dict[str, Any] | None
50
+ ) -> ucapi.StatusCodes:
51
+ """
52
+ Remote command handler.
53
+
54
+ Called by the integration-API if a command is sent to a configured remote-entity.
55
+
56
+ :param entity: remote entity
57
+ :param cmd_id: command
58
+ :param params: optional command parameters
59
+ :return: status of the command
60
+ """
61
+ print(f"Got {entity.id} command request: {cmd_id}")
62
+
63
+ state = None
64
+ match cmd_id:
65
+ case remote.Commands.ON:
66
+ state = remote.States.ON
67
+ case remote.Commands.OFF:
68
+ state = remote.States.OFF
69
+ case remote.Commands.TOGGLE:
70
+ if entity.attributes[remote.Attributes.STATE] == remote.States.OFF:
71
+ state = remote.States.ON
72
+ else:
73
+ state = remote.States.OFF
74
+ case remote.Commands.SEND_CMD:
75
+ command = params.get("command")
76
+ # It's up to the integration what to do with an unknown command.
77
+ # If the supported commands are provided as simple_commands, then it's
78
+ # easy to validate.
79
+ if command not in supported_commands:
80
+ print(f"Unknown command: {command}", file=sys.stderr)
81
+ return ucapi.StatusCodes.BAD_REQUEST
82
+
83
+ repeat = params.get("repeat", 1)
84
+ delay = params.get("delay", 0)
85
+ hold = params.get("hold", 0)
86
+ print(f"Command: {command} (repeat={repeat}, delay={delay}, hold={hold})")
87
+ case remote.Commands.SEND_CMD_SEQUENCE:
88
+ sequence = params.get("sequence")
89
+ repeat = params.get("repeat", 1)
90
+ delay = params.get("delay", 0)
91
+ hold = params.get("hold", 0)
92
+ print(
93
+ f"Command sequence: {sequence} (repeat={repeat}, delay={delay}, hold={hold})"
94
+ )
95
+ case _:
96
+ print(f"Unsupported command: {cmd_id}", file=sys.stderr)
97
+ return ucapi.StatusCodes.BAD_REQUEST
98
+
99
+ if state:
100
+ api.configured_entities.update_attributes(
101
+ entity.id, {remote.Attributes.STATE: state}
102
+ )
103
+
104
+ return ucapi.StatusCodes.OK
105
+
106
+
107
+ @api.listens_to(ucapi.Events.CONNECT)
108
+ async def on_connect() -> None:
109
+ """When the UCR2 connects, send the device state."""
110
+ # This example is ready all the time!
111
+ await api.set_device_state(ucapi.DeviceStates.CONNECTED)
112
+
113
+
114
+ def create_button_mappings() -> list[DeviceButtonMapping | dict[str, Any]]:
115
+ """Create a demo button mapping showing different composition options."""
116
+ return [
117
+ # simple short- and long-press mapping
118
+ create_btn_mapping(Buttons.HOME, "HOME", "GUIDE"),
119
+ # use channel buttons for volume control
120
+ create_btn_mapping(Buttons.CHANNEL_DOWN, "VOLUME_DOWN"),
121
+ create_btn_mapping(Buttons.CHANNEL_UP, "VOLUME_UP"),
122
+ create_btn_mapping(Buttons.DPAD_UP, "CURSOR_UP"),
123
+ create_btn_mapping(Buttons.DPAD_DOWN, "CURSOR_DOWN"),
124
+ create_btn_mapping(Buttons.DPAD_LEFT, "CURSOR_LEFT"),
125
+ create_btn_mapping(Buttons.DPAD_RIGHT, "CURSOR_RIGHT"),
126
+ # use a send command
127
+ create_btn_mapping(
128
+ Buttons.DPAD_MIDDLE, create_send_cmd("CONTEXT_MENU", hold=1000)
129
+ ),
130
+ # use a sequence command
131
+ create_btn_mapping(
132
+ Buttons.BLUE,
133
+ create_sequence_cmd(
134
+ [
135
+ "CURSOR_UP",
136
+ "CURSOR_RIGHT",
137
+ "CURSOR_DOWN",
138
+ "CURSOR_LEFT",
139
+ ],
140
+ delay=200,
141
+ ),
142
+ ),
143
+ # Safety off: don't use a DeviceButtonMapping data class but a dictionary.
144
+ # This is useful for directly reading a json configuration file.
145
+ {"button": "POWER", "short_press": {"cmd_id": "remote.toggle"}},
146
+ ]
147
+
148
+
149
+ def create_ui() -> list[UiPage | dict[str, Any]]:
150
+ """Create a demo user interface showing different composition options."""
151
+ # Safety off again: directly use json structure to read a configuration file
152
+ with open("remote_ui_page.json", "r", encoding="utf-8") as file:
153
+ main_page = json.load(file)
154
+
155
+ # On-the-fly UI composition
156
+ ui_page1 = UiPage("page1", "Main")
157
+ ui_page1.add(create_ui_text("Hello remote entity", 0, 0, size=Size(4, 1)))
158
+ ui_page1.add(create_ui_icon("uc:home", 0, 2, cmd="HOME"))
159
+ ui_page1.add(create_ui_icon("uc:up-arrow-bold", 2, 2, cmd="CURSOR_UP"))
160
+ ui_page1.add(create_ui_icon("uc:down-arrow-bold", 2, 4, cmd="CURSOR_DOWN"))
161
+ ui_page1.add(create_ui_icon("uc:left-arrow", 1, 3, cmd="CURSOR_LEFT"))
162
+ ui_page1.add(create_ui_icon("uc:right-arrow", 3, 3, cmd="CURSOR_RIGHT"))
163
+ ui_page1.add(create_ui_text("Ok", 2, 3, cmd="CURSOR_ENTER"))
164
+
165
+ ui_page2 = UiPage("page2", "Page 2")
166
+ ui_page2.add(
167
+ create_ui_text(
168
+ "Pump up the volume!",
169
+ 0,
170
+ 0,
171
+ size=Size(4, 2),
172
+ cmd=create_send_cmd("VOLUME_UP", repeat=5),
173
+ )
174
+ )
175
+ ui_page2.add(
176
+ create_ui_text(
177
+ "Test sequence",
178
+ 0,
179
+ 4,
180
+ size=Size(4, 1),
181
+ cmd=create_sequence_cmd(
182
+ [
183
+ "CURSOR_UP",
184
+ "CURSOR_RIGHT",
185
+ "CURSOR_DOWN",
186
+ "CURSOR_LEFT",
187
+ ],
188
+ delay=200,
189
+ ),
190
+ )
191
+ )
192
+ ui_page2.add(create_ui_text("On", 0, 5, cmd="on"))
193
+ ui_page2.add(create_ui_text("Off", 1, 5, cmd="off"))
194
+
195
+ return [main_page, ui_page1, ui_page2]
196
+
197
+
198
+ if __name__ == "__main__":
199
+ logging.basicConfig()
200
+
201
+ entity = ucapi.Remote(
202
+ "remote1",
203
+ "Demo remote",
204
+ [remote.Features.ON_OFF, remote.Features.TOGGLE],
205
+ {remote.Attributes.STATE: remote.States.OFF},
206
+ simple_commands=supported_commands,
207
+ button_mapping=create_button_mappings(),
208
+ ui_pages=create_ui(),
209
+ cmd_handler=cmd_handler,
210
+ )
211
+ api.available_entities.add(entity)
212
+
213
+ loop.run_until_complete(api.init("remote.json"))
214
+ loop.run_forever()
@@ -0,0 +1,65 @@
1
+ {
2
+ "page_id": "media",
3
+ "name": "Media",
4
+ "grid": { "width": 4, "height": 6 },
5
+ "items": [
6
+ {
7
+ "type": "text",
8
+ "text": "Recordings",
9
+ "command": {
10
+ "cmd_id": "MY_RECORDINGS"
11
+ },
12
+ "location": { "x": 0, "y": 2 },
13
+ "size": { "width": 2, "height": 1 }
14
+ },
15
+ {
16
+ "type": "text",
17
+ "text": "Apps",
18
+ "command": {
19
+ "cmd_id": "MY_APPS"
20
+ },
21
+ "location": { "x": 2, "y": 2 },
22
+ "size": { "width": 2, "height": 1 }
23
+ },
24
+ {
25
+ "type": "icon",
26
+ "icon": "uc:bw",
27
+ "command": {
28
+ "cmd_id": "REVERSE"
29
+ },
30
+ "location": { "x": 0, "y": 5 }
31
+ },
32
+ {
33
+ "type": "icon",
34
+ "icon": "uc:play",
35
+ "command": {
36
+ "cmd_id": "PLAY"
37
+ },
38
+ "location": { "x": 1, "y": 5 }
39
+ },
40
+ {
41
+ "type": "icon",
42
+ "icon": "uc:pause",
43
+ "command": {
44
+ "cmd_id": "PAUSE"
45
+ },
46
+ "location": { "x": 2, "y": 5 }
47
+ },
48
+ {
49
+ "type": "icon",
50
+ "icon": "uc:ff",
51
+ "command": {
52
+ "cmd_id": "FORWARD"
53
+ },
54
+ "location": { "x": 3, "y": 5 }
55
+ },
56
+ {
57
+ "type": "icon",
58
+ "icon": "uc:rec",
59
+ "command": {
60
+ "cmd_id": "RECORD"
61
+ },
62
+ "location": { "x": 2, "y": 4 }
63
+ }
64
+ ]
65
+ }
@@ -22,7 +22,7 @@ classifiers = [
22
22
  requires-python = ">=3.10"
23
23
  dependencies = [
24
24
  "pyee>=9.0",
25
- "websockets>=11.0",
25
+ "websockets>=14.0",
26
26
  "zeroconf>=0.120.0",
27
27
  ]
28
28
  dynamic = ["version"]
@@ -36,7 +36,7 @@ content-type = "text/markdown; charset=UTF-8"
36
36
  "Source Code" = "https://github.com/unfoldedcircle/integration-python-library"
37
37
  "Bug Reports" = "https://github.com/unfoldedcircle/integration-python-library/issues"
38
38
  "Discord" = "http://unfolded.chat/"
39
- "Forum" = "http://unfolded.community/"
39
+ "Forum" = "https://unfolded.community/"
40
40
 
41
41
  [project.optional-dependencies]
42
42
  testing = [
@@ -3,5 +3,5 @@
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
5
  pyee>=9.0
6
- websockets>=11.0
6
+ websockets>=14.0
7
7
  zeroconf>=0.120.0
@@ -35,6 +35,7 @@ from .climate import Climate # noqa: F401
35
35
  from .cover import Cover # noqa: F401
36
36
  from .light import Light # noqa: F401
37
37
  from .media_player import MediaPlayer # noqa: F401
38
+ from .remote import Remote # noqa: F401
38
39
  from .sensor import Sensor # noqa: F401
39
40
  from .switch import Switch # noqa: F401
40
41
 
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.1.7'
16
- __version_tuple__ = version_tuple = (0, 1, 7)
20
+ __version__ = version = '0.3.0'
21
+ __version_tuple__ = version_tuple = (0, 3, 0)
@@ -16,11 +16,11 @@ from typing import Any, Callable
16
16
  import websockets
17
17
  from pyee.asyncio import AsyncIOEventEmitter
18
18
 
19
+ # Note: websockets v14 doesn't have websockets.server anymore
20
+ from websockets import serve
21
+
19
22
  # workaround for pylint error: E0611: No name 'ConnectionClosedOK' in module 'websockets' (no-name-in-module) # noqa
20
23
  from websockets.exceptions import ConnectionClosedOK
21
-
22
- # workaround for pylint error: E1101: Module 'websockets' has no 'serve' member (no-member) # noqa
23
- from websockets.server import serve
24
24
  from zeroconf import IPVersion
25
25
  from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
26
26
 
@@ -28,6 +28,11 @@ import ucapi.api_definitions as uc
28
28
  from ucapi import media_player
29
29
  from ucapi.entities import Entities
30
30
 
31
+ try:
32
+ from ._version import version as __version__
33
+ except ImportError:
34
+ __version__ = "unknown"
35
+
31
36
  _LOG = logging.getLogger(__name__)
32
37
  _LOG.setLevel(logging.DEBUG)
33
38
 
@@ -125,9 +130,10 @@ class IntegrationAPI:
125
130
  )
126
131
 
127
132
  _LOG.info(
128
- "Driver is up: %s, version: %s, listening on: %s:%d",
133
+ "Driver is up: %s, version: %s, api: %s, listening on: %s:%d",
129
134
  self._driver_info["driver_id"],
130
135
  self._driver_info["version"],
136
+ __version__,
131
137
  host,
132
138
  port,
133
139
  )
@@ -163,6 +169,7 @@ class IntegrationAPI:
163
169
  _LOG.info("WS: Connection closed")
164
170
 
165
171
  except websockets.exceptions.ConnectionClosedError as e:
172
+ # no idea why they made code & reason deprecated...
166
173
  _LOG.info("WS: Connection closed with error %d: %s", e.code, e.reason)
167
174
 
168
175
  except websockets.exceptions.WebSocketException as e:
@@ -210,6 +217,7 @@ class IntegrationAPI:
210
217
  """
211
218
  await self._send_ws_response(websocket, req_id, "result", msg_data, status_code)
212
219
 
220
+ # pylint: disable=R0917
213
221
  async def _send_ws_response(
214
222
  self,
215
223
  websocket,
@@ -240,7 +248,9 @@ class IntegrationAPI:
240
248
 
241
249
  if websocket in self._clients:
242
250
  data_dump = json.dumps(data)
243
- _LOG.debug("[%s] ->: %s", websocket.remote_address, data_dump)
251
+ if _LOG.isEnabledFor(logging.DEBUG):
252
+ data_log = json.dumps(data) if filter_log_msg_data(data) else data_dump
253
+ _LOG.debug("[%s] ->: %s", websocket.remote_address, data_log)
244
254
  await websocket.send(data_dump)
245
255
  else:
246
256
  _LOG.error("Error sending response: connection no longer established")
@@ -261,12 +271,13 @@ class IntegrationAPI:
261
271
  data = {"kind": "event", "msg": msg, "msg_data": msg_data, "cat": category}
262
272
  data_dump = json.dumps(data)
263
273
  # filter fields
274
+ data_log = ""
264
275
  if _LOG.isEnabledFor(logging.DEBUG):
265
276
  data_log = json.dumps(data) if filter_log_msg_data(data) else data_dump
266
277
 
267
278
  for websocket in self._clients:
268
279
  if _LOG.isEnabledFor(logging.DEBUG):
269
- _LOG.debug("[%s] ->: %s", websocket.remote_address, data_log)
280
+ _LOG.debug("[%s] =>: %s", websocket.remote_address, data_log)
270
281
  try:
271
282
  await websocket.send(data_dump)
272
283
  except websockets.exceptions.WebSocketException:
@@ -469,7 +480,7 @@ class IntegrationAPI:
469
480
  self, websocket, req_id: int, msg_data: dict[str, Any] | None
470
481
  ) -> None:
471
482
  if not msg_data:
472
- _LOG.warning("Ignoring _entity_command: called with empty msg_data")
483
+ _LOG.warning("Ignoring entity command: called with empty msg_data")
473
484
  await self.acknowledge_command(
474
485
  websocket, req_id, uc.StatusCodes.BAD_REQUEST
475
486
  )
@@ -635,6 +646,7 @@ class IntegrationAPI:
635
646
  websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE
636
647
  )
637
648
 
649
+ # pylint: disable=R0917
638
650
  async def request_driver_setup_user_confirmation(
639
651
  self,
640
652
  websocket,
@@ -858,21 +870,41 @@ def filter_log_msg_data(data: dict[str, Any]) -> bool:
858
870
 
859
871
  Attention: the dictionary is modified!
860
872
 
861
- - Attributes are filtered in `data["msg_data"]["attributes"]`
873
+ - Attributes are filtered in `data["msg_data"]`:
874
+ - dict object: key `attributes`
875
+ - list object: every list item `attributes`
862
876
  - Filtered attributes: `MEDIA_IMAGE_URL`
863
877
 
864
878
  :param data: the message data dict
865
879
  :return: True if a field was filtered, False otherwise
866
880
  """
867
881
  # filter out base64 encoded images in the media player's media_image_url attribute
868
- if (
869
- "msg_data" in data
870
- and "attributes" in data["msg_data"]
871
- and media_player.Attributes.MEDIA_IMAGE_URL in data["msg_data"]["attributes"]
872
- and data["msg_data"]["attributes"][
873
- media_player.Attributes.MEDIA_IMAGE_URL
874
- ].startswith("data:")
875
- ):
876
- data["msg_data"]["attributes"][media_player.Attributes.MEDIA_IMAGE_URL] = "***"
877
- return True
882
+ if "msg_data" in data:
883
+ if (
884
+ "attributes" in data["msg_data"]
885
+ and media_player.Attributes.MEDIA_IMAGE_URL
886
+ in data["msg_data"]["attributes"]
887
+ and data["msg_data"]["attributes"][
888
+ media_player.Attributes.MEDIA_IMAGE_URL
889
+ ].startswith("data:")
890
+ ):
891
+ data["msg_data"]["attributes"][
892
+ media_player.Attributes.MEDIA_IMAGE_URL
893
+ ] = "data:***"
894
+ return True
895
+
896
+ if isinstance(data["msg_data"], list):
897
+ for item in data["msg_data"]:
898
+ if (
899
+ "attributes" in item
900
+ and media_player.Attributes.MEDIA_IMAGE_URL in item["attributes"]
901
+ and item["attributes"][
902
+ media_player.Attributes.MEDIA_IMAGE_URL
903
+ ].startswith("data:")
904
+ ):
905
+ item["attributes"][
906
+ media_player.Attributes.MEDIA_IMAGE_URL
907
+ ] = "data:***"
908
+ return True
909
+
878
910
  return False
@@ -59,8 +59,6 @@ class Button(Entity):
59
59
  EntityTypes.BUTTON,
60
60
  ["press"],
61
61
  {Attributes.STATE: States.AVAILABLE},
62
- None,
63
- None,
64
- area,
65
- cmd_handler,
62
+ area=area,
63
+ cmd_handler=cmd_handler,
66
64
  )
@@ -79,8 +79,9 @@ class Climate(Entity):
79
79
 
80
80
  See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_climate.md
81
81
  for more information.
82
- """ # noqa
82
+ """
83
83
 
84
+ # pylint: disable=R0917
84
85
  def __init__(
85
86
  self,
86
87
  identifier: str,
@@ -78,8 +78,9 @@ class Cover(Entity):
78
78
 
79
79
  See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_cover.md
80
80
  for more information.
81
- """ # noqa
81
+ """
82
82
 
83
+ # pylint: disable=R0917
83
84
  def __init__(
84
85
  self,
85
86
  identifier: str,
@@ -23,6 +23,7 @@ class EntityTypes(str, Enum):
23
23
  CLIMATE = "climate"
24
24
  LIGHT = "light"
25
25
  MEDIA_PLAYER = "media_player"
26
+ REMOTE = "remote"
26
27
  SENSOR = "sensor"
27
28
  SWITCH = "switch"
28
29
 
@@ -35,6 +36,7 @@ class Entity:
35
36
  for more information.
36
37
  """
37
38
 
39
+ # pylint: disable=R0917
38
40
  def __init__(
39
41
  self,
40
42
  identifier: str,
@@ -42,8 +44,8 @@ class Entity:
42
44
  entity_type: EntityTypes,
43
45
  features: list[str],
44
46
  attributes: dict[str, Any],
45
- device_class: str | None,
46
- options: dict[str, Any] | None,
47
+ device_class: str | None = None,
48
+ options: dict[str, Any] | None = None,
47
49
  area: str | None = None,
48
50
  cmd_handler: CommandHandler = None,
49
51
  ):
@@ -65,8 +65,9 @@ class Light(Entity):
65
65
 
66
66
  See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_light.md
67
67
  for more information.
68
- """ # noqa
68
+ """
69
69
 
70
+ # pylint: disable=R0917
70
71
  def __init__(
71
72
  self,
72
73
  identifier: str,
@@ -78,6 +78,7 @@ class Attributes(str, Enum):
78
78
  MUTED = "muted"
79
79
  MEDIA_DURATION = "media_duration"
80
80
  MEDIA_POSITION = "media_position"
81
+ MEDIA_POSITION_UPDATED_AT = "media_position_updated_at"
81
82
  MEDIA_TYPE = "media_type"
82
83
  MEDIA_IMAGE_URL = "media_image_url"
83
84
  MEDIA_TITLE = "media_title"
@@ -193,8 +194,9 @@ class MediaPlayer(Entity):
193
194
 
194
195
  See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_media_player.md
195
196
  for more information.
196
- """ # noqa
197
+ """
197
198
 
199
+ # pylint: disable=R0917
198
200
  def __init__(
199
201
  self,
200
202
  identifier: str,
@@ -0,0 +1,168 @@
1
+ """
2
+ Remote entity definitions.
3
+
4
+ :copyright: (c) 2024 by Unfolded Circle ApS.
5
+ :license: MPL-2.0, see LICENSE for more details.
6
+ """
7
+
8
+ import dataclasses
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+ from ucapi.api_definitions import CommandHandler
13
+ from ucapi.entity import Entity, EntityTypes
14
+ from ucapi.ui import DeviceButtonMapping, EntityCommand, UiPage
15
+
16
+
17
+ class States(str, Enum):
18
+ """Remote entity states."""
19
+
20
+ UNAVAILABLE = "UNAVAILABLE"
21
+ UNKNOWN = "UNKNOWN"
22
+ ON = "ON"
23
+ OFF = "OFF"
24
+
25
+
26
+ class Features(str, Enum):
27
+ """Remote entity features."""
28
+
29
+ ON_OFF = "on_off"
30
+ TOGGLE = "toggle"
31
+ SEND_CMD = "send_cmd"
32
+
33
+
34
+ class Attributes(str, Enum):
35
+ """Remote entity attributes."""
36
+
37
+ STATE = "state"
38
+
39
+
40
+ class Commands(str, Enum):
41
+ """Remote entity commands."""
42
+
43
+ ON = "on"
44
+ OFF = "off"
45
+ TOGGLE = "toggle"
46
+ SEND_CMD = "send_cmd"
47
+ SEND_CMD_SEQUENCE = "send_cmd_sequence"
48
+
49
+
50
+ class Options(str, Enum):
51
+ """Remote entity options."""
52
+
53
+ SIMPLE_COMMANDS = "simple_commands"
54
+ BUTTON_MAPPING = "button_mapping"
55
+ USER_INTERFACE = "user_interface"
56
+
57
+
58
+ def create_send_cmd(
59
+ command: str,
60
+ delay: int | None = None,
61
+ repeat: int | None = None,
62
+ hold: int | None = None,
63
+ ) -> EntityCommand:
64
+ """
65
+ Create a remote send command.
66
+
67
+ :param command: command to send.
68
+ :param delay: optional delay in milliseconds after the command or between repeats.
69
+ :param repeat: optional repeat count of the command.
70
+ :param hold: optional hold time in milliseconds.
71
+ :return: the created EntityCommand.
72
+ """
73
+ params = {"command": command}
74
+ if delay:
75
+ params["delay"] = delay
76
+ if repeat:
77
+ params["repeat"] = repeat
78
+ if hold:
79
+ params["hold"] = hold
80
+ return EntityCommand(Commands.SEND_CMD.value, params)
81
+
82
+
83
+ def create_sequence_cmd(
84
+ sequence: list[str],
85
+ delay: int | None = None,
86
+ repeat: int | None = None,
87
+ ) -> EntityCommand:
88
+ """
89
+ Create a remote send sequence command.
90
+
91
+ :param sequence: list of simple commands.
92
+ :param delay: optional delay in milliseconds between the commands in the sequence.
93
+ :param repeat: optional repeat count of the sequence.
94
+ :return: the created EntityCommand.
95
+ """
96
+ params = {"sequence": sequence}
97
+ if delay:
98
+ params["delay"] = delay
99
+ if repeat:
100
+ params["repeat"] = repeat
101
+ return EntityCommand(Commands.SEND_CMD_SEQUENCE.value, params)
102
+
103
+
104
+ def _list_items_asdict(obj: list[Any]):
105
+ """Convert a list with (mixed) dataclass items to dictionary mapping items."""
106
+ return list(
107
+ map(
108
+ lambda item: (
109
+ dataclasses.asdict(item) if dataclasses.is_dataclass(item) else item
110
+ ),
111
+ obj,
112
+ )
113
+ )
114
+
115
+
116
+ class Remote(Entity):
117
+ """
118
+ Remote entity class.
119
+
120
+ See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_remote.md
121
+ for more information.
122
+ """
123
+
124
+ # pylint: disable=R0917
125
+ def __init__(
126
+ self,
127
+ identifier: str,
128
+ name: str | dict[str, str],
129
+ features: list[Features],
130
+ attributes: dict[str, Any],
131
+ simple_commands: list[str] | None = None,
132
+ button_mapping: list[DeviceButtonMapping | dict[str, Any]] | None = None,
133
+ ui_pages: list[UiPage | dict[str, Any]] | None = None,
134
+ area: str | None = None,
135
+ cmd_handler: CommandHandler = None,
136
+ ):
137
+ """
138
+ Create remote entity instance.
139
+
140
+ :param identifier: entity identifier
141
+ :param name: friendly name
142
+ :param features: remote features
143
+ :param attributes: remote attributes
144
+ :param simple_commands: optional list of supported remote command identifiers
145
+ :param button_mapping: optional command mapping of physical buttons
146
+ Either with DeviceButtonMapping items or plain dictionary items.
147
+ :param ui_pages: optional user interface page definitions.
148
+ Either with UiPage items or plain dictionary items.
149
+ :param area: optional area
150
+ :param cmd_handler: handler for entity commands
151
+ """
152
+ options: dict[str, Any] = {}
153
+ if simple_commands:
154
+ options["simple_commands"] = simple_commands
155
+ if button_mapping:
156
+ options["button_mapping"] = _list_items_asdict(button_mapping)
157
+ if ui_pages:
158
+ options["user_interface"] = {"pages": _list_items_asdict(ui_pages)}
159
+ super().__init__(
160
+ identifier,
161
+ name,
162
+ EntityTypes.REMOTE,
163
+ features,
164
+ attributes,
165
+ options=options,
166
+ area=area,
167
+ cmd_handler=cmd_handler,
168
+ )
@@ -64,8 +64,9 @@ class Sensor(Entity):
64
64
 
65
65
  See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_sensor.md
66
66
  for more information.
67
- """ # noqa
67
+ """
68
68
 
69
+ # pylint: disable=R0917
69
70
  def __init__(
70
71
  self,
71
72
  identifier: str,
@@ -61,8 +61,9 @@ class Switch(Entity):
61
61
 
62
62
  See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_switch.md
63
63
  for more information.
64
- """ # noqa
64
+ """
65
65
 
66
+ # pylint: disable=R0917
66
67
  def __init__(
67
68
  self,
68
69
  identifier: str,
@@ -0,0 +1,191 @@
1
+ """
2
+ User interface definitions.
3
+
4
+ :copyright: (c) 2024 by Unfolded Circle ApS.
5
+ :license: MPL-2.0, see LICENSE for more details.
6
+ """
7
+
8
+ from dataclasses import KW_ONLY, dataclass
9
+ from enum import Enum
10
+
11
+
12
+ @dataclass
13
+ class EntityCommand:
14
+ """Remote command definition for a button mapping or UI page definition."""
15
+
16
+ cmd_id: str
17
+ params: dict[str, str | int | list[str]] | None = None
18
+
19
+
20
+ class Buttons(str, Enum):
21
+ """Physical buttons."""
22
+
23
+ BACK = "BACK"
24
+ HOME = "HOME"
25
+ VOICE = "VOICE"
26
+ VOLUME_UP = "VOLUME_UP"
27
+ VOLUME_DOWN = "VOLUME_DOWN"
28
+ MUTE = "MUTE"
29
+ DPAD_UP = "DPAD_UP"
30
+ DPAD_DOWN = "DPAD_DOWN"
31
+ DPAD_LEFT = "DPAD_LEFT"
32
+ DPAD_RIGHT = "DPAD_RIGHT"
33
+ DPAD_MIDDLE = "DPAD_MIDDLE"
34
+ GREEN = "GREEN"
35
+ YELLOW = "YELLOW"
36
+ RED = "RED"
37
+ BLUE = "BLUE"
38
+ CHANNEL_UP = "CHANNEL_UP"
39
+ CHANNEL_DOWN = "CHANNEL_DOWN"
40
+ PREV = "PREV"
41
+ PLAY = "PLAY"
42
+ NEXT = "NEXT"
43
+ POWER = "POWER"
44
+
45
+
46
+ @dataclass
47
+ class DeviceButtonMapping:
48
+ """Physical button command mapping."""
49
+
50
+ button: str
51
+ """Physical button identifier. See Buttons for Remote Two identifiers."""
52
+ short_press: EntityCommand | None = None
53
+ """Short press command of the button."""
54
+ long_press: EntityCommand | None = None
55
+ """Long press command of the button."""
56
+
57
+
58
+ def create_btn_mapping(
59
+ button: Buttons,
60
+ short: str | EntityCommand | None = None,
61
+ long: str | EntityCommand | None = None,
62
+ ) -> DeviceButtonMapping:
63
+ """
64
+ Create a physical button command mapping.
65
+
66
+ :param button: physical button identifier.
67
+ :param short: associated short-press command to the physical button.
68
+ A string parameter corresponds to a simple command, whereas an
69
+ ``EntityCommand`` allows to customize the command.
70
+ :param long: associated long-press command to the physical button
71
+ :return: the created DeviceButtonMapping
72
+ """
73
+ if isinstance(short, str):
74
+ short = EntityCommand(short)
75
+ if isinstance(long, str):
76
+ long = EntityCommand(long)
77
+ return DeviceButtonMapping(button.value, short_press=short, long_press=long)
78
+
79
+
80
+ @dataclass
81
+ class Size:
82
+ """Item size in the button grid. Default size if not specified: 1x1."""
83
+
84
+ width: int = 1
85
+ height: int = 1
86
+
87
+
88
+ @dataclass
89
+ class Location:
90
+ """Button placement in the grid with 0-based coordinates."""
91
+
92
+ x: int
93
+ y: int
94
+
95
+
96
+ @dataclass
97
+ class UiItem:
98
+ """
99
+ A user interface item is either an icon or text.
100
+
101
+ - Icon and text items can be static or linked to a command specified in the
102
+ `command` field.
103
+ - Default size is 1x1 if not specified.
104
+ """
105
+
106
+ type: str
107
+ location: Location
108
+ size: Size | None = None
109
+ icon: str | None = None
110
+ text: str | None = None
111
+ command: EntityCommand | None = None
112
+
113
+
114
+ def create_ui_text(
115
+ text: str,
116
+ x: int,
117
+ y: int,
118
+ size: Size | None = None,
119
+ cmd: str | EntityCommand | None = None,
120
+ ) -> UiItem:
121
+ """
122
+ Create a text UI item.
123
+
124
+ :param text: the text to show in the UI item.
125
+ :param x: x-position, 0-based.
126
+ :param y: y-position, 0-based.
127
+ :param size: item size, defaults to 1 x 1 if not specified.
128
+ :param cmd: associated command to the text item. A string parameter corresponds to
129
+ a simple command, whereas an ``EntityCommand`` allows to customize the
130
+ command for example with number of repeats.
131
+ :return: the created UiItem
132
+ """
133
+ if isinstance(cmd, str):
134
+ cmd = EntityCommand(cmd)
135
+ return UiItem("text", Location(x, y), size=size, text=text, command=cmd)
136
+
137
+
138
+ def create_ui_icon(
139
+ icon: str,
140
+ x: int,
141
+ y: int,
142
+ size: Size | None = None,
143
+ cmd: str | EntityCommand | None = None,
144
+ ) -> UiItem:
145
+ """
146
+ Create an icon UI item.
147
+
148
+ The icon identifier consists of a prefix and a resource identifier,
149
+ separated by `:`. Available prefixes:
150
+ - `uc:` - integrated icon font
151
+ - `custom:` - custom resource
152
+
153
+ :param icon: the icon identifier of the icon to show in the UI item.
154
+ :param x: x-position, 0-based.
155
+ :param y: y-position, 0-based.
156
+ :param size: item size, defaults to 1 x 1 if not specified.
157
+ :param cmd: associated command to the text item. A string parameter corresponds to
158
+ a simple command, whereas an ``EntityCommand`` allows to customize the
159
+ command for example with number of repeats.
160
+ :return: the created UiItem
161
+ """
162
+ if isinstance(cmd, str):
163
+ cmd = EntityCommand(cmd)
164
+ return UiItem("icon", Location(x, y), size=size, icon=icon, command=cmd)
165
+
166
+
167
+ @dataclass
168
+ class UiPage:
169
+ """
170
+ Definition of a complete user interface page.
171
+
172
+ Default grid size is 4x6 if not specified.
173
+ """
174
+
175
+ page_id: str
176
+ name: str
177
+ _: KW_ONLY
178
+ grid: Size = None
179
+ items: list[UiItem] = None
180
+
181
+ def __post_init__(self):
182
+ """Post initialization to set required fields."""
183
+ # grid and items are required Integration-API fields
184
+ if self.grid is None:
185
+ self.grid = Size(4, 6)
186
+ if self.items is None:
187
+ self.items = []
188
+
189
+ def add(self, item: UiItem):
190
+ """Append the given UiItem to the page items."""
191
+ self.items.append(item)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: ucapi
3
- Version: 0.1.7
3
+ Version: 0.3.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
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://www.unfoldedcircle.com/
8
8
  Project-URL: Source Code, https://github.com/unfoldedcircle/integration-python-library
9
9
  Project-URL: Bug Reports, https://github.com/unfoldedcircle/integration-python-library/issues
10
10
  Project-URL: Discord, http://unfolded.chat/
11
- Project-URL: Forum, http://unfolded.community/
11
+ Project-URL: Forum, https://unfolded.community/
12
12
  Platform: any
13
13
  Classifier: Development Status :: 3 - Alpha
14
14
  Classifier: Intended Audience :: Developers
@@ -22,7 +22,7 @@ Requires-Python: >=3.10
22
22
  Description-Content-Type: text/markdown; charset=UTF-8
23
23
  License-File: LICENSE
24
24
  Requires-Dist: pyee>=9.0
25
- Requires-Dist: websockets>=11.0
25
+ Requires-Dist: websockets>=14.0
26
26
  Requires-Dist: zeroconf>=0.120.0
27
27
  Provides-Extra: testing
28
28
  Requires-Dist: pylint; extra == "testing"
@@ -30,13 +30,14 @@ Requires-Dist: flake8-docstrings; extra == "testing"
30
30
  Requires-Dist: flake8; extra == "testing"
31
31
  Requires-Dist: black; extra == "testing"
32
32
  Requires-Dist: isort; extra == "testing"
33
+ Dynamic: license-file
33
34
 
34
35
  # Python API wrapper for the UC Integration API
35
36
  [![PyPi](https://img.shields.io/pypi/v/ucapi.svg)](https://pypi.org/project/ucapi)
36
37
  [![License](https://img.shields.io/github/license/unfoldedcircle/integration-python-library.svg)](LICENSE)
37
38
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
38
39
 
39
- This library simplifies writing Python based integrations for the [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/)
40
+ This library simplifies writing Python-based integrations for the [Unfolded Circle Remote devices](https://www.unfoldedcircle.com/)
40
41
  by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api).
41
42
 
42
43
  It's an alpha release (in our eyes). Breaking changes are to be expected and missing features will be continuously added.
@@ -11,6 +11,9 @@ docs/setup.md
11
11
  examples/README.md
12
12
  examples/hello_integration.json
13
13
  examples/hello_integration.py
14
+ examples/remote.json
15
+ examples/remote.py
16
+ examples/remote_ui_page.json
14
17
  examples/setup_flow.json
15
18
  examples/setup_flow.py
16
19
  ucapi/__init__.py
@@ -24,8 +27,10 @@ ucapi/entities.py
24
27
  ucapi/entity.py
25
28
  ucapi/light.py
26
29
  ucapi/media_player.py
30
+ ucapi/remote.py
27
31
  ucapi/sensor.py
28
32
  ucapi/switch.py
33
+ ucapi/ui.py
29
34
  ucapi.egg-info/PKG-INFO
30
35
  ucapi.egg-info/SOURCES.txt
31
36
  ucapi.egg-info/dependency_links.txt
@@ -1,5 +1,5 @@
1
1
  pyee>=9.0
2
- websockets>=11.0
2
+ websockets>=14.0
3
3
  zeroconf>=0.120.0
4
4
 
5
5
  [testing]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes