diff --git a/.github/workflows/python-code-format.yml b/.github/workflows/python-code-format.yml
index c71e7ad..da13f31 100644
--- a/.github/workflows/python-code-format.yml
+++ b/.github/workflows/python-code-format.yml
@@ -46,4 +46,4 @@ jobs:
python -m isort ucapi/. --check --verbose
- name: Check code formatting with black
run: |
- python -m black ucapi --check --verbose --line-length 120
+ python -m black ucapi --check --verbose
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..df77493
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/integration-python-library.iml b/.idea/integration-python-library.iml
new file mode 100644
index 0000000..bbb1500
--- /dev/null
+++ b/.idea/integration-python-library.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..1e35929
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..cc371ca
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Check_Black_style.xml b/.idea/runConfigurations/Check_Black_style.xml
new file mode 100644
index 0000000..68437b5
--- /dev/null
+++ b/.idea/runConfigurations/Check_Black_style.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Check_flake8.xml b/.idea/runConfigurations/Check_flake8.xml
new file mode 100644
index 0000000..5a19db2
--- /dev/null
+++ b/.idea/runConfigurations/Check_flake8.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Check_isort.xml b/.idea/runConfigurations/Check_isort.xml
new file mode 100644
index 0000000..344b25c
--- /dev/null
+++ b/.idea/runConfigurations/Check_isort.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Check_pylint.xml b/.idea/runConfigurations/Check_pylint.xml
new file mode 100644
index 0000000..852aeaa
--- /dev/null
+++ b/.idea/runConfigurations/Check_pylint.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run_isort.xml b/.idea/runConfigurations/Run_isort.xml
new file mode 100644
index 0000000..e44bb7f
--- /dev/null
+++ b/.idea/runConfigurations/Run_isort.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 8a04d39..612b221 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
# Python API wrapper for the UC Integration API
+[![PyPi](https://img.shields.io/pypi/v/ucapi.svg)](https://pypi.org/project/ucapi)
+[![License](https://img.shields.io/github/license/unfoldedcircle/ucapi.svg)](LICENSE)
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
This library simplifies writing Python based integrations for the [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/)
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api).
@@ -19,6 +22,12 @@ Not yet supported:
Requirements:
- Python 3.10 or newer
+## Installation
+
+Use pip:
+```shell
+pip3 install ucapi
+```
## Usage
Install build tools:
diff --git a/docs/code_guidelines.md b/docs/code_guidelines.md
index abbea37..8470923 100644
--- a/docs/code_guidelines.md
+++ b/docs/code_guidelines.md
@@ -20,7 +20,7 @@ Note: once is implemented, the requir
### Linting
```shell
-python -m pylint ucapi
+python3 -m pylint ucapi
```
- The tool is configured in `.pylintrc`.
@@ -34,7 +34,7 @@ Linting integration in PyCharm/IntelliJ IDEA:
Import statements must be sorted with [isort](https://pycqa.github.io/isort/):
```shell
-python -m isort ucapi/.
+python3 -m isort ucapi/.
```
- The tool is configured in `pyproject.toml` to use the `black` code-formatting profile.
@@ -44,7 +44,7 @@ python -m isort ucapi/.
Source code is formatted with the [Black code formatting tool](https://github.com/psf/black):
```shell
-python -m black ucapi --line-length 120
+python3 -m black ucapi --line-length 120
```
PyCharm/IntelliJ IDEA integration:
@@ -52,15 +52,14 @@ PyCharm/IntelliJ IDEA integration:
2. Configure:
- Python interpreter
- Use Black formatter: `On code reformat` & optionally `On save`
-- Arguments: `--line-length 120`
## Verify
The following tests are run as GitHub action for each push on the main branch and for pull requests.
They can also be run anytime on a local developer machine:
```shell
-python -m pylint ucapi
-python -m flake8 ucapi --count --show-source --statistics
-python -m isort ucapi/. --check --verbose
-python -m black ucapi --check --verbose --line-length 120
+python3 -m pylint ucapi
+python3 -m flake8 ucapi --count --show-source --statistics
+python3 -m isort ucapi/. --check --verbose
+python3 -m black ucapi --check --verbose
```
diff --git a/examples/hello_integration.py b/examples/hello_integration.py
index 6c0ffa6..15c5722 100644
--- a/examples/hello_integration.py
+++ b/examples/hello_integration.py
@@ -10,7 +10,9 @@
api = ucapi.IntegrationAPI(loop)
-async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None) -> ucapi.StatusCodes:
+async def cmd_handler(
+ entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None
+) -> ucapi.StatusCodes:
"""
Push button command handler.
@@ -28,7 +30,7 @@ async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any]
@api.listens_to(ucapi.Events.CONNECT)
async def on_connect() -> None:
- """When the remote connects, we just set the device state. We are ready all the time!"""
+ # When the remote connects, we just set the device state. We are ready all the time!
await api.set_device_state(ucapi.DeviceStates.CONNECTED)
diff --git a/examples/setup_flow.py b/examples/setup_flow.py
index d9fde2b..eefc522 100644
--- a/examples/setup_flow.py
+++ b/examples/setup_flow.py
@@ -16,7 +16,8 @@ async def driver_setup_handler(msg: ucapi.SetupDriver) -> ucapi.SetupAction:
Either start the setup process or handle the provided user input data.
- :param msg: the setup driver request object, either DriverSetupRequest or UserDataResponse
+ :param msg: the setup driver request object, either DriverSetupRequest,
+ UserDataResponse or UserConfirmationResponse
:return: the setup action on how to continue
"""
if isinstance(msg, ucapi.DriverSetupRequest):
@@ -31,16 +32,19 @@ async def driver_setup_handler(msg: ucapi.SetupDriver) -> ucapi.SetupAction:
return ucapi.SetupError()
-async def handle_driver_setup(msg: ucapi.DriverSetupRequest) -> ucapi.RequestUserInput | ucapi.SetupError:
+async def handle_driver_setup(
+ msg: ucapi.DriverSetupRequest,
+) -> ucapi.SetupAction:
"""
Start driver setup.
Initiated by Remote Two to set up the driver.
- :param msg: not used, value(s) of input fields in the first setup screen. See setup_data_schema in driver metadata.
+ :param msg: value(s) of input fields in the first setup screen.
:return: the setup action on how to continue
"""
- # for our demo we clear everything, a real driver might have to handle this differently
+ # For our demo we simply clear everything!
+ # A real driver might have to handle this differently
api.available_entities.clear()
api.configured_entities.clear()
@@ -141,7 +145,9 @@ async def handle_user_data_response(msg: ucapi.UserDataResponse) -> ucapi.SetupA
return ucapi.SetupError()
-async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None) -> ucapi.StatusCodes:
+async def cmd_handler(
+ entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None
+) -> ucapi.StatusCodes:
"""
Push button command handler.
@@ -159,7 +165,7 @@ async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any]
@api.listens_to(ucapi.Events.CONNECT)
async def on_connect() -> None:
- """When the remote connects, we just set the device state. We are ready all the time!"""
+ # When the remote connects, we just set the device state. We are ready all the time!
await api.set_device_state(ucapi.DeviceStates.CONNECTED)
diff --git a/pyproject.toml b/pyproject.toml
index 328d464..5b245d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -63,7 +63,7 @@ profile = "black"
overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
[tool.pylint.format]
-max-line-length = "120"
+max-line-length = "88"
[tool.pylint.MASTER]
ignore-paths = [
@@ -73,16 +73,20 @@ ignore-paths = [
[tool.pylint."messages control"]
# Reasons disabled:
+# duplicate-code - unavoidable
# global-statement - not yet considered
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
+# line-too-long - handled with black & flake8
# fixme - refactoring in progress
disable = [
+ "duplicate-code",
"global-statement",
"too-many-arguments",
"too-many-instance-attributes",
"too-few-public-methods",
+ "line-too-long",
"fixme"
]
diff --git a/setup.cfg b/setup.cfg
index 6deafc2..2bcd70e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,2 @@
[flake8]
-max-line-length = 120
+max-line-length = 88
diff --git a/ucapi/__init__.py b/ucapi/__init__.py
index 85e6158..1a25e0f 100644
--- a/ucapi/__init__.py
+++ b/ucapi/__init__.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
"""
Integration driver library for Remote Two.
diff --git a/ucapi/api.py b/ucapi/api.py
index 666edf7..1565bf3 100644
--- a/ucapi/api.py
+++ b/ucapi/api.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""
Integration driver API for Remote Two.
@@ -17,10 +16,10 @@
import websockets
from pyee.asyncio import AsyncIOEventEmitter
-# workaround for pylint error: E0611: No name 'ConnectionClosedOK' in module 'websockets' (no-name-in-module)
+# workaround for pylint error: E0611: No name 'ConnectionClosedOK' in module 'websockets' (no-name-in-module) # noqa
from websockets.exceptions import ConnectionClosedOK
-# workaround for pylint error: E1101: Module 'websockets' has no 'serve' member (no-member)
+# workaround for pylint error: E1101: Module 'websockets' has no 'serve' member (no-member) # noqa
from websockets.server import serve
from zeroconf import IPVersion
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
@@ -50,7 +49,9 @@ def __init__(self, loop: AbstractEventLoop):
self._server_task = None
self._clients = set()
- self._config_dir_path: str = os.getenv("UC_CONFIG_HOME") or os.getenv("HOME") or "./"
+ self._config_dir_path: str = (
+ os.getenv("UC_CONFIG_HOME") or os.getenv("HOME") or "./"
+ )
self._available_entities = Entities("available", self._loop)
self._configured_entities = Entities("configured", self._loop)
@@ -58,17 +59,22 @@ def __init__(self, loop: AbstractEventLoop):
# Setup event loop
asyncio.set_event_loop(self._loop)
- async def init(self, driver_path: str, setup_handler: uc.SetupHandler | None = None):
+ async def init(
+ self, driver_path: str, setup_handler: uc.SetupHandler | None = None
+ ):
"""
Load driver configuration and start integration-API WebSocket server.
:param driver_path: path to configuration file
- :param setup_handler: optional driver setup handler if the driver metadata contains a setup_data_schema object
+ :param setup_handler: optional driver setup handler if the driver metadata
+ contains a setup_data_schema object
"""
self._driver_path = driver_path
self._setup_handler = setup_handler
- self._configured_entities.add_listener(uc.Events.ENTITY_ATTRIBUTES_UPDATED, self._on_entity_attributes_updated)
+ self._configured_entities.add_listener(
+ uc.Events.ENTITY_ATTRIBUTES_UPDATED, self._on_entity_attributes_updated
+ )
# Load driver config
with open(self._driver_path, "r", encoding="utf-8") as file:
@@ -77,18 +83,24 @@ async def init(self, driver_path: str, setup_handler: uc.SetupHandler | None = N
# publishing interface, defaults to "0.0.0.0" if not set
interface = os.getenv("UC_INTEGRATION_INTERFACE")
port = int(
- os.getenv("UC_INTEGRATION_HTTP_PORT") or self._driver_info["port"] if "port" in self._driver_info else 9090
+ os.getenv("UC_INTEGRATION_HTTP_PORT") or self._driver_info["port"]
+ if "port" in self._driver_info
+ else 9090
)
_adjust_driver_url(self._driver_info, port)
- disable_mdns_publish = os.getenv("UC_DISABLE_MDNS_PUBLISH", "false").lower() in ("true", "1")
+ disable_mdns_publish = os.getenv(
+ "UC_DISABLE_MDNS_PUBLISH", "false"
+ ).lower() in ("true", "1")
if disable_mdns_publish is False:
# Setup zeroconf service info
name = f"{self._driver_info['driver_id']}._uc-integration._tcp.local."
hostname = local_hostname()
- driver_name = _get_default_language_string(self._driver_info["name"], "Unknown driver")
+ driver_name = _get_default_language_string(
+ self._driver_info["name"], "Unknown driver"
+ )
_LOG.debug("Publishing driver: name=%s, host=%s:%d", name, hostname, port)
@@ -108,7 +120,9 @@ async def init(self, driver_path: str, setup_handler: uc.SetupHandler | None = N
await zeroconf.async_register_service(info)
host = interface if interface is not None else "0.0.0.0"
- self._server_task = self._loop.create_task(self._start_web_socket_server(host, port))
+ self._server_task = self._loop.create_task(
+ self._start_web_socket_server(host, port)
+ )
_LOG.info(
"Driver is up: %s, version: %s, listening on: %s:%d",
@@ -125,7 +139,9 @@ async def _on_entity_attributes_updated(self, entity_id, entity_type, attributes
"attributes": attributes,
}
- await self._broadcast_ws_event(uc.WsMsgEvents.ENTITY_CHANGE, data, uc.EventCategory.ENTITY)
+ await self._broadcast_ws_event(
+ uc.WsMsgEvents.ENTITY_CHANGE, data, uc.EventCategory.ENTITY
+ )
async def _start_web_socket_server(self, host: str, port: int) -> None:
async with serve(self._handle_ws, host, port):
@@ -157,7 +173,9 @@ async def _handle_ws(self, websocket) -> None:
_LOG.info("WS: Client removed")
self._events.emit(uc.Events.DISCONNECT)
- async def _send_ok_result(self, websocket, req_id: int, msg_data: dict[str, Any] | list | None = None) -> None:
+ async def _send_ok_result(
+ self, websocket, req_id: int, msg_data: dict[str, Any] | list | None = None
+ ) -> None:
"""
Send a WebSocket success message with status code OK.
@@ -168,7 +186,9 @@ async def _send_ok_result(self, websocket, req_id: int, msg_data: dict[str, Any]
Raises:
websockets.ConnectionClosed: When the connection is closed.
"""
- await self._send_ws_response(websocket, req_id, "result", msg_data, uc.StatusCodes.OK)
+ await self._send_ws_response(
+ websocket, req_id, "result", msg_data, uc.StatusCodes.OK
+ )
async def _send_error_result(
self,
@@ -225,11 +245,14 @@ async def _send_ws_response(
else:
_LOG.error("Error sending response: connection no longer established")
- async def _broadcast_ws_event(self, msg: str, msg_data: dict[str, Any], category: uc.EventCategory) -> None:
+ async def _broadcast_ws_event(
+ self, msg: str, msg_data: dict[str, Any], category: uc.EventCategory
+ ) -> None:
"""
Send the given event-message to all connected WebSocket clients.
- If a client is no longer connected, a log message is printed and the remaining clients are notified.
+ If a client is no longer connected, a log message is printed and the remaining
+ clients are notified.
:param msg: event message name
:param msg_data: message data payload
@@ -245,7 +268,9 @@ async def _broadcast_ws_event(self, msg: str, msg_data: dict[str, Any], category
except websockets.exceptions.WebSocketException:
pass
- async def _send_ws_event(self, websocket, msg: str, msg_data: dict[str, Any], category: uc.EventCategory) -> None:
+ async def _send_ws_event(
+ self, websocket, msg: str, msg_data: dict[str, Any], category: uc.EventCategory
+ ) -> None:
"""
Send an event-message to the given WebSocket client.
@@ -277,17 +302,31 @@ async def _process_ws_message(self, websocket, message) -> None:
if kind == "req":
if req_id is None:
- _LOG.warning("Ignoring request message with missing 'req_id': %s", message)
+ _LOG.warning(
+ "Ignoring request message with missing 'req_id': %s", message
+ )
else:
await self._handle_ws_request_msg(websocket, msg, req_id, msg_data)
elif kind == "event":
self._handle_ws_event_msg(msg, msg_data)
- async def _handle_ws_request_msg(self, websocket, msg: str, req_id: int, msg_data: dict[str, Any] | None) -> None:
+ async def _handle_ws_request_msg(
+ self, websocket, msg: str, req_id: int, msg_data: dict[str, Any] | None
+ ) -> None:
if msg == uc.WsMessages.GET_DRIVER_VERSION:
- await self._send_ws_response(websocket, req_id, uc.WsMsgEvents.DRIVER_VERSION, self.get_driver_version())
+ await self._send_ws_response(
+ websocket,
+ req_id,
+ uc.WsMsgEvents.DRIVER_VERSION,
+ self.get_driver_version(),
+ )
elif msg == uc.WsMessages.GET_DEVICE_STATE:
- await self._send_ws_response(websocket, req_id, uc.WsMsgEvents.DEVICE_STATE, {"state": self.device_state})
+ await self._send_ws_response(
+ websocket,
+ req_id,
+ uc.WsMsgEvents.DEVICE_STATE,
+ {"state": self.device_state},
+ )
elif msg == uc.WsMessages.GET_AVAILABLE_ENTITIES:
available_entities = self._available_entities.get_all()
await self._send_ws_response(
@@ -313,7 +352,9 @@ async def _handle_ws_request_msg(self, websocket, msg: str, req_id: int, msg_dat
await self._unsubscribe_events(msg_data)
await self._send_ok_result(websocket, req_id)
elif msg == uc.WsMessages.GET_DRIVER_METADATA:
- await self._send_ws_response(websocket, req_id, uc.WsMsgEvents.DRIVER_METADATA, self._driver_info)
+ await self._send_ws_response(
+ websocket, req_id, uc.WsMsgEvents.DRIVER_METADATA, self._driver_info
+ )
elif msg == uc.WsMessages.SETUP_DRIVER:
if not await self._setup_driver(websocket, req_id, msg_data):
await self.driver_setup_error(websocket)
@@ -358,7 +399,9 @@ async def set_device_state(self, state: uc.DeviceStates) -> None:
self._state = state
await self._broadcast_ws_event(
- uc.WsMsgEvents.DEVICE_STATE, {"state": self.device_state}, uc.EventCategory.DEVICE
+ uc.WsMsgEvents.DEVICE_STATE,
+ {"state": self.device_state},
+ uc.EventCategory.DEVICE,
)
async def _subscribe_events(self, msg_data: dict[str, Any] | None) -> None:
@@ -392,49 +435,74 @@ async def _unsubscribe_events(self, msg_data: dict[str, Any] | None) -> bool:
return res
- async def _entity_command(self, websocket, req_id: int, msg_data: dict[str, Any] | None) -> None:
+ async def _entity_command(
+ self, websocket, req_id: int, msg_data: dict[str, Any] | None
+ ) -> None:
if not msg_data:
_LOG.warning("Ignoring _entity_command: called with empty msg_data")
- await self.acknowledge_command(websocket, req_id, uc.StatusCodes.BAD_REQUEST)
+ await self.acknowledge_command(
+ websocket, req_id, uc.StatusCodes.BAD_REQUEST
+ )
return
entity_id = msg_data["entity_id"] if "entity_id" in msg_data else None
cmd_id = msg_data["cmd_id"] if "cmd_id" in msg_data else None
if entity_id is None or cmd_id is None:
_LOG.warning("Ignoring command: missing entity_id or cmd_id")
- await self.acknowledge_command(websocket, req_id, uc.StatusCodes.BAD_REQUEST)
+ await self.acknowledge_command(
+ websocket, req_id, uc.StatusCodes.BAD_REQUEST
+ )
return
entity = self.configured_entities.get(entity_id)
if entity is None:
- _LOG.warning("Cannot execute command '%s' for '%s': no configured entity found", cmd_id, entity_id)
+ _LOG.warning(
+ "Cannot execute command '%s' for '%s': no configured entity found",
+ cmd_id,
+ entity_id,
+ )
await self.acknowledge_command(websocket, req_id, uc.StatusCodes.NOT_FOUND)
return
- result = await entity.command(cmd_id, msg_data["params"] if "params" in msg_data else None)
+ result = await entity.command(
+ cmd_id, msg_data["params"] if "params" in msg_data else None
+ )
await self.acknowledge_command(websocket, req_id, result)
- async def _setup_driver(self, websocket, req_id: int, msg_data: dict[str, Any] | None) -> bool:
+ async def _setup_driver(
+ self, websocket, req_id: int, msg_data: dict[str, Any] | None
+ ) -> bool:
if msg_data is None or "setup_data" not in msg_data:
_LOG.warning("Aborting setup_driver: called with empty msg_data")
- # TODO test if both messages are required, or if we first have to ack with OK, then abort the setup
- await self.acknowledge_command(websocket, req_id, uc.StatusCodes.BAD_REQUEST)
+ # TODO test if both messages are required,
+ # or if we first have to ack with OK, then abort the setup
+ await self.acknowledge_command(
+ websocket, req_id, uc.StatusCodes.BAD_REQUEST
+ )
return False
# make sure integration driver installed a setup handler
if not self._setup_handler:
- _LOG.error("Received setup_driver request, but no setup handler provided by the driver!")
- await self.acknowledge_command(websocket, req_id, uc.StatusCodes.SERVICE_UNAVAILABLE)
+ _LOG.error(
+ "Received setup_driver request, but no setup handler provided by the driver!" # noqa
+ )
+ await self.acknowledge_command(
+ websocket, req_id, uc.StatusCodes.SERVICE_UNAVAILABLE
+ )
return False
await self.acknowledge_command(websocket, req_id)
try:
- action = await self._setup_handler(uc.DriverSetupRequest(msg_data["setup_data"]))
+ action = await self._setup_handler(
+ uc.DriverSetupRequest(msg_data["setup_data"])
+ )
if isinstance(action, uc.RequestUserInput):
await self.driver_setup_progress(websocket)
- await self.request_driver_setup_user_input(websocket, action.title, action.settings)
+ await self.request_driver_setup_user_input(
+ websocket, action.title, action.settings
+ )
return True
if isinstance(action, uc.RequestUserConfirmation):
await self.driver_setup_progress(websocket)
@@ -447,34 +515,50 @@ async def _setup_driver(self, websocket, req_id: int, msg_data: dict[str, Any] |
return True
# error action is left, handled below
- except Exception as ex: # pylint: disable=W0718 # TODO define custom exceptions?
+ # TODO define custom exceptions?
+ except Exception as ex: # pylint: disable=W0718
_LOG.error("Exception in setup handler, aborting setup! Exception: %s", ex)
return False
- async def _set_driver_user_data(self, websocket, req_id: int, msg_data: dict[str, Any] | None) -> bool:
+ async def _set_driver_user_data(
+ self, websocket, req_id: int, msg_data: dict[str, Any] | None
+ ) -> bool:
if not self._setup_handler:
- # TODO test if both messages are required, or if we first have to ack with OK, then abort the setup
- await self.acknowledge_command(websocket, req_id, uc.StatusCodes.SERVICE_UNAVAILABLE)
+ # TODO test if both messages are required,
+ # or if we first have to ack with OK, then abort the setup
+ await self.acknowledge_command(
+ websocket, req_id, uc.StatusCodes.SERVICE_UNAVAILABLE
+ )
return False
if "input_values" in msg_data or "confirm" in msg_data:
await self.acknowledge_command(websocket, req_id)
await self.driver_setup_progress(websocket)
else:
- _LOG.warning("Unsupported set_driver_user_data payload received: %s", msg_data)
- await self.acknowledge_command(websocket, req_id, uc.StatusCodes.BAD_REQUEST)
+ _LOG.warning(
+ "Unsupported set_driver_user_data payload received: %s", msg_data
+ )
+ await self.acknowledge_command(
+ websocket, req_id, uc.StatusCodes.BAD_REQUEST
+ )
return False
try:
action = uc.SetupError()
if "input_values" in msg_data:
- action = await self._setup_handler(uc.UserDataResponse(msg_data["input_values"]))
+ action = await self._setup_handler(
+ uc.UserDataResponse(msg_data["input_values"])
+ )
elif "confirm" in msg_data:
- action = await self._setup_handler(uc.UserConfirmationResponse(msg_data["confirm"]))
+ action = await self._setup_handler(
+ uc.UserConfirmationResponse(msg_data["confirm"])
+ )
if isinstance(action, uc.RequestUserInput):
- await self.request_driver_setup_user_input(websocket, action.title, action.settings)
+ await self.request_driver_setup_user_input(
+ websocket, action.title, action.settings
+ )
return True
if isinstance(action, uc.RequestUserConfirmation):
await self.request_driver_setup_user_confirmation(
@@ -486,7 +570,9 @@ async def _set_driver_user_data(self, websocket, req_id: int, msg_data: dict[str
return True
# error action is left, handled below
- except Exception as ex: # pylint: disable=W0718 # TODO define custom exceptions?
+ except (
+ Exception # pylint: disable=W0718 # TODO define custom exceptions?
+ ) as ex:
_LOG.error("Exception in setup handler, aborting setup! Exception: %s", ex)
return False
@@ -517,7 +603,9 @@ async def driver_setup_progress(self, websocket) -> None:
"""
data = {"event_type": "SETUP", "state": "SETUP"}
- await self._send_ws_event(websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE)
+ await self._send_ws_event(
+ websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE
+ )
async def request_driver_setup_user_confirmation(
self,
@@ -552,7 +640,9 @@ async def request_driver_setup_user_confirmation(
},
}
- await self._send_ws_event(websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE)
+ await self._send_ws_event(
+ websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE
+ )
async def request_driver_setup_user_input(
self, websocket, title: str | dict[str, str], settings: dict[str, Any] | list
@@ -561,22 +651,30 @@ async def request_driver_setup_user_input(
data = {
"event_type": "SETUP",
"state": "WAIT_USER_ACTION",
- "require_user_action": {"input": {"title": _to_language_object(title), "settings": settings}},
+ "require_user_action": {
+ "input": {"title": _to_language_object(title), "settings": settings}
+ },
}
- await self._send_ws_event(websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE)
+ await self._send_ws_event(
+ websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE
+ )
async def driver_setup_complete(self, websocket) -> None:
"""Send a driver setup complete event to Remote Two."""
data = {"event_type": "STOP", "state": "OK"}
- await self._send_ws_event(websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE)
+ await self._send_ws_event(
+ websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE
+ )
async def driver_setup_error(self, websocket, error="OTHER") -> None:
"""Send a driver setup error event to Remote Two."""
data = {"event_type": "STOP", "state": "ERROR", "error": error}
- await self._send_ws_event(websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE)
+ await self._send_ws_event(
+ websocket, uc.WsMsgEvents.DRIVER_SETUP_CHANGE, data, uc.EventCategory.DEVICE
+ )
def add_listener(self, event: uc.Events, f: Callable) -> None:
"""
@@ -587,8 +685,13 @@ def add_listener(self, event: uc.Events, f: Callable) -> None:
"""
self._events.add_listener(event, f)
- def listens_to(self, event: str) -> Callable[[Callable], Callable]:
- """Return a decorator which will register the decorated function to the specified event."""
+ def listens_to(self, event: uc.Events) -> Callable[[Callable], Callable]:
+ """
+ Register the given event.
+
+ :return: a decorator which will register the decorated function to the specified
+ event.
+ """
def on(f: Callable) -> Callable:
self._events.add_listener(event, f)
@@ -653,7 +756,9 @@ def _to_language_object(text: str | dict[str, str] | None) -> dict[str, str] | N
return text
-def _get_default_language_string(text: str | dict[str, str] | None, default_text="Undefined") -> str:
+def _get_default_language_string(
+ text: str | dict[str, str] | None, default_text="Undefined"
+) -> str:
if text is None:
return default_text
@@ -709,7 +814,11 @@ def local_hostname() -> str:
:return: the local hostname
"""
# Override option for announced hostname.
- # Useful on macOS where it's broken for several years: local hostname keeps on changing!
+ # Useful on macOS where it's broken for several years:
+ # local hostname keeps on changing with a increasing number suffix!
# https://apple.stackexchange.com/questions/189350/my-macs-hostname-keeps-adding-a-2-to-the-end
- return os.getenv("UC_MDNS_LOCAL_HOSTNAME") or f"{socket.gethostname().split('.', 1)[0]}.local."
+ return (
+ os.getenv("UC_MDNS_LOCAL_HOSTNAME")
+ or f"{socket.gethostname().split('.', 1)[0]}.local."
+ )
diff --git a/ucapi/api_definitions.py b/ucapi/api_definitions.py
index 3cc9b0c..db42f0e 100644
--- a/ucapi/api_definitions.py
+++ b/ucapi/api_definitions.py
@@ -108,13 +108,15 @@ class DriverSetupRequest(SetupDriver):
"""
Start driver setup.
- If a driver includes a ``setup_data_schema`` object in its driver metadata, it enables the dynamic driver setup
- process. The setup process can be a simple "start-confirm-done" between the Remote Two and the integration
- driver, or a fully dynamic, multistep process with user interactions, where the user has to provide additional
- data or select different options.
-
- If the initial setup page contains input fields and not just text, the input values are returned in the
- ``setup_data`` dictionary. The key is the input field identifier, value contains the input value.
+ If a driver includes a ``setup_data_schema`` object in its driver metadata, it
+ enables the dynamic driver setup process. The setup process can be a simple
+ "start-confirm-done" between the Remote Two and the integration driver, or a fully
+ dynamic, multistep process with user interactions, where the user has to provide
+ additional data or select different options.
+
+ If the initial setup page contains input fields and not just text, the input values
+ are returned in the ``setup_data`` dictionary. The key is the input field
+ identifier, value contains the input value.
"""
setup_data: dict[str, str]
@@ -123,10 +125,10 @@ class DriverSetupRequest(SetupDriver):
@dataclass
class UserDataResponse(SetupDriver):
"""
- Provide requested driver setup data to the integration driver during a setup process.
+ Provide requested driver setup data to the integration driver in a setup process.
- The ``input_values`` dictionary contains the user input data. The key is the input field identifier,
- value contains the input value.
+ The ``input_values`` dictionary contains the user input data. The key is the input
+ field identifier, value contains the input value.
"""
input_values: dict[str, str]
@@ -134,7 +136,13 @@ class UserDataResponse(SetupDriver):
@dataclass
class UserConfirmationResponse(SetupDriver):
- """Provide user confirmation response to the integration driver during a setup process."""
+ """
+ Provide user confirmation response to the integration driver in a setup process.
+
+ The ``confirm`` field is set to ``true`` if the user had to perform an action like
+ pressing a button on a device and then confirms the action with continuing the
+ setup process.
+ """
confirm: bool
@@ -172,7 +180,9 @@ class SetupComplete(SetupAction):
"""Setup action to complete a successful setup process."""
-CommandHandler: TypeAlias = Callable[[Any, str, dict[str, Any] | None], Awaitable[StatusCodes]]
+CommandHandler: TypeAlias = Callable[
+ [Any, str, dict[str, Any] | None], Awaitable[StatusCodes]
+]
SetupHandler: TypeAlias = Callable[[SetupDriver], Awaitable[SetupAction]]
diff --git a/ucapi/button.py b/ucapi/button.py
index 6289877..f78d8ad 100644
--- a/ucapi/button.py
+++ b/ucapi/button.py
@@ -36,7 +36,7 @@ class Button(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_button.md
for more information.
- """
+ """ # noqa
def __init__(
self,
diff --git a/ucapi/climate.py b/ucapi/climate.py
index 6574881..2a56e32 100644
--- a/ucapi/climate.py
+++ b/ucapi/climate.py
@@ -1,4 +1,3 @@
-# pylint: disable=R0801
"""
Climate entity definitions.
@@ -80,7 +79,7 @@ class Climate(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_climate.md
for more information.
- """
+ """ # noqa
def __init__(
self,
@@ -106,5 +105,13 @@ def __init__(
:param cmd_handler: handler for entity commands
"""
super().__init__(
- identifier, name, EntityTypes.CLIMATE, features, attributes, device_class, options, area, cmd_handler
+ identifier,
+ name,
+ EntityTypes.CLIMATE,
+ features,
+ attributes,
+ device_class,
+ options,
+ area,
+ cmd_handler,
)
diff --git a/ucapi/cover.py b/ucapi/cover.py
index b41578a..145a941 100644
--- a/ucapi/cover.py
+++ b/ucapi/cover.py
@@ -1,4 +1,3 @@
-# pylint: disable=R0801
"""
Cover entity definitions.
@@ -79,7 +78,7 @@ class Cover(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_cover.md
for more information.
- """
+ """ # noqa
def __init__(
self,
diff --git a/ucapi/entities.py b/ucapi/entities.py
index 96eca9c..efa1ebb 100644
--- a/ucapi/entities.py
+++ b/ucapi/entities.py
@@ -67,7 +67,11 @@ def remove(self, entity_id: str) -> bool:
def update_attributes(self, entity_id: str, attributes: dict[str, Any]) -> bool:
"""Update entity attributes."""
if entity_id not in self._storage:
- _LOG.debug("[%s] cannot update entity attributes '%s': not found", self._id, entity_id)
+ _LOG.debug(
+ "[%s] cannot update entity attributes '%s': not found",
+ self._id,
+ entity_id,
+ )
return False
for key in attributes:
@@ -107,7 +111,11 @@ def get_all(self) -> list[dict[str, Any]]:
return entities
async def get_states(self) -> list[dict[str, Any]]:
- """Get all entity information with entity_id, entity_type, device_id, attributes."""
+ """
+ Get all entity state information.
+
+ The returned dict includes: entity_id, entity_type, device_id, attributes.
+ """
entities = []
for entity in self._storage.values():
diff --git a/ucapi/entity.py b/ucapi/entity.py
index fc5e335..67c9976 100644
--- a/ucapi/entity.py
+++ b/ucapi/entity.py
@@ -72,7 +72,9 @@ def __init__(
_LOG.debug("Created %s entity: %s", self.entity_type.value, self.id)
- async def command(self, cmd_id: str, params: dict[str, Any] | None = None) -> StatusCodes:
+ async def command(
+ self, cmd_id: str, params: dict[str, Any] | None = None
+ ) -> StatusCodes:
"""
Execute entity command with the installed command handler.
@@ -86,6 +88,9 @@ async def command(self, cmd_id: str, params: dict[str, Any] | None = None) -> St
return await self._cmd_handler(self, cmd_id, params)
_LOG.warning(
- "No command handler for %s: cannot execute command '%s' %s", self.id, cmd_id, params if params else ""
+ "No command handler for %s: cannot execute command '%s' %s",
+ self.id,
+ cmd_id,
+ params if params else "",
)
return StatusCodes.NOT_IMPLEMENTED
diff --git a/ucapi/light.py b/ucapi/light.py
index b7f56c1..f7f22f6 100644
--- a/ucapi/light.py
+++ b/ucapi/light.py
@@ -1,4 +1,3 @@
-# pylint: disable=R0801
"""
Light entity definitions.
@@ -66,7 +65,7 @@ class Light(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_light.md
for more information.
- """
+ """ # noqa
def __init__(
self,
@@ -92,5 +91,13 @@ def __init__(
:param cmd_handler: handler for entity commands
"""
super().__init__(
- identifier, name, EntityTypes.LIGHT, features, attributes, device_class, options, area, cmd_handler
+ identifier,
+ name,
+ EntityTypes.LIGHT,
+ features,
+ attributes,
+ device_class,
+ options,
+ area,
+ cmd_handler,
)
diff --git a/ucapi/media_player.py b/ucapi/media_player.py
index 06415e7..36b335b 100644
--- a/ucapi/media_player.py
+++ b/ucapi/media_player.py
@@ -1,4 +1,3 @@
-# pylint: disable=R0801
"""
Media-player entity definitions.
@@ -154,7 +153,7 @@ class MediaPlayer(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_media_player.md
for more information.
- """
+ """ # noqa
def __init__(
self,
@@ -180,5 +179,13 @@ def __init__(
:param cmd_handler: handler for entity commands
"""
super().__init__(
- identifier, name, EntityTypes.MEDIA_PLAYER, features, attributes, device_class, options, area, cmd_handler
+ identifier,
+ name,
+ EntityTypes.MEDIA_PLAYER,
+ features,
+ attributes,
+ device_class,
+ options,
+ area,
+ cmd_handler,
)
diff --git a/ucapi/sensor.py b/ucapi/sensor.py
index c098175..6da7fb3 100644
--- a/ucapi/sensor.py
+++ b/ucapi/sensor.py
@@ -1,4 +1,3 @@
-# pylint: disable=R0801
"""
Sensor entity definitions.
@@ -65,7 +64,7 @@ class Sensor(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_sensor.md
for more information.
- """
+ """ # noqa
def __init__(
self,
diff --git a/ucapi/switch.py b/ucapi/switch.py
index 8574767..70c26f2 100644
--- a/ucapi/switch.py
+++ b/ucapi/switch.py
@@ -61,7 +61,7 @@ class Switch(Entity):
See https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_switch.md
for more information.
- """
+ """ # noqa
def __init__(
self,