From e0948563371b8671955946214ed5037dac99ee76 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 09:49:51 -0600 Subject: [PATCH 01/17] added implementation for monoprice 6-zone amplifier. This implementation is based on and very similar to russoun_rnet implementaion --- .../components/media_player/monoprice.py | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 homeassistant/components/media_player/monoprice.py diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py new file mode 100644 index 00000000000000..dda9470babd9d3 --- /dev/null +++ b/homeassistant/components/media_player/monoprice.py @@ -0,0 +1,162 @@ +""" +Support for interfacing with Monoprice via serial interface +""" +import logging + +import voluptuous as vol +from homeassistant.components.media_player import ( + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['pymonoprice==0.2'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_MONOPRICE = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP |\ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, +}) + +SOURCE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, +}) + +CONF_ZONES = 'zones' +CONF_SOURCES = 'sources' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PORT): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({cv.positive_int: SOURCE_SCHEMA}), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the monoprice platform. """ + + port = config.get(CONF_PORT) + + if port is None: + _LOGGER.error("Invalid config. Expected %s", CONF_PORT) + return False + + from pymonoprice import Monoprice + monoprice = Monoprice(port) + + sources = { source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} + + for zone_id, extra in config[CONF_ZONES].items(): + _LOGGER.info("Adding zone {} - {}".format(zone_id, extra[CONF_NAME])) + add_devices([MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME])], True) + return True + +class MonopriceZone(MediaPlayerDevice): + """ Represents a monoprice zone. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, monoprice, sources, zone_id, zone_name): + self._monoprice = monoprice + # dict source_id -> source name + self._source_id_name = sources + # dict source name -> source_id + self._source_name_id = {v:k for k,v in sources.items()} + # ordered list of all source names + self._source_names = sorted(self._source_name_id.keys(), key=lambda v: self._source_name_id[v]) + self._zone_id = zone_id + self._name = zone_name + + self._state = None + self._volume = None + self._source = None + self._mute = None + + def update(self): + state = self._monoprice.zone_status(self._zone_id) + if not state: + return False + self._state = STATE_ON if state.power else STATE_OFF + self._volume = state.volume + self._mute = state.mute + idx = state.source + if idx in self._source_id_name: + self._source = self._source_id_name[idx] + else: + self._source = None + return True + + @property + def name(self): + """ Returns the name of the zone. """ + return self._name + + @property + def state(self): + """ Returns the state of the zone. """ + return self._state + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return self._volume / 38.0 + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self._mute + + @property + def supported_features(self): + """ Flags of media commands that are supported. """ + return SUPPORT_MONOPRICE + + @property + def source(self): + """"Return the current input source of the device.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return list(self._source_names) + + def select_source(self, source): + """ Set input source. """ + if source not in self._source_name_id: + return + idx = self._source_name_id[source] + self._monoprice.set_source(self._zone_id, idx) + + def turn_on(self): + """ turn the media player on. """ + self._monoprice.set_power(self._zone_id, True) + + def turn_off(self): + """ turn_off media player. """ + self._monoprice.set_power(self._zone_id, False) + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + self._monoprice.set_mute(self._zone_id, mute) + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + self._monoprice.set_volume(self._zone_id, int(volume * 38)) + + def volume_up(self): + """Volume up the media player.""" + self._volume = min(self._volume + 1, 38) + self._monoprice.set_volume(self._zone_id, int(self._volume)) + + def volume_down(self): + """Volume down media player.""" + self._volume = max(self._volume - 1, 0) + self._monoprice.set_volume(self._zone_id, int(self._volume)) + From 73a180d4c777f1ff520f626134ab7193ebda07ae Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 10:03:15 -0600 Subject: [PATCH 02/17] updated comments and cleaned up code --- .../components/media_player/monoprice.py | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index dda9470babd9d3..7231dda2bdd2e5 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -1,12 +1,16 @@ """ -Support for interfacing with Monoprice via serial interface +Support for interfacing with Monoprice 6 zone home audio controller +via serial interface. + +https://www.monoprice.com/product?p_id=10761 """ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET,SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, + MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -15,8 +19,9 @@ _LOGGER = logging.getLogger(__name__) -SUPPORT_MONOPRICE = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP |\ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE +SUPPORT_MONOPRICE = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\ + SUPPORT_VOLUME_STEP |SUPPORT_TURN_ON |\ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE ZONE_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, @@ -39,7 +44,7 @@ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the monoprice platform. """ + """ Sets up the Monoprice 6-zone amplifier platform. """ port = config.get(CONF_PORT) @@ -50,15 +55,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from pymonoprice import Monoprice monoprice = Monoprice(port) - sources = { source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} + sources = {source_id: extra[CONF_NAME] for source_id, extra + in config[CONF_SOURCES].items()} for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone {} - {}".format(zone_id, extra[CONF_NAME])) add_devices([MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME])], True) return True + class MonopriceZone(MediaPlayerDevice): - """ Represents a monoprice zone. """ + """ Represents a Monoprice amplifier zone. """ # pylint: disable=too-many-public-methods @@ -67,9 +74,10 @@ def __init__(self, monoprice, sources, zone_id, zone_name): # dict source_id -> source name self._source_id_name = sources # dict source name -> source_id - self._source_name_id = {v:k for k,v in sources.items()} + self._source_name_id = {v: k for k, v in sources.items()} # ordered list of all source names - self._source_names = sorted(self._source_name_id.keys(), key=lambda v: self._source_name_id[v]) + self._source_names = sorted(self._source_name_id.keys(), + key=lambda v: self._source_name_id[v]) self._zone_id = zone_id self._name = zone_name @@ -79,6 +87,7 @@ def __init__(self, monoprice, sources, zone_id, zone_name): self._mute = None def update(self): + """Retrieve latest state.""" state = self._monoprice.zone_status(self._zone_id) if not state: return False @@ -125,7 +134,7 @@ def source(self): @property def source_list(self): """List of available input sources.""" - return list(self._source_names) + return self._source_names def select_source(self, source): """ Set input source. """ @@ -143,20 +152,24 @@ def turn_off(self): self._monoprice.set_power(self._zone_id, False) def mute_volume(self, mute): - """ mute (true) or unmute (false) media player. """ + """ Mute (true) or unmute (false) media player. """ self._monoprice.set_mute(self._zone_id, mute) def set_volume_level(self, volume): - """ set volume level, range 0..1. """ + """ Set volume level, range 0..1. """ self._monoprice.set_volume(self._zone_id, int(volume * 38)) def volume_up(self): """Volume up the media player.""" - self._volume = min(self._volume + 1, 38) - self._monoprice.set_volume(self._zone_id, int(self._volume)) + if self._volume is None: + return + self._monoprice.set_volume(self._zone_id, + min(self._volume + 1, 38)) def volume_down(self): """Volume down media player.""" - self._volume = max(self._volume - 1, 0) - self._monoprice.set_volume(self._zone_id, int(self._volume)) + if self._volume is None: + return + self._monoprice.set_volume(self._zone_id, + max(self._volume - 1, 0)) From 71e3947f81ca6d4f398eb89942476b8461b0b1c1 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 10:05:18 -0600 Subject: [PATCH 03/17] updated comments and cleaned up code --- homeassistant/components/media_player/monoprice.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 7231dda2bdd2e5..9edb71766f1284 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__) -SUPPORT_MONOPRICE = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\ - SUPPORT_VOLUME_STEP |SUPPORT_TURN_ON |\ +SUPPORT_MONOPRICE = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ + SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | \ SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE ZONE_SCHEMA = vol.Schema({ @@ -60,7 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone {} - {}".format(zone_id, extra[CONF_NAME])) - add_devices([MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME])], True) + add_devices([MonopriceZone(monoprice, sources, + zone_id, extra[CONF_NAME])], True) return True @@ -172,4 +173,3 @@ def volume_down(self): return self._monoprice.set_volume(self._zone_id, max(self._volume - 1, 0)) - From f75471c023bcef41a169da97468f21d6a8d2fffa Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 11:10:59 -0600 Subject: [PATCH 04/17] added unit tests --- .../components/media_player/monoprice.py | 16 +- .../components/media_player/test_monoprice.py | 318 ++++++++++++++++++ 2 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 tests/components/media_player/test_monoprice.py diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 9edb71766f1284..d33711aa940540 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET,SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -34,11 +34,19 @@ CONF_ZONES = 'zones' CONF_SOURCES = 'sources' +# Valid zone ids: 11-16 or 21-26 or 31-36 +ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16), + vol.Range(min=21, max=26), + vol.Range(min=31, max=36))) + +# Valid source ids: 1-6 +SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=6)) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PORT): cv.string, vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}), - vol.Required(CONF_SOURCES): vol.Schema({cv.positive_int: SOURCE_SCHEMA}), + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), }) @@ -115,6 +123,8 @@ def state(self): @property def volume_level(self): """ Volume level of the media player (0..1). """ + if self._volume is None: + return None return self._volume / 38.0 @property diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py new file mode 100644 index 00000000000000..c20f57d0dc89b6 --- /dev/null +++ b/tests/components/media_player/test_monoprice.py @@ -0,0 +1,318 @@ +import unittest +import voluptuous as vol + +from collections import defaultdict + +from homeassistant.components.media_player import ( + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE) +from homeassistant.const import STATE_ON, STATE_OFF + +from components.media_player.monoprice import MonopriceZone, PLATFORM_SCHEMA + + +class MockState(object): + def __init__(self): + self.power = True + self.volume = 0 + self.mute = True + self.source = 1 + + +class MockMonoprice(object): + def __init__(self): + self.zones = defaultdict(lambda *a: MockState()) + + def zone_status(self, zone_id): + return self.zones[zone_id] + + def set_source(self, zone_id, source_idx): + self.zones[zone_id].source = source_idx + + def set_power(self, zone_id, power): + self.zones[zone_id].power = power + + def set_mute(self, zone_id, mute): + self.zones[zone_id].mute = mute + + def set_volume(self, zone_id, volume): + self.zones[zone_id].volume = volume + + +class TestMonopriceSchema(unittest.TestCase): + """Tests Monoprice schema.""" + def test_valid_schema(self): + """Tests valid schema.""" + valid_schema = { + 'platform': 'monoprice', + 'port': '/dev/ttyUSB0', + 'name': 'Monoprice', + 'zones': {11: {'name': 'a'}, + 12: {'name': 'a'}, + 13: {'name': 'a'}, + 14: {'name': 'a'}, + 15: {'name': 'a'}, + 16: {'name': 'a'}, + 21: {'name': 'a'}, + 22: {'name': 'a'}, + 23: {'name': 'a'}, + 24: {'name': 'a'}, + 25: {'name': 'a'}, + 26: {'name': 'a'}, + 31: {'name': 'a'}, + 32: {'name': 'a'}, + 33: {'name': 'a'}, + 34: {'name': 'a'}, + 35: {'name': 'a'}, + 36: {'name': 'a'}, + }, + 'sources': { + 1: {'name': 'a'}, + 2: {'name': 'a'}, + 3: {'name': 'a'}, + 4: {'name': 'a'}, + 5: {'name': 'a'}, + 6: {'name': 'a'} + } + } + PLATFORM_SCHEMA(valid_schema) + + def test_invalid_schemas(self): + """Tests invalid schemas.""" + schemas = ( + {}, # Empty + None, # None + # Missing port + { + 'platform': 'monoprice', + 'name': 'Name', + 'zones': {11: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Missing name + { + 'platform': 'monoprice', + 'port': 'aaa', + 'zones': {11: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Invalid zone number + { + 'platform': 'monoprice', + 'port': 'aaa', + 'name': 'Name', + 'zones': {10: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Invalid source number + { + 'platform': 'monoprice', + 'port': 'aaa', + 'name': 'Name', + 'zones': {11: {'name': 'a'}}, + 'sources': {0: {'name': 'b'}}, + }, + # Zone missing name + { + 'platform': 'monoprice', + 'port': 'aaa', + 'name': 'Name', + 'zones': {11: {}}, + 'sources': {1: {'name': 'b'}}, + }, + # Source missing name + { + 'platform': 'monoprice', + 'port': 'aaa', + 'name': 'Name', + 'zones': {11: {'name': 'a'}}, + 'sources': {1: {}}, + }, + + ) + for value in schemas: + with self.assertRaises(vol.MultipleInvalid) as cm: + PLATFORM_SCHEMA(value) + + +class TestMonopriceMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): + """Sets up the test case.""" + self.monoprice = MockMonoprice() + # Note, source dictionary is unsorted! + self.media_player = MonopriceZone(self.monoprice, {1: 'one', + 3: 'three', + 2: 'two'}, + 12, 'Zone name') + + def test_update(self): + """Tests updating values from monoprice.""" + self.assertIsNone(self.media_player.state) + self.assertIsNone(self.media_player.volume_level) + self.assertIsNone(self.media_player.is_volume_muted) + self.assertIsNone(self.media_player.source) + + self.media_player.update() + + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + self.assertTrue(self.media_player.is_volume_muted) + self.assertEqual('one', self.media_player.source) + + def test_name(self): + """Tests name property.""" + self.assertEqual('Zone name', self.media_player.name) + + def test_state(self): + """Tests state property.""" + self.assertIsNone(self.media_player.state) + + self.media_player.update() + self.assertEqual(STATE_ON, self.media_player.state) + + self.monoprice.zones[12].power = False + self.media_player.update() + self.assertEqual(STATE_OFF, self.media_player.state) + + def test_volume_level(self): + """Tests volume level property.""" + self.assertIsNone(self.media_player.volume_level) + self.media_player.update() + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + + self.monoprice.zones[12].volume = 38 + self.media_player.update() + self.assertEqual(1.0, self.media_player.volume_level, 0.0001) + + self.monoprice.zones[12].volume = 19 + self.media_player.update() + self.assertEqual(.5, self.media_player.volume_level, 0.0001) + + def test_is_volume_muted(self): + """Tests volume muted property.""" + self.assertIsNone(self.media_player.is_volume_muted) + + self.media_player.update() + self.assertTrue(self.media_player.is_volume_muted) + + self.monoprice.zones[12].mute = False + self.media_player.update() + self.assertFalse(self.media_player.is_volume_muted) + + def test_supported_features(self): + """Tests supported features property.""" + self.assertEqual(SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | + SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE, + self.media_player.supported_features) + + def test_source(self): + """Tests source property.""" + self.assertIsNone(self.media_player.source) + self.media_player.update() + self.assertEqual('one', self.media_player.source) + + def test_source_list(self): + """Tests source list property.""" + # Note, the list is sorted! + self.assertEqual(['one', 'two', 'three'], + self.media_player.source_list) + + def test_select_source(self): + """Tests source selection methods.""" + self.media_player.update() + + self.assertEqual('one', self.media_player.source) + + self.media_player.select_source('two') + self.assertEqual(2, self.monoprice.zones[12].source) + self.media_player.update() + self.assertEqual('two', self.media_player.source) + + # Trying to set unknown source + self.media_player.select_source('no name') + self.assertEqual(2, self.monoprice.zones[12].source) + self.media_player.update() + self.assertEqual('two', self.media_player.source) + + def test_turn_on(self): + """Tests turning on the zone.""" + self.monoprice.zones[12].power = False + self.media_player.update() + self.assertEqual(STATE_OFF, self.media_player.state) + + self.media_player.turn_on() + self.assertTrue(self.monoprice.zones[12].power) + self.media_player.update() + self.assertEqual(STATE_ON, self.media_player.state) + + def test_turn_off(self): + """Tests turning off the zone.""" + self.monoprice.zones[12].power = True + self.media_player.update() + self.assertEqual(STATE_ON, self.media_player.state) + + self.media_player.turn_off() + self.assertFalse(self.monoprice.zones[12].power) + self.media_player.update() + self.assertEqual(STATE_OFF, self.media_player.state) + + def test_mute_volume(self): + """Tests mute functionality.""" + self.monoprice.zones[12].mute = True + self.media_player.update() + self.assertTrue(self.media_player.is_volume_muted) + + self.media_player.mute_volume(False) + self.assertFalse(self.monoprice.zones[12].mute) + self.media_player.update() + self.assertFalse(self.media_player.is_volume_muted) + + self.media_player.mute_volume(True) + self.assertTrue(self.monoprice.zones[12].mute) + self.media_player.update() + self.assertTrue(self.media_player.is_volume_muted) + + def test_set_volume_level(self): + """Tests set volume level.""" + self.media_player.set_volume_level(1.0) + self.assertEqual(38, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) + + self.media_player.set_volume_level(0.0) + self.assertEqual(0, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) + + self.media_player.set_volume_level(0.5) + self.assertEqual(19, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) + + def test_volume_up(self): + """Tests increasing volume by one.""" + self.monoprice.zones[12].volume = 37 + self.media_player.update() + self.media_player.volume_up() + self.assertEqual(38, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) + + # Try to raise value beyond max + self.media_player.update() + self.media_player.volume_up() + self.assertEqual(38, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) + + def test_volume_down(self): + """Tests decreasing volume by one.""" + self.monoprice.zones[12].volume = 1 + self.media_player.update() + self.media_player.volume_down() + self.assertEqual(0, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) + + # Try to lower value beyond minimum + self.media_player.update() + self.media_player.volume_down() + self.assertEqual(0, self.monoprice.zones[12].volume) + self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) From 3edad0515780c3a68bc2b6c6972a55cb7a694482 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 11:17:50 -0600 Subject: [PATCH 05/17] removed 'name' attribute from platform schema. --- homeassistant/components/media_player/monoprice.py | 3 +-- tests/components/media_player/test_monoprice.py | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index d33711aa940540..71b766fc001b92 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PORT): cv.string, - vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), }) @@ -74,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MonopriceZone(MediaPlayerDevice): - """ Represents a Monoprice amplifier zone. """ + """ Represents an Monoprice amplifier zone. """ # pylint: disable=too-many-public-methods diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index c20f57d0dc89b6..d4391d3597eb14 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -46,7 +46,6 @@ def test_valid_schema(self): valid_schema = { 'platform': 'monoprice', 'port': '/dev/ttyUSB0', - 'name': 'Monoprice', 'zones': {11: {'name': 'a'}, 12: {'name': 'a'}, 13: {'name': 'a'}, @@ -89,13 +88,6 @@ def test_invalid_schemas(self): 'zones': {11: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, }, - # Missing name - { - 'platform': 'monoprice', - 'port': 'aaa', - 'zones': {11: {'name': 'a'}}, - 'sources': {1: {'name': 'b'}}, - }, # Invalid zone number { 'platform': 'monoprice', From 0b628ee89f782318f25c41fe7a9de09dd1a2f6f6 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 12:00:04 -0600 Subject: [PATCH 06/17] added monoprice.py to .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 2d3c64a79cda5b..9dd8179f2bbbb6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -372,6 +372,7 @@ omit = homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/liveboxplaytv.py + homeassistant/components/media_player/monoprice.py homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/nad.py From eafade6ae53d95999ff717e8e13010f2bf7f36dd Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 12:43:00 -0600 Subject: [PATCH 07/17] fixed lint --- .../components/media_player/monoprice.py | 29 +++++++++---------- .../components/media_player/test_monoprice.py | 2 +- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 71b766fc001b92..a15cc852b239b2 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -1,6 +1,5 @@ """ -Support for interfacing with Monoprice 6 zone home audio controller -via serial interface. +Support for interfacing with Monoprice 6 zone home audio controller. https://www.monoprice.com/product?p_id=10761 """ @@ -51,8 +50,7 @@ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Monoprice 6-zone amplifier platform. """ - + """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) if port is None: @@ -73,11 +71,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MonopriceZone(MediaPlayerDevice): - """ Represents an Monoprice amplifier zone. """ + """Represents an Monoprice amplifier zone.""" # pylint: disable=too-many-public-methods def __init__(self, monoprice, sources, zone_id, zone_name): + """Initialize new zone.""" self._monoprice = monoprice # dict source_id -> source name self._source_id_name = sources @@ -111,29 +110,29 @@ def update(self): @property def name(self): - """ Returns the name of the zone. """ + """Returns the name of the zone.""" return self._name @property def state(self): - """ Returns the state of the zone. """ + """Returns the state of the zone.""" return self._state @property def volume_level(self): - """ Volume level of the media player (0..1). """ + """Volume level of the media player (0..1).""" if self._volume is None: return None return self._volume / 38.0 @property def is_volume_muted(self): - """ Boolean if volume is currently muted. """ + """Boolean if volume is currently muted.""" return self._mute @property def supported_features(self): - """ Flags of media commands that are supported. """ + """Flags of media commands that are supported.""" return SUPPORT_MONOPRICE @property @@ -147,26 +146,26 @@ def source_list(self): return self._source_names def select_source(self, source): - """ Set input source. """ + """Set input source.""" if source not in self._source_name_id: return idx = self._source_name_id[source] self._monoprice.set_source(self._zone_id, idx) def turn_on(self): - """ turn the media player on. """ + """turn the media player on.""" self._monoprice.set_power(self._zone_id, True) def turn_off(self): - """ turn_off media player. """ + """turn_off media player.""" self._monoprice.set_power(self._zone_id, False) def mute_volume(self, mute): - """ Mute (true) or unmute (false) media player. """ + """Mute (true) or unmute (false) media player.""" self._monoprice.set_mute(self._zone_id, mute) def set_volume_level(self, volume): - """ Set volume level, range 0..1. """ + """Set volume level, range 0..1.""" self._monoprice.set_volume(self._zone_id, int(volume * 38)) def volume_up(self): diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index d4391d3597eb14..7c0397918f912b 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -123,7 +123,7 @@ def test_invalid_schemas(self): ) for value in schemas: - with self.assertRaises(vol.MultipleInvalid) as cm: + with self.assertRaises(vol.MultipleInvalid): PLATFORM_SCHEMA(value) From 091bd8d6fea44dfdf55af4f8a1a2bffb007d0d74 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 13:01:09 -0600 Subject: [PATCH 08/17] fixed lint errors --- homeassistant/components/media_player/monoprice.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index a15cc852b239b2..aecdf808188dc4 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): in config[CONF_SOURCES].items()} for zone_id, extra in config[CONF_ZONES].items(): - _LOGGER.info("Adding zone {} - {}".format(zone_id, extra[CONF_NAME])) + _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) add_devices([MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME])], True) return True @@ -110,12 +110,12 @@ def update(self): @property def name(self): - """Returns the name of the zone.""" + """Return the name of the zone.""" return self._name @property def state(self): - """Returns the state of the zone.""" + """Return the state of the zone.""" return self._state @property @@ -132,7 +132,7 @@ def is_volume_muted(self): @property def supported_features(self): - """Flags of media commands that are supported.""" + """Return flag of media commands that are supported.""" return SUPPORT_MONOPRICE @property @@ -153,11 +153,11 @@ def select_source(self, source): self._monoprice.set_source(self._zone_id, idx) def turn_on(self): - """turn the media player on.""" + """Turn the media player on.""" self._monoprice.set_power(self._zone_id, True) def turn_off(self): - """turn_off media player.""" + """Turn the media player off.""" self._monoprice.set_power(self._zone_id, False) def mute_volume(self, mute): From d4530a70c56dd28bf8c0e59ac4a77abdc45174ec Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 13:46:31 -0600 Subject: [PATCH 09/17] fixed lint errors --- .../components/media_player/test_monoprice.py | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index 7c0397918f912b..9e56f1388987bc 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -12,7 +12,9 @@ class MockState(object): + """Mock for zone state object.""" def __init__(self): + """Init zone state.""" self.power = True self.volume = 0 self.mute = True @@ -20,29 +22,38 @@ def __init__(self): class MockMonoprice(object): + """Mock for pymonoprice object.""" + def __init__(self): + """Init mock object.""" self.zones = defaultdict(lambda *a: MockState()) def zone_status(self, zone_id): + """Get zone status.""" return self.zones[zone_id] def set_source(self, zone_id, source_idx): + """Set source for zone.""" self.zones[zone_id].source = source_idx def set_power(self, zone_id, power): + """Turn zone on/off.""" self.zones[zone_id].power = power def set_mute(self, zone_id, mute): + """Mute/unmute zone.""" self.zones[zone_id].mute = mute def set_volume(self, zone_id, volume): + """Set volume for zone.""" self.zones[zone_id].volume = volume class TestMonopriceSchema(unittest.TestCase): - """Tests Monoprice schema.""" + """Test Monoprice schema.""" + def test_valid_schema(self): - """Tests valid schema.""" + """Test valid schema.""" valid_schema = { 'platform': 'monoprice', 'port': '/dev/ttyUSB0', @@ -77,7 +88,7 @@ def test_valid_schema(self): PLATFORM_SCHEMA(valid_schema) def test_invalid_schemas(self): - """Tests invalid schemas.""" + """Test invalid schemas.""" schemas = ( {}, # Empty None, # None @@ -131,7 +142,7 @@ class TestMonopriceMediaPlayer(unittest.TestCase): """Test the media_player module.""" def setUp(self): - """Sets up the test case.""" + """Set up the test case.""" self.monoprice = MockMonoprice() # Note, source dictionary is unsorted! self.media_player = MonopriceZone(self.monoprice, {1: 'one', @@ -140,7 +151,7 @@ def setUp(self): 12, 'Zone name') def test_update(self): - """Tests updating values from monoprice.""" + """Test updating values from monoprice.""" self.assertIsNone(self.media_player.state) self.assertIsNone(self.media_player.volume_level) self.assertIsNone(self.media_player.is_volume_muted) @@ -154,11 +165,11 @@ def test_update(self): self.assertEqual('one', self.media_player.source) def test_name(self): - """Tests name property.""" + """Test name property.""" self.assertEqual('Zone name', self.media_player.name) def test_state(self): - """Tests state property.""" + """Test state property.""" self.assertIsNone(self.media_player.state) self.media_player.update() @@ -169,7 +180,7 @@ def test_state(self): self.assertEqual(STATE_OFF, self.media_player.state) def test_volume_level(self): - """Tests volume level property.""" + """Test volume level property.""" self.assertIsNone(self.media_player.volume_level) self.media_player.update() self.assertEqual(0.0, self.media_player.volume_level, 0.0001) @@ -183,7 +194,7 @@ def test_volume_level(self): self.assertEqual(.5, self.media_player.volume_level, 0.0001) def test_is_volume_muted(self): - """Tests volume muted property.""" + """Test volume muted property.""" self.assertIsNone(self.media_player.is_volume_muted) self.media_player.update() @@ -194,26 +205,26 @@ def test_is_volume_muted(self): self.assertFalse(self.media_player.is_volume_muted) def test_supported_features(self): - """Tests supported features property.""" + """Test supported features property.""" self.assertEqual(SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE, self.media_player.supported_features) def test_source(self): - """Tests source property.""" + """Test source property.""" self.assertIsNone(self.media_player.source) self.media_player.update() self.assertEqual('one', self.media_player.source) def test_source_list(self): - """Tests source list property.""" + """Test source list property.""" # Note, the list is sorted! self.assertEqual(['one', 'two', 'three'], self.media_player.source_list) def test_select_source(self): - """Tests source selection methods.""" + """Test source selection methods.""" self.media_player.update() self.assertEqual('one', self.media_player.source) @@ -230,7 +241,7 @@ def test_select_source(self): self.assertEqual('two', self.media_player.source) def test_turn_on(self): - """Tests turning on the zone.""" + """Test turning on the zone.""" self.monoprice.zones[12].power = False self.media_player.update() self.assertEqual(STATE_OFF, self.media_player.state) @@ -241,7 +252,7 @@ def test_turn_on(self): self.assertEqual(STATE_ON, self.media_player.state) def test_turn_off(self): - """Tests turning off the zone.""" + """Test turning off the zone.""" self.monoprice.zones[12].power = True self.media_player.update() self.assertEqual(STATE_ON, self.media_player.state) @@ -252,7 +263,7 @@ def test_turn_off(self): self.assertEqual(STATE_OFF, self.media_player.state) def test_mute_volume(self): - """Tests mute functionality.""" + """Test mute functionality.""" self.monoprice.zones[12].mute = True self.media_player.update() self.assertTrue(self.media_player.is_volume_muted) @@ -268,7 +279,7 @@ def test_mute_volume(self): self.assertTrue(self.media_player.is_volume_muted) def test_set_volume_level(self): - """Tests set volume level.""" + """Test set volume level.""" self.media_player.set_volume_level(1.0) self.assertEqual(38, self.monoprice.zones[12].volume) self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) @@ -282,7 +293,7 @@ def test_set_volume_level(self): self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) def test_volume_up(self): - """Tests increasing volume by one.""" + """Test increasing volume by one.""" self.monoprice.zones[12].volume = 37 self.media_player.update() self.media_player.volume_up() @@ -296,7 +307,7 @@ def test_volume_up(self): self.assertTrue(isinstance(self.monoprice.zones[12].volume, int)) def test_volume_down(self): - """Tests decreasing volume by one.""" + """Test decreasing volume by one.""" self.monoprice.zones[12].volume = 1 self.media_player.update() self.media_player.volume_down() From 3a10e74154120bb05c51d1ae3dadf928d7041fec Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 13:58:21 -0600 Subject: [PATCH 10/17] added monoprice to requirements_all.txt --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 4c6a1d91558b40..86d6564fe64dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -674,6 +674,9 @@ pymochad==0.1.1 # homeassistant.components.modbus pymodbus==1.3.1 +# homeassistant.components.media_player.monoprice +pymonoprice==0.2 + # homeassistant.components.media_player.yamaha_musiccast pymusiccast==0.1.2 From a460ccc4224235f4f100307e5fbbb4c381665a51 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Mon, 2 Oct 2017 14:08:01 -0600 Subject: [PATCH 11/17] fixed lint errors again --- tests/components/media_player/test_monoprice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index 9e56f1388987bc..451b6b51febdad 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -1,3 +1,4 @@ +"""The tests for Monoprice Media player platform.""" import unittest import voluptuous as vol @@ -13,6 +14,7 @@ class MockState(object): """Mock for zone state object.""" + def __init__(self): """Init zone state.""" self.power = True From b5442db5051cfac57db94c501b61ef27d2d33ddb Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Fri, 6 Oct 2017 08:09:10 -0600 Subject: [PATCH 12/17] implemented change requests --- .coveragerc | 1 - .../components/media_player/monoprice.py | 22 +++++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9dd8179f2bbbb6..2d3c64a79cda5b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -372,7 +372,6 @@ omit = homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/liveboxplaytv.py - homeassistant/components/media_player/monoprice.py homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/nad.py diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index aecdf808188dc4..57c16c90437d5e 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -1,17 +1,20 @@ """ Support for interfacing with Monoprice 6 zone home audio controller. - https://www.monoprice.com/product?p_id=10761 + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.monoprice/ """ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, - MediaPlayerDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) + +from homeassistant.const import (CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_VOLUME_MUTE, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) REQUIREMENTS = ['pymonoprice==0.2'] @@ -53,10 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) - if port is None: - _LOGGER.error("Invalid config. Expected %s", CONF_PORT) - return False - from pymonoprice import Monoprice monoprice = Monoprice(port) @@ -67,11 +66,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) add_devices([MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME])], True) - return True class MonopriceZone(MediaPlayerDevice): - """Represents an Monoprice amplifier zone.""" + """Representation of a a Monoprice amplifier zone.""" # pylint: disable=too-many-public-methods From 91e92d0abed4a00756dca4f182f0bbc062a81ff2 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Fri, 6 Oct 2017 08:30:50 -0600 Subject: [PATCH 13/17] fixed lint error --- homeassistant/components/media_player/monoprice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 57c16c90437d5e..f95ce91e5f6d9b 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -1,6 +1,5 @@ """ Support for interfacing with Monoprice 6 zone home audio controller. -https://www.monoprice.com/product?p_id=10761 For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.monoprice/ From a5503963afb2341223519dccb25b1d68c33c26a4 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Sun, 8 Oct 2017 10:23:33 -0600 Subject: [PATCH 14/17] added exception handling to setup_platform() --- homeassistant/components/media_player/monoprice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index f95ce91e5f6d9b..512abac527e478 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -56,7 +56,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get(CONF_PORT) from pymonoprice import Monoprice - monoprice = Monoprice(port) + try: + monoprice = Monoprice(port) + except: + _LOGGER.error('Error connecting to Monoprice controller.') + return sources = {source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} From e72e14461068d7cd92e220548ebbfe0f93487886 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Sun, 8 Oct 2017 10:46:16 -0600 Subject: [PATCH 15/17] replaced catchall with SerialException only --- homeassistant/components/media_player/monoprice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 512abac527e478..b9a25367660829 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -55,10 +55,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) + from serial import SerialException from pymonoprice import Monoprice try: monoprice = Monoprice(port) - except: + except SerialException: _LOGGER.error('Error connecting to Monoprice controller.') return From 707af2d6c7cf3faf4a6cb5a38afba1a345798b08 Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Sun, 8 Oct 2017 11:10:13 -0600 Subject: [PATCH 16/17] added myself to CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index ad9345c3ab6d12..03fec40ed60dde 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -39,6 +39,7 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/media_player/kodi.py @armills +homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/light/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/climate/eq3btsmart.py @rytilahti From 0e5aa9548a64772ff9455d35380325621fa21c5c Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Sun, 8 Oct 2017 11:16:37 -0600 Subject: [PATCH 17/17] fixed weird merge to CODEOWNERS --- CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 82e598d9ecc4e4..0f5b452cf3255f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -43,11 +43,10 @@ homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills -homeassistant/components/media_player/kodi.py @armills -homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/tplink.py @rytilahti homeassistant/components/media_player/kodi.py @armills +homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/*/xiaomi_miio.py @rytilahti