@@ -9,7 +9,140 @@ local utils = require "utils"
99local SonosApi = require " api"
1010local SSDP_SCAN_INTERVAL_SECONDS = 600
1111
12+ local SONOS_DEFAULT_PORT = 1443
13+ local SONOS_DEFAULT_WSS_PATH = " websocket/api"
14+ local SONOS_DEFAULT_REST_PATH = " api/v1"
15+
16+ --- Cached information gathered during discovery scanning, created from a subset of the
17+ --- found [SonosSSDPInfo](lua://SonosSSDPInfo) and [SonosDiscoveryInfoObject](lua://SonosDiscoveryInfoObject)
18+ ---
19+ --- @class SpeakerDiscoveryInfo
20+ --- @field public unique_key UniqueKey
21+ --- @field public mac_addr string
22+ --- @field public expires_at integer
23+ --- @field public ipv4 string
24+ --- @field public port integer
25+ --- @field public household_id HouseholdId
26+ --- @field public player_id PlayerId
27+ --- @field public name string
28+ --- @field public model string
29+ --- @field public model_display_name string
30+ --- @field public sw_gen integer
31+ --- @field public wss_url table
32+ --- @field public rest_url table
33+ --- @field public is_group_coordinator boolean
34+ --- @field public group_name string ? nil if a speaker is the non-primary in a bonded set
35+ --- @field public group_id GroupId ? nil if a speaker is the non-primary in a bonded set
36+ --- @field package wss_path string ? nil if equivalent to the default value ; does not include leading slash !
37+ --- @field package rest_path string ? nil if equivalent to the default value ; does not include leading slash !
38+ local SpeakerDiscoveryInfo = {}
39+
40+ --- @param ssdp_info SonosSSDPInfo
41+ --- @param discovery_info SonosDiscoveryInfoObject
42+ --- @return SpeakerDiscoveryInfo info
43+ function SpeakerDiscoveryInfo .new (ssdp_info , discovery_info )
44+ local mac_addr = utils .extract_mac_addr (discovery_info .device )
45+ local port , rest_path = string.match (discovery_info .restUrl , " ^.*:(%d*)/(.*)$" )
46+ local _ , wss_path = string.match (discovery_info .websocketUrl , " ^.*:(%d*)/(.*)$" )
47+ port = tonumber (port ) or SONOS_DEFAULT_PORT
48+
49+ local ret = {
50+ unique_key = utils .sonos_unique_key_from_ssdp (ssdp_info ),
51+ expires_at = ssdp_info .expires_at ,
52+ ipv4 = ssdp_info .ip ,
53+ port = port ,
54+ mac_addr = mac_addr ,
55+ household_id = ssdp_info .household_id ,
56+ player_id = ssdp_info .player_id ,
57+ name = discovery_info .device .name ,
58+ model = discovery_info .device .model ,
59+ model_display_name = discovery_info .device .modelDisplayName ,
60+ sw_gen = discovery_info .device .swGen ,
61+ is_group_coordinator = ssdp_info .is_group_coordinator ,
62+ }
63+
64+ if type (ssdp_info .group_name ) == " string" and # ssdp_info .group_name > 0 then
65+ ret .group_name = ssdp_info .group_name
66+ end
67+
68+ if type (ssdp_info .group_id ) == " string" and # ssdp_info .group_id > 0 then
69+ ret .group_id = ssdp_info .group_id
70+ end
71+
72+ if type (wss_path ) == " string" and # wss_path > 0 and wss_path ~= SONOS_DEFAULT_WSS_PATH then
73+ ret .wss_path = wss_path
74+ end
75+
76+ if type (rest_path ) == " string" and # rest_path > 0 and rest_path ~= SONOS_DEFAULT_REST_PATH then
77+ ret .rest_path = rest_path
78+ end
79+
80+ local proxy_index = function (_ , k )
81+ if k == " rest_url" and not rawget (ret , " rest_url" ) then
82+ rawset (
83+ ret ,
84+ " rest_url" ,
85+ net_url .parse (
86+ string.format (
87+ " https://%s:%s/%s" ,
88+ ret .ipv4 ,
89+ ret .port ,
90+ ret .rest_path or SONOS_DEFAULT_REST_PATH
91+ )
92+ )
93+ )
94+ end
95+
96+ if k == " wss_url" and not rawget (ret , " wss_url" ) then
97+ rawset (
98+ ret ,
99+ " wss_url" ,
100+ net_url .parse (
101+ string.format (
102+ " https://%s:%s/%s" ,
103+ ret .ipv4 ,
104+ ret .port ,
105+ ret .wss_path or SONOS_DEFAULT_WSS_PATH
106+ )
107+ )
108+ )
109+ end
110+
111+ return rawget (ret , k )
112+ end
113+
114+ local proxy_newindex = function (_ , _ , _ )
115+ error (" attempt to index a read-only table" , 2 )
116+ end
117+
118+ for k , v in pairs (SpeakerDiscoveryInfo ) do
119+ rawset (ret , k , v )
120+ end
121+
122+ return setmetatable (ret , { __index = proxy_index , __newindex = proxy_newindex })
123+ end
124+
125+ function SpeakerDiscoveryInfo :is_bonded ()
126+ return (self .group_id == nil )
127+ end
128+
129+ --- @return SonosSSDPInfo
130+ function SpeakerDiscoveryInfo :as_ssdp_info ()
131+ --- @type SonosSSDPInfo
132+ return {
133+ ip = self .ipv4 ,
134+ group_id = self .group_id or " " ,
135+ group_name = self .group_name or " " ,
136+ expires_at = self .expires_at ,
137+ player_id = self .player_id ,
138+ wss_url = self .wss_url :build (),
139+ household_id = self .household_id ,
140+ is_group_coordinator = self .is_group_coordinator ,
141+ }
142+ end
143+
12144local sonos_ssdp = {}
145+ sonos_ssdp .SpeakerDiscoveryInfo = SpeakerDiscoveryInfo
13146
14147--- @module ' luncheon.headers'
15148
160293
161294--- @class SonosPersistentSsdpTask
162295--- @field package ssdp_search_handle SsdpSearchHandle
163- --- @field package player_info_by_sonos_ids table<UniqueKey , { ssdp_info : SonosSSDPInfo , discovery_info : SonosDiscoveryInfo } >
164- --- @field package player_info_by_mac_addrs table<string , { ssdp_info : SonosSSDPInfo , discovery_info : SonosDiscoveryInfo } >
296+ --- @field package player_info_by_sonos_ids table<UniqueKey , SpeakerDiscoveryInfo >
297+ --- @field package player_info_by_mac_addrs table<string , SpeakerDiscoveryInfo >
165298--- @field package waiting_for_unique_key table<UniqueKey , table[]>
166299--- @field package waiting_for_mac_addr table<string , table[]>
167300--- @field package control_tx table
@@ -217,7 +350,7 @@ function SonosPersistentSsdpTask:get_player_info(reply_tx, ...)
217350 end
218351
219352 local maybe_existing = lookup_table [lookup_key ]
220- if maybe_existing and maybe_existing .ssdp_info . expires_at > os.time () then
353+ if maybe_existing and maybe_existing .expires_at > os.time () then
221354 reply_tx :send (maybe_existing )
222355 return
223356 end
@@ -267,11 +400,12 @@ function sonos_ssdp.spawn_persistent_ssdp_task()
267400 local maybe_known = task_handle .player_info_by_sonos_ids [unique_key ]
268401 local is_new_information = not (
269402 maybe_known
270- and maybe_known .ssdp_info . expires_at > os.time ()
271- and sonos_ssdp .ssdp_info_eq (maybe_known . ssdp_info , sonos_ssdp_info )
403+ and maybe_known .expires_at > os.time ()
404+ and sonos_ssdp .ssdp_info_eq (maybe_known : as_ssdp_info () , sonos_ssdp_info )
272405 )
273406
274- local info_to_send
407+ local speaker_info
408+ local event_bus_msg
275409
276410 if is_new_information then
277411 local headers = SonosApi .make_headers ()
@@ -283,30 +417,21 @@ function sonos_ssdp.spawn_persistent_ssdp_task()
283417 )
284418 if not discovery_info then
285419 log .error (string.format (" Error getting discovery info from SSDP response: %s" , err ))
420+ elseif discovery_info ._objectType == " globalError" then
421+ log .error (string.format (" Error message in discovery info: %s" , discovery_info .errorCode ))
286422 else
287- local unified_info =
288- { ssdp_info = sonos_ssdp_info , discovery_info = discovery_info , force_refresh = true }
289- task_handle .player_info_by_sonos_ids [unique_key ] = unified_info
290- info_to_send = unified_info
423+ speaker_info = SpeakerDiscoveryInfo .new (sonos_ssdp_info , discovery_info )
424+ task_handle .player_info_by_sonos_ids [unique_key ] = speaker_info
425+ event_bus_msg = { speaker_info = speaker_info , force_refresh = true }
291426 end
292427 else
293- info_to_send = {
294- ssdp_info = maybe_known .ssdp_info ,
295- discovery_info = maybe_known .discovery_info ,
296- force_refresh = false ,
297- }
428+ speaker_info = maybe_known
429+ event_bus_msg = { speaker_info = speaker_info , force_refresh = false }
298430 end
299431
300- if info_to_send then
301- if not (info_to_send .discovery_info and info_to_send .discovery_info .device ) then
302- log .error_with (
303- { hub_logs = false },
304- st_utils .stringify_table (info_to_send , " Sonos Discovery Info has unexpected structure" )
305- )
306- return
307- end
308- event_bus :send (info_to_send )
309- local mac_addr = utils .extract_mac_addr (info_to_send .discovery_info .device )
432+ if speaker_info then
433+ event_bus :send (event_bus_msg )
434+ local mac_addr = speaker_info .mac_addr
310435 local waiting_handles = task_handle .waiting_for_unique_key [unique_key ] or {}
311436
312437 log .debug (st_utils .stringify_table (waiting_handles , " waiting for unique keys" , true ))
@@ -318,7 +443,7 @@ function sonos_ssdp.spawn_persistent_ssdp_task()
318443 st_utils .stringify_table (waiting_handles , " waiting for unique keys and mac addresses" , true )
319444 )
320445 for _ , reply_tx in ipairs (waiting_handles ) do
321- reply_tx :send (info_to_send )
446+ reply_tx :send (speaker_info )
322447 end
323448
324449 task_handle .waiting_for_unique_key [unique_key ] = {}
0 commit comments