Skip to content

Websocket requests including supported entity types#47

Open
albaintor wants to merge 44 commits intounfoldedcircle:mainfrom
albaintor:websocket_requests
Open

Websocket requests including supported entity types#47
albaintor wants to merge 44 commits intounfoldedcircle:mainfrom
albaintor:websocket_requests

Conversation

@albaintor
Copy link
Copy Markdown
Contributor

This is the PR I have modified from #46

@albaintor
Copy link
Copy Markdown
Contributor Author

albaintor commented Feb 1, 2026

All done and tested : #47

To test it and include it in the integrations :
python -m build on PR repository => this will generate an whl file in the dist folder
Just copy this whl in the source folder of the integration and edit requirements.txt with the right path like this :

ucapi
 file:./src/ucapi-0.5.4.dev3+geb80630e7.d20260201-py3-none-any.whl

This is what I have done for Kodi and it now works on Remote 2

Thanks @kennymc-c for the work, chatgpt helped me also for the rest :-)

@kennymc-c
Copy link
Copy Markdown

I'm now getting timeouts no matter how high I set the timeout value. Right after the timeout the response is shown in the log. Could something blocking the response?

@albaintor
Copy link
Copy Markdown
Contributor Author

I have fixed some stuff in the meantime : after authentication the integration is requesting supported entity types. Normally the remote should respond quickly to this request.

@albaintor
Copy link
Copy Markdown
Contributor Author

@zehnm could advise otherwise

@kennymc-c
Copy link
Copy Markdown

kennymc-c commented Feb 2, 2026

Also with the current version I still get a timeout

2026-02-02 14:11:19.083 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] ->: {'kind': 'req', 'id': 1, 'msg': 'get_supported_entity_types'}
2026-02-02 14:11:19.092 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] <-: {"kind":"req","id":3,"msg":"setup_driver","msg_data":{"reconfigure":false,"setup_data":{}}}
2026-02-02 14:11:19.092 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] ->: {'kind': 'resp', 'req_id': 3, 'code': 200, 'msg': 'result', 'msg_data': {}}
2026-02-02 14:11:19.092 | DEBUG    | config         | Stored setup_reconfigure: False into runtime storage
2026-02-02 14:11:19.092 | INFO     | setup          | Starting basic setup for a new device
2026-02-02 14:11:19.092 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] ->: {'kind': 'req', 'id': 2, 'msg': 'get_version'}
2026-02-02 14:11:24.089 | ERROR    | ucapi.api      | [('192.168.1.115', 37322)] Timeout waiting for response to get_supported_entity_types (req_id=1) 
2026-02-02 14:11:24.090 | ERROR    | ucapi.api      | [('192.168.1.115', 37322)] Unable to retrieve entity types 
2026-02-02 14:11:24.102 | ERROR    | ucapi.api      | [('192.168.1.115', 37322)] Timeout waiting for response to get_version (req_id=2) 
2026-02-02 14:11:24.102 | ERROR    | setup          | Could not retrieve remote information
2026-02-02 14:11:24.102 | DEBUG    | config         | Stored setup_step: basic into runtime storage
2026-02-02 14:11:24.103 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] ->: {'kind': 'event', 'msg': <WsMsgEvents.DRIVER_SETUP_CHANGE: 'driver_setup_change'>, 'msg_data': {'event_type': 'SETUP', 'state': 'SETUP'}, 'cat': <EventCategory.DEVICE: 'DEVICE'>}
2026-02-02 14:11:24.103 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] ->: {'kind': 'event', 'msg': <WsMsgEvents.DRIVER_SETUP_CHANGE: 'driver_setup_change'>, 'msg_data': {'event_type': 'SETUP', 'state': 'WAIT_USER_ACTION', 'require_user_action': {'input': {'title': {'en': 'Basic Setup', 'de': 'Allgemeine Einrichtung'}, 'settings': [{'id': 'notes', 'label': {'en': 'Basic Setup', 'de': 'Allgemeine Einrichtung'}, 'field': {'label': {'value': {'en': 'If you leave the ip field empty an attempt is made to **automatically find projectors**     via the SDAP advertisement service in your local network. \n\nData is only send **every 30 seconds by default**.     The search runs for 31 seconds to find all devices in your network.', 'de': 'Wenn du das Feld für die IP-Adresse leer lässt, wird versucht den Projektor per SDAP **automatisch     in deinem lokalen Netzwerk zu finden**\n\nDie Daten werden **standardmäßig nur alle 30 Sekunden** vom Projektor gesendet.     Die Suche läuft 31 Sekunden, um alle Geräte im Netzwerk zu finden.'}}}}, {'id': 'ip', 'label': {'en': 'Projector IP (leave empty to use auto discovery):', 'de': 'Projektor-IP (leer lassen zur automatischen Erkennung):'}, 'field': {'text': {'value': ''}}}, {'id': 'adcp_password', 'label': {'en': 'ADCP / WebUI password (only required if ADCP authentication is turned on):', 'de': 'ADCP / WebUI-Passwort (nur erforderlich bei aktivierter ADCP-Authentifizierung):'}, 'field': {'text': {'value': ''}}}, {'id': 'notes', 'label': {'en': 'Advanced settings', 'de': 'Erweiterte Einstellungen'}, 'field': {'label': {'value': {'en': 'If you have changed the default ADCP or SDAP ports, change timeouts     or the poller intervals you need to configure them in the advanced settings', 'de': 'Wenn du die ADCP oder SDAP Standard-Ports geändert hast, Timeouts oder     Poller-Intervalle ändern möchtest, musst du diese in den erweiterten Einstellungen konfigurieren'}}}}, {'id': 'advanced_settings', 'label': {'en': 'Configure advanced settings', 'de': 'Erweiterte Einstellungen konfigurieren'}, 'field': {'checkbox': {'value': False}}}]}}}, 'cat': <EventCategory.DEVICE: 'DEVICE'>}
2026-02-02 14:11:24.104 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] <-: {"id":4,"kind":"req","msg":"get_driver_version"}
2026-02-02 14:11:24.104 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] ->: {'kind': 'resp', 'req_id': 4, 'code': 200, 'msg': <WsMsgEvents.DRIVER_VERSION: 'driver_version'>, 'msg_data': {'name': 'Sony Projector (ADCP)', 'version': {'api': '0.25.0', 'driver': '1.4.2'}}}
2026-02-02 14:11:24.104 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] <-: {"kind":"resp","req_id":1,"msg":"supported_entity_types","code":200,"msg_data":["button","switch","climate","cover","light","media_player","sensor","activity","macro","remote","ir_emitter"]}
2026-02-02 14:11:24.104 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] WS: No pending map for resp_id=1 (late resp?)
2026-02-02 14:11:24.104 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] <-: {"kind":"resp","req_id":2,"msg":"version","code":200,"msg_data":{"address":"xxx","api":"0.13.0","core":"0.64.0-bt","device_name":"Remote Two","hostname":"RemoteTwo-dxxx.local","model":"UCR2","os":"2.7.0","ui":"0.62.2"}}
2026-02-02 14:11:24.104 | DEBUG    | ucapi.api      | [('192.168.1.115', 37322)] WS: No pending map for resp_id=2 (late resp?)

