Skip to content

Instantly share code, notes, and snippets.

@billchurch
Last active May 17, 2024 19:16
Show Gist options
  • Save billchurch/8dc2c8835dedf140075dbc60ed483b05 to your computer and use it in GitHub Desktop.
Save billchurch/8dc2c8835dedf140075dbc60ed483b05 to your computer and use it in GitHub Desktop.
Python script to find all sonos units on your network with uPNP and then connect to a unifi switch to tell you what switch port they're on
import socket
import requests
import paramiko
import time
import xml.etree.ElementTree as ET
def discover_sonos_devices():
MCAST_GRP = '239.255.255.250'
MCAST_PORT = 1900
msg = (
'M-SEARCH * HTTP/1.1\r\n'
'HOST: 239.255.255.250:1900\r\n'
'MAN: "ssdp:discover"\r\n'
'MX: 1\r\n'
'ST: urn:schemas-upnp-org:device:ZonePlayer:1\r\n'
'\r\n'
)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
sock.settimeout(2)
sock.sendto(msg.encode('utf-8'), (MCAST_GRP, MCAST_PORT))
devices = {}
try:
while True:
data, addr = sock.recvfrom(1024)
response = data.decode('utf-8')
if 'Sonos' in response:
for line in response.split('\r\n'):
if line.startswith('LOCATION:'):
location = line.split(' ')[1].strip()
devices[addr[0]] = location
except socket.timeout:
pass
return devices
def get_mac_address_from_description(url):
try:
response = requests.get(url)
if response.status_code == 200:
root = ET.fromstring(response.content)
ns = {'upnp': 'urn:schemas-upnp-org:device-1-0'}
mac_address = root.find('.//upnp:MACAddress', ns).text
return mac_address
else:
print(f'Failed to get device description from {url}')
return None
except requests.RequestException as e:
print(f'Error getting device description from {url}: {e}')
return None
def ssh_to_telnet(host, username, password, telnet_command):
try:
# Establish SSH connection
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(host, username=username, password=password)
# Start an interactive session
channel = ssh.invoke_shell()
# Wait for the SSH prompt
time.sleep(1)
channel.recv(9999)
# Enter telnet mode
channel.send('telnet localhost\n')
time.sleep(1)
# Wait for the Telnet prompt
channel.recv(9999)
# Send Telnet command
channel.send(telnet_command + '\n')
time.sleep(2)
# Receive output
output = channel.recv(9999).decode('ascii')
# Close the Telnet session
channel.send('exit\n')
time.sleep(1)
# Close the SSH session
channel.send('exit\n')
ssh.close()
return output
except Exception as e:
print(f'Error executing SSH to Telnet command: {e}')
return None
def get_mac_address_table_via_telnet(host, username, password, mac_address):
telnet_command = f'show mac-addr-table {mac_address}'
result = ssh_to_telnet(host, username, password, telnet_command)
return result
def parse_mac_table_output(output):
mac_port_mapping = {}
lines = output.split('\n')
for line in lines:
parts = line.split()
if len(parts) > 4 and parts[1].count(':') == 5: # Ensuring it's a MAC address line
mac = parts[1].strip()
port = parts[2].strip()
mac_port_mapping[mac] = port
return mac_port_mapping
if __name__ == '__main__':
# Discover Sonos devices
sonos_devices = discover_sonos_devices()
mac_addresses = []
if sonos_devices:
print(f'Discovered Sonos devices: {list(sonos_devices.keys())}')
for ip, location_url in sonos_devices.items():
mac_address = get_mac_address_from_description(location_url)
if mac_address:
mac_addresses.append(mac_address)
print(f'MAC: {mac_address}')
else:
print('No Sonos devices found on the network.')
# Connect to UniFi switch and find ports for MAC addresses
switch_ip = '192.168.0.30' # Replace with your switch IP address
username = 'your-username' # Replace with your SSH username
password = 'your-password' # Replace with your SSH password
for mac in mac_addresses:
output = get_mac_address_table_via_telnet(switch_ip, username, password, mac)
if output:
mac_port_mapping = parse_mac_table_output(output)
for mac, port in mac_port_mapping.items():
print(f'MAC address {mac} is connected to port {port}')
else:
print(f'No output for MAC address {mac}')
@billchurch
Copy link
Author

Sonos Device Discovery and Port Mapping Script

This script discovers Sonos devices on your network using SSDP (Simple Service Discovery Protocol), retrieves their MAC addresses, and then connects to a UniFi switch to find which ports the MAC addresses are connected to using Telnet commands.

Requirements

  • Python 3.x
  • requests library
  • paramiko library

You can install the required libraries using pip:

pip3 install requests paramiko

How It Works

  1. Discover Sonos Devices: The script sends a UPnP multicast message to discover Sonos devices on the network. It collects the IP addresses and LOCATION URLs from the SSDP responses.
  2. Retrieve MAC Addresses: For each discovered Sonos device, the script fetches the device description XML from the LOCATION URL and extracts the MACAddress.
  3. SSH and Telnet to UniFi Switch: The script establishes an SSH connection to the UniFi switch, starts a Telnet session within the SSH session, and runs the show mac-addr-table command to find the port each MAC address is connected to.
  4. Parse and Print Results: The script parses the output from the Telnet command to extract the MAC address and corresponding port information, and then prints the results.

Usage

  1. Update Script Configuration: Replace the placeholder values in the script with your actual network details.

    • switch_ip: IP address of your UniFi switch.
    • username: SSH username for your UniFi switch.
    • password: SSH password for your UniFi switch. paramiko support ssh public key auth, at least it does on macOS so you can leave this as is if you have that setup in unifi
  2. Run the Script: Execute the script using Python.

python3 sonos_unifi_discovery.py

Notes

  • Ensure your network allows multicast traffic and that the Sonos devices are on the same network as the machine running the script.
  • Make sure your UniFi switch and SSH credentials are correct.
  • Adjust the script as necessary for your specific environment and requirements.

License

This project is licensed under the MIT License.

@billchurch
Copy link
Author

billchurch commented May 17, 2024

Sample output

% python3 sonos_unifi_discovery.py
Discovered Sonos devices: ['192.168.0.94', '192.168.0.90', '192.168.0.92', '192.168.0.98', '192.168.0.93', '192.168.0.97', '192.168.0.95', '192.168.0.91', '192.168.0.96']
MAC: 34:7E:5C:72:1D:65
MAC: 78:28:CA:A9:2D:2C
MAC: 34:7E:5C:36:E5:8C
MAC: 5C:AA:FD:6E:F2:41
MAC: 94:9F:3E:F8:7E:19
MAC: 5C:AA:FD:94:16:31
MAC: 34:7E:5C:B2:1B:BD
MAC: 78:28:CA:24:14:AB
MAC: 94:9F:3E:F6:03:A2
MAC address 34:7E:5C:72:1D:65 is connected to port 0/19
MAC address 78:28:CA:A9:2D:2C is connected to port 0/19
MAC address 34:7E:5C:36:E5:8C is connected to port 0/19
MAC address 5C:AA:FD:6E:F2:41 is connected to port 0/40
MAC address 94:9F:3E:F8:7E:19 is connected to port 0/19
MAC address 5C:AA:FD:94:16:31 is connected to port 0/19
MAC address 34:7E:5C:B2:1B:BD is connected to port 0/19
MAC address 78:28:CA:24:14:AB is connected to port 0/19
MAC address 94:9F:3E:F6:03:A2 is connected to port 0/16

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment