Closed keatontaylor closed 5 years ago
I'm not too familiar with WebSockets. Will this give us push functionality so we can avoid polling?
That's the goal, looks like amazon is pushing events over web sockets for:
So in theory this could eliminate all polling.
Looks like this is absolutely possible and the messages can be properly decoded with little effort. Here is the output from a sample program I wrote.
Looks like each time that someone talks to an echo device a message is pushed to connected WebSocket clients. This is the "PUSH_ACTIVITY" you see in the screenshot. You can also see other push messages for different messages.
Each message does contain the deviceID that is being affected by the player state change or the device that initiated the activity. Getting PUSH_ACTIVITY can be used to gather information from the last_called alexa device and other messages can directly update media players or give us a hint on when to fetch/pull information like track, playlist, etc when things change.
@alandtse what do you think?
This looks good. Can you implement websockets in alexapi in a parallel set of functions so we can fallback to the http? Or does this require switching to a purely async model? I'm not planning to do any major work in the API for a little bit if you need it stable.
Currently I want to do as minimal changes as possible. Specifically, we can instantiate a WebSocket class within the device alexa_setup() method and simply get a callback for messages from the WebSocket code. This will be entirely one way, only digesting events over the WebSocket and not sending any commands to amazon, those will still be handled with the http RESTful calls.
Basically, once we have a callback with a particular message, when can use the same mechanism that is used to update the devices with the last called to fire an HA event that can be consumed by the individual media players to update their state.
I agree we can use WS for listen only as that's the biggest issue to solve.
So we're on the same page, the current architecture has HA __init__.py
performing spawning an AlexaLogin class to perform the login. Then on successful login setup_alexa()
will use AlexaAPI
to populate data into the hass.data
structure, including the successful AlexaLogin
object and pass control to media_player.py
to spawn AlexaClient
devices based on hass.data
. Those spawned AlexaClients
then internally use the AlexaLogin
object to access AlexaApi
objects to interact with Amazon.
Is your thought that during setup_alexa()
we'll also spawn the websocket object for all the AlexaClient
objects to subscribe to?
I think that should work, but wonder if architecturally it makes sense to define a websocket object outside alexaapi. Or perhaps I'm misunderstanding you on that point.
@keatontaylor alternatively, if you share the sample program code, I may have some time to integrate it in this weekend.
@alandtse uploading it as a new class in a branch on GitLab once I get the code cleaned up a bit. Look for it in an hour or so.
Oh awesome! Looking forward to it.
Not perfect, but should work: https://gitlab.com/keatontaylor/alexapy/tree/websocket-notify
Create an instance of WebSocket_EchoClient with the login session, and a python function you'd like to be called when a message is received.
The Message and Content class describe the contents of each message.
@keatontaylor I think there's something wrong with your cookie building routine. You're not actually using cookie
in your for loop.
https://gitlab.com/keatontaylor/alexapy/blob/websocket-notify/alexapy/alexawebsocket.py#L32
for cookie in self._cookies:
cookies += cookies + "; "
cookies = "Cookie: " + cookie
url += url + str(cookies['ubid-main'])
url += "-" + str(int(time.time())) + "000"
The initial error is that ubid-main
is not a valid key for a string. I assume you wanted self._cookies['ubid-main']
since that's a dict. I've converted that section, but still get a SSL error cause I'm not sure of the cookie structure.
EDIT: Looks like urls should be fixed too.
cookies = "dp-gw-na-js "
for cookie in self._cookies:
cookies += cookie + "; "
cookies = "Cookie: " + cookies
url += str(self._cookies['ubid-main'])
Note, I blanked out my ubid-main value below, but otherwise untouched.
--- request header ---
GET /?x-amz-device-type=ALEGCNGL9K0HM&x-amz-device-serial=wss://dp-gw-na-js.amazon.com/?x-amz-device-type=ALEGCNGL9K0HM&x-amz-device-serial=134-0082565-00*****-1552776909000 HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: dp-gw-na-js.amazon.com
Origin: http://dp-gw-na-js.amazon.com
Sec-WebSocket-Key: Yi898f+BTJ5SEBqhfLEaBQ==
Sec-WebSocket-Version: 13
Cookie: dp-gw-na-js x-main; session-id; session-id-time; sid; ubid-main; sess-at-main; session-token; at-main; csrf;
-----------------------
--- response header ---
HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: FDkVGAgzs30Y2qvYhfJRu0YPYDU=
-----------------------
send: b'\x82\x9de9+\xd0UA\x12\xe9\x01\rM\xe7TX\x0b\xe0\x1d\t\x1b\xe0U\t\x1b\xe1\x01\x19j\xea-m~\x9e '
send: b"\x82\xfe\x00\x9cg\xa8H\xdaW\xd0)\xec\x01\x9e)\xe3R\x99h\xea\x1f\x98x\xeaW\x98x\xe3\x04\x883\xf8\x17\xda'\xae\x08\xcb'\xb6)\xc9%\xbfE\x92j\x9b]\xe0j\xf6E\xd8)\xa8\x06\xc5-\xae\x02\xda;\xf8]\xd3j\x9b\x0b\xd8 \xbb7\xda'\xae\x08\xcb'\xb6/\xc9&\xbe\x0b\xcd:\xf4\x15\xcd+\xbf\x0e\xde-\x8d\x0e\xc6,\xb5\x10\xfb!\xa0\x02\x8ar\xf8V\x9ej\xf6E\xe9$\xaa\x0f\xc9\x18\xa8\x08\xdc'\xb9\x08\xc4\x00\xbb\t\xcc$\xbf\x15\x86%\xbb\x1f\xee:\xbb\x00\xc5-\xb4\x13\xfb!\xa0\x02\x8ar\xf8V\x9ex\xeaW\x8a5\xa73\xfd\x06\x9f"
error from callback <function WebSocket_EchoClient.__init__.<locals>.<lambda> at 0x7f934e42d598>: [Errno 32] Broken pipe
2019-03-16 22:55:10 ERROR (Thread-3) [websocket] error from callback <function WebSocket_EchoClient.__init__.<locals>.<lambda> at 0x7f934e42d598>: [Errno 32] Broken pipe
File "/root/venv/lib/python3.5/site-packages/websocket/_app.py", line 345, in _callback
callback(self, *args)
File "/root/venv/lib/python3.5/site-packages/alexapy/alexawebsocket.py", line 47, in <lambda>
ws),
File "/root/venv/lib/python3.5/site-packages/alexapy/alexawebsocket.py", line 143, in on_open
ws.send(self._encodeWSHandshake(), OPCODE_BINARY)
File "/root/venv/lib/python3.5/site-packages/websocket/_app.py", line 153, in send
if not self.sock or self.sock.send(data, opcode) == 0:
File "/root/venv/lib/python3.5/site-packages/websocket/_core.py", line 253, in send
return self.send_frame(frame)
File "/root/venv/lib/python3.5/site-packages/websocket/_core.py", line 278, in send_frame
l = self._send(data)
File "/root/venv/lib/python3.5/site-packages/websocket/_core.py", line 448, in _send
return send(self.sock, data)
File "/root/venv/lib/python3.5/site-packages/websocket/_socket.py", line 151, in send
return _send()
File "/root/venv/lib/python3.5/site-packages/websocket/_socket.py", line 136, in _send
return sock.send(data)
File "/usr/lib/python3.5/ssl.py", line 869, in send
return self._sslobj.write(data)
File "/usr/lib/python3.5/ssl.py", line 594, in write
return self._sslobj.write(data)
send: b"\x88\x82-'\x1a\x1d.\xcf"
2019-03-16 22:55:10 ERROR (Thread-3) [alexapy.alexawebsocket] WebSocket Error [SSL: BAD_LENGTH] bad length (_ssl.c:1949)
The error is caused by the _encodeWSHandshake()
call I believe.
I'll have to take a closer look at this, I didn't immediately test with a valid cookie once in the class, but will.
Do you have a spec or example you're using for how to build the handshake?
Branch on gitlab updated. handshake and message parsing is pulled from the OpenHAB Alexa component: https://github.com/openhab/openhab2-addons/blob/master/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java
Ok, I think I have a working version. Will probably push it with some other updates after some more testing.
Currently amazon supports WebSockets for sending events on certain state changes. This includes media player play/pause/volume/etc events along with last-triggered echo events when a request is made.
This would be extremely valuable for getting real-time dynamic updates based on state changes within the Alexa echo-system and should be reasonably straight forward to implement now that we have the Auth token from a successful login.
I'm going to play around with this and report back. If successful even if we cannot decode the inner WebSocket payload, an event over WebSockets should be sufficient for us to trigger an update.