@albaintor
Copy link
Copy Markdown
Contributor Author

The problem comes from the remote core : it cannot accept any requests from the integration if it itself is waiting for a response from the integration after sending a request.
In your example :

  1. Remote -> integration : setup flow request
  2. Integration -> remote : get supported entity types => IGNORED by the remote
  3. integration -> remote : response to setup flow request
  4. NOW the remote can accept requests from the integration (and this is the case next in your example where it succeeds to retrieve entity types)

This is the reason why I chose to ignore the timeout in the ucapi and try again later, but the right way would be to accept requests at anytime on the core side. Don't know if this is easy to implement

@kennymc-c
Copy link
Copy Markdown

I'm back at my original non-blocking version that works fine in the same case. I also added a timeout to prevent a loop while waiting for an answer. As the timeout is 30 seconds just like the setup timeout I should get a possible exception before the setup itself times out.

@albaintor
Copy link
Copy Markdown
Contributor Author

Can you be more specific : in your version if I remember correctly the requests took the control over the websocket server until the response occurs ? In that case no others requests from the remote would be handled.

Anyway I don't understand how the result would differ from the new implementation : if you are in the setup flow and the remote expects a response from the integration, I don't get how you could send a request and get a response

@albaintor
Copy link
Copy Markdown
Contributor Author

Ok I have found the cause : the core is not in cause. The websocket handle of requests is blocked until a response is done.
I created tasks for each message to handle. Now we can make requests at anytime, including during the setup flow

@kennymc-c
Copy link
Copy Markdown

kennymc-c commented Feb 3, 2026

While it now works during the setup process I think I'm running into a race condition after a driver restart when adding the available entitites after receiving the connect event. The remote is asking for the available entitites but they have not yet all been added. Also the connect event is sent right after the setup again so I don't think it's the right place for this anway.
I used to add all available entities before the connect event but this doesn't seem to work anymore as I get a no active websocket connection error when using the request.

