Skip to content

Commit 3f04c23

Browse files
authored
Merge pull request #2170 from SmartThingsCommunity/fix/sonos-backwards-compat-crashes
2 parents 9db4783 + 2418bf4 commit 3f04c23

File tree

6 files changed

+229
-57
lines changed

6 files changed

+229
-57
lines changed

drivers/SmartThings/sonos/src/api/sonos_connection.lua

Lines changed: 129 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local api_version = require "version".api
12
local cosock = require "cosock"
23
local json = require "st.json"
34
local log = require "log"
@@ -74,7 +75,13 @@ local _update_subscriptions_helper = function(
7475
{},
7576
}
7677
local payload = json.encode(payload_table)
77-
Router.send_message_to_player(utils.sonos_unique_key(householdId, playerId), payload, reply_tx)
78+
local unique_key, bad_key_part = utils.sonos_unique_key(householdId, playerId)
79+
if not unique_key then
80+
local err_msg = string.format("Invalid Sonos Unique Key Part: %s", bad_key_part)
81+
reply_tx:send(table.pack(nil, err_msg))
82+
return
83+
end
84+
Router.send_message_to_player(unique_key, payload, reply_tx)
7885
end
7986
end
8087

@@ -168,10 +175,13 @@ local function _open_coordinator_socket(sonos_conn, household_id, self_player_id
168175
end
169176

170177
local listener_id
171-
listener_id, err = Router.register_listener_for_socket(
172-
sonos_conn,
173-
utils.sonos_unique_key(household_id, coordinator_id)
174-
)
178+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, coordinator_id)
179+
180+
if not unique_key then
181+
log.error(string.format("Invalid Sonos Unique Key Part: %s", bad_key_part))
182+
end
183+
184+
listener_id, err = Router.register_listener_for_socket(sonos_conn, unique_key)
175185
if err ~= nil or not listener_id then
176186
log.error(err)
177187
else
@@ -208,7 +218,22 @@ local function backoff_builder(max, inc, rand)
208218
end
209219

210220
---@param sonos_conn SonosConnection
211-
local function _spawn_reconnect_task(sonos_conn)
221+
local function _legacy_reconnect_task(sonos_conn)
222+
log.debug("Spawning reconnect task for ", sonos_conn.device.label)
223+
cosock.spawn(function()
224+
local backoff = backoff_builder(60, 1, 0.1)
225+
while not sonos_conn:is_running() do
226+
local start_success = sonos_conn:start()
227+
if start_success then
228+
return
229+
end
230+
cosock.socket.sleep(backoff())
231+
end
232+
end, string.format("%s Reconnect Task", sonos_conn.device.label))
233+
end
234+
235+
---@param sonos_conn SonosConnection
236+
local function _oauth_reconnect_task(sonos_conn)
212237
log.debug("Spawning reconnect task for ", sonos_conn.device.label)
213238
if not sonos_conn.driver:is_waiting_for_oauth_token() then
214239
sonos_conn.driver:request_oauth_token()
@@ -243,6 +268,15 @@ local function _spawn_reconnect_task(sonos_conn)
243268
end, string.format("%s Reconnect Task", sonos_conn.device.label))
244269
end
245270

271+
---@param sonos_conn SonosConnection
272+
local function _spawn_reconnect_task(sonos_conn)
273+
if type(api_version) == "number" and api_version >= 14 then
274+
_oauth_reconnect_task(sonos_conn)
275+
else
276+
_legacy_reconnect_task(sonos_conn)
277+
end
278+
end
279+
246280
--- Create a new Sonos connection to manage the given device
247281
--- @param driver SonosDriver
248282
--- @param device SonosDevice
@@ -276,15 +310,29 @@ function SonosConnection.new(driver, device)
276310
local header, body = table.unpack(table.unpack(json_result))
277311
if header.type == "globalError" then
278312
if body.errorCode == "ERROR_NOT_AUTHORIZED" then
279-
local household_id, player_id = driver.sonos:get_player_for_device(device)
280-
device.log.warn(string.format("WebSocket connection no longer authorized, disconnecting"))
281-
local _, security_err = driver:request_oauth_token()
282-
if security_err then
283-
log.warn(string.format("Error during request for oauth token: %s", security_err))
313+
if api_version < 14 then
314+
log.error(
315+
"Unable to authenticate Sonos WebSocket Connection, and current API does not support Sonos OAuth"
316+
)
317+
self:stop()
318+
else
319+
local household_id, player_id = driver.sonos:get_player_for_device(device)
320+
device.log.warn(
321+
string.format("WebSocket connection no longer authorized, disconnecting")
322+
)
323+
local _, security_err = driver:request_oauth_token()
324+
if security_err then
325+
log.warn(string.format("Error during request for oauth token: %s", security_err))
326+
end
327+
-- closing the socket directly without calling `:stop()` triggers the reconnect loop,
328+
-- which is where we wait for the token to come in.
329+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id)
330+
if not unique_key then
331+
log.error(string.format("Invalid Sonos Unique Key Part: %s", bad_key_part))
332+
else
333+
Router.close_socket_for_player(unique_key)
334+
end
284335
end
285-
-- closing the socket directly without calling `:stop()` triggers the reconnect loop,
286-
-- which is where we wait for the token to come in.
287-
Router.close_socket_for_player(utils.sonos_unique_key(household_id, player_id))
288336
end
289337
elseif header.type == "groups" then
290338
log.trace(string.format("Groups type message for %s", device_name))
@@ -494,15 +542,32 @@ end
494542
function SonosConnection:self_running()
495543
local household_id = self.device:get_field(PlayerFields.HOUSEHOLD_ID)
496544
local player_id = self.device:get_field(PlayerFields.PLAYER_ID)
497-
return Router.is_connected(utils.sonos_unique_key(household_id, player_id)) and self._initialized
545+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id)
546+
if bad_key_part then
547+
self.device.log.warn(
548+
string.format(
549+
"Bad Unique Key Part While Inspecting Connections in 'self_running': %s",
550+
bad_key_part
551+
)
552+
)
553+
end
554+
return type(unique_key) == "string" and Router.is_connected(unique_key) and self._initialized
498555
end
499556

500557
--- Whether or not the connection has a live websocket connection to its coordinator
501558
--- @return boolean
502559
function SonosConnection:coordinator_running()
503560
local household_id, coordinator_id = self.driver.sonos:get_coordinator_for_device(self.device)
504-
return Router.is_connected(utils.sonos_unique_key(household_id, coordinator_id))
505-
and self._initialized
561+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, coordinator_id)
562+
if bad_key_part then
563+
self.device.log.warn(
564+
string.format(
565+
"Bad Unique Key Part While Inspecting Connections in 'coordinator_running': %s",
566+
bad_key_part
567+
)
568+
)
569+
end
570+
return unique_key and Router.is_connected(unique_key) and self._initialized
506571
end
507572

508573
function SonosConnection:refresh_subscriptions(maybe_reply_tx)
@@ -524,10 +589,12 @@ function SonosConnection:send_command(cmd)
524589
if err or not json_payload then
525590
log.error("Json encoding error: " .. err)
526591
else
527-
Router.send_message_to_player(
528-
utils.sonos_unique_key(household_id, coordinator_id),
529-
json_payload
530-
)
592+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, coordinator_id)
593+
if not unique_key then
594+
self.device.log.error(string.format("Invalid Sonos Unique Key Part: %s", bad_key_part))
595+
return
596+
end
597+
Router.send_message_to_player(unique_key, json_payload)
531598
end
532599
end
533600

@@ -562,12 +629,17 @@ function SonosConnection:start()
562629
return false
563630
end
564631

565-
local listener_id, register_listener_err =
566-
Router.register_listener_for_socket(self, utils.sonos_unique_key(household_id, player_id))
567-
if register_listener_err ~= nil or not listener_id then
568-
log.error(register_listener_err)
632+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id)
633+
if bad_key_part then
634+
self.device.log.error(string.format("Invalid Sonos Unique Key Part: %s", bad_key_part))
569635
else
570-
self._self_listener_uuid = listener_id
636+
local listener_id, register_listener_err =
637+
Router.register_listener_for_socket(self, unique_key)
638+
if register_listener_err ~= nil or not listener_id then
639+
log.error(register_listener_err)
640+
else
641+
self._self_listener_uuid = listener_id
642+
end
571643
end
572644
end
573645

@@ -578,16 +650,39 @@ function SonosConnection:start()
578650

579651
self:refresh_subscriptions()
580652
local coordinator_id = self.driver.sonos:get_coordinator_for_player(household_id, player_id)
653+
local self_unique_key, self_bad_key_part = utils.sonos_unique_key(household_id, player_id)
654+
local coordinator_unique_key, coordinator_bad_key_part =
655+
utils.sonos_unique_key(household_id, coordinator_id)
581656
if
582-
Router.is_connected(utils.sonos_unique_key(household_id, player_id))
583-
and Router.is_connected(utils.sonos_unique_key(household_id, coordinator_id))
657+
self_unique_key
658+
and Router.is_connected(self_unique_key)
659+
and coordinator_unique_key
660+
and Router.is_connected(coordinator_unique_key)
584661
then
585662
self.device:online()
586663
self._initialized = true
587664
self._keepalive = true
588665
return true
589666
end
590667

668+
if self_bad_key_part then
669+
self.device.log.error(
670+
string.format(
671+
"Invalid Unique Key Part for 'self' Player during connection bring-up: %s",
672+
self_bad_key_part
673+
)
674+
)
675+
end
676+
677+
if coordinator_bad_key_part then
678+
self.device.log.error(
679+
string.format(
680+
"Invalid Unique Key Part for 'coordinator' Player during connection bring-up: %s",
681+
coordinator_bad_key_part
682+
)
683+
)
684+
end
685+
591686
return false
592687
end
593688

@@ -601,7 +696,12 @@ function SonosConnection:stop()
601696
local coordinator_id = self.driver.sonos:get_coordinator_for_group(household_id, group_id)
602697

603698
if player_id ~= coordinator_id then
604-
Router.close_socket_for_player(utils.sonos_unique_key(household_id, player_id))
699+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id)
700+
if not unique_key then
701+
self.device.log.error(string.format("Invalid Sonos Unique Key Part: %s", bad_key_part))
702+
return
703+
end
704+
Router.close_socket_for_player(unique_key)
605705
end
606706
end
607707

drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,19 @@ function SonosPersistentSsdpTask:get_player_info(reply_tx, ...)
200200
local household_id_or_mac = select(1, ...)
201201
local player_id = select(2, ...)
202202

203-
local lookup_table, lookup_key
203+
local lookup_table, lookup_key, bad_key_part
204204

205205
if player_id ~= nil and type(player_id) == "string" then
206206
lookup_table = self.player_info_by_sonos_ids
207-
lookup_key = utils.sonos_unique_key(household_id_or_mac, player_id)
207+
lookup_key, bad_key_part = utils.sonos_unique_key(household_id_or_mac, player_id)
208208
else
209209
lookup_table = self.player_info_by_mac_addrs
210210
lookup_key = household_id_or_mac
211211
end
212212

213+
if not lookup_key and bad_key_part then
214+
log.error(string.format("Invalid Unique Key Part: %s", bad_key_part))
215+
end
213216
local maybe_existing = lookup_table[lookup_key]
214217
if maybe_existing and maybe_existing.ssdp_info.expires_at > os.time() then
215218
reply_tx:send(maybe_existing)

drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
local log = require "log"
1+
local channel = require "cosock.channel"
22
local cosock = require "cosock"
3+
local log = require "log"
34
local socket = require "cosock.socket"
4-
local channel = require "cosock.channel"
55
local ssl = require "cosock.ssl"
66
local LustreConfig = require "lustre".Config
77
local WebSocket = require "lustre".WebSocket
@@ -10,8 +10,8 @@ local CloseCode = require "lustre.frame.close".CloseCode
1010
local lb_utils = require "lunchbox.util"
1111
local st_utils = require "st.utils"
1212

13-
local utils = require "utils"
1413
local SonosApi = require "api"
14+
local utils = require "utils"
1515

1616
--- A "singleton" module that maintains all of the websockets for a
1717
--- home's Sonos players. A player as modeled in the driver will have
@@ -105,7 +105,13 @@ cosock.spawn(function()
105105
local wss = websockets[target]
106106
if wss == nil then
107107
--TODO is this silencing a crash that is an indication of a state management bug in the run loop?
108-
log.error(st_utils.stringify_table({ msg = msg }, "Coordinator doesn't exist for player", false))
108+
log.error(
109+
st_utils.stringify_table(
110+
{ msg = msg },
111+
"Coordinator doesn't exist for player",
112+
false
113+
)
114+
)
109115
goto continue
110116
end
111117

@@ -123,7 +129,10 @@ cosock.spawn(function()
123129
if listener ~= nil then
124130
if listener.device and listener.device.label then
125131
log.debug(
126-
string.format("SonosConnection for device %s handling websocket message", listener.device.label)
132+
string.format(
133+
"SonosConnection for device %s handling websocket message",
134+
listener.device.label
135+
)
127136
)
128137
end
129138
listener.on_message(uuid, msg)
@@ -153,7 +162,13 @@ local function _make_websocket(url_table, api_key)
153162
return nil, "Could not set TCP socket timeout: " .. sock_operation_err
154163
end
155164

156-
log.trace(string.format("Opening up websocket connection for host/port %s %s", url_table.host, url_table.port))
165+
log.trace(
166+
string.format(
167+
"Opening up websocket connection for host/port %s %s",
168+
url_table.host,
169+
url_table.port
170+
)
171+
)
157172

158173
_, sock_operation_err = sock:connect(url_table.host, url_table.port)
159174
if sock_operation_err ~= nil then
@@ -239,14 +254,16 @@ end
239254
--- @return boolean|nil success true on success, nil otherwise
240255
--- @return nil|string error the error message in the failure case
241256
function SonosWebSocketRouter.open_socket_for_player(household_id, player_id, wss_url, api_key)
242-
local unique_key = utils.sonos_unique_key(household_id, player_id)
257+
local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id)
243258
if not websockets[unique_key] then
244259
log.debug("Opening websocket for player id " .. unique_key)
245260
local url_table = lb_utils.force_url_table(wss_url)
246261
local wss, err = _make_websocket(url_table, api_key)
247262

248263
if err or not wss then
249264
return nil, string.format("Could not create websocket connection for %s: %s", unique_key, err)
265+
elseif not unique_key then
266+
return nil, string.format("Invalid Sonos Unique Key Part: %s", bad_key_part)
250267
else
251268
websockets[unique_key] = wss
252269
return true
@@ -307,8 +324,17 @@ function SonosWebSocketRouter.cleanup_unused_sockets(driver)
307324

308325
for _, device in ipairs(known_devices) do
309326
local household_id, coordinator_id = driver.sonos:get_coordinator_for_device(device)
310-
local coordinator_unique_key = utils.sonos_unique_key(household_id, coordinator_id)
311-
if should_keep[coordinator_unique_key] == false then -- looking for false specifically, not nil
327+
local coordinator_unique_key, bad_key_part =
328+
utils.sonos_unique_key(household_id, coordinator_id)
329+
if bad_key_part then
330+
log.warn(
331+
string.format(
332+
"Invalid Sonos Unique Key Part while cleaning up unused websockets: %s",
333+
bad_key_part
334+
)
335+
)
336+
end
337+
if coordinator_unique_key and should_keep[coordinator_unique_key] == false then -- looking for false specifically, not nil
312338
log.trace("Preserving coordinator socket " .. coordinator_id)
313339
should_keep[coordinator_unique_key] = true
314340
end

drivers/SmartThings/sonos/src/lifecycle_handlers.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device)
7171
local auth_success, api_key_or_err = driver:check_auth(info)
7272
if not auth_success then
7373
device:offline()
74-
if auth_success == false then
74+
if auth_success == false and api_version >= 14 then
7575
local token_event_receive = driver:oauth_token_event_subscribe()
7676
if not token_event_receive then
7777
log.error("token event bus closed, aborting initialization")

0 commit comments

Comments
 (0)