-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathssmb.py
274 lines (233 loc) · 10.4 KB
/
ssmb.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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/usr/bin/python
# -*- coding: utf-8 -*-
# What this does:
#
# Start this as a daemon. It connects to your Sonos Connect and your MARANTZ
# 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 MARANTZ Receiver has to be connected to the LAN.
# - Both your MARANTZ 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 MARANTZ 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
# - sudo pip install denonavr
# - sudo pip install aiohttp
import os
from soco import events_asyncio
from pprint import pprint
from datetime import datetime
import logging
import denonavr
import asyncio
import signal
import soco
import time
import sys
class Unbuffered(object):
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def writelines(self, datas):
self.stream.writelines(datas)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
soco.config.EVENTS_MODULE = events_asyncio
__version__ = '0.4'
# --- Please adapt these settings ---------------------------------------------
# logging.basicConfig(level=logging.NOTSET)
# The UUID of your device, if you dont have it, we will try to search for it later
SONOS_UUID = 'RINCON_949F3EB99CCA01400'
# IP address of your MARANTZ Receiver. Look it up in your router or set it in the Receiver menu.
MARANTZ_IP = '192.168.11.4'
# The ip address of your device, if you dont have it, we will try to search for it later
SONOS_IP = '192.168.11.27'
# Name of your Receiver's input the Sonos Connect is connected to. Should be one
MARANTZ_INPUT = 'CD'
# 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.
# Volume the Receiver is set to when started. -20.0 equals 60 on Marantz devices. Set to None if you don't want to change it.
MARANTZ_VOLUME = -15.0
# DSP Sound Program to set the Receiver to when started. Set to None if you don't want to change it.
MARANTZ_SOUNDPRG = '5ch Stereo'
avr = None
break_loop = False
global last_status
last_status = None
sonos_device = None
renewal_time = 120
subscription = None
async def main():
print("SSMB launched")
# --- Connect to Marantz AVR --------------------------------------------------
await setup_avr()
avr.register_callback("ALL", update_avr_callback)
# --- Discover SONOS zones ----------------------------------------------------
if len(sys.argv) == 2:
connect_uid = sys.argv[1]
else:
connect_uid = SONOS_UUID
global sonos_device
sonos_device = soco.SoCo(SONOS_IP)
# --- Initial MARANTZ status ---------------------------------------------------
print("MARANTZ Power status: " + avr.power)
print("MARANTZ Input select: " + avr.input_func)
print("MARANTZ Volume: " + str(avr.volume))
print()
# --- Main loop ---------------------------------------------------------------
# catch SIGTERM gracefully
signal.signal(signal.SIGTERM, handle_sigterm)
# non-buffered STDOUT so we can use it for logging
sys.stdout = Unbuffered(sys.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.
try:
loop = asyncio.get_event_loop()
loop.create_task(check_subscription())
threshold = renewal_time - 5
while threshold != 0 and not break_loop:
await asyncio.sleep(1)
threshold-=1
except KeyboardInterrupt:
handle_sigterm()
if break_loop:
print("{} *** Unsubscribing from SONOS device {} events".format(datetime.now(),sonos_device.player_name))
await subscription.unsubscribe()
await events_asyncio.event_listener.async_stop()
break
async def check_subscription():
global subscription
if subscription:
print("{} *** Unsubscribing from SONOS device {} events".format(datetime.now(),sonos_device.player_name))
try:
subscription.unsubscribe()
await events_asyncio.event_listener.async_stop()
except Exception as e:
print('{} *** Unsubscribe for renewal failed: {}'.format(datetime.now(), e))
print("{} *** Subscribing to SONOS device {} events".format(datetime.now(),sonos_device.player_name))
try:
subscription = await sonos_device.avTransport.subscribe(requested_timeout=renewal_time, auto_renew=True)
subscription.callback = update_sonos_callback
except Exception as e:
print("{} *** Subscribe failed: {}".format(datetime.now(), e))
# subscription failed (e.g. sonos is disconnected for a longer period of time): wait 10 seconds
# and retry
time.sleep(10)
def handle_sigterm(*args):
global break_loop
print(("SIGTERM caught. Exiting gracefully."))
break_loop = True
return
# coroutine that will start another coroutine after a delay in seconds
async def delay(coro, seconds):
# suspend for a time limit in seconds
await asyncio.sleep(seconds)
# execute the other coroutine
await coro
async def update_avr_callback(zone, event, parameter):
print("{} Marantz callback #zone: {} #event: {} #parameter: {}".format(datetime.now(), zone, event, parameter))
return
def update_sonos_callback(event):
status = event.variables.get('transport_state')
global last_status
global avr_last_input
global avr_last_volume
global startup
print(str(datetime.now()) + " Callback fired, last status is " + str(last_status) + " status is: "+str(status))
loop = asyncio.get_event_loop()
if not status:
print("{} Invalid SONOS status: {}".format(datetime.now(), event.variables))
if last_status != status:
print("{} SONOS play status: {}".format(datetime.now(), status))
if (last_status != 'PLAYING' and status == 'PLAYING'):
if not avr.power == 'ON':
startup = True
print("{} Set AVR to status ON".format(datetime.now()))
avr_last_input = 'NONE'
loop.create_task(avr.async_power_on())
else:
startup = False
avr_last_input = avr.input_func
avr_last_volume = avr.volume
print(str(datetime.now()) + " Current input is " + str(avr.input_func))
if startup:
loop.create_task(delay(avr.async_set_input_func(MARANTZ_INPUT),2))
else:
loop.create_task(avr.async_set_input_func(MARANTZ_INPUT))
if MARANTZ_VOLUME is not None:
if not avr.volume == MARANTZ_VOLUME:
print("{} Current volume is {}, try to set AVR volume to 65".format(datetime.now(),avr.volume))
loop.create_task(avr.async_mute(False))
loop.create_task(delay(avr.async_set_volume(MARANTZ_VOLUME),2))
# if MARANTZ_SOUNDPRG is not None:
# MARANTZ_set_value('MAIN:SOUNDPRG', MARANTZ_SOUNDPRG)
if last_status == 'PLAYING' and status == 'PAUSED_PLAYBACK':
if avr.input_func == MARANTZ_INPUT:
if not avr_last_input == 'NONE' and not startup:
loop.create_task(avr.async_set_input_func(avr_last_input))
if not avr_last_volume == avr.volume:
print("{} Current volume is {}, set back to set to {}".format(datetime.now(),avr.volume,avr_last_volume))
loop.create_task(delay(avr.async_set_volume(avr_last_volume),2))
elif not avr.power == 'OFF':
print("{} Set AVR to status ON".format(datetime.now()))
loop.create_task(avr.async_power_off())
avr_last_input = 'NONE'
last_status = status
return
async def setup_avr():
global avr
avr = denonavr.DenonAVR(MARANTZ_IP)
# test connectivity
await avr.async_setup()
await avr.async_telnet_connect()
await avr.async_update()
return
try:
loop = asyncio.get_running_loop()
except RuntimeError: # 'RuntimeError: There is no current event loop...'
loop = None
if loop and loop.is_running():
print('Async event loop already running. Adding coroutine to the event loop.')
tsk = loop.create_task(main())
# ^-- https://docs.python.org/3/library/asyncio-task.html#task-object
# Optionally, a callback function can be executed when the coroutine completes
tsk.add_done_callback(
lambda t: print(f'Task done with result={t.result()} << return val of main()'))
else:
print()
print("------------------------------------")
print('Fresh start, starting new event loop')
result = asyncio.run(main())