@albaintor
Copy link
Copy Markdown
Contributor Author

While it now works during the setup process I think I'm running into a race condition after a driver restart when adding the available entitites after receiving the connect event. The remote is asking for the available entitites but they have not yet all been added. Also the connect event is sent right after the setup again so I don't think it's the right place for this anway. I used to add all available entities before the connect event but this doesn't seem to work anymore as I get a no active websocket connection error when using the request.

This could be due to this code but I don't understand the problem : the supported entity types are extracted right after authentication but before the connected event so before the request for available entities. This should introduce just a request/response but no race condition... do you have logs ?

            await self._authenticate(websocket, True)

            # Request supported entity types from remote
            asyncio.create_task(self._update_supported_entity_types(websocket))

            self._events.emit(uc.Events.CLIENT_CONNECTED)

@kennymc-c
Copy link
Copy Markdown

According to my log the authentication respone comes after my request:

2026-02-03 21:19:48.468 | ERROR    | driver         | No active websocket connection!
2026-02-03 21:19:48.468 | ERROR    | driver         | Failed to get supported entity types from remote
2026-02-03 21:19:48.468 | WARNING  | driver         | Skip adding select entities as available entities
2026-02-03 21:19:48.873 | INFO     | ucapi.api      | WS: Client added: ('192.168.1.115', 43868)
2026-02-03 21:19:48.874 | DEBUG    | ucapi.api      | [('192.168.1.115', 43868)] ->: {'kind': 'resp', 'req_id': 0, 'code': 200, 'msg': <WsMessages.AUTHENTICATION: 'authentication'>, 'msg_data': {}}
2026-02-03 21:19:48.874 | DEBUG    | driver         | Remote websocket client connected to this integration websockets server
2026-02-03 21:19:48.874 | DEBUG    | driver         | There are currently 1 websocket clients connected to this integration websockets server
2026-02-03 21:19:48.875 | DEBUG    | ucapi.api      | [('192.168.1.115', 43868)] ->: {'kind': 'req', 'id': 1, 'msg': 'get_supported_entity_types'}
2026-02-03 21:19:48.889 | DEBUG    | ucapi.api      | [('192.168.1.115', 43868)] <-: {"kind":"resp","req_id":1,"msg":"supported_entity_types","code":200,"msg_data":["button","switch","climate","cover","light","media_player","sensor","activity","macro","remote","ir_emitter"]}
2026-02-03 21:19:48.889 | DEBUG    | ucapi.api      | [('192.168.1.115', 43868)] Supported entity types [<EntityTypes.BUTTON: 'button'>, <EntityTypes.SWITCH: 'switch'>, <EntityTypes.CLIMATE: 'climate'>, <EntityTypes.COVER: 'cover'>, <EntityTypes.LIGHT: 'light'>, <EntityTypes.MEDIA_PLAYER: 'media_player'>, <EntityTypes.SENSOR: 'sensor'>, <EntityTypes.REMOTE: 'remote'>, <EntityTypes.IR_EMITTER: 'ir_emitter'>]
2026-02-03 21:19:48.892 | DEBUG    | ucapi.api      | [('192.168.1.115', 43868)] <-: {"kind":"req","id":11,"msg":"subscribe_events","msg_data":{"entity_ids":["......
_LOG.debug("Starting driver")

await setup.init()

try:
    config.Setup.load()
    config.Devices.load()
except (OSError, Exception) as e:
    _LOG.critical(e)
    _LOG.critical("Stopping integration driver")
    raise SystemExit(0) from e

if config.Setup.get("setup_complete"):

	#Add media player, remote and sensor entities

    try:
        supported_entity_types = await api.get_supported_entity_types()
    except Exception as e:
        error_msg = str(e)
        if error_msg:
            _LOG.error(error_msg)
            _LOG.error("Failed to get supported entity types from remote")
            _LOG.warning("Skip adding select entities as available entities")
        else:
            _LOG.error("Failed to get supported entity types from remote")
            _LOG.warning("Skip adding select entities as available entities")
        supported_entity_types = []
    else:
        if ucapi.EntityTypes.SELECT in supported_entity_types:
            for select_type in config.Setup.get("select_types"):
                await selects.add(device_id, select_type)
        else:
            _LOG.warning("The currently installed remote firmware does not support select entities")
            _LOG.warning("Skip adding select entities as available entities")
            _LOG.info("Please update the remote firmware to version 2.8.3 or newer that supports select entities")

@albaintor
Copy link
Copy Markdown
Contributor Author

Normally the driver should be started before the setup flow like this

await api.init("driver.json", setup_flow.driver_setup_handler)

Anyway, I have moved the call to entity types extraction inside the request for available entities. The problem should not occur anymore hopefully

@zehnm zehnm self-requested a review February 4, 2026 12:51
@zehnm
Copy link
Copy Markdown
Contributor

zehnm commented Feb 4, 2026

Is this ready for review? Otherwise please mark this PR as draft if there are more updates or fixes coming.
I should be able to look at it towards the end of the week.

@albaintor albaintor marked this pull request as draft February 4, 2026 15:26
@albaintor
Copy link
Copy Markdown
Contributor Author

Is this ready for review? Otherwise please mark this PR as draft if there are more updates or fixes coming. I should be able to look at it towards the end of the week.

We thought it was but after testings it needed some adjustements. I will test the modifications before switching it back to review

Comment thread ucapi/api.py Outdated
await self._handle_ws_event_msg(websocket, msg, msg_data)
elif kind == "resp":
# Response to a previously sent request
# Some implementations use "req_id", others use "id"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you see an id field in a response message?
Response messages may not contain an id field, but a req_id for the corresponding request message.

Copy link
Copy Markdown
Contributor

@zehnm zehnm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a better solution for the incoming message processing that keeps message ordering.
I can start working on that after the next firmware release. Otherwise let me know if you'd like to implement it.

Comment thread ucapi/api.py Outdated
if isinstance(message, str):
# JSON text message
await self._process_ws_message(websocket, message)
asyncio.create_task(self._process_ws_message(websocket, message))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After further consideration, creating a dedicated task per received JSON message is not a good solution and can create more issues than it solves.

  • Increases concurrency without a bound.
  • That makes message ordering non-deterministic. Especially for the press-and-hold key commands and events this could easily introduce hard to trace bugs.
  • Multiple tasks are accessing the shared connection state in the class, easily creating more issues.

I suggest a proper solution with a consumer/producer model with queues. One task reads from the socket and enqueues messages, one task writes outbound messages, and worker tasks or inline logic process messages from the queue. This keeps websocket access centralized, gives you explicit backpressure, and makes ordering and shutdown behavior much easier to control.

A simple demo implementation based on the Websockets library documentation https://websockets.readthedocs.io/en/15.0.1/howto/patterns.html

import asyncio
from websockets import serve
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError

async def consumer(ws, incoming: asyncio.Queue):
    try:
        async for message in ws:
            await incoming.put(message)
    finally:
        await incoming.put(None)  # sentinel

async def producer(ws, outgoing: asyncio.Queue):
    try:
        while True:
            msg = await outgoing.get()
            if msg is None:
                break
            await ws.send(msg)
    except (ConnectionClosedOK, ConnectionClosedError):
        pass

async def router(incoming: asyncio.Queue, outgoing: asyncio.Queue):
    while True:
        msg = await incoming.get()
        if msg is None:
            break
        # process message (possibly slow)
        reply = await process_message(msg)
        # enqueue reply without directly touching ws
        await outgoing.put(reply)

async def handler(ws):
    incoming = asyncio.Queue()
    outgoing = asyncio.Queue()

    tasks = [
        asyncio.create_task(consumer(ws, incoming)),
        asyncio.create_task(producer(ws, outgoing)),
        asyncio.create_task(router(incoming, outgoing)),
    ]

    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
    for t in pending:
        t.cancel()

@albaintor
Copy link
Copy Markdown
Contributor Author

Hi,

I agree about appending tasks (a network packet shouldn't be handled as a task even if tasks are not really threads).
I will work on your suggestion this weekend.
There is still the question of how to handle the websocket client in entities.

@zehnm
Copy link
Copy Markdown
Contributor

zehnm commented Apr 10, 2026

There is still the question of how to handle the websocket client in entities.

See #47 (comment) and PR #49

@albaintor albaintor requested a review from zehnm April 11, 2026 07:45
@albaintor
Copy link
Copy Markdown
Contributor Author

I have applied and tested the suggested approach. IA helped me this was much easier this way. I have done a few tests but this should be tested deeper.
Tests : setup flow, entity commands, browse media

@albaintor albaintor marked this pull request as draft April 11, 2026 08:02
@albaintor
Copy link
Copy Markdown
Contributor Author

Hmm this needs additional work because you cannot request the client inside a request with this code.

@albaintor albaintor marked this pull request as ready for review April 11, 2026 08:30
@albaintor
Copy link
Copy Markdown
Contributor Author

This is better now, the queues are not blocked when a request/response occur within a pending request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants