From ca81180e6dae7c33c308a56ad810b5ec1ac6a00e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Dec 2017 10:06:06 +0100 Subject: [PATCH 01/11] Bump release to 0.60.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4f075249e572..dd15e1fb75d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 60 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From f4d7bbd0446c609939b4c1d82a2731d42e22a7ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Dec 2017 23:35:37 -0800 Subject: [PATCH 02/11] Update frontend --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9d97a7439bd6..cd206135dde3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20171206.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20171216.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6466be2f6511..89711fe9c967 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -340,7 +340,7 @@ hipnotify==1.0.8 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171206.0 +home-assistant-frontend==20171216.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 363f561de0d1..877e129e0ffb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -77,7 +77,7 @@ hbmqtt==0.9.1 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171206.0 +home-assistant-frontend==20171216.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 7db8bbf38599e97560dc224d8259a9b9d2c4265d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 16 Dec 2017 15:52:40 -0800 Subject: [PATCH 03/11] Fix X10 commands for mochad light turn on (#11146) * Fix X10 commands for mochad light turn on This commit attempts to address issues that a lot of people are having with the x10 light component. Originally this was written to use the xdim (extended dim) X10 command. However, not every X10 dimmer device supports the xdim command. Additionally, it turns out the number of dim/brighness levels the X10 device supports is device specific and there is no way to detect this (given the mostly 1 way nature of X10) To address these issues, this commit removes the usage of xdim and instead relies on using the 'on' command and the 'dim' command. This should work on all x10 light devices. In an attempt to address the different dim/brightness levels supported by different devices this commit also adds a new optional config value, 'brightness_levels', to specify if it's either 32, 64, or 256. By default 32 levels are used as this is the normal case and what is documented by mochad. Fixes #8943 * make code more readable * fix style * fix lint * fix tests --- homeassistant/components/light/mochad.py | 45 ++++++++++++++-- tests/components/light/test_mochad.py | 68 +++++++++++++++++++++++- 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/light/mochad.py index 3d67edaf7cb6..efc62b054345 100644 --- a/homeassistant/components/light/mochad.py +++ b/homeassistant/components/light/mochad.py @@ -12,13 +12,15 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) from homeassistant.components import mochad -from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_DEVICES, - CONF_ADDRESS) +from homeassistant.const import ( + CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS) from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['mochad'] _LOGGER = logging.getLogger(__name__) +CONF_BRIGHTNESS_LEVELS = 'brightness_levels' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): mochad.DOMAIN, @@ -26,6 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32): + vol.All(vol.Coerce(int), vol.In([32, 64, 256])), }] }) @@ -54,6 +58,7 @@ class MochadLight(Light): comm_type=self._comm_type) self._brightness = 0 self._state = self._get_device_status() + self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1 @property def brightness(self): @@ -86,12 +91,38 @@ class MochadLight(Light): """X10 devices are normally 1-way so we have to assume the state.""" return True + def _calculate_brightness_value(self, value): + return int(value * (float(self._brightness_levels) / 255.0)) + + def _adjust_brightness(self, brightness): + if self._brightness > brightness: + bdelta = self._brightness - brightness + mochad_brightness = self._calculate_brightness_value(bdelta) + self.device.send_cmd("dim {}".format(mochad_brightness)) + self._controller.read_data() + elif self._brightness < brightness: + bdelta = brightness - self._brightness + mochad_brightness = self._calculate_brightness_value(bdelta) + self.device.send_cmd("bright {}".format(mochad_brightness)) + self._controller.read_data() + def turn_on(self, **kwargs): """Send the command to turn the light on.""" - self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) with mochad.REQ_LOCK: - self.device.send_cmd("xdim {}".format(self._brightness)) - self._controller.read_data() + if self._brightness_levels > 32: + out_brightness = self._calculate_brightness_value(brightness) + self.device.send_cmd('xdim {}'.format(out_brightness)) + self._controller.read_data() + else: + self.device.send_cmd("on") + self._controller.read_data() + # There is no persistence for X10 modules so a fresh on command + # will be full brightness + if self._brightness == 0: + self._brightness = 255 + self._adjust_brightness(brightness) + self._brightness = brightness self._state = True def turn_off(self, **kwargs): @@ -99,4 +130,8 @@ class MochadLight(Light): with mochad.REQ_LOCK: self.device.send_cmd('off') self._controller.read_data() + # There is no persistence for X10 modules so we need to prepare + # to track a fresh on command will full brightness + if self._brightness_levels == 31: + self._brightness = 0 self._state = False diff --git a/tests/components/light/test_mochad.py b/tests/components/light/test_mochad.py index e69ebdb4aef7..5c82ab060853 100644 --- a/tests/components/light/test_mochad.py +++ b/tests/components/light/test_mochad.py @@ -60,7 +60,8 @@ class TestMochadLight(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() controller_mock = mock.MagicMock() - dev_dict = {'address': 'a1', 'name': 'fake_light'} + dev_dict = {'address': 'a1', 'name': 'fake_light', + 'brightness_levels': 32} self.light = mochad.MochadLight(self.hass, controller_mock, dev_dict) @@ -72,6 +73,39 @@ class TestMochadLight(unittest.TestCase): """Test the name.""" self.assertEqual('fake_light', self.light.name) + def test_turn_on_with_no_brightness(self): + """Test turn_on.""" + self.light.turn_on() + self.light.device.send_cmd.assert_called_once_with('on') + + def test_turn_on_with_brightness(self): + """Test turn_on.""" + self.light.turn_on(brightness=45) + self.light.device.send_cmd.assert_has_calls( + [mock.call('on'), mock.call('dim 25')]) + + def test_turn_off(self): + """Test turn_off.""" + self.light.turn_off() + self.light.device.send_cmd.assert_called_once_with('off') + + +class TestMochadLight256Levels(unittest.TestCase): + """Test for mochad light platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + controller_mock = mock.MagicMock() + dev_dict = {'address': 'a1', 'name': 'fake_light', + 'brightness_levels': 256} + self.light = mochad.MochadLight(self.hass, controller_mock, + dev_dict) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + def test_turn_on_with_no_brightness(self): """Test turn_on.""" self.light.turn_on() @@ -86,3 +120,35 @@ class TestMochadLight(unittest.TestCase): """Test turn_off.""" self.light.turn_off() self.light.device.send_cmd.assert_called_once_with('off') + + +class TestMochadLight64Levels(unittest.TestCase): + """Test for mochad light platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + controller_mock = mock.MagicMock() + dev_dict = {'address': 'a1', 'name': 'fake_light', + 'brightness_levels': 64} + self.light = mochad.MochadLight(self.hass, controller_mock, + dev_dict) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_turn_on_with_no_brightness(self): + """Test turn_on.""" + self.light.turn_on() + self.light.device.send_cmd.assert_called_once_with('xdim 63') + + def test_turn_on_with_brightness(self): + """Test turn_on.""" + self.light.turn_on(brightness=45) + self.light.device.send_cmd.assert_called_once_with('xdim 11') + + def test_turn_off(self): + """Test turn_off.""" + self.light.turn_off() + self.light.device.send_cmd.assert_called_once_with('off') From ec9638f4d153922374fd50c67a7535f5c03ee765 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Dec 2017 21:22:36 +0100 Subject: [PATCH 04/11] Homematic next (#11156) * Cleanup logic & New gen of HomeMatic * fix lint * cleanup * fix coverage * cleanup * name consistenc * fix lint * Rename ip * cleanup wrong property * fix bug * handle callback better * fix lint * Running now --- .coveragerc | 2 +- .../{homematic.py => homematic/__init__.py} | 197 +++++++++--------- .../components/homematic/services.yaml | 52 +++++ homeassistant/components/services.yaml | 49 ----- 4 files changed, 146 insertions(+), 154 deletions(-) rename homeassistant/components/{homematic.py => homematic/__init__.py} (83%) create mode 100644 homeassistant/components/homematic/services.yaml diff --git a/.coveragerc b/.coveragerc index b73d847f4310..96936655c51c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -88,7 +88,7 @@ omit = homeassistant/components/hive.py homeassistant/components/*/hive.py - homeassistant/components/homematic.py + homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py homeassistant/components/insteon_local.py diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic/__init__.py similarity index 83% rename from homeassistant/components/homematic.py rename to homeassistant/components/homematic/__init__.py index 0ab6f01805f1..af3a54b861d6 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic/__init__.py @@ -12,14 +12,13 @@ from functools import partial import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.config import load_yaml_config_file from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD, - CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID) + EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, + CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN) from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval -from homeassistant.config import load_yaml_config_file +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyhomematic==0.1.36'] @@ -41,7 +40,7 @@ ATTR_CHANNEL = 'channel' ATTR_NAME = 'name' ATTR_ADDRESS = 'address' ATTR_VALUE = 'value' -ATTR_PROXY = 'proxy' +ATTR_INTERFACE = 'interface' ATTR_ERRORCODE = 'error' ATTR_MESSAGE = 'message' @@ -51,8 +50,8 @@ EVENT_ERROR = 'homematic.error' SERVICE_VIRTUALKEY = 'virtualkey' SERVICE_RECONNECT = 'reconnect' -SERVICE_SET_VAR_VALUE = 'set_var_value' -SERVICE_SET_DEV_VALUE = 'set_dev_value' +SERVICE_SET_VARIABLE_VALUE = 'set_variable_value' +SERVICE_SET_DEVICE_VALUE = 'set_device_value' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ @@ -90,12 +89,14 @@ HM_ATTRIBUTE_SUPPORT = { 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], 'BATTERY_STATE': ['battery', {}], - 'CONTROL_MODE': ['mode', {0: 'Auto', - 1: 'Manual', - 2: 'Away', - 3: 'Boost', - 4: 'Comfort', - 5: 'Lowering'}], + 'CONTROL_MODE': ['mode', { + 0: 'Auto', + 1: 'Manual', + 2: 'Away', + 3: 'Boost', + 4: 'Comfort', + 5: 'Lowering' + }], 'POWER': ['power', {}], 'CURRENT': ['current', {}], 'VOLTAGE': ['voltage', {}], @@ -124,12 +125,12 @@ CONF_RESOLVENAMES_OPTIONS = [ ] DATA_HOMEMATIC = 'homematic' -DATA_DEVINIT = 'homematic_devinit' DATA_STORE = 'homematic_store' +DATA_CONF = 'homematic_conf' +CONF_INTERFACES = 'interfaces' CONF_LOCAL_IP = 'local_ip' CONF_LOCAL_PORT = 'local_port' -CONF_IP = 'ip' CONF_PORT = 'port' CONF_PATH = 'path' CONF_CALLBACK_IP = 'callback_ip' @@ -146,37 +147,33 @@ DEFAULT_PORT = 2001 DEFAULT_PATH = '' DEFAULT_USERNAME = 'Admin' DEFAULT_PASSWORD = '' -DEFAULT_VARIABLES = False -DEFAULT_DEVICES = True -DEFAULT_PRIMARY = False DEVICE_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'homematic', vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_ADDRESS): cv.string, - vol.Required(ATTR_PROXY): cv.string, + vol.Required(ATTR_INTERFACE): cv.string, vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int), vol.Optional(ATTR_PARAM): cv.string, }) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_HOSTS): {cv.match_all: { - vol.Required(CONF_IP): cv.string, + vol.Optional(CONF_INTERFACES, default={}): {cv.match_all: { + vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): - cv.boolean, vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.In(CONF_RESOLVENAMES_OPTIONS), - vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean, - vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean, vol.Optional(CONF_CALLBACK_IP): cv.string, vol.Optional(CONF_CALLBACK_PORT): cv.port, }}, + vol.Optional(CONF_HOSTS, default={}): {cv.match_all: { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + }}, vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string, vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port, }), @@ -186,33 +183,33 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({ vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_PARAM): cv.string, - vol.Optional(ATTR_PROXY): cv.string, + vol.Optional(ATTR_INTERFACE): cv.string, }) -SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({ +SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema({ vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.match_all, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) -SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({ +SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({ vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), vol.Required(ATTR_VALUE): cv.match_all, - vol.Optional(ATTR_PROXY): cv.string, + vol.Optional(ATTR_INTERFACE): cv.string, }) SCHEMA_SERVICE_RECONNECT = vol.Schema({}) -def virtualkey(hass, address, channel, param, proxy=None): +def virtualkey(hass, address, channel, param, interface=None): """Send virtual keypress to homematic controlller.""" data = { ATTR_ADDRESS: address, ATTR_CHANNEL: channel, ATTR_PARAM: param, - ATTR_PROXY: proxy, + ATTR_INTERFACE: interface, } hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) @@ -225,20 +222,20 @@ def set_var_value(hass, entity_id, value): ATTR_VALUE: value, } - hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data) + hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data) -def set_dev_value(hass, address, channel, param, value, proxy=None): - """Call setValue XML-RPC method of supplied proxy.""" +def set_dev_value(hass, address, channel, param, value, interface=None): + """Call setValue XML-RPC method of supplied interface.""" data = { ATTR_ADDRESS: address, ATTR_CHANNEL: channel, ATTR_PARAM: param, ATTR_VALUE: value, - ATTR_PROXY: proxy, + ATTR_INTERFACE: interface, } - hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data) + hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data) def reconnect(hass): @@ -250,31 +247,32 @@ def setup(hass, config): """Set up the Homematic component.""" from pyhomematic import HMConnection - hass.data[DATA_DEVINIT] = {} + conf = config[DOMAIN] + hass.data[DATA_CONF] = remotes = {} hass.data[DATA_STORE] = set() # Create hosts-dictionary for pyhomematic - remotes = {} - hosts = {} - for rname, rconfig in config[DOMAIN][CONF_HOSTS].items(): - server = rconfig.get(CONF_IP) + for rname, rconfig in conf[CONF_INTERFACES].items(): + remotes[rname] = { + 'ip': rconfig.get(CONF_HOST), + 'port': rconfig.get(CONF_PORT), + 'path': rconfig.get(CONF_PATH), + 'resolvenames': rconfig.get(CONF_RESOLVENAMES), + 'username': rconfig.get(CONF_USERNAME), + 'password': rconfig.get(CONF_PASSWORD), + 'callbackip': rconfig.get(CONF_CALLBACK_IP), + 'callbackport': rconfig.get(CONF_CALLBACK_PORT), + 'connect': True, + } - remotes[rname] = {} - remotes[rname][CONF_IP] = server - remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT) - remotes[rname][CONF_PATH] = rconfig.get(CONF_PATH) - remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES) - remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME) - remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD) - remotes[rname]['callbackip'] = rconfig.get(CONF_CALLBACK_IP) - remotes[rname]['callbackport'] = rconfig.get(CONF_CALLBACK_PORT) - - if server not in hosts or rconfig.get(CONF_PRIMARY): - hosts[server] = { - CONF_VARIABLES: rconfig.get(CONF_VARIABLES), - CONF_NAME: rname, - } - hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES) + for sname, sconfig in conf[CONF_HOSTS].items(): + remotes[sname] = { + 'ip': sconfig.get(CONF_HOST), + 'port': DEFAULT_PORT, + 'username': sconfig.get(CONF_USERNAME), + 'password': sconfig.get(CONF_PASSWORD), + 'connect': False, + } # Create server thread bound_system_callback = partial(_system_callback_handler, hass, config) @@ -295,9 +293,8 @@ def setup(hass, config): # Init homematic hubs entity_hubs = [] - for _, hub_data in hosts.items(): - entity_hubs.append(HMHub( - hass, homematic, hub_data[CONF_NAME], hub_data[CONF_VARIABLES])) + for hub_name in conf[CONF_HOSTS].keys(): + entity_hubs.append(HMHub(hass, homematic, hub_name)) # Register HomeMatic services descriptions = load_yaml_config_file( @@ -331,8 +328,7 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey, - descriptions[DOMAIN][SERVICE_VIRTUALKEY], - schema=SCHEMA_SERVICE_VIRTUALKEY) + descriptions[SERVICE_VIRTUALKEY], schema=SCHEMA_SERVICE_VIRTUALKEY) def _service_handle_value(service): """Service to call setValue method for HomeMatic system variable.""" @@ -354,9 +350,9 @@ def setup(hass, config): hub.hm_set_variable(name, value) hass.services.register( - DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value, - descriptions[DOMAIN][SERVICE_SET_VAR_VALUE], - schema=SCHEMA_SERVICE_SET_VAR_VALUE) + DOMAIN, SERVICE_SET_VARIABLE_VALUE, _service_handle_value, + descriptions[SERVICE_SET_VARIABLE_VALUE], + schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE) def _service_handle_reconnect(service): """Service to reconnect all HomeMatic hubs.""" @@ -364,8 +360,7 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect, - descriptions[DOMAIN][SERVICE_RECONNECT], - schema=SCHEMA_SERVICE_RECONNECT) + descriptions[SERVICE_RECONNECT], schema=SCHEMA_SERVICE_RECONNECT) def _service_handle_device(service): """Service to call setValue method for HomeMatic devices.""" @@ -383,9 +378,9 @@ def setup(hass, config): hmdevice.setValue(param, value, channel) hass.services.register( - DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device, - descriptions[DOMAIN][SERVICE_SET_DEV_VALUE], - schema=SCHEMA_SERVICE_SET_DEV_VALUE) + DOMAIN, SERVICE_SET_DEVICE_VALUE, _service_handle_device, + descriptions[SERVICE_SET_DEVICE_VALUE], + schema=SCHEMA_SERVICE_SET_DEVICE_VALUE) return True @@ -395,10 +390,10 @@ def _system_callback_handler(hass, config, src, *args): # New devices available at hub if src == 'newDevices': (interface_id, dev_descriptions) = args - proxy = interface_id.split('-')[-1] + interface = interface_id.split('-')[-1] # Device support active? - if not hass.data[DATA_DEVINIT][proxy]: + if not hass.data[DATA_CONF][interface]['connect']: return addresses = [] @@ -410,9 +405,9 @@ def _system_callback_handler(hass, config, src, *args): # Register EVENTS # Search all devices with an EVENTNODE that includes data - bound_event_callback = partial(_hm_event_handler, hass, proxy) + bound_event_callback = partial(_hm_event_handler, hass, interface) for dev in addresses: - hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev) + hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(dev) if hmdevice.EVENTNODE: hmdevice.setEventCallback( @@ -429,7 +424,7 @@ def _system_callback_handler(hass, config, src, *args): ('climate', DISCOVER_CLIMATE)): # Get all devices of a specific type found_devices = _get_devices( - hass, discovery_type, addresses, proxy) + hass, discovery_type, addresses, interface) # When devices of this type are found # they are setup in HASS and an discovery event is fired @@ -448,12 +443,12 @@ def _system_callback_handler(hass, config, src, *args): }) -def _get_devices(hass, discovery_type, keys, proxy): +def _get_devices(hass, discovery_type, keys, interface): """Get the HomeMatic devices for given discovery_type.""" device_arr = [] for key in keys: - device = hass.data[DATA_HOMEMATIC].devices[proxy][key] + device = hass.data[DATA_HOMEMATIC].devices[interface][key] class_name = device.__class__.__name__ metadata = {} @@ -485,7 +480,7 @@ def _get_devices(hass, discovery_type, keys, proxy): device_dict = { CONF_PLATFORM: "homematic", ATTR_ADDRESS: key, - ATTR_PROXY: proxy, + ATTR_INTERFACE: interface, ATTR_NAME: name, ATTR_CHANNEL: channel } @@ -521,12 +516,12 @@ def _create_ha_name(name, channel, param, count): return "{} {} {}".format(name, channel, param) -def _hm_event_handler(hass, proxy, device, caller, attribute, value): +def _hm_event_handler(hass, interface, device, caller, attribute, value): """Handle all pyhomematic device events.""" try: channel = int(device.split(":")[1]) address = device.split(":")[0] - hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address) + hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(address) except (TypeError, ValueError): _LOGGER.error("Event handling channel convert error!") return @@ -561,14 +556,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value): def _device_from_servicecall(hass, service): """Extract HomeMatic device from service call.""" address = service.data.get(ATTR_ADDRESS) - proxy = service.data.get(ATTR_PROXY) + interface = service.data.get(ATTR_INTERFACE) if address == 'BIDCOS-RF': address = 'BidCoS-RF' - if proxy: - return hass.data[DATA_HOMEMATIC].devices[proxy].get(address) + if interface: + return hass.data[DATA_HOMEMATIC].devices[interface].get(address) - for _, devices in hass.data[DATA_HOMEMATIC].devices.items(): + for devices in hass.data[DATA_HOMEMATIC].devices.values(): if address in devices: return devices[address] @@ -576,25 +571,23 @@ def _device_from_servicecall(hass, service): class HMHub(Entity): """The HomeMatic hub. (CCU2/HomeGear).""" - def __init__(self, hass, homematic, name, use_variables): + def __init__(self, hass, homematic, name): """Initialize HomeMatic hub.""" self.hass = hass self.entity_id = "{}.{}".format(DOMAIN, name.lower()) self._homematic = homematic self._variables = {} self._name = name - self._state = STATE_UNKNOWN - self._use_variables = use_variables + self._state = None # Load data - track_time_interval( - self.hass, self._update_hub, SCAN_INTERVAL_HUB) + self.hass.helpers.event.track_time_interval( + self._update_hub, SCAN_INTERVAL_HUB) self.hass.add_job(self._update_hub, None) - if self._use_variables: - track_time_interval( - self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES) - self.hass.add_job(self._update_variables, None) + self.hass.helpers.event.track_time_interval( + self._update_variables, SCAN_INTERVAL_VARIABLES) + self.hass.add_job(self._update_variables, None) @property def name(self): @@ -672,7 +665,7 @@ class HMDevice(Entity): """Initialize a generic HomeMatic device.""" self._name = config.get(ATTR_NAME) self._address = config.get(ATTR_ADDRESS) - self._proxy = config.get(ATTR_PROXY) + self._interface = config.get(ATTR_INTERFACE) self._channel = config.get(ATTR_CHANNEL) self._state = config.get(ATTR_PARAM) self._data = {} @@ -700,11 +693,6 @@ class HMDevice(Entity): """Return the name of the device.""" return self._name - @property - def assumed_state(self): - """Return true if unable to access real state of the device.""" - return not self._available - @property def available(self): """Return true if device is available.""" @@ -728,7 +716,7 @@ class HMDevice(Entity): # Static attributes attr['id'] = self._hmdevice.ADDRESS - attr['proxy'] = self._proxy + attr['interface'] = self._interface return attr @@ -739,7 +727,8 @@ class HMDevice(Entity): # Initialize self._homematic = self.hass.data[DATA_HOMEMATIC] - self._hmdevice = self._homematic.devices[self._proxy][self._address] + self._hmdevice = \ + self._homematic.devices[self._interface][self._address] self._connected = True try: diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml new file mode 100644 index 000000000000..76ecdbd0a4f5 --- /dev/null +++ b/homeassistant/components/homematic/services.yaml @@ -0,0 +1,52 @@ +# Describes the format for available component services + +virtualkey: + description: Press a virtual key from CCU/Homegear or simulate keypress. + fields: + address: + description: Address of homematic device or BidCoS-RF for virtual remote. + example: BidCoS-RF + channel: + description: Channel for calling a keypress. + example: 1 + param: + description: Event to send i.e. PRESS_LONG, PRESS_SHORT. + example: PRESS_LONG + interface: + description: (Optional) for set a hosts value. + example: Hosts name from config + +set_variable_value: + description: Set the name of a node. + fields: + entity_id: + description: Name(s) of homematic central to set value. + example: 'homematic.ccu2' + name: + description: Name of the variable to set. + example: 'testvariable' + value: + description: New value + example: 1 + +set_device_value: + description: Set a device property on RPC XML interface. + fields: + address: + description: Address of homematic device or BidCoS-RF for virtual remote + example: BidCoS-RF + channel: + description: Channel for calling a keypress + example: 1 + param: + description: Event to send i.e. PRESS_LONG, PRESS_SHORT + example: PRESS_LONG + interface: + description: (Optional) for set a hosts value + example: Hosts name from config + value: + description: New value + example: 1 + +reconnect: + description: Reconnect to all Homematic Hubs. diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index c532c0dfd208..90a1bbbc6137 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -32,55 +32,6 @@ foursquare: description: Vertical accuracy of the user's location, in meters. example: 1 -homematic: - virtualkey: - description: Press a virtual key from CCU/Homegear or simulate keypress. - fields: - address: - description: Address of homematic device or BidCoS-RF for virtual remote. - example: BidCoS-RF - channel: - description: Channel for calling a keypress. - example: 1 - param: - description: Event to send i.e. PRESS_LONG, PRESS_SHORT. - example: PRESS_LONG - proxy: - description: (Optional) for set a hosts value. - example: Hosts name from config - set_var_value: - description: Set the name of a node. - fields: - entity_id: - description: Name(s) of homematic central to set value. - example: 'homematic.ccu2' - name: - description: Name of the variable to set. - example: 'testvariable' - value: - description: New value - example: 1 - set_dev_value: - description: Set a device property on RPC XML interface. - fields: - address: - description: Address of homematic device or BidCoS-RF for virtual remote - example: BidCoS-RF - channel: - description: Channel for calling a keypress - example: 1 - param: - description: Event to send i.e. PRESS_LONG, PRESS_SHORT - example: PRESS_LONG - proxy: - description: (Optional) for set a hosts value - example: Hosts name from config - value: - description: New value - example: 1 - reconnect: - description: Reconnect to all Homematic Hubs. - microsoft_face: create_group: description: Create a new person group. From 5860267410bdd23fd775991e09a5ec263715a35c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Dec 2017 22:54:54 +0100 Subject: [PATCH 05/11] Resolve hostnames (#11160) --- homeassistant/components/homematic/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index af3a54b861d6..ffee6278f409 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -5,10 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematic/ """ import asyncio -import os -import logging from datetime import timedelta from functools import partial +import logging +import os +import socket import voluptuous as vol @@ -254,7 +255,7 @@ def setup(hass, config): # Create hosts-dictionary for pyhomematic for rname, rconfig in conf[CONF_INTERFACES].items(): remotes[rname] = { - 'ip': rconfig.get(CONF_HOST), + 'ip': socket.gethostbyname(rconfig.get(CONF_HOST)), 'port': rconfig.get(CONF_PORT), 'path': rconfig.get(CONF_PATH), 'resolvenames': rconfig.get(CONF_RESOLVENAMES), @@ -267,7 +268,7 @@ def setup(hass, config): for sname, sconfig in conf[CONF_HOSTS].items(): remotes[sname] = { - 'ip': sconfig.get(CONF_HOST), + 'ip': socket.gethostbyname(sconfig.get(CONF_HOST)), 'port': DEFAULT_PORT, 'username': sconfig.get(CONF_USERNAME), 'password': sconfig.get(CONF_PASSWORD), From 564ed26aebce0b01699b579643150e87a93eb5c9 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sat, 16 Dec 2017 03:04:27 -0500 Subject: [PATCH 06/11] Perform logbook filtering on the worker thread (#11161) --- homeassistant/components/logbook.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 63a271acdd55..1dc0861d7371 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -135,9 +135,8 @@ class LogbookView(HomeAssistantView): hass = request.app['hass'] events = yield from hass.async_add_job( - _get_events, hass, start_day, end_day) - events = _exclude_events(events, self.config) - return self.json(humanify(events)) + _get_events, hass, self.config, start_day, end_day) + return self.json(events) class Entry(object): @@ -274,7 +273,7 @@ def humanify(events): entity_id) -def _get_events(hass, start_day, end_day): +def _get_events(hass, config, start_day, end_day): """Get events for a period of time.""" from homeassistant.components.recorder.models import Events from homeassistant.components.recorder.util import ( @@ -285,7 +284,8 @@ def _get_events(hass, start_day, end_day): Events.time_fired).filter( (Events.time_fired > start_day) & (Events.time_fired < end_day)) - return execute(query) + events = execute(query) + return humanify(_exclude_events(events, config)) def _exclude_events(events, config): From 5ca006cc9c6f7ce190879e6185f1646bd1ba1f43 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Dec 2017 00:42:25 -0800 Subject: [PATCH 07/11] Don't connect to cloud if subscription expired (#11163) * Final touch for cloud component * Fix test --- homeassistant/components/cloud/__init__.py | 14 +++++--------- homeassistant/components/cloud/const.py | 13 ++++++------- homeassistant/config.py | 3 +++ tests/components/cloud/test_init.py | 9 --------- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2844b0c88f3a..58a2152f8989 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -27,7 +27,7 @@ CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' MODE_DEV = 'development' -DEFAULT_MODE = MODE_DEV +DEFAULT_MODE = 'production' DEPENDENCIES = ['http'] ALEXA_SCHEMA = vol.Schema({ @@ -42,10 +42,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In([MODE_DEV] + list(SERVERS)), # Change to optional when we include real servers - vol.Required(CONF_COGNITO_CLIENT_ID): str, - vol.Required(CONF_USER_POOL_ID): str, - vol.Required(CONF_REGION): str, - vol.Required(CONF_RELAYER): str, + vol.Optional(CONF_COGNITO_CLIENT_ID): str, + vol.Optional(CONF_USER_POOL_ID): str, + vol.Optional(CONF_REGION): str, + vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA }), }, extra=vol.ALLOW_EXTRA) @@ -117,10 +117,6 @@ class Cloud: @property def subscription_expired(self): """Return a boolen if the subscription has expired.""" - # For now, don't enforce subscriptions to exist - if 'custom:sub-exp' not in self.claims: - return False - return dt_util.utcnow() > self.expiration_date @property diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 440e4179eea9..b13ec6d1e45d 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -4,13 +4,12 @@ CONFIG_DIR = '.cloud' REQUEST_TIMEOUT = 10 SERVERS = { - # Example entry: - # 'production': { - # 'cognito_client_id': '', - # 'user_pool_id': '', - # 'region': '', - # 'relayer': '' - # } + 'production': { + 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', + 'user_pool_id': 'us-east-1_87ll5WOP8', + 'region': 'us-east-1', + 'relayer': 'wss://cloud.hass.io:8000/websocket' + } } MESSAGE_EXPIRATION = """ diff --git a/homeassistant/config.py b/homeassistant/config.py index c4c96804fca2..fee7572a2c2a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -110,6 +110,9 @@ sensor: tts: - platform: google +# Cloud +cloud: + group: !include groups.yaml automation: !include automations.yaml script: !include scripts.yaml diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index c05fdabf4657..c5bb6f7fda75 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -132,15 +132,6 @@ def test_write_user_info(): } -@asyncio.coroutine -def test_subscription_not_expired_without_sub_in_claim(): - """Test that we do not enforce subscriptions yet.""" - cl = cloud.Cloud(None, cloud.MODE_DEV) - cl.id_token = jwt.encode({}, 'test') - - assert not cl.subscription_expired - - @asyncio.coroutine def test_subscription_expired(): """Test subscription being expired.""" From a46ddcf6ddaadd85710db7a829ec1fb0fda91734 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 16 Dec 2017 14:22:23 +0100 Subject: [PATCH 08/11] Add install mode to homematic (#11164) --- .../components/homematic/__init__.py | 53 +++++++++++++++++-- .../components/homematic/services.yaml | 24 +++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index ffee6278f409..a11c8c0f22c7 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,10 +20,11 @@ from homeassistant.const import ( from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass REQUIREMENTS = ['pyhomematic==0.1.36'] - DOMAIN = 'homematic' +_LOGGER = logging.getLogger(__name__) SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) @@ -44,6 +45,8 @@ ATTR_VALUE = 'value' ATTR_INTERFACE = 'interface' ATTR_ERRORCODE = 'error' ATTR_MESSAGE = 'message' +ATTR_MODE = 'mode' +ATTR_TIME = 'time' EVENT_KEYPRESS = 'homematic.keypress' EVENT_IMPULSE = 'homematic.impulse' @@ -53,6 +56,7 @@ SERVICE_VIRTUALKEY = 'virtualkey' SERVICE_RECONNECT = 'reconnect' SERVICE_SET_VARIABLE_VALUE = 'set_variable_value' SERVICE_SET_DEVICE_VALUE = 'set_device_value' +SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ @@ -116,8 +120,6 @@ HM_IMPULSE_EVENTS = [ 'SEQUENCE_OK', ] -_LOGGER = logging.getLogger(__name__) - CONF_RESOLVENAMES_OPTIONS = [ 'metadata', 'json', @@ -203,7 +205,16 @@ SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({ SCHEMA_SERVICE_RECONNECT = vol.Schema({}) +SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({ + vol.Required(ATTR_INTERFACE): cv.string, + vol.Optional(ATTR_TIME, default=60): cv.positive_int, + vol.Optional(ATTR_MODE, default=1): + vol.All(vol.Coerce(int), vol.In([1, 2])), + vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), +}) + +@bind_hass def virtualkey(hass, address, channel, param, interface=None): """Send virtual keypress to homematic controlller.""" data = { @@ -216,7 +227,8 @@ def virtualkey(hass, address, channel, param, interface=None): hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) -def set_var_value(hass, entity_id, value): +@bind_hass +def set_variable_value(hass, entity_id, value): """Change value of a Homematic system variable.""" data = { ATTR_ENTITY_ID: entity_id, @@ -226,7 +238,8 @@ def set_var_value(hass, entity_id, value): hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data) -def set_dev_value(hass, address, channel, param, value, interface=None): +@bind_hass +def set_device_value(hass, address, channel, param, value, interface=None): """Call setValue XML-RPC method of supplied interface.""" data = { ATTR_ADDRESS: address, @@ -239,6 +252,22 @@ def set_dev_value(hass, address, channel, param, value, interface=None): hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data) +@bind_hass +def set_install_mode(hass, interface, mode=None, time=None, address=None): + """Call setInstallMode XML-RPC method of supplied inteface.""" + data = { + key: value for key, value in ( + (ATTR_INTERFACE, interface), + (ATTR_MODE, mode), + (ATTR_TIME, time), + (ATTR_ADDRESS, address) + ) if value + } + + hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data) + + +@bind_hass def reconnect(hass): """Reconnect to CCU/Homegear.""" hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) @@ -383,6 +412,20 @@ def setup(hass, config): descriptions[SERVICE_SET_DEVICE_VALUE], schema=SCHEMA_SERVICE_SET_DEVICE_VALUE) + def _service_handle_install_mode(service): + """Service to set interface into install mode.""" + interface = service.data.get(ATTR_INTERFACE) + mode = service.data.get(ATTR_MODE) + time = service.data.get(ATTR_TIME) + address = service.data.get(ATTR_ADDRESS) + + homematic.setInstallMode(interface, t=time, mode=mode, address=address) + + hass.services.register( + DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode, + descriptions[SERVICE_SET_INSTALL_MODE], + schema=SCHEMA_SERVICE_SET_INSTALL_MODE) + return True diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index 76ecdbd0a4f5..bf4d99af9e70 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -13,8 +13,8 @@ virtualkey: description: Event to send i.e. PRESS_LONG, PRESS_SHORT. example: PRESS_LONG interface: - description: (Optional) for set a hosts value. - example: Hosts name from config + description: (Optional) for set a interface value. + example: Interfaces name from config set_variable_value: description: Set the name of a node. @@ -42,11 +42,27 @@ set_device_value: description: Event to send i.e. PRESS_LONG, PRESS_SHORT example: PRESS_LONG interface: - description: (Optional) for set a hosts value - example: Hosts name from config + description: (Optional) for set a interface value + example: Interfaces name from config value: description: New value example: 1 reconnect: description: Reconnect to all Homematic Hubs. + +set_install_mode: + description: Set a RPC XML interface into installation mode. + fields: + interface: + description: Select the given interface into install mode + example: Interfaces name from config + mode: + description: (Default 1) 1= Normal mode / 2= Remove exists old links + example: 1 + time: + description: (Default 60) Time in seconds to run in install mode + example: 1 + address: + description: (Optional) Address of homematic device or BidCoS-RF to learn + example: LEQ3948571 From 294d8171a220841a1a24affe0172eeaed53d0c05 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Sat, 16 Dec 2017 15:52:59 -0800 Subject: [PATCH 09/11] convert alarmdecoder interface from async to sync (#11168) * convert alarmdecoder interface from async to sync * Convert he rest of alarmdecoder rom async to sync * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py * Update alarmdecoder.py --- .../alarm_control_panel/alarmdecoder.py | 52 ++++++------------- homeassistant/components/alarmdecoder.py | 51 ++++++------------ .../components/binary_sensor/alarmdecoder.py | 40 +++++--------- .../components/sensor/alarmdecoder.py | 21 +++----- 4 files changed, 54 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 3b58eb0b71d4..d5fbbec59989 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -7,30 +7,21 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/ import asyncio import logging -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.components.alarm_control_panel as alarm - -from homeassistant.components.alarmdecoder import (DATA_AD, - SIGNAL_PANEL_MESSAGE) - +from homeassistant.components.alarmdecoder import ( + DATA_AD, SIGNAL_PANEL_MESSAGE) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN, STATE_ALARM_TRIGGERED) + STATE_ALARM_TRIGGERED) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['alarmdecoder'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up for AlarmDecoder alarm panels.""" - _LOGGER.debug("AlarmDecoderAlarmPanel: setup") - - device = AlarmDecoderAlarmPanel("Alarm Panel", hass) - - async_add_devices([device]) + add_devices([AlarmDecoderAlarmPanel()]) return True @@ -38,38 +29,35 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" - def __init__(self, name, hass): + def __init__(self): """Initialize the alarm panel.""" self._display = "" - self._name = name - self._state = STATE_UNKNOWN - - _LOGGER.debug("Setting up panel") + self._name = "Alarm Panel" + self._state = None @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback) - @callback def _message_callback(self, message): if message.alarm_sounding or message.fire_alarm: if self._state != STATE_ALARM_TRIGGERED: self._state = STATE_ALARM_TRIGGERED - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() elif message.armed_away: if self._state != STATE_ALARM_ARMED_AWAY: self._state = STATE_ALARM_ARMED_AWAY - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() elif message.armed_home: if self._state != STATE_ALARM_ARMED_HOME: self._state = STATE_ALARM_ARMED_HOME - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() else: if self._state != STATE_ALARM_DISARMED: self._state = STATE_ALARM_DISARMED - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() @property def name(self): @@ -91,26 +79,20 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + def alarm_disarm(self, code=None): """Send disarm command.""" - _LOGGER.debug("alarm_disarm: %s", code) if code: _LOGGER.debug("alarm_disarm: sending %s1", str(code)) self.hass.data[DATA_AD].send("{!s}1".format(code)) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + def alarm_arm_away(self, code=None): """Send arm away command.""" - _LOGGER.debug("alarm_arm_away: %s", code) if code: _LOGGER.debug("alarm_arm_away: sending %s2", str(code)) self.hass.data[DATA_AD].send("{!s}2".format(code)) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + def alarm_arm_home(self, code=None): """Send arm home command.""" - _LOGGER.debug("alarm_arm_home: %s", code) if code: _LOGGER.debug("alarm_arm_home: sending %s3", str(code)) self.hass.data[DATA_AD].send("{!s}3".format(code)) diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py index 011cc3ad21d6..6e30a83d96a2 100644 --- a/homeassistant/components/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder.py @@ -4,16 +4,13 @@ Support for AlarmDecoder devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alarmdecoder/ """ -import asyncio import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.discovery import load_platform REQUIREMENTS = ['alarmdecoder==0.12.3'] @@ -71,9 +68,9 @@ ZONE_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA, - DEVICE_SERIAL_SCHEMA, - DEVICE_USB_SCHEMA), + vol.Required(CONF_DEVICE): vol.Any( + DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, + DEVICE_USB_SCHEMA), vol.Optional(CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY): cv.boolean, vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, @@ -81,8 +78,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +def setup(hass, config): """Set up for the AlarmDecoder devices.""" from alarmdecoder import AlarmDecoder from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) @@ -99,32 +95,25 @@ def async_setup(hass, config): path = DEFAULT_DEVICE_PATH baud = DEFAULT_DEVICE_BAUD - sync_connect = asyncio.Future(loop=hass.loop) - - def handle_open(device): - """Handle the successful connection.""" - _LOGGER.info("Established a connection with the alarmdecoder") - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) - sync_connect.set_result(True) - - @callback def stop_alarmdecoder(event): """Handle the shutdown of AlarmDecoder.""" _LOGGER.debug("Shutting down alarmdecoder") controller.close() - @callback def handle_message(sender, message): """Handle message from AlarmDecoder.""" - async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message) + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_PANEL_MESSAGE, message) def zone_fault_callback(sender, zone): """Handle zone fault from AlarmDecoder.""" - async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone) + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_ZONE_FAULT, zone) def zone_restore_callback(sender, zone): """Handle zone restore from AlarmDecoder.""" - async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone) + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_ZONE_RESTORE, zone) controller = False if device_type == 'socket': @@ -139,7 +128,6 @@ def async_setup(hass, config): AlarmDecoder(USBDevice.find()) return False - controller.on_open += handle_open controller.on_message += handle_message controller.on_zone_fault += zone_fault_callback controller.on_zone_restore += zone_restore_callback @@ -148,21 +136,16 @@ def async_setup(hass, config): controller.open(baud) - result = yield from sync_connect + _LOGGER.debug("Established a connection with the alarmdecoder") + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) - if not result: - return False - - hass.async_add_job( - async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf, - config)) + load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config) if zones: - hass.async_add_job(async_load_platform( - hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) + load_platform( + hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config) if display: - hass.async_add_job(async_load_platform( - hass, 'sensor', DOMAIN, conf, config)) + load_platform(hass, 'sensor', DOMAIN, conf, config) return True diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index bc05e4d84d80..f42d0de4bb08 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -7,39 +7,29 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/ import asyncio import logging -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.binary_sensor import BinarySensorDevice - -from homeassistant.components.alarmdecoder import (ZONE_SCHEMA, - CONF_ZONES, - CONF_ZONE_NAME, - CONF_ZONE_TYPE, - SIGNAL_ZONE_FAULT, - SIGNAL_ZONE_RESTORE) - +from homeassistant.components.alarmdecoder import ( + ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, + SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE) DEPENDENCIES = ['alarmdecoder'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the AlarmDecoder binary sensor devices.""" configured_zones = discovery_info[CONF_ZONES] devices = [] - for zone_num in configured_zones: device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] - device = AlarmDecoderBinarySensor( - hass, zone_num, zone_name, zone_type) + device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type) devices.append(device) - async_add_devices(devices) + add_devices(devices) return True @@ -47,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class AlarmDecoderBinarySensor(BinarySensorDevice): """Representation of an AlarmDecoder binary sensor.""" - def __init__(self, hass, zone_number, zone_name, zone_type): + def __init__(self, zone_number, zone_name, zone_type): """Initialize the binary_sensor.""" self._zone_number = zone_number self._zone_type = zone_type @@ -55,16 +45,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self._name = zone_name self._type = zone_type - _LOGGER.debug("Setup up zone: %s", self._name) - @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_FAULT, self._fault_callback) - async_dispatcher_connect( - self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback) + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_RESTORE, self._restore_callback) @property def name(self): @@ -97,16 +85,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): """Return the class of this sensor, from DEVICE_CLASSES.""" return self._zone_type - @callback def _fault_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: self._state = 1 - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @callback def _restore_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: self._state = 0 - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/sensor/alarmdecoder.py index 6b026298db08..ce709eee94cf 100644 --- a/homeassistant/components/sensor/alarmdecoder.py +++ b/homeassistant/components/sensor/alarmdecoder.py @@ -7,25 +7,21 @@ https://home-assistant.io/components/sensor.alarmdecoder/ import asyncio import logging -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE) -from homeassistant.const import (STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['alarmdecoder'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up for AlarmDecoder sensor devices.""" - _LOGGER.debug("AlarmDecoderSensor: async_setup_platform") + _LOGGER.debug("AlarmDecoderSensor: setup_platform") device = AlarmDecoderSensor(hass) - async_add_devices([device]) + add_devices([device]) class AlarmDecoderSensor(Entity): @@ -34,23 +30,20 @@ class AlarmDecoderSensor(Entity): def __init__(self, hass): """Initialize the alarm panel.""" self._display = "" - self._state = STATE_UNKNOWN + self._state = None self._icon = 'mdi:alarm-check' self._name = 'Alarm Panel Display' - _LOGGER.debug("Setting up panel") - @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback) - @callback def _message_callback(self, message): if self._display != message.text: self._display = message.text - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() @property def icon(self): From 432304be82245c71b5e78ba2e91321b94d8ee7ec Mon Sep 17 00:00:00 2001 From: Mike Megally Date: Sat, 16 Dec 2017 13:29:40 -0800 Subject: [PATCH 10/11] Remove logging (#11173) An error was being log that seems more like debug info --- homeassistant/components/sensor/octoprint.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index 85b388a1919b..71b72b0a6712 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) tools = octoprint_api.get_tools() - _LOGGER.error(str(tools)) if "Temperatures" in monitored_conditions: if not tools: From c03d04d826d5e13efa0042b8959effc2cf696805 Mon Sep 17 00:00:00 2001 From: Brad Dixon Date: Sun, 17 Dec 2017 07:08:35 -0500 Subject: [PATCH 11/11] Revbump to SoCo 0.13 and add support for Night Sound and Speech Enhancement. (#10765) Sonos Playbar and Playbase devices support Night Sound and Speech Enhancement effects when playing from sources such as a TV. Adds a new service "sonos_set_option" whichs accepts boolean options to control these audio features. --- .../components/media_player/services.yaml | 12 ++++ .../components/media_player/sonos.py | 55 ++++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/media_player/test_sonos.py | 18 ++++++ 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index f2d7b8e07dd4..b2f98d378cf2 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -215,6 +215,18 @@ sonos_clear_sleep_timer: description: Name(s) of entities that will have the timer cleared. example: 'media_player.living_room_sonos' +sonos_set_option: + description: Set Sonos sound options. + fields: + entity_id: + description: Name(s) of entities that will have options set. + example: 'media_player.living_room_sonos' + night_sound: + description: Enable Night Sound mode + example: 'true' + speech_enhance: + description: Enable Speech Enhancement mode + example: 'true' soundtouch_play_everywhere: description: Play on all Bose Soundtouch devices. diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index f9a18a212f50..3bd3a722b461 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -27,7 +27,7 @@ from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['SoCo==0.12'] +REQUIREMENTS = ['SoCo==0.13'] _LOGGER = logging.getLogger(__name__) @@ -52,6 +52,7 @@ SERVICE_RESTORE = 'sonos_restore' SERVICE_SET_TIMER = 'sonos_set_sleep_timer' SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' SERVICE_UPDATE_ALARM = 'sonos_update_alarm' +SERVICE_SET_OPTION = 'sonos_set_option' DATA_SONOS = 'sonos' @@ -69,6 +70,8 @@ ATTR_ENABLED = 'enabled' ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones' ATTR_MASTER = 'master' ATTR_WITH_GROUP = 'with_group' +ATTR_NIGHT_SOUND = 'night_sound' +ATTR_SPEECH_ENHANCE = 'speech_enhance' ATTR_IS_COORDINATOR = 'is_coordinator' @@ -105,6 +108,11 @@ SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({ vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean, }) +SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({ + vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, + vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sonos platform.""" @@ -192,6 +200,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device.clear_sleep_timer() elif service.service == SERVICE_UPDATE_ALARM: device.update_alarm(**service.data) + elif service.service == SERVICE_SET_OPTION: + device.update_option(**service.data) device.schedule_update_ha_state(True) @@ -224,6 +234,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): descriptions.get(SERVICE_UPDATE_ALARM), schema=SONOS_UPDATE_ALARM_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_SET_OPTION, service_handle, + descriptions.get(SERVICE_SET_OPTION), + schema=SONOS_SET_OPTION_SCHEMA) + def _parse_timespan(timespan): """Parse a time-span into number of seconds.""" @@ -337,6 +352,8 @@ class SonosDevice(MediaPlayerDevice): self._support_shuffle_set = True self._support_stop = False self._support_pause = False + self._night_sound = None + self._speech_enhance = None self._current_track_uri = None self._current_track_is_radio_stream = False self._queue = None @@ -457,6 +474,8 @@ class SonosDevice(MediaPlayerDevice): self._support_shuffle_set = False self._support_stop = False self._support_pause = False + self._night_sound = None + self._speech_enhance = None self._is_playing_tv = False self._is_playing_line_in = False self._source_name = None @@ -529,6 +548,9 @@ class SonosDevice(MediaPlayerDevice): media_position_updated_at = None source_name = None + night_sound = self._player.night_mode + speech_enhance = self._player.dialog_mode + is_radio_stream = \ current_media_uri.startswith('x-sonosapi-stream:') or \ current_media_uri.startswith('x-rincon-mp3radio:') @@ -705,6 +727,8 @@ class SonosDevice(MediaPlayerDevice): self._support_shuffle_set = support_shuffle_set self._support_stop = support_stop self._support_pause = support_pause + self._night_sound = night_sound + self._speech_enhance = speech_enhance self._is_playing_tv = is_playing_tv self._is_playing_line_in = is_playing_line_in self._source_name = source_name @@ -848,6 +872,16 @@ class SonosDevice(MediaPlayerDevice): return self._media_title + @property + def night_sound(self): + """Get status of Night Sound.""" + return self._night_sound + + @property + def speech_enhance(self): + """Get status of Speech Enhancement.""" + return self._speech_enhance + @property def supported_features(self): """Flag media player features that are supported.""" @@ -1179,7 +1213,24 @@ class SonosDevice(MediaPlayerDevice): a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES] a.save() + @soco_error + def update_option(self, **data): + """Modify playback options.""" + if ATTR_NIGHT_SOUND in data and self.night_sound is not None: + self.soco.night_mode = data[ATTR_NIGHT_SOUND] + + if ATTR_SPEECH_ENHANCE in data and self.speech_enhance is not None: + self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE] + @property def device_state_attributes(self): """Return device specific state attributes.""" - return {ATTR_IS_COORDINATOR: self.is_coordinator} + attributes = {ATTR_IS_COORDINATOR: self.is_coordinator} + + if self.night_sound is not None: + attributes[ATTR_NIGHT_SOUND] = self.night_sound + + if self.speech_enhance is not None: + attributes[ATTR_SPEECH_ENHANCE] = self.speech_enhance + + return attributes diff --git a/requirements_all.txt b/requirements_all.txt index 89711fe9c967..02a53b9c26e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ PyXiaomiGateway==0.6.0 RtmAPI==0.7.0 # homeassistant.components.media_player.sonos -SoCo==0.12 +SoCo==0.13 # homeassistant.components.sensor.travisci TravisPy==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 877e129e0ffb..a96c3af1fd91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ freezegun>=0.3.8 PyJWT==1.5.3 # homeassistant.components.media_player.sonos -SoCo==0.12 +SoCo==0.13 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.4 diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 33f7a0e882d0..815204e718ac 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -389,3 +389,21 @@ class TestSonosMediaPlayer(unittest.TestCase): device.restore() self.assertEqual(restoreMock.call_count, 1) self.assertEqual(restoreMock.call_args, mock.call(False)) + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_sonos_set_option(self, option_mock, *args): + """Ensuring soco methods called for sonos_set_option service.""" + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) + device = self.hass.data[sonos.DATA_SONOS][-1] + device.hass = self.hass + + option_mock.return_value = True + device._snapshot_coordinator = mock.MagicMock() + device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17') + + device.update_option(night_sound=True, speech_enhance=True) + + self.assertEqual(option_mock.call_count, 1)