svrooij / node-sonos-ts

:speaker: Sonos control library, use this library in your own appliction.
https://sonos-ts.svrooij.io/
MIT License
81 stars 18 forks source link

BUG: Discovery not working (for symfonic speaker) #150

Closed svrooij closed 2 years ago

svrooij commented 2 years ago

What happened

Device not discovered https://github.com/svrooij/sonos-api-docs/pull/31

What did you expect to happen

How to reproduce it (minimal and precise)

  1. Step 1
  2. ...

Debug logging

Environment

Checklist

Please confirm the following before creating the issue.

svrooij commented 2 years ago

@mandar1jn I was already working on a new version of the discovery, because it was unstable at my end. See https://github.com/svrooij/node-sonos-ts/pull/149

mandar1jn commented 2 years ago

Ok. Thanks

github-actions[bot] commented 2 years ago

:tada: This issue has been resolved in version 2.5.0-beta.4 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

svrooij commented 2 years ago

@mandar1jn can you check if that fixed your issue? Sometimes it just takes some time to discover. It’s not really stable on Windows.

might be better to give your speakers a static ip in your router and start from that static ip. This will be much faster and will always work

mandar1jn commented 2 years ago

@svrooij using the beta branch, no speakers are found for me. It just gives me an error that no devices could be found.

svrooij commented 2 years ago

What is your environment? Windows / Linux / Mac Docker Node version Sonos S1/S2?

Firewall?

I can possibly not fix this the discovery works janky. Static IP works much better

mandar1jn commented 2 years ago

Windows Node 16.something S2 No

I decided to use up already

hklages commented 2 years ago

Have the same in Windows 10. Discovery works fine in Linux. Newest NodeJS, 2.5.0-beta.4

svrooij commented 2 years ago

My guess is that Windows won't let you open an UDP port most of the time. So if I try to do discovery on Windows 1 out of 20 times it works.

hklages commented 2 years ago

It seems that the broadcast on windows "does not work". I installed a small udp listener and could capture the broadcast from a linux system but not from the windows system.

const dgram = require ('dgram')
const receiver = dgram.createSocket('udp4')
receiver.on('error', (err) => {
  console.log(JSON.stringify(err))
})
receiver.on('listening', () => {
  console.log('listening')
})
receiver.on('message', (msg, rinfo) => {
  console.log(`receive got: ${msg} from ${rinfo.address}\r\n`)
})
receiver.bind(1900)

Tomorrow I am going to do the opposite - install the listener on Linux system and try to discover from a window and a linux system.

In your "Broadcast" block there is a small difference compared to documentation: The " are missing in your command. Dont know whether that has an impact.

'MAN: "ssdp:discover" '

svrooij commented 2 years ago

I'm now guessing that you cannot open a socket on Windows and use the same socket to send the message. Maybe I need to create 1 socket for sending and one for receiving.

hklages commented 2 years ago

After having removed Hyper-V the following script works for me in VScode, Window10. @mandar1jn : does this script shows your IKEA player?

// Send SSDP broadcast and output to console
// Call: node <filename>
// credits: https://gist.github.com/chrishulbert/895382

const dgram = require('dgram') 

const sendBroadcast = dgram.createSocket({type:'udp4',reuseAddr:true})

const PORT = 1900
const ADDRESS = '239.255.255.250'

// set up upd sender - bind is async, use a random port
sendBroadcast.bind( () => {
  console.log('random port:' + sendBroadcast.address().port)

  // set up udp listener
  const udpListener = dgram.createSocket({type:'udp4',reuseAddr:true})
  udpListener.on('error', (err) => {
    console.log(JSON.stringify(err))
  })
  udpListener.on('listening', () => { 
    udpListener.setBroadcast(true)
    udpListener.addMembership(ADDRESS)
    console.log('start listening ...')
  })
    udpListener.on('message', (msg, rinfo) => {
        console.log(rinfo.address + ':' + rinfo.port + '\r\nmessage: ' + msg)
  })
  // async
  udpListener.bind(sendBroadcast.address().port, () => {

    console.log('listener port: ' + udpListener.address().port)
    sendBroadcast.setBroadcast(true)

    // Alternatives - both work
    // urn:smartspeaker-audio:service:SpeakerGroup:1
    // urn:schemas-upnp-org:device:ZonePlayer:1
    const ssdp_search = Buffer.from( 
      'M-SEARCH * HTTP/1.1\r\n'
        + `HOST: ${ADDRESS}:${PORT}\r\n`
        + 'MAN: "ssdp:discover"\r\n'
        + 'MX: 1\r\n'
        + 'ST: urn:smartspeaker-audio:service:SpeakerGroup:1\r\n',
        )

    sendBroadcast.send(ssdp_search, 0, ssdp_search.length, PORT, ADDRESS, (err) => {
      if (err) {
        console.log(JSON.stringify(err))
      } else {
        console.log('OK sent')
      }
      sendBroadcast.close()
    })

  }) 
}) 
hklages commented 2 years ago

