-
Notifications
You must be signed in to change notification settings - Fork 502
fix(sonos): Improved handling of bonded set membership changes. #2302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -166,8 +166,12 @@ local function _open_coordinator_socket(sonos_conn, household_id, self_player_id | |
return | ||
end | ||
|
||
_, err = | ||
Router.open_socket_for_player(household_id, coordinator_id, coordinator.websocketUrl, api_key) | ||
_, err = Router.open_socket_for_player( | ||
household_id, | ||
coordinator_id, | ||
coordinator.player.websocketUrl, | ||
api_key | ||
) | ||
if err ~= nil then | ||
log.error( | ||
string.format( | ||
|
@@ -302,10 +306,13 @@ end | |
--- @return SonosConnection | ||
function SonosConnection.new(driver, device) | ||
log.debug(string.format("Creating new SonosConnection for %s", device.label)) | ||
local self = setmetatable( | ||
{ driver = driver, device = device, _listener_uuids = {}, _initialized = false, _reconnecting = false }, | ||
SonosConnection | ||
) | ||
local self = setmetatable({ | ||
driver = driver, | ||
device = device, | ||
_listener_uuids = {}, | ||
_initialized = false, | ||
_reconnecting = false, | ||
}, SonosConnection) | ||
|
||
-- capture the label here in case something goes wonky like a callback being fired after a | ||
-- device is removed | ||
|
@@ -358,19 +365,28 @@ function SonosConnection.new(driver, device) | |
local household_id, current_coordinator = | ||
self.driver.sonos:get_coordinator_for_device(self.device) | ||
local _, player_id = self.driver.sonos:get_player_for_device(self.device) | ||
self.driver.sonos:update_household_info(header.householdId, body, self.device) | ||
self.driver.sonos:update_household_info(header.householdId, body, self.driver) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this change seems odd, but looking at the function definition change, it looks like the device was just an extra arg before? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this looks weird on the surface. before the OAuth refactor, this method would conditionally take a device record as a flag that it needed to perform some updates. Which was a bad heuristic, so when I refactored it, I got rid of that heuristic, but forgot to remove it from the call sites; Lua being Lua, it didn't throw any errors or cause any problems. With the refactor for the bonded sets, we changed it so that it always takes a driver record to be able to do some tracking of bonded/unbonded state transitions, hence the updates to the call sites. |
||
self.driver.sonos:update_device_record_from_state(header.householdId, self.device) | ||
local _, updated_coordinator = self.driver.sonos:get_coordinator_for_device(self.device) | ||
|
||
local bonded = self.device:get_field(PlayerFields.BONDED) | ||
if bonded then | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be combined with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not easily; we want to conditionally call The reason being is that This cleanup logic is applicable on any Group membership update for the topology regardless of whether or not bonded set membership shifts. After we do the cleanup, we then want to validate our own coordinator connection (if our coordinator changed) and subscriptions only in the case that we're not part of a bonded set. |
||
self:stop() | ||
end | ||
|
||
Router.cleanup_unused_sockets(self.driver) | ||
|
||
if not self:coordinator_running() then | ||
--TODO this is not infallible | ||
_open_coordinator_socket(self, household_id, player_id) | ||
end | ||
if not bonded then | ||
if not self:coordinator_running() then | ||
--TODO this is not infallible | ||
_open_coordinator_socket(self, household_id, player_id) | ||
end | ||
|
||
if current_coordinator ~= updated_coordinator then | ||
self:refresh_subscriptions() | ||
if current_coordinator ~= updated_coordinator then | ||
self:refresh_subscriptions() | ||
end | ||
else | ||
self.device:offline() | ||
end | ||
elseif header.type == "playerVolume" then | ||
log.trace(string.format("PlayerVolume type message for %s", device_name)) | ||
|
@@ -477,7 +493,7 @@ function SonosConnection.new(driver, device) | |
return | ||
end | ||
|
||
local url_ip = lb_utils.force_url_table(coordinator_player.websocketUrl).host | ||
local url_ip = lb_utils.force_url_table(coordinator_player.player.websocketUrl).host | ||
local base_url = lb_utils.force_url_table( | ||
string.format("https://%s:%s", url_ip, SonosApi.DEFAULT_SONOS_PORT) | ||
) | ||
|
@@ -590,7 +606,7 @@ function SonosConnection:coordinator_running() | |
) | ||
) | ||
end | ||
return type(unique_key) == "string" and Router.is_connected(unique_key) and self._initialized | ||
return type(unique_key) == "string" and Router.is_connected(unique_key) | ||
end | ||
|
||
function SonosConnection:refresh_subscriptions(maybe_reply_tx) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -300,7 +300,7 @@ function sonos_ssdp.spawn_persistent_ssdp_task() | |
if info_to_send then | ||
if not (info_to_send.discovery_info and info_to_send.discovery_info.device) then | ||
log.error_with( | ||
{ hub_logs = true }, | ||
{ hub_logs = false }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we don't want to include this in hub logs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be chatty, and we've been asked by trim down the amount of logging that the LAN Edge Drivers forward to hub-core. It was set to |
||
st_utils.stringify_table(info_to_send, "Sonos Discovery Info has unexpected structure") | ||
) | ||
return | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -164,10 +164,12 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) | |
return | ||
end | ||
log.error_with( | ||
{ hub_logs = true }, | ||
"Error handling Sonos player initialization: %s, error code: %s", | ||
error, | ||
(error_code or "N/A") | ||
{ hub_logs = false }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question about not including this in hub logs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be chatty as well, and based on logs that I've looked at from the fleet, it's almost always only hit by non-primary bonded players, so it's not providing useful information. |
||
string.format( | ||
"Error handling Sonos player initialization: %s, error code: %s", | ||
error, | ||
(error_code or "N/A") | ||
) | ||
) | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,11 +42,27 @@ local ONE_HOUR_IN_SECONDS = 3600 | |
---@field private waiting_for_oauth_token boolean | ||
---@field private startup_state_received boolean | ||
---@field private devices_waiting_for_startup_state SonosDevice[] | ||
---@field package bonded_devices table<string, boolean> map of Device device_network_id to a boolean indicating if the device is currently known as a bonded device. | ||
--- | ||
---@field public ssdp_task SonosPersistentSsdpTask? | ||
---@field private ssdp_event_thread_handle table? | ||
local SonosDriver = {} | ||
|
||
---@param device SonosDevice | ||
function SonosDriver:update_bonded_device_tracking(device) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic in this function is very confusing to me, can you explain what is intended to be tracked here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intent is to track when a device record's bonded status changes. The device's current bonded status is tracked on the device itself via the The driver shadows this information in a map that is updated independently and with less urgency, so that it can be used to look up the previous state before updating itself. This lets us determine if a device's bonded status changes. If a device becomes bonded, we mark it offline. If a device was bonded and is no longer bonded, we re-initialize it. |
||
local already_bonded = self.bonded_devices[device.device_network_id] | ||
local currently_bonded = device:get_field(PlayerFields.BONDED) | ||
self.bonded_devices[device.device_network_id] = currently_bonded | ||
|
||
if currently_bonded and not already_bonded then | ||
device:offline() | ||
end | ||
|
||
if already_bonded and not currently_bonded then | ||
SonosDriverLifecycleHandlers.initialize_device(self, device) | ||
end | ||
end | ||
|
||
function SonosDriver:has_received_startup_state() | ||
return self.startup_state_received | ||
end | ||
|
@@ -127,13 +143,13 @@ end | |
function SonosDriver:handle_augmented_store_delete(update_key) | ||
if update_key == "endpointAppInfo" then | ||
if update_key == "endpointAppInfo" then | ||
log.trace "deleting endpoint app info" | ||
log.trace("deleting endpoint app info") | ||
self.oauth.endpoint_app_info = nil | ||
elseif update_key == "sonosOAuthToken" then | ||
log.trace "deleting OAuth Token" | ||
log.trace("deleting OAuth Token") | ||
self.oauth.token = nil | ||
elseif update_key == "force_oauth" then | ||
log.trace "deleting Force OAuth" | ||
log.trace("deleting Force OAuth") | ||
self.oauth.force_oauth = nil | ||
else | ||
log.debug(string.format("received delete of unexpected key: %s", update_key)) | ||
|
@@ -423,9 +439,15 @@ local function make_ssdp_event_handler( | |
local event, recv_err = discovery_event_subscription:receive() | ||
|
||
if event then | ||
local mac_addr = utils.extract_mac_addr(event.discovery_info.device) | ||
local unique_key = utils.sonos_unique_key_from_ssdp(event.ssdp_info) | ||
if | ||
event.force_refresh or not (unauthorized[unique_key] or discovered[unique_key]) | ||
event.force_refresh | ||
or not ( | ||
unauthorized[unique_key] | ||
or discovered[unique_key] | ||
or driver.bonded_devices[mac_addr] | ||
) | ||
then | ||
local _, api_key = driver:check_auth(event) | ||
local success, handle_err, err_code = | ||
|
@@ -435,7 +457,7 @@ local function make_ssdp_event_handler( | |
unauthorized[unique_key] = event | ||
end | ||
log.warn_with( | ||
{ hub_logs = true }, | ||
{ hub_logs = false }, | ||
string.format("Failed to handle discovered speaker: %s", handle_err) | ||
) | ||
else | ||
|
@@ -483,12 +505,22 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) | |
-- speaker in a bonded set (e.g. a home theater system, a stereo pair, etc). | ||
-- These aren't the same as speaker groups, and bonded speakers can't be controlled | ||
-- via websocket at all. So we ignore all bonded non-primary speakers | ||
if #info.ssdp_info.group_id == 0 then | ||
return nil, | ||
string.format( | ||
"Player %s is a non-primary bonded Sonos device, ignoring", | ||
info.discovery_info.device.name | ||
) | ||
-- if then | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commented code |
||
-- return nil, | ||
-- string.format( | ||
-- "Player %s is a non-primary bonded Sonos device, ignoring", | ||
-- info.discovery_info.device.name | ||
-- ) | ||
-- end | ||
|
||
local discovery_info_mac_addr = utils.extract_mac_addr(info.discovery_info.device) | ||
local bonded = (#info.ssdp_info.group_id == 0) | ||
self.bonded_devices[discovery_info_mac_addr] = bonded | ||
|
||
local maybe_device = self:get_device_by_dni(discovery_info_mac_addr) | ||
if maybe_device then | ||
maybe_device:set_field(PlayerFields.BONDED, bonded, { persist = false }) | ||
self:update_bonded_device_tracking(maybe_device) | ||
end | ||
|
||
api_key = api_key or self:get_fallback_api_key() | ||
|
@@ -543,7 +575,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) | |
end | ||
|
||
--- @cast response SonosGroupsResponseBody | ||
self.sonos:update_household_info(info.ssdp_info.household_id, response) | ||
self.sonos:update_household_info(info.ssdp_info.household_id, response, self) | ||
|
||
local device_to_update, device_mac_addr | ||
|
||
|
@@ -565,7 +597,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) | |
if not (info and info.discovery_info and info.discovery_info.device) then | ||
return nil, st_utils.stringify_table(info, "Sonos Discovery Info has unexpected structure") | ||
end | ||
device_mac_addr = utils.extract_mac_addr(info.discovery_info.device) | ||
device_mac_addr = discovery_info_mac_addr | ||
end | ||
|
||
if not device_to_update then | ||
|
@@ -578,7 +610,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) | |
if device_to_update then | ||
self.dni_to_device_id[device_mac_addr] = device_to_update.id | ||
self.sonos:associate_device_record(device_to_update, info) | ||
else | ||
elseif not bonded then | ||
local name = info.discovery_info.device.name | ||
or info.discovery_info.device.modelDisplayName | ||
or "Unknown Sonos Player" | ||
|
@@ -631,6 +663,7 @@ function SonosDriver.new_driver_template() | |
waiting_for_oauth_token = false, | ||
startup_state_received = false, | ||
devices_waiting_for_startup_state = {}, | ||
bonded_devices = utils.new_mac_address_keyed_table(), | ||
dni_to_device_id = utils.new_mac_address_keyed_table(), | ||
lifecycle_handlers = SonosDriverLifecycleHandlers, | ||
capability_handlers = { | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we collapsed everything down to this helper function is because certain activity payloads that come in on a single speaker will contain information about all Players in a group/set/zone/etc., and we will dispatch to all appropriate device records that can be associated with one of the payload entries.
Emitting information about that speaker if it's known to be part of a bonded set would lead to the device record being marked as Online in certain situations, which we don't want.