|
|
@@ -0,0 +1,407 @@ |
|
|
|
#!/usr/bin/env python |
|
|
|
|
|
|
|
import mock |
|
|
|
import serial |
|
|
|
import unittest |
|
|
|
import threading |
|
|
|
import time |
|
|
|
|
|
|
|
class DenonAVR(object): |
|
|
|
def __init__(self, serdev): |
|
|
|
self._ser = serial.serial_for_url(serdev, baudrate=9600, |
|
|
|
timeout=.5) |
|
|
|
self._power = None |
|
|
|
self._vol = None |
|
|
|
self._volmax = None |
|
|
|
self._speakera = None |
|
|
|
self._speakerb = None |
|
|
|
self._z2mute = None |
|
|
|
self._zm = None |
|
|
|
self._ms = None |
|
|
|
|
|
|
|
@property |
|
|
|
def ms(self): |
|
|
|
'Surround mode' |
|
|
|
|
|
|
|
return self._ms |
|
|
|
|
|
|
|
@property |
|
|
|
def power(self): |
|
|
|
'Power status, True if on' |
|
|
|
|
|
|
|
return self._power |
|
|
|
|
|
|
|
@power.setter |
|
|
|
def power(self, arg): |
|
|
|
arg = bool(arg) |
|
|
|
|
|
|
|
if arg != self._power: |
|
|
|
args = { True: 'ON', False: 'STANDBY' } |
|
|
|
self._sendcmd('PW', args[arg]) |
|
|
|
self.process_events(till='PW') |
|
|
|
time.sleep(1) |
|
|
|
self.update() |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def _makevolarg(arg): |
|
|
|
arg = int(arg) |
|
|
|
if arg < 0 or arg > 99: |
|
|
|
raise ValueError('Volume out of range.') |
|
|
|
|
|
|
|
arg -= 1 |
|
|
|
arg %= 100 |
|
|
|
|
|
|
|
return '%02d' % arg |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def _parsevolarg(arg): |
|
|
|
arg = int(arg) |
|
|
|
if arg < 0 or arg > 99: |
|
|
|
raise ValueError('Volume out of range.') |
|
|
|
|
|
|
|
arg += 1 |
|
|
|
arg %= 100 |
|
|
|
|
|
|
|
return arg |
|
|
|
|
|
|
|
@property |
|
|
|
def vol(self): |
|
|
|
'Volumn, range 0 through 99' |
|
|
|
|
|
|
|
return self._vol |
|
|
|
|
|
|
|
@vol.setter |
|
|
|
def vol(self, arg): |
|
|
|
if arg == self._vol: |
|
|
|
return |
|
|
|
|
|
|
|
if self._volmax is not None and arg > self._volmax: |
|
|
|
raise ValueError('volume %d, exceeds max: %d' % (arg, |
|
|
|
self._volmax)) |
|
|
|
arg = self._makevolarg(arg) |
|
|
|
|
|
|
|
time.sleep(1) |
|
|
|
self._sendcmd('MV', arg) |
|
|
|
self.process_events(till='MV') |
|
|
|
self.process_events(till='MV') |
|
|
|
|
|
|
|
@property |
|
|
|
def volmax(self): |
|
|
|
'Maximum volume supported.' |
|
|
|
|
|
|
|
return self._volmax |
|
|
|
|
|
|
|
def proc_PW(self, arg): |
|
|
|
if arg == 'STANDBY': |
|
|
|
self._power = False |
|
|
|
elif arg == 'ON': |
|
|
|
self._power = True |
|
|
|
else: |
|
|
|
raise RuntimeError('unknown PW arg: %s' % `arg`) |
|
|
|
|
|
|
|
def proc_MU(self, arg): |
|
|
|
if arg == 'ON': |
|
|
|
self._mute = True |
|
|
|
elif arg == 'OFF': |
|
|
|
self._mute = False |
|
|
|
else: |
|
|
|
raise RuntimeError('unknown MU arg: %s' % `arg`) |
|
|
|
|
|
|
|
def proc_ZM(self, arg): |
|
|
|
if arg == 'ON': |
|
|
|
self._zm = True |
|
|
|
elif arg == 'OFF': |
|
|
|
self._zm = False |
|
|
|
else: |
|
|
|
raise RuntimeError('unknown ZM arg: %s' % `arg`) |
|
|
|
|
|
|
|
def proc_MV(self, arg): |
|
|
|
if arg[:4] == 'MAX ': |
|
|
|
self._volmax = self._parsevolarg(arg[4:]) |
|
|
|
else: |
|
|
|
self._vol = self._parsevolarg(arg) |
|
|
|
|
|
|
|
def proc_MS(self, arg): |
|
|
|
self._ms = arg |
|
|
|
|
|
|
|
def proc_PS(self, arg): |
|
|
|
if arg == 'FRONT A': |
|
|
|
self._speakera = True |
|
|
|
self._speakerb = False |
|
|
|
else: |
|
|
|
raise RuntimeError('unknown PS arg: %s' % `arg`) |
|
|
|
|
|
|
|
def proc_Z2(self, arg): |
|
|
|
if arg == 'MUOFF': |
|
|
|
self._z2mute = False |
|
|
|
else: |
|
|
|
raise RuntimeError('unknown Z2 arg: %s' % `arg`) |
|
|
|
|
|
|
|
def _sendcmd(self, cmd, args): |
|
|
|
cmd = '%s%s\r' % (cmd, args) |
|
|
|
|
|
|
|
print 'scmd:', `cmd` |
|
|
|
self._ser.write(cmd) |
|
|
|
self._ser.flush() |
|
|
|
|
|
|
|
def _readcmd(self, timo=None): |
|
|
|
'''If timo == 0, and the first read returns the empty string, |
|
|
|
it will return an empty command, otherwise returns a |
|
|
|
command.''' |
|
|
|
|
|
|
|
cmd = '' |
|
|
|
|
|
|
|
#while True: |
|
|
|
if timo is not None: |
|
|
|
oldtimo = self._ser.timeout |
|
|
|
self._ser.timeout = timo |
|
|
|
|
|
|
|
for i in xrange(30): |
|
|
|
c = self._ser.read() |
|
|
|
if (timo == 0 or timo is None) and c == '': |
|
|
|
break |
|
|
|
|
|
|
|
#print 'r:', `c`, `str(c)` |
|
|
|
if c == '\r': |
|
|
|
break |
|
|
|
|
|
|
|
cmd += c |
|
|
|
else: |
|
|
|
raise RuntimeError('overrun!') |
|
|
|
|
|
|
|
if timo is not None: |
|
|
|
self._ser.timeout = oldtimo |
|
|
|
|
|
|
|
print 'rc:', `cmd` |
|
|
|
return cmd |
|
|
|
|
|
|
|
def process_events(self, till=None): |
|
|
|
'''Process events until the till command is received, otherwise |
|
|
|
process a single event.''' |
|
|
|
|
|
|
|
assert till is None or len(till) == 2 |
|
|
|
print 'till:', `till` |
|
|
|
while True: |
|
|
|
event = self._readcmd() |
|
|
|
|
|
|
|
if len(event) >= 2: |
|
|
|
fun = getattr(self, 'proc_%s' % event[:2]) |
|
|
|
fun(event[2:]) |
|
|
|
|
|
|
|
if till is None or event[:2] == till: |
|
|
|
return event |
|
|
|
|
|
|
|
def update(self): |
|
|
|
'''Update the status of the AVR.''' |
|
|
|
|
|
|
|
self._sendcmd('PW', '?') |
|
|
|
self._sendcmd('MV', '?') |
|
|
|
self.process_events(till='MV') # first vol |
|
|
|
self.process_events(till='MV') # second max vol |
|
|
|
|
|
|
|
class TestDenon(unittest.TestCase): |
|
|
|
TEST_DEV = '/dev/tty.usbserial-FTC8DHBJ' |
|
|
|
|
|
|
|
# comment out to make it easy to restore skip |
|
|
|
@unittest.skip('perf') |
|
|
|
def test_comms(self): |
|
|
|
avr = DenonAVR(self.TEST_DEV) |
|
|
|
self.assertIsNone(avr.power) |
|
|
|
|
|
|
|
avr.update() |
|
|
|
|
|
|
|
self.assertIsNotNone(avr.power) |
|
|
|
self.assertIsNotNone(avr.vol) |
|
|
|
|
|
|
|
avr.power = False |
|
|
|
|
|
|
|
time.sleep(1) |
|
|
|
|
|
|
|
avr.power = True |
|
|
|
|
|
|
|
self.assertTrue(avr.power) |
|
|
|
|
|
|
|
print 'foostart' |
|
|
|
|
|
|
|
time.sleep(1) |
|
|
|
|
|
|
|
avr.update() |
|
|
|
|
|
|
|
time.sleep(1) |
|
|
|
|
|
|
|
avr.vol = 0 |
|
|
|
|
|
|
|
self.assertEqual(avr.vol, 0) |
|
|
|
|
|
|
|
time.sleep(1) |
|
|
|
|
|
|
|
avr.vol = 5 |
|
|
|
|
|
|
|
avr.update() |
|
|
|
self.assertEqual(avr.vol, 5) |
|
|
|
|
|
|
|
avr.vol = 50 |
|
|
|
|
|
|
|
avr.update() |
|
|
|
self.assertEqual(avr.vol, 50) |
|
|
|
|
|
|
|
avr.power = False |
|
|
|
|
|
|
|
self.assertFalse(avr.power) |
|
|
|
|
|
|
|
self.assertIsNotNone(avr.volmax) |
|
|
|
|
|
|
|
class TestStaticMethods(unittest.TestCase): |
|
|
|
def test_makevolarg(self): |
|
|
|
self.assertRaises(ValueError, DenonAVR._makevolarg, -1) |
|
|
|
self.assertRaises(ValueError, DenonAVR._makevolarg, 3874) |
|
|
|
self.assertRaises(ValueError, DenonAVR._makevolarg, 100) |
|
|
|
|
|
|
|
self.assertEqual(DenonAVR._makevolarg(0), '99') |
|
|
|
self.assertEqual(DenonAVR._makevolarg(1), '00') |
|
|
|
self.assertEqual(DenonAVR._makevolarg(99), '98') |
|
|
|
|
|
|
|
def test_parsevolarg(self): |
|
|
|
self.assertEqual(DenonAVR._parsevolarg('99'), 0) |
|
|
|
self.assertEqual(DenonAVR._parsevolarg('00'), 1) |
|
|
|
self.assertEqual(DenonAVR._parsevolarg('98'), 99) |
|
|
|
|
|
|
|
self.assertRaises(ValueError, DenonAVR._parsevolarg, '-1') |
|
|
|
|
|
|
|
class TestMethods(unittest.TestCase): |
|
|
|
@mock.patch('serial.serial_for_url') |
|
|
|
def setUp(self, sfu): |
|
|
|
self.avr = DenonAVR('null') |
|
|
|
|
|
|
|
def test_proc_events(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr._ser.read.side_effect = 'PWON\r' |
|
|
|
avr.process_events() |
|
|
|
|
|
|
|
self.assertTrue(avr._ser.read.called) |
|
|
|
|
|
|
|
avr._ser.read.reset() |
|
|
|
|
|
|
|
avr._ser.read.side_effect = 'MUON\r' + 'PWON\r' |
|
|
|
avr.process_events(till='PW') |
|
|
|
|
|
|
|
avr._ser.read.assert_has_calls([ mock.call(), mock.call() ]) |
|
|
|
|
|
|
|
@mock.patch('denon.DenonAVR._sendcmd') |
|
|
|
@mock.patch('denon.DenonAVR.process_events') |
|
|
|
@mock.patch('time.sleep') |
|
|
|
@mock.patch('denon.DenonAVR.update') |
|
|
|
def test_proc_PW(self, mupdate, msleep, mpevents, msendcmd): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_PW('STANDBY') |
|
|
|
self.assertEqual(avr.power, False) |
|
|
|
|
|
|
|
avr.proc_PW('ON') |
|
|
|
self.assertEqual(avr.power, True) |
|
|
|
|
|
|
|
self.assertRaises(RuntimeError, avr.proc_PW, 'foobar') |
|
|
|
|
|
|
|
avr.power = False |
|
|
|
msendcmd.assert_any_call('PW', 'STANDBY') |
|
|
|
|
|
|
|
def test_proc_MU(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_MU('ON') |
|
|
|
self.assertEqual(avr._mute, True) |
|
|
|
|
|
|
|
avr.proc_MU('OFF') |
|
|
|
self.assertEqual(avr._mute, False) |
|
|
|
|
|
|
|
self.assertRaises(RuntimeError, avr.proc_MU, 'foobar') |
|
|
|
|
|
|
|
def test_proc_PS(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_PS('FRONT A') |
|
|
|
self.assertEqual(avr._speakera, True) |
|
|
|
self.assertEqual(avr._speakerb, False) |
|
|
|
|
|
|
|
self.assertRaises(RuntimeError, avr.proc_PS, 'foobar') |
|
|
|
|
|
|
|
def test_proc_Z2(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_Z2('MUOFF') |
|
|
|
self.assertEqual(avr._z2mute, False) |
|
|
|
|
|
|
|
self.assertRaises(RuntimeError, avr.proc_Z2, 'foobar') |
|
|
|
|
|
|
|
def test_proc_MS(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_MS('STEREO') |
|
|
|
self.assertEqual(avr.ms, 'STEREO') |
|
|
|
|
|
|
|
def test_proc_ZM(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_ZM('ON') |
|
|
|
self.assertEqual(avr._zm, True) |
|
|
|
|
|
|
|
avr.proc_ZM('OFF') |
|
|
|
self.assertEqual(avr._zm, False) |
|
|
|
|
|
|
|
self.assertRaises(RuntimeError, avr.proc_ZM, 'foobar') |
|
|
|
|
|
|
|
@mock.patch('denon.DenonAVR.process_events') |
|
|
|
def test_proc_MV(self, pe): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
avr.proc_MV('MAX 80') |
|
|
|
self.assertEqual(avr._volmax, 81) |
|
|
|
|
|
|
|
avr.proc_MV('99') |
|
|
|
self.assertEqual(avr._vol, 0) |
|
|
|
|
|
|
|
avr.vol = 0 |
|
|
|
|
|
|
|
# we don't call this as we don't get a response |
|
|
|
pe.assert_not_called() |
|
|
|
|
|
|
|
self.assertRaises(ValueError, setattr, avr, 'vol', 82) |
|
|
|
|
|
|
|
def test_readcmd(self): |
|
|
|
avr = self.avr |
|
|
|
|
|
|
|
# Test no pending cmd and that timeout is set |
|
|
|
timov = .5 |
|
|
|
timovcur = [ timov ] |
|
|
|
def curtimo(*args): |
|
|
|
assert len(args) in (0, 1) |
|
|
|
|
|
|
|
if len(args): |
|
|
|
timovcur[0] = args[0] |
|
|
|
else: |
|
|
|
return timovcur[0] |
|
|
|
|
|
|
|
timo = mock.PropertyMock(side_effect=curtimo) |
|
|
|
type(avr._ser).timeout = timo |
|
|
|
avr._ser.read.side_effect = [ '' ] |
|
|
|
|
|
|
|
r = avr._readcmd(timo=0) |
|
|
|
|
|
|
|
# original value restored |
|
|
|
self.assertEqual(avr._ser.timeout, timov) |
|
|
|
|
|
|
|
# that the timeout was set |
|
|
|
timo.assert_any_call(0) |
|
|
|
|
|
|
|
# and it returned an empty command |
|
|
|
self.assertEqual(r, '') |
|
|
|
|
|
|
|
# that it got returned the the old value |
|
|
|
self.assertEqual(avr._ser.timeout, timov) |
|
|
|
|
|
|
|
avr._ser.read.side_effect = 'MUON\r' |
|
|
|
r = avr._readcmd(timo=1) |
|
|
|
|
|
|
|
self.assertEqual(r, 'MUON') |
|
|
|
self.assertEqual(avr._ser.timeout, timov) |