diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 1e260854687..223159eb415 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,7 @@ """Provide configuration end points for Automations.""" import asyncio from collections import OrderedDict +import uuid from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView @@ -29,7 +30,12 @@ class EditAutomationConfigView(EditIdBasedConfigView): """Set value.""" index = None for index, cur_value in enumerate(data): - if cur_value[CONF_ID] == config_key: + # When people copy paste their automations to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: break else: cur_value = OrderedDict() diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 7e746d48bed..27d993aee76 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -102,18 +102,23 @@ class _GoogleEntity: if state.state == STATE_UNAVAILABLE: return None + entity_config = self.config.entity_config.get(state.entity_id, {}) + name = (entity_config.get(CONF_NAME) or state.name).strip() + + # If an empty string + if not name: + return None + traits = self.traits() # Found no supported traits for this entity if not traits: return None - entity_config = self.config.entity_config.get(state.entity_id, {}) - device = { 'id': state.entity_id, 'name': { - 'name': entity_config.get(CONF_NAME) or state.name + 'name': name }, 'attributes': {}, 'traits': [trait.name for trait in traits], diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 24c6dfa8a76..6af470e80be 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,6 +3,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ +import ipaddress import logging from zlib import adler32 @@ -12,8 +13,8 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -35,6 +36,8 @@ REQUIREMENTS = ['HAP-python==1.1.9'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): + vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, @@ -48,11 +51,12 @@ async def async_setup(hass, config): conf = config[DOMAIN] port = conf[CONF_PORT] + ip_address = conf.get(CONF_IP_ADDRESS) auto_start = conf[CONF_AUTO_START] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) homekit.setup() if auto_start: @@ -151,10 +155,11 @@ def generate_aid(entity_id): class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port, entity_filter, entity_config): + def __init__(self, hass, port, ip_address, entity_filter, entity_config): """Initialize a HomeKit object.""" self.hass = hass self._port = port + self._ip_address = ip_address self._filter = entity_filter self._config = entity_config self.started = False @@ -169,9 +174,10 @@ class HomeKit(): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self.stop) + ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 164e7d50e4d..e36e7439e09 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN) _LOGGER = logging.getLogger(__name__) -def homekit_http_send(self, message_body=None): +def homekit_http_send(self, message_body=None, encode_chunked=False): r"""Send the currently buffered request and clear the buffer. Appends an extra \r\n to the buffer. diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6eb8de99c99..9f662718514 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -242,26 +242,13 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # pylint: disable=redefined-outer-name mode = self._color_mode - - if mode not in ('hs', 'xy'): - return - source = self.light.action if self.is_group else self.light.state - hue = source.get('hue') - sat = source.get('sat') + if mode in ('xy', 'hs'): + return color.color_xy_to_hs(*source['xy']) - # Sometimes the state will not include valid hue/sat values. - # Reported as issue 13434 - if hue is not None and sat is not None: - return hue / 65535 * 360, sat / 255 * 100 - - if 'xy' not in source: - return None - - return color.color_xy_to_hs(*source['xy']) + return None @property def color_temp(self): diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 0bbec010282..4101eab2150 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin @@ -90,15 +90,15 @@ class TPLinkSmartBulb(Light): if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - if ATTR_KELVIN in kwargs: - self.smartbulb.color_temp = kwargs[ATTR_KELVIN] - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - self.smartbulb.brightness = brightness_to_percentage(brightness) + + brightness = brightness_to_percentage( + kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)) if ATTR_HS_COLOR in kwargs: hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (hue, sat, 100) + hsv = (int(hue), int(sat), brightness) self.smartbulb.hsv = hsv + elif ATTR_BRIGHTNESS in kwargs: + self.smartbulb.brightness = brightness def turn_off(self, **kwargs): """Turn the light off.""" diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 632ab4214b8..a9bea9e4c1d 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -306,13 +306,18 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) self.hass.async_add_job(self.async_set_cast_info(discover)) + async def async_stop(event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" - self._async_disconnect() + await self._async_disconnect() if self._cast_info.uuid is not None: # Remove the entity from the added casts so that it can dynamically # be re-added again. @@ -328,7 +333,7 @@ class CastDevice(MediaPlayerDevice): if old_cast_info.host_port == cast_info.host_port: # Nothing connection-related updated return - self._async_disconnect() + await self._async_disconnect() # Failed connection will unfortunately never raise an exception, it # will instead just try connecting indefinitely. @@ -348,22 +353,27 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Connection successful!") self.async_schedule_update_ha_state() - @callback - def _async_disconnect(self): + async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return - _LOGGER.debug("Disconnecting from previous chromecast socket.") + _LOGGER.debug("Disconnecting from chromecast socket.") self._available = False - self._chromecast.disconnect(blocking=False) + self.async_schedule_update_ha_state() + + await self.hass.async_add_job(self._chromecast.disconnect) + # Invalidate some attributes self._chromecast = None self.cast_status = None self.media_status = None self.media_status_received = None - self._status_listener.invalidate() - self._status_listener = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + self.async_schedule_update_ha_state() # ========== Callbacks ========== def new_cast_status(self, cast_status): diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 3e736ed719f..23c397053c5 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,7 +14,8 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['eliqonline==1.0.13'] +# pylint: disable=import-error, no-member +REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0a69f166b43..4014a719912 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 68 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/requirements_all.txt b/requirements_all.txt index 7cc644129b3..ff6e680051d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,9 +276,6 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 -# homeassistant.components.sensor.eliqonline -eliqonline==1.0.13 - # homeassistant.components.enocean enocean==0.40 diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 327283e74aa..2c888dd2dd2 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -42,13 +42,13 @@ async def test_update_device_config(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) orig_data = [ - { - 'id': 'sun', - }, - { - 'id': 'moon', - } - ] + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] def mock_read(path): """Mock reading data.""" @@ -81,3 +81,56 @@ async def test_update_device_config(hass, aiohttp_client): 'action': [], } assert written[0] == orig_data + + +async def test_bad_formatted_automations(hass, aiohttp_client): + """Test that we handle automations without ID.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + # No ID + 'action': { + 'event': 'hello' + } + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + # Verify ID added to orig_data + assert 'id' in orig_data[0] + + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index e284b026ad8..cdaf4200c97 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -286,3 +286,29 @@ async def test_unavailable_state_doesnt_sync(hass): 'devices': [] } } + + +async def test_empty_name_doesnt_sync(hass): + """Test that an entity with empty name does not sync over.""" + light = DemoLight( + None, ' ', + state=False, + ) + light.hass = hass + light.entity_id = 'light.demo_light' + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index d1ad232d279..7ae37becbd5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -11,7 +11,8 @@ from homeassistant.components.homekit.const import ( DEFAULT_PORT, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( - CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_IP_ADDRESS, CONF_PORT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce @@ -59,7 +60,7 @@ class TestHomeKit(unittest.TestCase): self.hass, DOMAIN, {DOMAIN: {}})) self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, ANY, {}), + call(self.hass, DEFAULT_PORT, None, ANY, {}), call().setup()]) # Test auto start enabled @@ -74,7 +75,8 @@ class TestHomeKit(unittest.TestCase): """Test async_setup with auto start disabled and test service calls.""" mock_homekit.return_value = homekit = Mock() - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) @@ -82,7 +84,7 @@ class TestHomeKit(unittest.TestCase): self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, ANY, {}), + call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) # Test start call with driver stopped. @@ -101,7 +103,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_setup(self): """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: @@ -117,9 +119,17 @@ class TestHomeKit(unittest.TestCase): self.assertEqual( self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + def test_homekit_setup_ip_address(self): + """Test setup with given IP address.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + homekit.setup() + mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) + def test_homekit_add_accessory(self): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) homekit.bridge = HomeBridge(self.hass) with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ @@ -142,7 +152,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_entity_filter(self): """Test the entity filter.""" entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, entity_filter, {}) + homekit = HomeKit(self.hass, None, None, entity_filter, {}) with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.return_value = None @@ -162,7 +172,7 @@ class TestHomeKit(unittest.TestCase): @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) homekit.bridge = HomeBridge(self.hass) homekit.driver = Mock() @@ -184,7 +194,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None) + homekit = HomeKit(None, None, None, None, None) homekit.driver = Mock() # Test if started = False diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 712cd17a7c7..8f5b52ea6de 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -237,7 +237,7 @@ async def test_lights(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) lamp_2 = hass.states.get('light.hue_lamp_2') assert lamp_2 is not None @@ -253,7 +253,7 @@ async def test_lights_color_mode(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) assert 'color_temp' not in lamp_1.attributes new_light1_on = LIGHT_1_ON.copy() @@ -650,37 +650,11 @@ def test_hs_color(): assert light.hs_color is None - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'hs', - 'hue': 1234, - 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', 'hue': 1234, 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'xy', - 'hue': None, - 'sat': 123, 'xy': [0.4, 0.5] }), request_bridge_update=None, diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index ee69ec1c85d..41cf6749b71 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -346,8 +346,16 @@ async def test_switched_host(hass: HomeAssistantType): async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) await hass.async_block_till_done() assert get_chromecast.call_count == 1 - chromecast.disconnect.assert_called_once_with(blocking=False) + assert chromecast.disconnect.call_count == 1 - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - chromecast.disconnect.assert_called_once_with(blocking=False) + +async def test_disconnect_on_stop(hass: HomeAssistantType): + """Test cast device disconnects socket on stop.""" + info = get_fake_chromecast_info() + + with patch('pychromecast.dial.get_device_status', return_value=info): + chromecast, _ = await async_setup_media_player_cast(hass, info) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert chromecast.disconnect.call_count == 1