Skip to content

feat: Preliminary Support for Viper-backed OAuth in Sonos #2077

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

dljsjr
Copy link
Contributor

@dljsjr dljsjr commented Apr 15, 2025

Type of Change

  • New feature

Checklist

  • I have performed a self-review of my code
  • I have commented my code in hard-to-understand areas
  • I have verified my changes by testing with a device or have communicated a plan for testing (see note)
  • I am adding new behavior, such as adding a sub-driver, and have added and run new unit tests to cover the new behavior (see note)

Description of Change

This change adds preliminary support for the Sonos OAuth requirements that will take effect in August, allowing us to integrate with the new Hub functionality in the upcoming release that will allow for the Sonos driver to get an OAuth token from the mobile app.

Note

There are a few small TODO's here based on the shape of errors. We didn't have the ability to readily trigger the failure case for this, and we need to be able to do that to finish those TODO's.

We have a way forward on this now, though, so we'll be able to fill those in shortly.

Copy link

Copy link

github-actions bot commented Apr 15, 2025

Test Results

   66 files    422 suites   0s ⏱️
2 171 tests 2 171 ✅ 0 💤 0 ❌
3 694 runs  3 694 ✅ 0 💤 0 ❌

Results for commit 9a22827.

♻️ This comment has been updated with latest results.

Copy link

github-actions bot commented Apr 15, 2025

Minimum allowed coverage is 90%

Generated by 🐒 cobertura-action against 9a22827

@dljsjr dljsjr force-pushed the feat/sonos-oauth-support branch from 90f2310 to 3e52f1a Compare April 15, 2025 23:01
end

if maybe_token then
wss_msg_header.authorization = string.format("Bearer %s", maybe_token.access_token)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the WSS connection, the Bearer token is part of the JSON payload's "header", not the HTTP headers. And the authorization key must be lowercase, according to the docs.

local start_success = sonos_conn:start()
if start_success then return end
if sonos_conn.driver.waiting_for_token and token_receive_handle then
local token, channel_error = token_receive_handle:receive()
Copy link
Contributor Author

@dljsjr dljsjr Apr 15, 2025

Choose a reason for hiding this comment

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

I don't know that we actually want to handle asking for the token in a loop; that said, I believe that this won't actually loop.

get_oauth_token() calls through to security.get_sonos_oauth_token(), which we have designed to be completely out-of-band async. So we fire-and-forget, with the return value either being nothing or an error due to perms/API compat.

The token_receive_handle is half of a cosock channel. We haven't put a timeout on it, and it's on a non-main thread, so it should just yield forever until it gets something. And that "something" should only come through if the send half of the channel gets dropped, or if the token arrives on the environment info update in the augmented store.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll have to think about this, but I don't know if no timeout is truly what we want. I could see a very large timeout, but at the same time I understand the potential load caused by thousands of hubs repeatedly checking for a token when the user doesn't link their account.

end
end

self.on_error = function(uuid, err)
-- TODO: Implement a call to `getToken` here on certain failures, once I actually
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once we know what an auth error actually looks like, we'll propagate it out of the Router to the Connection via this callback. This will allow us to handle propagating the oauth flow request to the firmware + marking the speaker offline. I should be able to figure this out tomorrow morning with the new guidance we got from devrel late today.


self:refresh_subscriptions(reply_tx)

local reply = reply_rx:receive()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From the docs, the WSS connection itself won't actually be refused if we're missing credentials. Instead, the first command we try to send (which is a subscribe command in our case) will let us know if the auth fails.

So we've added this reply channel API to be able to make the initial subscription establishment in the :start() method wait until we know that the subscription is allowed.

@@ -178,12 +180,22 @@ local function scan_for_ssdp_updates(driver)
end)
end

local startup_state_received = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We want all device activity to be more or less frozen until we get the start up state/initial contents of the augmented data store, so we have to queue the devices up.

if not startup_state_received then
log.warn(string.format("startup state not yet received, delaying init for %s", (device and device.label or "<unknown device>")))
if device and device.id then
devices_waiting_for_startup_state[device.id] = device
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's said queue.

get_oauth_token_receive_handle = function(self)
oauth_token_tx:subscribe()
end,
get_oauth_token = function(self)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

get_oauth_token will only return a token if we actually have one. Otherwise, it'll trigger the OAuth activity in the hub firwmare, and immediately return with an error indicating why.

@dljsjr dljsjr force-pushed the feat/sonos-oauth-support branch from 3e52f1a to 49cb2c5 Compare April 15, 2025 23:17
if decode_success then
self.oauth.token = decoded
self.waiting_for_token = false
self.oauth_token_tx:send(decoded)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is where token receipt actually happens. Notify anyone waiting for one.

-- We use the same handler for added and init here because at the time of authoring
-- this driver, there is a bug with LAN Edge Drivers where `init` may not be called
-- on every device that gets created using `try_create_device`. This makes sure that
-- a device is fully initialized whether we come from fresh join or restart.
-- See: https://smartthings.atlassian.net/browse/CHAD-9683
local function _initialize_device(driver, device)
if not startup_state_received then
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we also need to do an API version check. Otherwise on old hubs this will be broken. Those old integrations will fail come august (unless the person enabled unsecure integrations from sonos), but I think we should be able to handle the driver running on an older version of the lua libs.

true
)
)
-- log.trace_with({ hub_logs = false },
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this intended?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants