Skip to content

Commit 634161b

Browse files
authored
Merge pull request #2156 from SmartThingsCommunity/fix/additional-sonos-feedback-fixes
2 parents 83fc098 + 5be1c72 commit 634161b

File tree

7 files changed

+125
-68
lines changed

7 files changed

+125
-68
lines changed

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

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1+
local json = require "st.json"
12
local log = require "log"
2-
local net_url = require "net.url"
33
local st_utils = require "st.utils"
4-
local json = require "st.json"
54

65
local RestClient = require "lunchbox.rest"
76

@@ -13,11 +12,13 @@ local RestClient = require "lunchbox.rest"
1312
--- @return string|nil partial contents of partial read if successful
1413
local function process_rest_response(response, err, partial, err_callback)
1514
if err == nil and response == nil then
16-
log.error(st_utils.stringify_table({
17-
resp = response,
18-
maybe_err = err,
19-
maybe_partial = partial,
20-
}, "[SonosRestApi] Unexpected nil for both response and error processing REST reply", false))
15+
log.error(
16+
st_utils.stringify_table({
17+
resp = response,
18+
maybe_err = err,
19+
maybe_partial = partial,
20+
}, "[SonosRestApi] Unexpected nil for both response and error processing REST reply", false)
21+
)
2122
end
2223
if err ~= nil then
2324
if type(err_callback) == "function" then
@@ -71,70 +72,32 @@ end
7172
local SonosRestApi = {}
7273

7374
--- Query a Sonos Group IP address for individual player info
74-
---@param ip_or_url string|table
75-
---@param ... unknown
75+
---@param url table a URL table created by `net_url`
76+
---@param headers table<string,string>?
7677
---@return SonosDiscoveryInfo|SonosErrorResponse|nil
7778
---@return string|nil error
78-
---@overload fun(ip_or_url: table, headers: table<string,string>): SonosDiscoveryInfo?,string?
79-
---@overload fun(ip_or_url: string, port: number, headers: table<string,string>): SonosDiscoveryInfo?,string?
80-
function SonosRestApi.get_player_info(ip_or_url, ...)
81-
local url
82-
local headers
83-
if type(ip_or_url) == "table" then
84-
headers = select(1, ...)
85-
ip_or_url.path = "/api/v1/players/local/info"
86-
url = ip_or_url
87-
else
88-
local port = select(1, ...)
89-
headers = select(2, ...)
90-
url = net_url.parse(string.format("https://%s:%s/api/v1/players/local/info", ip_or_url, port))
91-
end
79+
function SonosRestApi.get_player_info(url, headers)
80+
url.path = "/api/v1/players/local/info"
9281
return process_rest_response(RestClient.one_shot_get(url, headers))
9382
end
9483

95-
---@param ip_or_url string|table
96-
---@param ... unknown
84+
---@param url table a URL table created by `net_url`
85+
---@param household HouseholdId
86+
---@param headers table<string,string>?
9787
---@return SonosGroupsResponseBody|SonosErrorResponse|nil response
9888
---@return nil|string error
99-
---@overload fun(ip_or_url: table, household: HouseholdId, headers: table<string,string>?): SonosGroupsResponseBody?,string?
100-
---@overload fun(ip_or_url: string, port: number, household: HouseholdId, headers: table<string,string>?): SonosGroupsResponseBody?,string?
101-
function SonosRestApi.get_groups_info(ip_or_url, ...)
102-
local url
103-
local headers
104-
if type(ip_or_url) == "table" then
105-
local household = select(1, ...)
106-
headers = select(2, ...)
107-
ip_or_url.path = string.format("/api/v1/households/%s/groups", household)
108-
url = ip_or_url
109-
else
110-
local port = select(1, ...)
111-
local household = select(2, ...)
112-
headers = select(3, ...)
113-
url = net_url.parse(string.format("https://%s:%s/api/v1/households/%s/groups", ip_or_url, port, household))
114-
end
89+
function SonosRestApi.get_groups_info(url, household, headers)
90+
url.path = string.format("/api/v1/households/%s/groups", household)
11591
return process_rest_response(RestClient.one_shot_get(url, headers))
11692
end
11793

118-
---@param ip_or_url string|table
119-
---@param ... unknown
94+
---@param url table a URL table created by `net_url`
95+
---@param household HouseholdId
96+
---@param headers table<string,string>?
12097
---@return SonosFavoritesResponseBody|SonosErrorResponse|nil response
12198
---@return nil|string error
122-
---@overload fun(ip_or_url: table, household: HouseholdId, headers: table<string,string>?): SonosFavoritesResponseBody?,string?
123-
---@overload fun(ip_or_url: string, port: number, household: HouseholdId, headers: table<string,string>?): SonosFavoritesResponseBody?,string?
124-
function SonosRestApi.get_favorites(ip_or_url, ...)
125-
local url
126-
local headers
127-
if type(ip_or_url) == "table" then
128-
local household = select(1, ...)
129-
headers = select(2, ...)
130-
ip_or_url.path = string.format("/api/v1/households/%s/favorites", household)
131-
url = ip_or_url
132-
else
133-
local port = select(1, ...)
134-
local household = select(2, ...)
135-
headers = select(3, ...)
136-
url = net_url.parse(string.format("https://%s:%s/api/v1/households/%s/favorites", ip_or_url, port, household))
137-
end
99+
function SonosRestApi.get_favorites(url, household, headers)
100+
url.path = string.format("/api/v1/households/%s/favorites", household)
138101
return process_rest_response(RestClient.one_shot_get(url, headers))
139102
end
140103

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,11 @@ function SonosConnection.new(driver, device)
410410
end
411411

412412
local url_ip = lb_utils.force_url_table(coordinator_player.websocketUrl).host
413-
413+
local base_url = lb_utils.force_url_table(
414+
string.format("https://%s:%s", url_ip, SonosApi.DEFAULT_SONOS_PORT)
415+
)
414416
local favorites_response, err, _ =
415-
SonosRestApi.get_favorites(url_ip, SonosApi.DEFAULT_SONOS_PORT, header.householdId)
417+
SonosRestApi.get_favorites(base_url, header.householdId)
416418

417419
if err or not favorites_response then
418420
log.error("Error querying for favorites: " .. err)

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local cosock = require "cosock"
22
local log = require "log"
3+
local net_url = require "net.url"
34
local ssdp = require "ssdp"
45
local st_utils = require "st.utils"
56

@@ -268,8 +269,12 @@ function sonos_ssdp.spawn_persistent_ssdp_task()
268269

269270
if is_new_information then
270271
local headers = SonosApi.make_headers()
271-
local discovery_info, err =
272-
SonosApi.RestApi.get_player_info(sonos_ssdp_info.ip, SonosApi.DEFAULT_SONOS_PORT, headers)
272+
local discovery_info, err = SonosApi.RestApi.get_player_info(
273+
net_url.parse(
274+
string.format("https://%s:%s", sonos_ssdp_info.ip, SonosApi.DEFAULT_SONOS_PORT)
275+
),
276+
headers
277+
)
273278
if not discovery_info then
274279
log.error(string.format("Error getting discovery info from SSDP response: %s", err))
275280
else

drivers/SmartThings/sonos/src/init.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
local api_version = require("version").api
2+
local cosock = require "cosock"
3+
4+
if type(cosock.bus) == "nil" then
5+
local cosock_bus = require "cosock.bus"
6+
cosock.bus = cosock_bus
7+
end
8+
19
---@module 'result'
210
require "result" {
311
register_globals = true,
@@ -22,6 +30,15 @@ if driver.datastore["dni_to_device_id"] ~= nil then
2230
driver.datastore["dni_to_device_id"] = nil
2331
end
2432

33+
-- API Version 14 was the version that came out with 0.57.x
34+
--
35+
-- In API >= 14, the SSDP task will start when the driver receives the startup state.
36+
-- To support older versions of hub core, we start the SSDP task before the run loop,
37+
-- where we won't be using the startup state handling.
38+
if api_version < 14 then
39+
driver:start_ssdp_event_task()
40+
end
41+
2542
log.info "Starting Sonos run loop"
2643
driver:run()
2744
log.info "Exiting Sonos run loop"

drivers/SmartThings/sonos/src/lifecycle_handlers.lua

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local api_version = require("version").api
12
local capabilities = require "st.capabilities"
23

34
local cosock = require "cosock"
@@ -45,7 +46,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device)
4546
capabilities.mediaTrackControl.commands.previousTrack.NAME,
4647
}))
4748

48-
if not driver:has_received_startup_state() then
49+
if api_version >= 14 and not driver:has_received_startup_state() then
4950
device.log.debug("Driver startup state not yet received, delaying initialization of device.")
5051
driver:queue_device_init_for_startup_state(device)
5152
return
@@ -76,8 +77,76 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device)
7677
log.error("token event bus closed, aborting initialization")
7778
return
7879
end
79-
driver:request_oauth_token()
80-
token_event_receive:receive()
80+
token_event_receive:settimeout(30)
81+
local token, token_recv_err
82+
-- max 30 mins
83+
local backoff_builder = utils.backoff_builder(60 * 30, 30, 2)
84+
if not driver:is_waiting_for_oauth_token() then
85+
local _, request_token_err = driver:request_oauth_token()
86+
if request_token_err then
87+
log.warn(string.format("Error sending token request: %s", request_token_err))
88+
end
89+
end
90+
91+
local backoff_timer = nil
92+
while not token do
93+
local send_request = false
94+
-- we use the backoff to create a timer and utilize a select loop here, instead of
95+
-- utilizing a sleep, so that we can create a long delay on our polling of the cloud
96+
-- without putting ourselves in a situation where we're sleeping for an extended period
97+
-- of time so that we don't sleep through the users's log-in attempt and fail to resume
98+
-- our connection attempts in a timely manner.
99+
--
100+
-- The backoff caps at 30 mins, as commented above
101+
if not backoff_timer then
102+
backoff_timer = cosock.timer.create_oneshot(backoff_builder())
103+
end
104+
local token_recv_ready, _, token_select_err =
105+
cosock.socket.select({ token_event_receive, backoff_timer }, nil, nil)
106+
107+
if token_select_err then
108+
log.warn(string.format("select error: %s", token_select_err))
109+
end
110+
111+
token, token_recv_err = nil, nil
112+
for _, receiver in pairs(token_recv_ready or {}) do
113+
if receiver == backoff_timer then
114+
-- we just make a note that the backoff has elapsed, rather than
115+
-- put a request in flight immediately.
116+
--
117+
-- This is just in case both receivers are ready, so that we can prioritize
118+
-- handling the token instead of putting another request in flight.
119+
send_request = true
120+
backoff_timer = nil
121+
end
122+
123+
if receiver == token_event_receive then
124+
token, token_recv_err = token_event_receive:receive()
125+
end
126+
end
127+
128+
if token_recv_err == "timeout" then
129+
log.debug("timeout waiting for OAuth token in reconnect task")
130+
elseif token_recv_err and not token then
131+
log.warn(
132+
string.format(
133+
"Unexpected error on token event receive bus: %s",
134+
token_recv_err
135+
)
136+
)
137+
end
138+
139+
if send_request then
140+
if not driver:is_waiting_for_oauth_token() then
141+
local _, request_token_err = driver:request_oauth_token()
142+
if request_token_err then
143+
log.warn(
144+
string.format("Error sending token request: %s", request_token_err)
145+
)
146+
end
147+
end
148+
end
149+
end
81150
else
82151
device.log.error(
83152
string.format(

drivers/SmartThings/sonos/src/sonos_driver.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ end
296296
---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not
297297
---@return "token expired"|"no token"|nil reason the reason a token was not provided, nil if there is a valid token available
298298
function SonosDriver:get_oauth_token()
299+
self.hub_augmented_driver_data = self.hub_augmented_driver_data or {}
299300
local decode_success, maybe_token =
300301
pcall(json.decode, self.hub_augmented_driver_data.sonosOAuthToken)
301302
if
@@ -316,7 +317,7 @@ function SonosDriver:get_oauth_token()
316317
if now < expiration then
317318
-- token is expiring soon, so we pre-emptively refresh
318319
if math.abs(expiration - now) < ONE_HOUR_IN_SECONDS then
319-
local result, err = self:request_oauth_token()
320+
local result, err = security.get_sonos_oauth()
320321
if not result then
321322
log.warn(string.format("Error requesting OAuth token via Security API: %s", err))
322323
end

drivers/SmartThings/sonos/src/ssdp.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ function _ssdp_mt:setwaker(kind, waker)
355355

356356
assert(
357357
self.waker_ref == nil or waker == nil,
358-
"Waker already set, cannot await SSDP serach instance from multiple locations."
358+
"Waker already set, cannot await SSDP search instance from multiple locations."
359359
)
360360

361361
self.waker_ref = waker

0 commit comments

Comments
 (0)