diff --git a/AUTHORS.rst b/AUTHORS.rst index 65e7d950..357cb311 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -6,3 +6,4 @@ Contributors to Mopidy in the order of appearance: * Stein Magnus Jodal * Johannes Knutsen * Thomas Adamcik +* Kristian Klette diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py new file mode 100644 index 00000000..426b2db3 --- /dev/null +++ b/mopidy/mixers/denon.py @@ -0,0 +1,60 @@ +import logging +from threading import Lock + +from serial import Serial + +from mopidy.mixers import BaseMixer +from mopidy.settings import MIXER_PORT + +logger = logging.getLogger(u'mopidy.mixers.denon') + +""" + Mixer for controlling Denon recivers and amplifiers using the RS-232 protocol. + + Connects using the serial specifications from + Denon's RS-232 Protocol specification. + + Communication speed : 9600bps + Character length : 8 bits + Parity control : None + Start bit : 1 bit + Stop bit : 1 bit + Communication procedure : Non procedural + Communication data length : 135 bytes (maximum) + + The external mixer is the authoritative source for the current volume. + This allows the user to use his remote control the volume without + mopidy cancelling the volume setting. +""" + +class DenonMixer(BaseMixer): + def __init__(self): + self._device = Serial(port=MIXER_PORT, timeout=0.2) + self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] + self._volume = 0 + self._lock = Lock() + + def _get_volume(self): + self._lock.acquire(); + self.ensure_open_device() + self._device.write('MV?\r') + vol = str(self._device.readline()[2:4]) + self._lock.release() + logger.debug(u'_get_volume() = %s' % vol) + return self._levels.index(vol) + + def _set_volume(self, volume): + # Clamp according to Denon-spec + if volume > 99: + volume = 99 + self._lock.acquire() + self.ensure_open_device() + self._device.write('MV%s\r'% self._levels[volume]) + vol = self._device.readline()[2:4] + self._lock.release() + self._volume = self._levels.index(vol) + + def ensure_open_device(self): + if not self._device.isOpen(): + logger.debug(u'(re)connecting to Denon device') + self._device.open() diff --git a/mopidy/settings/default.py b/mopidy/settings/default.py index 96d5cd23..7f6d1d88 100644 --- a/mopidy/settings/default.py +++ b/mopidy/settings/default.py @@ -38,12 +38,26 @@ CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s [%(threadName)s] %(name)s\n #: Default on other operating systems:: #: #: MIXER = u'mopidy.mixers.dummy.DummyMixer' +#: +#: **Available external mixers** +#: +#: .. note:: +#: Using external mixers depends on the pyserial library. +#: +#: Denon AVR/AVC via RS-232:: +#: +#: MIXER = u'mopidy.mixers.denon.DenonMixer' +#: MIXER = u'mopidy.mixers.dummy.DummyMixer' if sys.platform == 'linux2': MIXER = u'mopidy.mixers.alsa.AlsaMixer' elif sys.platform == 'darwin': MIXER = u'mopidy.mixers.osa.OsaMixer' +#: Which port the mixer is connected to if using an external mixer. +#: This must point to the device port like ``/dev/ttyUSB0`` or similar. +MIXER_PORT = None + #: Which address Mopidy should bind to. Examples: #: #: ``localhost`` diff --git a/tests/__main__.py b/tests/__main__.py index 11677b0e..d3adfca0 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -10,6 +10,7 @@ def main(): os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) r = CoverageTestRunner() r.add_pair('mopidy/mixers/dummy.py', 'tests/mixers/dummytest.py') + r.add_pair('mopidy/mixers/denon.py', 'tests/mixers/denontest.py') r.add_pair('mopidy/models.py', 'tests/modelstest.py') r.add_pair('mopidy/mpd/handler.py', 'tests/mpd/handlertest.py') r.run() diff --git a/tests/mixers/denontest.py b/tests/mixers/denontest.py new file mode 100644 index 00000000..ad7743ce --- /dev/null +++ b/tests/mixers/denontest.py @@ -0,0 +1,45 @@ +import unittest +import os + +from mopidy.mixers.denon import DenonMixer + +class DenonMixerDeviceMock(object): + def __init__(self): + self._open = True + self.ret_val = bytes('MV00\r') + + def write(self, x): + if x[2] != '?': + self.ret_val = bytes(x) + def read(self, x): + return self.ret_val + def isOpen(self): + return self._open + def open(self): + self._open = True + +class DenonMixerTest(unittest.TestCase): + def setUp(self): + self.m = DenonMixer() + self.m._device = DenonMixerDeviceMock() + + def test_volume_set_to_min(self): + self.m.volume = 0 + self.assertEqual(self.m.volume, 0) + + def test_volume_set_to_max(self): + self.m.volume = 100 + self.assertEqual(self.m.volume, 99) + + def test_volume_set_to_below_min_results_in_min(self): + self.m.volume = -10 + self.assertEqual(self.m.volume, 0) + + def test_volume_set_to_above_max_results_in_max(self): + self.m.volume = 110 + self.assertEqual(self.m.volume, 99) + + def test_reopen_device(self): + self.m._device._open = False + self.m.volume = 10 + self.assertTrue(self.m._device._open)