-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsonos-monitor.py
218 lines (175 loc) · 8.79 KB
/
sonos-monitor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#!/usr/bin/python
# -*- coding: utf-8 -*-
# What this does:
#
# Start this as a daemon. It connects to your Sonos Connect and your Yamaha
# Receiver. Whenever the Sonos Connect starts playing music, radio or whatever,
# it turns on the Receiver, switches to the appropriate input, sets the volume
# and changes to the Sound Program you want to (e.g. "5ch Stereo").
#
# If the Receiver is already turned on, it just switches the input and the
# Sound Program, not the Volume.
#
# If you set the standby time of the Receiver to 20 minutes, you'll have a
# decent instant-on solution for your Sonos Connect - it behaves just like
# one of Sonos' other players.
#
# Optimized for minimum use of resources. I leave this running on a Raspberry
# Pi at my place.
#
# Before installing it as a daemon, try it out first: Adapt the settings in the
# script below. Then just run the script. It'll auto-discover your Sonos
# Connect. If that fails (e.g. because you have more than one Connect in your
# home or for other reasons), you can use the UID of your Sonos Connect as the
# first and only parameter of the script. The script will output all UIDs
# neatly for your comfort.
#
# Prerequisites:
# - Your Yamaha Receiver has to be connected to the LAN.
# - Both your Yamaha Receiver and your Sonos Connect have to use fixed IP
# addresses. You probably have to set this in your router (or whichever
# device is your DHCP).
# - Your Yamaha Receiver's setting of "Network Standby" has to be "On".
# Otherwise the Receiver cannot be turned on from standby mode.
#
# Software prerequisites:
# - sudo pip install soco
import os
import sys
import time
import re
import urllib, urllib2
import telnetlib
import soco
import Queue
import signal
from datetime import datetime
__version__ = '0.3'
# --- Please adapt these settings ---------------------------------------------
YAMAHA_IP = '192.168.2.23' # IP address of your Yamaha Receiver. Look it up in your router or set it in the Receiver menu.
YAMAHA_PORT = 50000 # Port your Yamaha Receiver listens to (should be 50000 unless you changed it)
YAMAHA_INPUT = 'AV1' # Name of your Receiver's input the Sonos Connect is connected to. Should be one
# of AV1, AV2, ..., HDMI1, HDMI2, ..., AUDIO1, AUDIO2, ..., TUNER, PHONO, V-AUX, DOCK,
# iPod, Bluetooth, UAW, NET, Napster, PC, NET RADIO, USB, iPod (USB) or the like.
# Don't use an input name you set yourself in the Receiver's setup menu.
YAMAHA_VOLUME = -20.0 # Volume the Receiver is set to when started. Set to None if you don't want to change it.
YAMAHA_SOUNDPRG = '5ch Stereo' # DSP Sound Program to set the Receiver to when started. Set to None if you don't want to change it.
# Should be one of Standard, 2ch Stereo, 5ch Stereo, 7ch Stereo, Music Video, Hall in Munich,
# Hall in Vienna, Chamber, Cellar Club, The Roxy Theatre, The Bottom Line, Sports, Action Game,
# Roleplaying Game, Spectacle, Sci-Fi, Adventure, Drama, Mono Movie, Surround Decoder or the like.
# basic in/out with the receiver
def _yamaha_send_receive(out):
rec = ''
try:
tn = telnetlib.Telnet(YAMAHA_IP, YAMAHA_PORT, 10)
tn.write(out + "\r\n")
rec = tn.read_until("\r\n", 5)
tn.close()
except Exception as e:
print u"Connecting to Yamaha Receiver failed: {}".format(e).encode('utf-8')
return rec[0:-2]
def yamaha_get_value(variable):
return _yamaha_send_receive("@{}=?".format(variable))[len(variable)+2:]
def yamaha_set_value(variable, value):
cv = yamaha_get_value(variable)
# set the value only when it is different from the current value
# because otherwise the receiver won't answer and then you're
# timeout-ing waiting for the answer ...
if str(cv) != str(value):
print u"Setting Yamaha {} to {} (was: {})".format(variable, value, cv).encode('utf-8')
_yamaha_send_receive("@{}={}".format(variable, value))
def auto_flush_stdout():
unbuffered = os.fdopen(sys.stdout.fileno(), 'w', 0)
sys.stdout.close()
sys.stdout = unbuffered
def handle_sigterm(*args):
global break_loop
print u"SIGTERM caught. Exiting gracefully.".encode('utf-8')
break_loop = True
# --- Discover SONOS zones ----------------------------------------------------
if len(sys.argv) == 2:
connect_uid = sys.argv[1]
else:
connect_uid = None
print u"Discovering Sonos zones".encode('utf-8')
match_ips = []
for zone in soco.discover():
print u" {} (UID: {})".format(zone.player_name, zone.uid).encode('utf-8')
if connect_uid:
if zone.uid.lower() == connect_uid.lower():
match_ips.append(zone.ip_address)
else:
# we recognize Sonos Connect and ZP90 by their hardware revision number
if zone.get_speaker_info().get('hardware_version')[:4] == '1.1.':
match_ips.append(zone.ip_address)
print u" => possible match".encode('utf-8')
print
if len(match_ips) != 1:
print u"The number of Sonos Connect devices found was not exactly 1.".encode('utf-8')
print u"Please specify which Sonos Connect device should be used by".encode('utf-8')
print u"using its UID as the first parameter.".encode('utf-8')
sys.exit(1)
sonos_device = soco.SoCo(match_ips[0])
subscription = None
renewal_time = 120
# --- Initial Yamaha status ---------------------------------------------------
print u"Yamaha Power status: {}".format(yamaha_get_value('MAIN:PWR')).encode('utf-8')
print u"Yamaha Input select: {}".format(yamaha_get_value('MAIN:INP')).encode('utf-8')
print u"Yamaha Volume: {}".format(yamaha_get_value('MAIN:VOL')).encode('utf-8')
print u"Yamaha Sound Program: {}".format(yamaha_get_value('MAIN:SOUNDPRG')).encode('utf-8')
print
# --- Main loop ---------------------------------------------------------------
break_loop = False
last_status = None
# catch SIGTERM gracefully
signal.signal(signal.SIGTERM, handle_sigterm)
# non-buffered STDOUT so we can use it for logging
auto_flush_stdout()
while True:
# if not subscribed to SONOS connect for any reason (first start or disconnect while monitoring), (re-)subscribe
if not subscription or not subscription.is_subscribed or subscription.time_left <= 5:
# The time_left should normally not fall below 0.85*renewal_time - or something is wrong (connection lost).
# Unfortunately, the soco module handles the renewal in a separate thread that just barfs on renewal
# failure and doesn't set is_subscribed to False. So we check ourselves.
# After testing, this is so robust, it survives a reboot of the SONOS. At maximum, it needs 2 minutes
# (renewal_time) for recovery.
if subscription:
print u"{} *** Unsubscribing from SONOS device events".format(datetime.now()).encode('utf-8')
try:
subscription.unsubscribe()
soco.events.event_listener.stop()
except Exception as e:
print u"{} *** Unsubscribe failed: {}".format(datetime.now(), e).encode('utf-8')
print u"{} *** Subscribing to SONOS device events".format(datetime.now()).encode('utf-8')
try:
subscription = sonos_device.avTransport.subscribe(requested_timeout=renewal_time, auto_renew=True)
except Exception as e:
print u"{} *** Subscribe failed: {}".format(datetime.now(), e).encode('utf-8')
# subscription failed (e.g. sonos is disconnected for a longer period of time): wait 10 seconds
# and retry
time.sleep(10)
continue
try:
event = subscription.events.get(timeout=10)
status = event.variables.get('transport_state')
if not status:
print u"{} Invalid SONOS status: {}".format(datetime.now(), event.variables).encode('utf-8')
if last_status != status:
print u"{} SONOS play status: {}".format(datetime.now(), status).encode('utf-8')
if last_status != 'PLAYING' and status == 'PLAYING':
if not yamaha_get_value('MAIN:PWR') == 'On':
yamaha_set_value('MAIN:PWR', 'On')
if YAMAHA_VOLUME is not None:
yamaha_set_value('MAIN:VOL', float(YAMAHA_VOLUME))
if YAMAHA_SOUNDPRG is not None:
yamaha_set_value('MAIN:SOUNDPRG', YAMAHA_SOUNDPRG)
yamaha_set_value('MAIN:INP', YAMAHA_INPUT)
last_status = status
except Queue.Empty:
pass
except KeyboardInterrupt:
handle_sigterm()
if break_loop:
subscription.unsubscribe()
soco.events.event_listener.stop()
break