I did some more testing with an optimized script (see bottom). The scripts works fine in the following environement: Windows + Nodejs, Windows + NodeRED, Synology + Docker + Node-RED, Home Assistant + Docker + Node-RED.

Comparing the script with your implementation I only see 2 differences. a) bind with random port instead of 1900 b) MAN is with " .

Random port seems to make the difference.

// Demonstrator code - not for production!
// USAGE: Send SSDP broadcast and output to console
// CALL: node <filename>

// CAUTION: infinite loop, may block ports

// CREDITS: https://gist.github.com/chrishulbert/895382

const dgram = require('dgram') 

// Broadcast to specific address, port with specific message
const PORT = 1900
const ADDRESS = '239.255.255.250'
// Alternatives - both work for SONOS. First provides only the group!
// ST urn:smartspeaker-audio:service:SpeakerGroup:1
// ST urn:schemas-upnp-org:device:ZonePlayer:1
//
// To discover also NON-SONOS devices use:
// ST ssdp:all
// 
// MAN should use " " although SONOS works without.
const messageAsBuffer = Buffer.from( 
  'M-SEARCH * HTTP/1.1\r\n'
      + `HOST: ${ADDRESS}:${PORT}\r\n`
      + 'MAN: "ssdp:discover"\r\n'
      + 'MX: 1\r\n'
      + 'ST: urn:schemas-upnp-org:device:ZonePlayer:1\r\n',
)

const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true })
// set up error, listening, messages
socket.on('error', (err) => {
  console.log(JSON.stringify(err))
  socket.close()
})
socket.on('listening', () => { 
  // following are not needed
  // socket.setBroadcast(true)
  // socket.addMembership(ADDRESS)
  // socket.setMulticastTTL(128)
  console.log('Start listening ...')
})
socket.on('message', (msg, rinfo) => {
  // msg is buffer and contains line breaks. 
  const msgArray = msg.toString().split(/\r\n|\n|\r/)
  // LOCATION is in line 4 -> so it is number 3
  console.log(rinfo.address + '-->' + msgArray[3].replace('LOCATION: ', '').trim())
})

// bind with random port and send broadcast. bind is async!
socket.bind(() => {
  console.log('random port:' + socket.address().port)

  socket.send(messageAsBuffer, 0, messageAsBuffer.length, PORT, ADDRESS, (err) => {
    if (err) {
      console.log(JSON.stringify(err))
      socket.close()
    } else {
      console.log('OK broadcast was sent!')
    }
  })
})
mandar1jn commented 2 years ago

This does not work. It discovers our other sonos speaker and our philips heu control center but not the sonos

mandar1jn commented 2 years ago

hell, it discovers our hue control hub 7 times

mandar1jn commented 2 years ago
Start listening ...                                                                                                     
random port:55684                                                                                                       
OK broadcast was sent!                                                                                                  
192.168.178.116-->http://192.168.178.116:1400/xml/device_description.xml                                                
192.168.178.87-->CACHE-CONTROL: max-age=100                                                                             
192.168.178.87-->CACHE-CONTROL: max-age=100                                                                             
192.168.178.87-->CACHE-CONTROL: max-age=100                                                                             
192.168.178.87-->CACHE-CONTROL: max-age=100                                                                             
192.168.178.87-->CACHE-CONTROL: max-age=100                                                                             
192.168.178.87-->CACHE-CONTROL: max-age=100
hklages commented 2 years ago

Don't worry. Your hue gateways only seems to be "talkative". The scripts asks the devices in the network to identify themselves. One answer would be enough and that is what the Sonos player does.

What are the network addresses of your not-detected SONOS/symphonic player?

mandar1jn commented 2 years ago

192.168.178.159

mandar1jn commented 2 years ago

Could it be a setting maybe? I remember disabling shit I didn't think I would need. Not sure if it's related

svrooij commented 2 years ago

With your random port mention, I changed the code a bit and got a one time discovery:

  sonos:device-discovery Device discovery created +0ms
  sonos:device-discovery Search one device in 60 seconds +1ms
  sonos:device-discovery sendBroadcast() +1ms
  sonos:device-discovery UDP port not bound, waiting 100ms +0ms
  sonos:device-discovery UDP socket started listening +3ms
  sonos:device-discovery Bound to port 55557 +0ms
  sonos:device-discovery sendBroadcast() +101ms
  sonos:device-discovery SSDP message
  sonos:device-discovery HTTP/1.1 200 OK
CACHE-CONTROL: max-age = 1800
EXT:
LOCATION: http://192.168.96.53:1400/xml/device_description.xml
SERVER: Linux UPnP/1.0 Sonos/57.9-23290 (ZPS1)
ST: urn:schemas-upnp-org:device:ZonePlayer:1
USN: uuid:RINCON_B8E9375A170401400::urn:schemas-upnp-org:device:ZonePlayer:1
X-RINCON-HOUSEHOLD: Sonos_uUgqHh___________________Q4Qr
X-RINCON-BOOTSEQ: 110
X-RINCON-WIFIMODE: 0
X-RINCON-VARIANT: 0
HOUSEHOLD.SMARTSPEAKER.AUDIO: Sonos_uUgqHhGjYV_____Qr.EDEvBlPq_____s7TSo

 +134ms
  sonos:device-discovery Found ZPS1 on 192.168.96.53 +2ms

After that still no discovery. It seems you don't need to create a socket at port 1900 (which I thought). That leaves me with the question, why is hyper-v holding that 1900 port. And why isn't the new solution stable. Because after this first result it's quite again.

hklages commented 2 years ago

@svrooij Hyper-V: Maybe it does not block 1900. For sure Hyper-V creates a new vEthernet adapter and set it as default with a total different network address. All my devise have network 192.168.178... but the Laptop uses the vEthernet adapter with a different network address. This network address was used when sending the broadcast but on my router this address did not show up.
vEthernet

If you just run the above script - does it detect all your player? What if you use ssdp:all instead of urn:schemas-upnp-org:device:ZonePlayer:1? (and filter duplicates, not having Location in 4th line)

hklages commented 2 years ago

MX 5 might be another option? After 6 seconds we can no expect any response. But it gives the devices a good range to answer.

The maximum wait response time (MX) in seconds that a root device can take before responding. The MX field is an attempt to overcome a scaling issue implicit with SSDP. SSDP is a chatty protocol, in a network with a significant number of nodes that host SSDP services, sending an M-SEARCH message could result in accidentally DDOS-ing the questing node due to too many services responding at once. The MX field instructs the root device to wait a random time between 0 and MX before attempting to respond - this should allow the responses to be spaced out enough to ease the processing strain on the control point. The MX value should be between 1 and 5. Even with the MX workaround, SSDP is recommended only to be used in home or small office networks.

svrooij commented 2 years ago

Just another idea, instead of using SSDP which doesn't work in some situations (docker with regular network config, vlans). Why don't we just portscan for port 1400 and then try to fetch the device description.

As an input I would expect to set a range in where the Sonos speakers should be.

Still using discovery (SSDP or portscan) would not be the recommend way since it will still be slower then starting from a static ip

github-actions[bot] commented 2 years ago

:tada: This issue has been resolved in version 2.5.0-beta.5 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

hklages commented 2 years ago

Just another idea, instead of using SSDP which doesn't work in some situations (docker with regular network config, vlans). Why don't we just portscan for port 1400 and then try to fetch the device description.

I don't know how "portscan" may work. It should not take longer then a few seconds. You could offer both options - the SSDP and the portscan and let the user choose his preferred option.

For my package in Node-RED I recommend to use static ip address. Just for convenience there is the option to discover.
So I can life with SSDP (in a modified version supporting Windows10). My next release will allow the user to enter DNS names - that is another nice option and maybe the best. It will make discovery obsolete (for many). Names can be remembered (and ip addresses can be changed).

hklages commented 2 years ago

@mandar1jn

Could it be a setting maybe? I remember disabling shit I didn't think I would need. Not sure if it's related

I don't think so. You could check your network (not unique ip addresses?), DNS conflicts, are devices switched off? maybe 2 device bound, gateways or bridges interfere, access via ping possible?

You can check the traffic on port 1900 with the following script. You should see you player there:

// listen on port 1900, broadcast mode and write all received messages to console
// Test for SSDP - discovery of SONOS player in network

const dgram = require('dgram')
const receiver = dgram.createSocket({ type: 'udp4', reuseAddr: true })

const PORT = 1900
const ADDRESS = '239.255.255.250'

receiver.on('error', (err) => {
  console.log(JSON.stringify(err))
})

receiver.on('listening', () => {
  // maybe next 3 statements are not necessary
  receiver.setBroadcast(true)
  receiver.setMulticastTTL(128)
  receiver.addMembership(ADDRESS)
  console.log('start listening ...')
})

receiver.on('message', (msg, rinfo) => {
  console.log(`${rinfo.address}:${rinfo.port}\r\n${msg} `)
})

receiver.bind(PORT)
svrooij commented 2 years ago

@hklages 2.5.0-beta5 has a second new version for discovery. That also works stable on my Windows 10 config. I also added you as contributor and added the script you provided so everybody can test discovery

@mandar1jn can you check if that also works for you?

hklages commented 2 years ago

I tested beta.5: First it did not work but after rebooting the Windows System it works now (many times). Thanks!

github-actions[bot] commented 2 years ago

:tada: This issue has been resolved in version 2.5.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket: