From 7f5ca314ecf6e94cc5fb81d154237eb3f137191b Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 23 Feb 2018 23:33:12 +0100 Subject: [PATCH 01/29] Fix mclimate accounts with not only melissa components (#12427) * Fixes for mclimate accounts with not only melissa components * Fixes melissa sensor to only use HVAC * Bumping version to 1.0.3 and remove OP_MODE that is not supported * Removes STATE_AUTO from translation and tests --- homeassistant/components/climate/melissa.py | 15 ++++++--------- homeassistant/components/melissa.py | 2 +- homeassistant/components/sensor/melissa.py | 5 +++-- requirements_all.txt | 2 +- tests/components/climate/test_melissa.py | 4 +--- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py index 96bd66d05a5..9c005b62dcc 100644 --- a/homeassistant/components/climate/melissa.py +++ b/homeassistant/components/climate/melissa.py @@ -26,7 +26,7 @@ SUPPORT_FLAGS = (SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE) OP_MODES = [ - STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT + STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT ] FAN_MODES = [ @@ -42,8 +42,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): all_devices = [] for device in devices: - all_devices.append(MelissaClimate( - api, device['serial_number'], device)) + if device['type'] == 'melissa': + all_devices.append(MelissaClimate( + api, device['serial_number'], device)) add_devices(all_devices) @@ -199,9 +200,7 @@ class MelissaClimate(ClimateDevice): def melissa_op_to_hass(self, mode): """Translate Melissa modes to hass states.""" - if mode == self._api.MODE_AUTO: - return STATE_AUTO - elif mode == self._api.MODE_HEAT: + if mode == self._api.MODE_HEAT: return STATE_HEAT elif mode == self._api.MODE_COOL: return STATE_COOL @@ -228,9 +227,7 @@ class MelissaClimate(ClimateDevice): def hass_mode_to_melissa(self, mode): """Translate hass states to melissa modes.""" - if mode == STATE_AUTO: - return self._api.MODE_AUTO - elif mode == STATE_HEAT: + if mode == STATE_HEAT: return self._api.MODE_HEAT elif mode == STATE_COOL: return self._api.MODE_COOL diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa.py index ae82b96222e..f5a757dbcf3 100644 --- a/homeassistant/components/melissa.py +++ b/homeassistant/components/melissa.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ["py-melissa-climate==1.0.1"] +REQUIREMENTS = ["py-melissa-climate==1.0.6"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/melissa.py b/homeassistant/components/sensor/melissa.py index 58313428861..f67722b0198 100644 --- a/homeassistant/components/sensor/melissa.py +++ b/homeassistant/components/sensor/melissa.py @@ -22,8 +22,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = api.fetch_devices().values() for device in devices: - sensors.append(MelissaTemperatureSensor(device, api)) - sensors.append(MelissaHumiditySensor(device, api)) + if device['type'] == 'melissa': + sensors.append(MelissaTemperatureSensor(device, api)) + sensors.append(MelissaHumiditySensor(device, api)) add_devices(sensors) diff --git a/requirements_all.txt b/requirements_all.txt index 6742e842657..898df747067 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ py-canary==0.4.0 py-cpuinfo==3.3.0 # homeassistant.components.melissa -py-melissa-climate==1.0.1 +py-melissa-climate==1.0.6 # homeassistant.components.camera.synology py-synology==0.1.5 diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py index 446eec9aba1..5022c556b7d 100644 --- a/tests/components/climate/test_melissa.py +++ b/tests/components/climate/test_melissa.py @@ -122,7 +122,7 @@ class TestMelissa(unittest.TestCase): def test_operation_list(self): """Test the operation list.""" self.assertEqual( - [STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT], + [STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT], self.thermostat.operation_list ) @@ -226,7 +226,6 @@ class TestMelissa(unittest.TestCase): def test_melissa_op_to_hass(self): """Test for translate melissa operations to hass.""" - self.assertEqual(STATE_AUTO, self.thermostat.melissa_op_to_hass(0)) self.assertEqual(STATE_FAN_ONLY, self.thermostat.melissa_op_to_hass(1)) self.assertEqual(STATE_HEAT, self.thermostat.melissa_op_to_hass(2)) self.assertEqual(STATE_COOL, self.thermostat.melissa_op_to_hass(3)) @@ -245,7 +244,6 @@ class TestMelissa(unittest.TestCase): @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') def test_hass_mode_to_melissa(self, mocked_warning): """Test for hass operations to melssa.""" - self.assertEqual(0, self.thermostat.hass_mode_to_melissa(STATE_AUTO)) self.assertEqual( 1, self.thermostat.hass_mode_to_melissa(STATE_FAN_ONLY)) self.assertEqual(2, self.thermostat.hass_mode_to_melissa(STATE_HEAT)) From 2261ce30e3c010ce710c34fecc76eadf079e7b1b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 23 Feb 2018 18:31:22 +0100 Subject: [PATCH 02/29] Cast unique_id and async discovery (#12474) * Cast unique_id and async discovery * Lazily load chromecasts * Lint * Fixes & Improvements * Fixes * Improve disconnects cast.disconnect with blocking=False does **not** do I/O; it simply sets an event for the socket client looper * Add tests * Remove unnecessary calls * Lint * Fix use of hass object --- homeassistant/components/media_player/cast.py | 240 +++++++++++---- tests/components/media_player/test_cast.py | 279 +++++++++++++----- 2 files changed, 391 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index f011e86ecf9..a07ff74ccae 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -5,10 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ # pylint: disable=import-error +import asyncio import logging +import threading import voluptuous as vol +from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import (dispatcher_send, + async_dispatcher_connect) from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, @@ -16,7 +22,7 @@ from homeassistant.components.media_player import ( SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN) + STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -33,7 +39,13 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY -KNOWN_HOSTS_KEY = 'cast_known_hosts' +INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' +# UUID -> CastDevice mapping; cast devices without UUID are not stored +ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' +# Stores every discovered (host, port, uuid) +KNOWN_CHROMECASTS_KEY = 'cast_all_chromecasts' + +SIGNAL_CAST_DISCOVERED = 'cast_discovered' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, @@ -41,67 +53,144 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +def _setup_internal_discovery(hass: HomeAssistantType) -> None: + """Set up the pychromecast internal discovery.""" + hass.data.setdefault(INTERNAL_DISCOVERY_RUNNING_KEY, threading.Lock()) + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): + # Internal discovery is already running + return + + import pychromecast + + def internal_callback(name): + """Called when zeroconf has discovered a new chromecast.""" + mdns = listener.services[name] + ip_address, port, uuid, _, _ = mdns + key = (ip_address, port, uuid) + + if key in hass.data[KNOWN_CHROMECASTS_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", mdns) + return + + _LOGGER.debug("Discovered new chromecast %s", mdns) + try: + # pylint: disable=protected-access + chromecast = pychromecast._get_chromecast_from_host( + mdns, blocking=True) + except pychromecast.ChromecastConnectionError: + _LOGGER.debug("Can't set up cast with mDNS info %s. " + "Assuming it's not a Chromecast", mdns) + return + hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast + dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, chromecast) + + _LOGGER.debug("Starting internal pychromecast discovery.") + listener, browser = pychromecast.start_discovery(internal_callback) + + def stop_discovery(event): + """Stop discovery of new chromecasts.""" + pychromecast.stop_discovery(browser) + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) + + +@callback +def _async_create_cast_device(hass, chromecast): + """Create a CastDevice Entity from the chromecast object. + + Returns None if the cast device has already been added. Additionally, + automatically updates existing chromecast entities. + """ + if chromecast.uuid is None: + # Found a cast without UUID, we don't store it because we won't be able + # to update it anyway. + return CastDevice(chromecast) + + # Found a cast with UUID + added_casts = hass.data[ADDED_CAST_DEVICES_KEY] + old_cast_device = added_casts.get(chromecast.uuid) + if old_cast_device is None: + # -> New cast device + cast_device = CastDevice(chromecast) + added_casts[chromecast.uuid] = cast_device + return cast_device + + old_key = (old_cast_device.cast.host, + old_cast_device.cast.port, + old_cast_device.cast.uuid) + new_key = (chromecast.host, chromecast.port, chromecast.uuid) + + if old_key == new_key: + # Re-discovered with same data, ignore + return None + + # -> Cast device changed host + # Remove old pychromecast.Chromecast from global list, because it isn't + # valid anymore + old_cast_device.async_set_chromecast(chromecast) + return None + + +@asyncio.coroutine +def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the cast platform.""" import pychromecast # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) + hass.data.setdefault(ADDED_CAST_DEVICES_KEY, {}) + hass.data.setdefault(KNOWN_CHROMECASTS_KEY, {}) - known_hosts = hass.data.get(KNOWN_HOSTS_KEY) - if known_hosts is None: - known_hosts = hass.data[KNOWN_HOSTS_KEY] = [] - + # None -> use discovery; (host, port) -> manually specify chromecast. + want_host = None if discovery_info: - host = (discovery_info.get('host'), discovery_info.get('port')) - - if host in known_hosts: - return - - hosts = [host] - + want_host = (discovery_info.get('host'), discovery_info.get('port')) elif CONF_HOST in config: - host = (config.get(CONF_HOST), DEFAULT_PORT) + want_host = (config.get(CONF_HOST), DEFAULT_PORT) - if host in known_hosts: - return + enable_discovery = False + if want_host is None: + # We were explicitly told to enable pychromecast discovery. + enable_discovery = True + elif want_host[1] != DEFAULT_PORT: + # We're trying to add a group, so we have to use pychromecast's + # discovery to get the correct friendly name. + enable_discovery = True - hosts = [host] + if enable_discovery: + @callback + def async_cast_discovered(chromecast): + """Callback for when a new chromecast is discovered.""" + if want_host is not None and \ + (chromecast.host, chromecast.port) != want_host: + return # for groups, only add requested device + cast_device = _async_create_cast_device(hass, chromecast) + if cast_device is not None: + async_add_devices([cast_device]) + + async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + # Re-play the callback for all past chromecasts, store the objects in + # a list to avoid concurrent modification resulting in exception. + for chromecast in list(hass.data[KNOWN_CHROMECASTS_KEY].values()): + async_cast_discovered(chromecast) + + hass.async_add_job(_setup_internal_discovery, hass) else: - hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts() - if tuple(dev[:2]) not in known_hosts] - - casts = [] - - # get_chromecasts() returns Chromecast objects with the correct friendly - # name for grouped devices - all_chromecasts = pychromecast.get_chromecasts() - - for host in hosts: - (_, port) = host - found = [device for device in all_chromecasts - if (device.host, device.port) == host] - if found: - try: - casts.append(CastDevice(found[0])) - known_hosts.append(host) - except pychromecast.ChromecastConnectionError: - pass - - # do not add groups using pychromecast.Chromecast as it leads to names - # collision since pychromecast.Chromecast will get device name instead - # of group name - elif port == DEFAULT_PORT: - try: - # add the device anyway, get_chromecasts couldn't find it - casts.append(CastDevice(pychromecast.Chromecast(*host))) - known_hosts.append(host) - except pychromecast.ChromecastConnectionError: - pass - - add_devices(casts) + # Manually add a "normal" Chromecast, we can do that without discovery. + try: + chromecast = pychromecast.Chromecast(*want_host) + except pychromecast.ChromecastConnectionError: + _LOGGER.warning("Can't set up chromecast on %s", want_host[0]) + raise + key = (chromecast.host, chromecast.port, chromecast.uuid) + cast_device = _async_create_cast_device(hass, chromecast) + if cast_device is not None: + hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast + async_add_devices([cast_device]) class CastDevice(MediaPlayerDevice): @@ -109,16 +198,13 @@ class CastDevice(MediaPlayerDevice): def __init__(self, chromecast): """Initialize the Cast device.""" - self.cast = chromecast - - self.cast.socket_client.receiver_controller.register_status_listener( - self) - self.cast.socket_client.media_controller.register_status_listener(self) - - self.cast_status = self.cast.status - self.media_status = self.cast.media_controller.status + self.cast = None # type: pychromecast.Chromecast + self.cast_status = None + self.media_status = None self.media_status_received = None + self.async_set_chromecast(chromecast) + @property def should_poll(self): """No polling needed.""" @@ -325,3 +411,39 @@ class CastDevice(MediaPlayerDevice): self.media_status = status self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + if self.cast.uuid is not None: + return str(self.cast.uuid) + return None + + @callback + def async_set_chromecast(self, chromecast): + """Set the internal Chromecast object and disconnect the previous.""" + self._async_disconnect() + + self.cast = chromecast + + self.cast.socket_client.receiver_controller.register_status_listener( + self) + self.cast.socket_client.media_controller.register_status_listener(self) + + self.cast_status = self.cast.status + self.media_status = self.cast.media_controller.status + + @asyncio.coroutine + def async_will_remove_from_hass(self): + """Disconnect Chromecast object when removed.""" + self._async_disconnect() + + @callback + def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + if self.cast is None: + return + _LOGGER.debug("Disconnecting existing chromecast object") + old_key = (self.cast.host, self.cast.port, self.cast.uuid) + self.hass.data[KNOWN_CHROMECASTS_KEY].pop(old_key) + self.cast.disconnect(blocking=False) diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 0bcfc9b9a1a..6eeb9136b07 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,12 +1,15 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access -import unittest -from unittest.mock import patch, MagicMock +import asyncio +from typing import Optional +from unittest.mock import patch, MagicMock, Mock +from uuid import UUID import pytest +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.media_player import cast -from tests.common import get_test_home_assistant @pytest.fixture(autouse=True) @@ -18,83 +21,221 @@ def cast_mock(): yield -class FakeChromeCast(object): - """A fake Chrome Cast.""" - - def __init__(self, host, port): - """Initialize the fake Chrome Cast.""" - self.host = host - self.port = port +# pylint: disable=invalid-name +FakeUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e2') -class TestCastMediaPlayer(unittest.TestCase): - """Test the media_player module.""" +def get_fake_chromecast(host='192.168.178.42', port=8009, + uuid: Optional[UUID] = FakeUUID): + """Generate a Fake Chromecast object with the specified arguments.""" + return MagicMock(host=host, port=port, uuid=uuid) - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() +@asyncio.coroutine +def async_setup_cast(hass, config=None, discovery_info=None): + """Helper to setup the cast platform.""" + if config is None: + config = {} + add_devices = Mock() - @patch('homeassistant.components.media_player.cast.CastDevice') - @patch('pychromecast.get_chromecasts') - def test_filter_duplicates(self, mock_get_chromecasts, mock_device): - """Test filtering of duplicates.""" - mock_get_chromecasts.return_value = [ - FakeChromeCast('some_host', cast.DEFAULT_PORT) - ] + yield from cast.async_setup_platform(hass, config, add_devices, + discovery_info=discovery_info) + yield from hass.async_block_till_done() - # Test chromecasts as if they were hardcoded in configuration.yaml - cast.setup_platform(self.hass, { - 'host': 'some_host' - }, lambda _: _) + return add_devices - assert mock_device.called - mock_device.reset_mock() - assert not mock_device.called +@asyncio.coroutine +def async_setup_cast_internal_discovery(hass, config=None, + discovery_info=None, + no_from_host_patch=False): + """Setup the cast platform and the discovery.""" + listener = MagicMock(services={}) - # Test chromecasts as if they were automatically discovered - cast.setup_platform(self.hass, {}, lambda _: _, { - 'host': 'some_host', - 'port': cast.DEFAULT_PORT, - }) - assert not mock_device.called + with patch('pychromecast.start_discovery', + return_value=(listener, None)) as start_discovery: + add_devices = yield from async_setup_cast(hass, config, discovery_info) + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() - @patch('homeassistant.components.media_player.cast.CastDevice') - @patch('pychromecast.get_chromecasts') - @patch('pychromecast.Chromecast') - def test_fallback_cast(self, mock_chromecast, mock_get_chromecasts, - mock_device): - """Test falling back to creating Chromecast when not discovered.""" - mock_get_chromecasts.return_value = [ - FakeChromeCast('some_host', cast.DEFAULT_PORT) - ] + assert start_discovery.call_count == 1 - # Test chromecasts as if they were hardcoded in configuration.yaml - cast.setup_platform(self.hass, { - 'host': 'some_other_host' - }, lambda _: _) + discovery_callback = start_discovery.call_args[0][0] - assert mock_chromecast.called - assert mock_device.called + def discover_chromecast(service_name, chromecast): + """Discover a chromecast device.""" + listener.services[service_name] = ( + chromecast.host, chromecast.port, chromecast.uuid, None, None) + if no_from_host_patch: + discovery_callback(service_name) + else: + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast): + discovery_callback(service_name) - @patch('homeassistant.components.media_player.cast.CastDevice') - @patch('pychromecast.get_chromecasts') - @patch('pychromecast.Chromecast') - def test_fallback_cast_group(self, mock_chromecast, mock_get_chromecasts, - mock_device): - """Test not creating Cast Group when not discovered.""" - mock_get_chromecasts.return_value = [ - FakeChromeCast('some_host', cast.DEFAULT_PORT) - ] + return discover_chromecast, add_devices - # Test chromecasts as if they were automatically discovered - cast.setup_platform(self.hass, {}, lambda _: _, { - 'host': 'some_other_host', - 'port': 43546, - }) - assert not mock_chromecast.called - assert not mock_device.called + +@asyncio.coroutine +def test_start_discovery_called_once(hass): + """Test pychromecast.start_discovery called exactly once.""" + with patch('pychromecast.start_discovery', + return_value=(None, None)) as start_discovery: + yield from async_setup_cast(hass) + + assert start_discovery.call_count == 1 + + yield from async_setup_cast(hass) + assert start_discovery.call_count == 1 + + +@asyncio.coroutine +def test_stop_discovery_called_on_stop(hass): + """Test pychromecast.stop_discovery called on shutdown.""" + with patch('pychromecast.start_discovery', + return_value=(None, 'the-browser')) as start_discovery: + yield from async_setup_cast(hass) + + assert start_discovery.call_count == 1 + + with patch('pychromecast.stop_discovery') as stop_discovery: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + yield from hass.async_block_till_done() + + stop_discovery.assert_called_once_with('the-browser') + + with patch('pychromecast.start_discovery', + return_value=(None, 'the-browser')) as start_discovery: + yield from async_setup_cast(hass) + + assert start_discovery.call_count == 1 + + +@asyncio.coroutine +def test_internal_discovery_callback_only_generates_once(hass): + """Test _get_chromecast_from_host only called once per device.""" + discover_cast, _ = yield from async_setup_cast_internal_discovery( + hass, no_from_host_patch=True) + chromecast = get_fake_chromecast() + + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast) as gen_chromecast: + discover_cast('the-service', chromecast) + mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None) + gen_chromecast.assert_called_once_with(mdns, blocking=True) + + discover_cast('the-service', chromecast) + gen_chromecast.reset_mock() + assert gen_chromecast.call_count == 0 + + +@asyncio.coroutine +def test_internal_discovery_callback_calls_dispatcher(hass): + """Test internal discovery calls dispatcher.""" + discover_cast, _ = yield from async_setup_cast_internal_discovery(hass) + chromecast = get_fake_chromecast() + + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast): + signal = MagicMock() + + async_dispatcher_connect(hass, 'cast_discovered', signal) + discover_cast('the-service', chromecast) + yield from hass.async_block_till_done() + + signal.assert_called_once_with(chromecast) + + +@asyncio.coroutine +def test_internal_discovery_callback_with_connection_error(hass): + """Test internal discovery not calling dispatcher on ConnectionError.""" + import pychromecast # imports mock pychromecast + + pychromecast.ChromecastConnectionError = IOError + + discover_cast, _ = yield from async_setup_cast_internal_discovery( + hass, no_from_host_patch=True) + chromecast = get_fake_chromecast() + + with patch('pychromecast._get_chromecast_from_host', + side_effect=pychromecast.ChromecastConnectionError): + signal = MagicMock() + + async_dispatcher_connect(hass, 'cast_discovered', signal) + discover_cast('the-service', chromecast) + yield from hass.async_block_till_done() + + assert signal.call_count == 0 + + +def test_create_cast_device_without_uuid(hass): + """Test create a cast device without a UUID.""" + chromecast = get_fake_chromecast(uuid=None) + cast_device = cast._async_create_cast_device(hass, chromecast) + assert cast_device is not None + + +def test_create_cast_device_with_uuid(hass): + """Test create cast devices with UUID.""" + added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} + chromecast = get_fake_chromecast() + cast_device = cast._async_create_cast_device(hass, chromecast) + assert cast_device is not None + assert chromecast.uuid in added_casts + + with patch.object(cast_device, 'async_set_chromecast') as mock_set: + assert cast._async_create_cast_device(hass, chromecast) is None + assert mock_set.call_count == 0 + + chromecast = get_fake_chromecast(host='192.168.178.1') + assert cast._async_create_cast_device(hass, chromecast) is None + assert mock_set.call_count == 1 + mock_set.assert_called_once_with(chromecast) + + +@asyncio.coroutine +def test_normal_chromecast_not_starting_discovery(hass): + """Test cast platform not starting discovery when not required.""" + chromecast = get_fake_chromecast() + + with patch('pychromecast.Chromecast', return_value=chromecast): + add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + assert add_devices.call_count == 1 + + # Same entity twice + add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + assert add_devices.call_count == 0 + + hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} + add_devices = yield from async_setup_cast( + hass, discovery_info={'host': 'host1', 'port': 8009}) + assert add_devices.call_count == 1 + + hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} + add_devices = yield from async_setup_cast( + hass, discovery_info={'host': 'host1', 'port': 42}) + assert add_devices.call_count == 0 + + +@asyncio.coroutine +def test_replay_past_chromecasts(hass): + """Test cast platform re-playing past chromecasts when adding new one.""" + cast_group1 = get_fake_chromecast(host='host1', port=42) + cast_group2 = get_fake_chromecast(host='host2', port=42, uuid=UUID( + '9462202c-e747-4af5-a66b-7dce0e1ebc09')) + + discover_cast, add_dev1 = yield from async_setup_cast_internal_discovery( + hass, discovery_info={'host': 'host1', 'port': 42}) + discover_cast('service2', cast_group2) + yield from hass.async_block_till_done() + assert add_dev1.call_count == 0 + + discover_cast('service1', cast_group1) + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() # having jobs that add jobs + assert add_dev1.call_count == 1 + + add_dev2 = yield from async_setup_cast( + hass, discovery_info={'host': 'host2', 'port': 42}) + yield from hass.async_block_till_done() + assert add_dev2.call_count == 1 From 6aa89166541d4cc5d65716869634fda565a5ed7e Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Fri, 23 Feb 2018 13:03:00 +0100 Subject: [PATCH 03/29] Add Tahoma scenes (#12498) * add scenes to platform * add scene.tahoma * requires tahoma-api 0.0.12 * update requirements_all.txt * hound * fix pylint error --- homeassistant/components/scene/tahoma.py | 48 ++++++++++++++++++++++++ homeassistant/components/tahoma.py | 11 ++++-- requirements_all.txt | 2 +- 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/scene/tahoma.py diff --git a/homeassistant/components/scene/tahoma.py b/homeassistant/components/scene/tahoma.py new file mode 100644 index 00000000000..39206623901 --- /dev/null +++ b/homeassistant/components/scene/tahoma.py @@ -0,0 +1,48 @@ +""" +Support for Tahoma scenes. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/scene.tahoma/ +""" +import logging + +from homeassistant.components.scene import Scene +from homeassistant.components.tahoma import ( + DOMAIN as TAHOMA_DOMAIN) + +DEPENDENCIES = ['tahoma'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tahoma scenes.""" + controller = hass.data[TAHOMA_DOMAIN]['controller'] + scenes = [] + for scene in hass.data[TAHOMA_DOMAIN]['scenes']: + scenes.append(TahomaScene(scene, controller)) + add_devices(scenes, True) + + +class TahomaScene(Scene): + """Representation of a Tahoma scene entity.""" + + def __init__(self, tahoma_scene, controller): + """Initialize the scene.""" + self.tahoma_scene = tahoma_scene + self.controller = controller + self._name = self.tahoma_scene.name + + def activate(self): + """Activate the scene.""" + self.controller.launch_action_group(self.tahoma_scene.oid) + + @property + def name(self): + """Return the name of the scene.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the scene.""" + return {'tahoma_scene_oid': self.tahoma_scene.oid} diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 00ebc78a40b..b288a704d74 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['tahoma-api==0.0.11'] +REQUIREMENTS = ['tahoma-api==0.0.12'] _LOGGER = logging.getLogger(__name__) @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) TAHOMA_COMPONENTS = [ - 'sensor', 'cover' + 'scene', 'sensor', 'cover' ] TAHOMA_TYPES = { @@ -63,13 +63,15 @@ def setup(hass, config): try: api.get_setup() devices = api.get_devices() + scenes = api.get_action_groups() except RequestException: _LOGGER.exception("Error when getting devices from the Tahoma API") return False hass.data[DOMAIN] = { 'controller': api, - 'devices': defaultdict(list) + 'devices': defaultdict(list), + 'scenes': [] } for device in devices: @@ -82,6 +84,9 @@ def setup(hass, config): continue hass.data[DOMAIN]['devices'][device_type].append(_device) + for scene in scenes: + hass.data[DOMAIN]['scenes'].append(scene) + for component in TAHOMA_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) diff --git a/requirements_all.txt b/requirements_all.txt index 898df747067..7de7131b562 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1159,7 +1159,7 @@ steamodd==4.21 suds-py3==1.3.3.0 # homeassistant.components.tahoma -tahoma-api==0.0.11 +tahoma-api==0.0.12 # homeassistant.components.sensor.tank_utility tank_utility==1.4.0 From 9ca67c36cded80f86208da5b4ddff282cc0756d9 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 23 Feb 2018 00:40:58 +0100 Subject: [PATCH 04/29] Optimize logbook SQL query (#12608) --- homeassistant/components/logbook.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index e6e447884cb..1fc6d1587fc 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -47,6 +47,11 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +ALL_EVENT_TYPES = [ + EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +] + GROUP_BY_MINUTES = 15 CONTINUOUS_DOMAINS = ['proximity', 'sensor'] @@ -266,15 +271,18 @@ def humanify(events): 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.models import Events, States from homeassistant.components.recorder.util import ( execute, session_scope) with session_scope(hass=hass) as session: - query = session.query(Events).order_by( - Events.time_fired).filter( - (Events.time_fired > start_day) & - (Events.time_fired < end_day)) + query = session.query(Events).order_by(Events.time_fired) \ + .outerjoin(States, (Events.event_id == States.event_id)) \ + .filter(Events.event_type.in_(ALL_EVENT_TYPES)) \ + .filter((Events.time_fired > start_day) + & (Events.time_fired < end_day)) \ + .filter((States.last_updated == States.last_changed) + | (States.last_updated.is_(None))) events = execute(query) return humanify(_exclude_events(events, config)) From 7c80ef714e7ca8cbf674f595e3981e6d3bbba34e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Feb 2018 23:29:42 -0800 Subject: [PATCH 05/29] Fix voluptuous breaking change things (#12611) * Fix voluptuous breaking change things * Change xiaomi aqara back --- homeassistant/components/binary_sensor/knx.py | 2 +- .../components/media_player/braviatv_psk.py | 2 +- tests/helpers/test_config_validation.py | 14 +++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index d63a5a5b400..2b33d6850d6 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -35,7 +35,7 @@ DEPENDENCIES = ['knx'] AUTOMATION_SCHEMA = vol.Schema({ vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string, vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port, - vol.Required(CONF_ACTION, default=None): cv.SCRIPT_SCHEMA + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA }) AUTOMATIONS_SCHEMA = vol.All( diff --git a/homeassistant/components/media_player/braviatv_psk.py b/homeassistant/components/media_player/braviatv_psk.py index c78951e91b7..122eb3b9739 100755 --- a/homeassistant/components/media_player/braviatv_psk.py +++ b/homeassistant/components/media_player/braviatv_psk.py @@ -42,7 +42,7 @@ TV_NO_INFO = 'No info: TV resumed after pause' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PSK): cv.string, - vol.Optional(CONF_MAC, default=None): cv.string, + vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_AMP, default=False): cv.boolean, vol.Optional(CONF_ANDROID, default=True): cv.boolean, diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 26262f50ac4..66f0597fc93 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -524,18 +524,14 @@ def test_enum(): def test_socket_timeout(): # pylint: disable=invalid-name """Test socket timeout validator.""" - TEST_CONF_TIMEOUT = 'timeout' # pylint: disable=invalid-name - - schema = vol.Schema( - {vol.Required(TEST_CONF_TIMEOUT, default=None): cv.socket_timeout}) + schema = vol.Schema(cv.socket_timeout) with pytest.raises(vol.Invalid): - schema({TEST_CONF_TIMEOUT: 0.0}) + schema(0.0) with pytest.raises(vol.Invalid): - schema({TEST_CONF_TIMEOUT: -1}) + schema(-1) - assert _GLOBAL_DEFAULT_TIMEOUT == schema({TEST_CONF_TIMEOUT: - None})[TEST_CONF_TIMEOUT] + assert _GLOBAL_DEFAULT_TIMEOUT == schema(None) - assert schema({TEST_CONF_TIMEOUT: 1})[TEST_CONF_TIMEOUT] == 1.0 + assert schema(1) == 1.0 From 19d34daef078e9f45a0750ecce5feecc4cc764d2 Mon Sep 17 00:00:00 2001 From: Scott Bradshaw Date: Fri, 23 Feb 2018 00:53:08 -0500 Subject: [PATCH 06/29] OpenGarage - correctly handle offline status (#12612) (#12613) --- homeassistant/components/cover/opengarage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index 38fbaf0acdb..d68021d7db3 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -115,7 +115,7 @@ class OpenGarageCover(CoverDevice): @property def is_closed(self): """Return if the cover is closed.""" - if self._state == STATE_UNKNOWN: + if self._state in [STATE_UNKNOWN, STATE_OFFLINE]: return None return self._state in [STATE_CLOSED, STATE_OPENING] From 43ad3ae2d43b7544e3847c0c19359f227d9c930d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 23 Feb 2018 10:12:40 +0100 Subject: [PATCH 07/29] Move recorder query out of event loop (#12615) --- homeassistant/components/recorder/__init__.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 53be6f33837..bffe29ec59b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -256,28 +256,6 @@ class Recorder(threading.Thread): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, notify_hass_started) - if self.keep_days and self.purge_interval: - @callback - def async_purge(now): - """Trigger the purge and schedule the next run.""" - self.queue.put( - PurgeTask(self.keep_days, repack=not self.did_vacuum)) - self.hass.helpers.event.async_track_point_in_time( - async_purge, now + timedelta(days=self.purge_interval)) - - earliest = dt_util.utcnow() + timedelta(minutes=30) - run = latest = dt_util.utcnow() + \ - timedelta(days=self.purge_interval) - with session_scope(session=self.get_session()) as session: - event = session.query(Events).first() - if event is not None: - session.expunge(event) - run = dt_util.as_utc(event.time_fired) + \ - timedelta(days=self.keep_days+self.purge_interval) - run = min(latest, max(run, earliest)) - self.hass.helpers.event.async_track_point_in_time( - async_purge, run) - self.hass.add_job(register) result = hass_started.result() @@ -285,6 +263,29 @@ class Recorder(threading.Thread): if result is shutdown_task: return + # Start periodic purge + if self.keep_days and self.purge_interval: + @callback + def async_purge(now): + """Trigger the purge and schedule the next run.""" + self.queue.put( + PurgeTask(self.keep_days, repack=not self.did_vacuum)) + self.hass.helpers.event.async_track_point_in_time( + async_purge, now + timedelta(days=self.purge_interval)) + + earliest = dt_util.utcnow() + timedelta(minutes=30) + run = latest = dt_util.utcnow() + \ + timedelta(days=self.purge_interval) + with session_scope(session=self.get_session()) as session: + event = session.query(Events).first() + if event is not None: + session.expunge(event) + run = dt_util.as_utc(event.time_fired) + timedelta( + days=self.keep_days+self.purge_interval) + run = min(latest, max(run, earliest)) + + self.hass.helpers.event.track_point_in_time(async_purge, run) + while True: event = self.queue.get() From 286baed9ad0763e8c0acd9022726697f6242af2a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 23 Feb 2018 19:13:04 +0100 Subject: [PATCH 08/29] Hassio update timeout filter list (#12617) * Update timeout filter list * Update http.py --- homeassistant/components/hassio/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index d94826653e8..9dd6427ec38 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -31,6 +31,8 @@ NO_TIMEOUT = { re.compile(r'^addons/[^/]*/rebuild$'), re.compile(r'^snapshots/.*/full$'), re.compile(r'^snapshots/.*/partial$'), + re.compile(r'^snapshots/[^/]*/upload$'), + re.compile(r'^snapshots/[^/]*/download$'), } NO_AUTH = { From 781b7687a4b12820590c5b2d3e7fcf590eba03af Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 23 Feb 2018 19:11:54 +0100 Subject: [PATCH 09/29] The name of the enum must be used here because of the speed_list. (#12625) The fan.set_speed example value is lower-case and led to confusion. Both spellings are possible now: Idle & idle --- homeassistant/components/fan/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 264962b9d56..b9bc54b5c79 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -302,7 +302,7 @@ class XiaomiAirPurifier(FanEntity): yield from self._try_command( "Setting operation mode of the air purifier failed.", - self._air_purifier.set_mode, OperationMode(speed)) + self._air_purifier.set_mode, OperationMode[speed.title()]) @asyncio.coroutine def async_set_buzzer_on(self): From 8d0d676ff2ecef614ac3c07538a864c7372beea5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 24 Feb 2018 00:13:48 +0100 Subject: [PATCH 10/29] Fix cast doing I/O in event loop (#12632) --- homeassistant/components/media_player/cast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index a07ff74ccae..40e09ea328c 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -182,7 +182,8 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, else: # Manually add a "normal" Chromecast, we can do that without discovery. try: - chromecast = pychromecast.Chromecast(*want_host) + chromecast = yield from hass.async_add_job( + pychromecast.Chromecast, *want_host) except pychromecast.ChromecastConnectionError: _LOGGER.warning("Can't set up chromecast on %s", want_host[0]) raise From 6c614df96e46e7b461dfff9c7c16fa33b5157280 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Feb 2018 11:50:06 -0800 Subject: [PATCH 11/29] Remove braviatv_psk (#12669) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/media_player/braviatv_psk.py | 363 ------------------ requirements_all.txt | 3 - 4 files changed, 368 deletions(-) delete mode 100755 homeassistant/components/media_player/braviatv_psk.py diff --git a/.coveragerc b/.coveragerc index a1022dcb42e..bd99e3ac2e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -432,7 +432,6 @@ omit = homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/braviatv.py - homeassistant/components/media_player/braviatv_psk.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/clementine.py homeassistant/components/media_player/cmus.py diff --git a/CODEOWNERS b/CODEOWNERS index f3ddfc3b3e6..a5b5cfcb32c 100755 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,7 +54,6 @@ homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti -homeassistant/components/media_player/braviatv_psk.py @gerard33 homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko diff --git a/homeassistant/components/media_player/braviatv_psk.py b/homeassistant/components/media_player/braviatv_psk.py deleted file mode 100755 index 122eb3b9739..00000000000 --- a/homeassistant/components/media_player/braviatv_psk.py +++ /dev/null @@ -1,363 +0,0 @@ -""" -Support for interface with a Sony Bravia TV. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.braviatv_psk/ -""" -import logging -import voluptuous as vol - -from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, - SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, - PLATFORM_SCHEMA, MEDIA_TYPE_TVSHOW, SUPPORT_STOP) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_MAC, STATE_OFF, STATE_ON) -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['pySonyBraviaPSK==0.1.5'] - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_STOP - -DEFAULT_NAME = 'Sony Bravia TV' - -# Config file -CONF_PSK = 'psk' -CONF_AMP = 'amp' -CONF_ANDROID = 'android' -CONF_SOURCE_FILTER = 'sourcefilter' - -# Some additional info to show specific for Sony Bravia TV -TV_WAIT = 'TV started, waiting for program info' -TV_APP_OPENED = 'App opened' -TV_NO_INFO = 'No info: TV resumed after pause' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PSK): cv.string, - vol.Optional(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_AMP, default=False): cv.boolean, - vol.Optional(CONF_ANDROID, default=True): cv.boolean, - vol.Optional(CONF_SOURCE_FILTER, default=[]): vol.All( - cv.ensure_list, [cv.string])}) - -# pylint: disable=unused-argument - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Sony Bravia TV platform.""" - host = config.get(CONF_HOST) - psk = config.get(CONF_PSK) - mac = config.get(CONF_MAC) - name = config.get(CONF_NAME) - amp = config.get(CONF_AMP) - android = config.get(CONF_ANDROID) - source_filter = config.get(CONF_SOURCE_FILTER) - - if host is None or psk is None: - _LOGGER.error( - "No TV IP address or Pre-Shared Key found in configuration file") - return - - add_devices( - [BraviaTVDevice(host, psk, mac, name, amp, android, source_filter)]) - - -class BraviaTVDevice(MediaPlayerDevice): - """Representation of a Sony Bravia TV.""" - - def __init__(self, host, psk, mac, name, amp, android, source_filter): - """Initialize the Sony Bravia device.""" - _LOGGER.info("Setting up Sony Bravia TV") - from braviapsk import sony_bravia_psk - - self._braviarc = sony_bravia_psk.BraviaRC(host, psk, mac) - self._name = name - self._amp = amp - self._android = android - self._source_filter = source_filter - self._state = STATE_OFF - self._muted = False - self._program_name = None - self._channel_name = None - self._channel_number = None - self._source = None - self._source_list = [] - self._original_content_list = [] - self._content_mapping = {} - self._duration = None - self._content_uri = None - self._id = None - self._playing = False - self._start_date_time = None - self._program_media_type = None - self._min_volume = None - self._max_volume = None - self._volume = None - self._start_time = None - self._end_time = None - - _LOGGER.debug( - "Set up Sony Bravia TV with IP: %s, PSK: %s, MAC: %s", host, psk, - mac) - - self.update() - - def update(self): - """Update TV info.""" - try: - power_status = self._braviarc.get_power_status() - if power_status == 'active': - self._state = STATE_ON - self._refresh_volume() - self._refresh_channels() - playing_info = self._braviarc.get_playing_info() - self._reset_playing_info() - if playing_info is None or not playing_info: - self._program_name = TV_NO_INFO - else: - self._program_name = playing_info.get('programTitle') - self._channel_name = playing_info.get('title') - self._program_media_type = playing_info.get( - 'programMediaType') - self._channel_number = playing_info.get('dispNum') - self._source = playing_info.get('source') - self._content_uri = playing_info.get('uri') - self._duration = playing_info.get('durationSec') - self._start_date_time = playing_info.get('startDateTime') - # Get time info from TV program - if self._start_date_time is not None and \ - self._duration is not None: - time_info = self._braviarc.playing_time( - self._start_date_time, self._duration) - self._start_time = time_info.get('start_time') - self._end_time = time_info.get('end_time') - else: - if self._program_name == TV_WAIT: - # TV is starting up, takes some time before it responds - _LOGGER.info("TV is starting, no info available yet") - else: - self._state = STATE_OFF - - except Exception as exception_instance: # pylint: disable=broad-except - _LOGGER.error( - "No data received from TV. Error message: %s", - exception_instance) - self._state = STATE_OFF - - def _reset_playing_info(self): - self._program_name = None - self._channel_name = None - self._program_media_type = None - self._channel_number = None - self._source = None - self._content_uri = None - self._duration = None - self._start_date_time = None - self._start_time = None - self._end_time = None - - def _refresh_volume(self): - """Refresh volume information.""" - volume_info = self._braviarc.get_volume_info() - if volume_info is not None: - self._volume = volume_info.get('volume') - self._min_volume = volume_info.get('minVolume') - self._max_volume = volume_info.get('maxVolume') - self._muted = volume_info.get('mute') - - def _refresh_channels(self): - if not self._source_list: - self._content_mapping = self._braviarc.load_source_list() - self._source_list = [] - if not self._source_filter: # list is empty - for key in self._content_mapping: - self._source_list.append(key) - else: - filtered_dict = {title: uri for (title, uri) in - self._content_mapping.items() - if any(filter_title in title for filter_title - in self._source_filter)} - for key in filtered_dict: - self._source_list.append(key) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def source(self): - """Return the current input source.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - if self._volume is not None: - return self._volume / 100 - return None - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def supported_features(self): - """Flag media player features that are supported.""" - supported = SUPPORT_BRAVIA - # Remove volume slider if amplifier is attached to TV - if self._amp: - supported = supported ^ SUPPORT_VOLUME_SET - return supported - - @property - def media_content_type(self): - """Content type of current playing media. - - Used for program information below the channel in the state card. - """ - return MEDIA_TYPE_TVSHOW - - @property - def media_title(self): - """Title of current playing media. - - Used to show TV channel info. - """ - return_value = None - if self._channel_name is not None: - if self._channel_number is not None: - return_value = '{0!s}: {1}'.format( - self._channel_number.lstrip('0'), self._channel_name) - else: - return_value = self._channel_name - return return_value - - @property - def media_series_title(self): - """Title of series of current playing media, TV show only. - - Used to show TV program info. - """ - return_value = None - if self._program_name is not None: - if self._start_time is not None and self._end_time is not None: - return_value = '{0} [{1} - {2}]'.format( - self._program_name, self._start_time, self._end_time) - else: - return_value = self._program_name - else: - if not self._channel_name: # This is empty when app is opened - return_value = TV_APP_OPENED - return return_value - - @property - def media_content_id(self): - """Content ID of current playing media.""" - return self._channel_name - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self._braviarc.set_volume_level(volume) - - def turn_on(self): - """Turn the media player on. - - Use a different command for Android as WOL is not working. - """ - if self._android: - self._braviarc.turn_on_command() - else: - self._braviarc.turn_on() - - # Show that TV is starting while it takes time - # before program info is available - self._reset_playing_info() - self._state = STATE_ON - self._program_name = TV_WAIT - - def turn_off(self): - """Turn off media player.""" - self._state = STATE_OFF - self._braviarc.turn_off() - - def volume_up(self): - """Volume up the media player.""" - self._braviarc.volume_up() - - def volume_down(self): - """Volume down media player.""" - self._braviarc.volume_down() - - def mute_volume(self, mute): - """Send mute command.""" - self._braviarc.mute_volume() - - def select_source(self, source): - """Set the input source.""" - if source in self._content_mapping: - uri = self._content_mapping[source] - self._braviarc.play_content(uri) - - def media_play_pause(self): - """Simulate play pause media player.""" - if self._playing: - self.media_pause() - else: - self.media_play() - - def media_play(self): - """Send play command.""" - self._playing = True - self._braviarc.media_play() - - def media_pause(self): - """Send media pause command to media player. - - Will pause TV when TV tuner is on. - """ - self._playing = False - if self._program_media_type == 'tv' or self._program_name is not None: - self._braviarc.media_tvpause() - else: - self._braviarc.media_pause() - - def media_next_track(self): - """Send next track command. - - Will switch to next channel when TV tuner is on. - """ - if self._program_media_type == 'tv' or self._program_name is not None: - self._braviarc.send_command('ChannelUp') - else: - self._braviarc.media_next_track() - - def media_previous_track(self): - """Send the previous track command. - - Will switch to previous channel when TV tuner is on. - """ - if self._program_media_type == 'tv' or self._program_name is not None: - self._braviarc.send_command('ChannelDown') - else: - self._braviarc.media_previous_track() diff --git a/requirements_all.txt b/requirements_all.txt index 7de7131b562..7deac085466 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,9 +643,6 @@ pyHS100==0.3.0 # homeassistant.components.rfxtrx pyRFXtrx==0.21.1 -# homeassistant.components.media_player.braviatv_psk -pySonyBraviaPSK==0.1.5 - # homeassistant.components.sensor.tibber pyTibber==0.2.1 From 84c156e8f584e1e4c334e08b0cd76a4fe971af21 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Feb 2018 13:17:47 -0800 Subject: [PATCH 12/29] Version bump to 0.64 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 10c29d19107..a067e14d344 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 64 -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 a783006f925184cc2ad8a42e8be55401b2d448f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Feb 2018 22:23:53 -0800 Subject: [PATCH 13/29] Version bump to 0.64.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a067e14d344..82ebadd80f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 64 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From f7fc4c6f15d67c31b526ab0c377ef17bdf2a84da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Feb 2018 22:20:58 -0800 Subject: [PATCH 14/29] Frontend bump to 20180227.0 --- 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 2a9a7a8a38a..3863a4d390b 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==20180221.1', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180227.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 7deac085466..5213c9ecff0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180221.1 +home-assistant-frontend==20180227.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 1e443e3ad00..9fa6e7f5cde 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ hbmqtt==0.9.1 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180221.1 +home-assistant-frontend==20180227.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e14893416fb98d3ef9a37d816e0ee719105b33a1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 24 Feb 2018 19:27:44 +0100 Subject: [PATCH 15/29] Cast automatically drop connection (#12635) --- homeassistant/components/media_player/cast.py | 17 +++++++++++------ tests/components/media_player/test_cast.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 40e09ea328c..d3cf2f7b501 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -8,9 +8,11 @@ https://home-assistant.io/components/media_player.cast/ import asyncio import logging import threading +import functools import voluptuous as vol +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.core import callback from homeassistant.helpers.dispatcher import (dispatcher_send, @@ -34,6 +36,7 @@ CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' DEFAULT_PORT = 8009 +SOCKET_CLIENT_RETRIES = 10 SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ @@ -76,7 +79,7 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: try: # pylint: disable=protected-access chromecast = pychromecast._get_chromecast_from_host( - mdns, blocking=True) + mdns, blocking=True, tries=SOCKET_CLIENT_RETRIES) except pychromecast.ChromecastConnectionError: _LOGGER.debug("Can't set up cast with mDNS info %s. " "Assuming it's not a Chromecast", mdns) @@ -182,11 +185,13 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, else: # Manually add a "normal" Chromecast, we can do that without discovery. try: - chromecast = yield from hass.async_add_job( - pychromecast.Chromecast, *want_host) - except pychromecast.ChromecastConnectionError: - _LOGGER.warning("Can't set up chromecast on %s", want_host[0]) - raise + func = functools.partial(pychromecast.Chromecast, *want_host, + tries=SOCKET_CLIENT_RETRIES) + chromecast = yield from hass.async_add_job(func) + except pychromecast.ChromecastConnectionError as err: + _LOGGER.warning("Can't set up chromecast on %s: %s", + want_host[0], err) + raise PlatformNotReady key = (chromecast.host, chromecast.port, chromecast.uuid) cast_device = _async_create_cast_device(hass, chromecast) if cast_device is not None: diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 6eeb9136b07..aaaad47d8dc 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -7,6 +7,7 @@ from uuid import UUID import pytest +from homeassistant.exceptions import PlatformNotReady from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.media_player import cast @@ -122,7 +123,7 @@ def test_internal_discovery_callback_only_generates_once(hass): return_value=chromecast) as gen_chromecast: discover_cast('the-service', chromecast) mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None) - gen_chromecast.assert_called_once_with(mdns, blocking=True) + gen_chromecast.assert_called_once_with(mdns, blocking=True, tries=10) discover_cast('the-service', chromecast) gen_chromecast.reset_mock() @@ -196,6 +197,10 @@ def test_create_cast_device_with_uuid(hass): @asyncio.coroutine def test_normal_chromecast_not_starting_discovery(hass): """Test cast platform not starting discovery when not required.""" + import pychromecast # imports mock pychromecast + + pychromecast.ChromecastConnectionError = IOError + chromecast = get_fake_chromecast() with patch('pychromecast.Chromecast', return_value=chromecast): @@ -216,6 +221,11 @@ def test_normal_chromecast_not_starting_discovery(hass): hass, discovery_info={'host': 'host1', 'port': 42}) assert add_devices.call_count == 0 + with patch('pychromecast.Chromecast', + side_effect=pychromecast.ChromecastConnectionError): + with pytest.raises(PlatformNotReady): + yield from async_setup_cast(hass, {'host': 'host3'}) + @asyncio.coroutine def test_replay_past_chromecasts(hass): From 68f92d2e7c3f0196a958a6cc38fedc142552180c Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 26 Feb 2018 09:43:26 +0200 Subject: [PATCH 16/29] Roomba timeout (#12645) * Roomba timeout * PlatformNotReady --- homeassistant/components/vacuum/roomba.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index 6485f0025e2..b983b20bd0c 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -8,12 +8,15 @@ import asyncio import logging import voluptuous as vol +import async_timeout + from homeassistant.components.vacuum import ( VacuumDevice, PLATFORM_SCHEMA, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -90,7 +93,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): ) _LOGGER.info("Initializing communication with host %s (username: %s)", host, username) - yield from hass.async_add_job(roomba.connect) + + try: + with async_timeout.timeout(9): + yield from hass.async_add_job(roomba.connect) + except asyncio.TimeoutError: + raise PlatformNotReady + roomba_vac = RoombaVacuum(name, roomba) hass.data[PLATFORM][host] = roomba_vac From 0a067f4cc7d3d366e7529e0cc66d9327e0df8f02 Mon Sep 17 00:00:00 2001 From: Lev Aronsky Date: Tue, 27 Feb 2018 08:31:47 +0200 Subject: [PATCH 17/29] Fix a problem with calling `deconz.close` (#12657) * Fix a problem with calling `deconz.close` The event object (`EVENT_HOMEASSISTANT_STOP`) is sent as an argument to the callable passed to `async_listen_once`. However, `deconz.close` is a bound method that takes no arguments. Therefore, it needs a wrapper to discard the event object. * Removed unnecessary code and added a docstring. * Fix the docstring according to guidelines. * Removed unnecessary whitespace. --- homeassistant/components/deconz/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 693f3e4470a..18197b84b61 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -143,7 +144,18 @@ def async_setup_deconz(hass, config, deconz_config): hass.services.async_register( DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz.close) + @callback + def deconz_shutdown(event): + """ + Wrap the call to deconz.close. + + Used as an argument to EventBus.async_listen_once - EventBus calls + this method with the event as the first argument, which should not + be passed on to deconz.close. + """ + deconz.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown) return True From 9e8340432c07d89d71285ff28f22f954dd00fd1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Feb 2018 01:11:28 -0800 Subject: [PATCH 18/29] Harmony: make activity optional (#12679) --- homeassistant/components/remote/harmony.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 25a1a684d3c..c84d214d826 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -31,7 +31,7 @@ CONF_DEVICE_CACHE = 'harmony_device_cache' SERVICE_SYNC = 'harmony_sync' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(ATTR_ACTIVITY): cv.string, + vol.Optional(ATTR_ACTIVITY): cv.string, vol.Required(CONF_NAME): cv.string, vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), From 948ef7523e3257ae2bd134276b06aa549bbc1f4b Mon Sep 17 00:00:00 2001 From: Jesse Hills Date: Tue, 27 Feb 2018 20:07:39 +1300 Subject: [PATCH 19/29] Fix getting state from iglo (#12685) --- homeassistant/components/light/iglo.py | 12 ++++++------ requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index e39b5dbf540..1e110b5c397 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['iglo==1.2.5'] +REQUIREMENTS = ['iglo==1.2.6'] _LOGGER = logging.getLogger(__name__) @@ -56,13 +56,13 @@ class IGloLamp(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" - return int((self._lamp.state['brightness'] / 200.0) * 255) + return int((self._lamp.state()['brightness'] / 200.0) * 255) @property def color_temp(self): """Return the color temperature.""" return color_util.color_temperature_kelvin_to_mired( - self._lamp.state['white']) + self._lamp.state()['white']) @property def min_mireds(self): @@ -79,12 +79,12 @@ class IGloLamp(Light): @property def rgb_color(self): """Return the RGB value.""" - return self._lamp.state['rgb'] + return self._lamp.state()['rgb'] @property def effect(self): """Return the current effect.""" - return self._lamp.state['effect'] + return self._lamp.state()['effect'] @property def effect_list(self): @@ -100,7 +100,7 @@ class IGloLamp(Light): @property def is_on(self): """Return true if light is on.""" - return self._lamp.state['on'] + return self._lamp.state()['on'] def turn_on(self, **kwargs): """Turn the light on.""" diff --git a/requirements_all.txt b/requirements_all.txt index 5213c9ecff0..0a95bb120bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -400,7 +400,7 @@ https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # i2csense==0.0.4 # homeassistant.components.light.iglo -iglo==1.2.5 +iglo==1.2.6 # homeassistant.components.ihc ihcsdk==2.1.1 From de3a9d552aff58dc2db01ac984a84533c242720b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Feb 2018 08:01:00 -0800 Subject: [PATCH 20/29] Fix mysensor defaults (#12687) --- homeassistant/components/mysensors.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 390da7ed0e0..37e257e5eb9 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -152,10 +152,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int, vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, - vol.Optional(CONF_TOPIC_IN_PREFIX, default=''): - valid_subscribe_topic, - vol.Optional(CONF_TOPIC_OUT_PREFIX, default=''): - valid_publish_topic, + vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, + vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, }] ), @@ -358,8 +356,8 @@ def setup(hass, config): hass.config.path('mysensors{}.pickle'.format(index + 1))) baud_rate = gway.get(CONF_BAUD_RATE) tcp_port = gway.get(CONF_TCP_PORT) - in_prefix = gway.get(CONF_TOPIC_IN_PREFIX) - out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX) + in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') + out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') ready_gateway = setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) From d128f4e51f99cae05f149c115900514c5accceef Mon Sep 17 00:00:00 2001 From: tumik Date: Tue, 27 Feb 2018 08:44:57 +0200 Subject: [PATCH 21/29] Component deconz: Fix dark attribute on presence sensors (#12691) pydeconz changed PRESENCE to be an array in v25, so this code hasn't worked since that change. --- homeassistant/components/binary_sensor/deconz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 8fea7891c3d..28e78db90ec 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -99,6 +99,6 @@ class DeconzBinarySensor(BinarySensorDevice): attr = { ATTR_BATTERY_LEVEL: self._sensor.battery, } - if self._sensor.type == PRESENCE: + if self._sensor.type in PRESENCE: attr['dark'] = self._sensor.dark return attr From 113ea2d1dc175de4411305f9bee355a94b177984 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 27 Feb 2018 07:44:11 +0100 Subject: [PATCH 22/29] Bugfix: Update of sources for non AVR-X devices always fails (#12711) * Basic support of post 2016 AVR-X receivers * Bugfix: Update of sources for non AVR-X devices always fails --- homeassistant/components/media_player/denonavr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 5bc16d11d64..fe8fc46c24b 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.6.0'] +REQUIREMENTS = ['denonavr==0.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0a95bb120bb..ac2040e8406 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ defusedxml==0.5.0 deluge-client==1.0.5 # homeassistant.components.media_player.denonavr -denonavr==0.6.0 +denonavr==0.6.1 # homeassistant.components.media_player.directv directpy==0.2 From 83203a10a7416169a1b58103949f376ab5ed7b1f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 27 Feb 2018 10:58:45 +0100 Subject: [PATCH 23/29] Don't allow to use a old unsecure library (#12715) * Don't allow to use a old unsecury library * Update gen_requirements_all.py * Cryptodome fix for python-broadlink * Coinbase cryptodome fix --- homeassistant/components/coinbase.py | 4 +++- homeassistant/components/sensor/broadlink.py | 4 +++- homeassistant/components/switch/broadlink.py | 4 +++- homeassistant/package_constraints.txt | 3 +++ requirements_all.txt | 14 +++++++------- script/gen_requirements_all.py | 3 +++ 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py index 10123752c99..515da3e4f54 100644 --- a/homeassistant/components/coinbase.py +++ b/homeassistant/components/coinbase.py @@ -14,7 +14,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle -REQUIREMENTS = ['coinbase==2.0.7'] +REQUIREMENTS = [ + 'https://github.com/balloob/coinbase-python/archive/' + '3a35efe13ef728a1cc18204b4f25be1fcb1c6006.zip#coinbase==2.0.8a1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 1440e2496fe..47cefe50aec 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,7 +19,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['broadlink==0.5'] +REQUIREMENTS = [ + 'https://github.com/balloob/python-broadlink/archive/' + '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 91ecc9c7111..38888733ba6 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -22,7 +22,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.util.dt import utcnow -REQUIREMENTS = ['broadlink==0.5'] +REQUIREMENTS = [ + 'https://github.com/balloob/python-broadlink/archive/' + '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2eb42b94389..7b6a5f09330 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,3 +15,6 @@ attrs==17.4.0 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 + +# This is a old unmaintained library and is replaced with pycryptodome +pycrypto==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index ac2040e8406..f99f65ec89f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -165,10 +165,6 @@ boto3==1.4.7 # homeassistant.scripts.credstash botocore==1.7.34 -# homeassistant.components.sensor.broadlink -# homeassistant.components.switch.broadlink -broadlink==0.5 - # homeassistant.components.sensor.buienradar # homeassistant.components.weather.buienradar buienradar==0.91 @@ -179,9 +175,6 @@ caldav==0.5.0 # homeassistant.components.notify.ciscospark ciscosparkapi==0.4.2 -# homeassistant.components.coinbase -coinbase==2.0.7 - # homeassistant.components.sensor.coinmarketcap coinmarketcap==4.2.1 @@ -370,6 +363,13 @@ httplib2==0.10.3 # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 +# homeassistant.components.coinbase +https://github.com/balloob/coinbase-python/archive/3a35efe13ef728a1cc18204b4f25be1fcb1c6006.zip#coinbase==2.0.8a1 + +# homeassistant.components.sensor.broadlink +# homeassistant.components.switch.broadlink +https://github.com/balloob/python-broadlink/archive/3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1 + # homeassistant.components.media_player.spotify https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 460c998f556..31e0635e411 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -113,6 +113,9 @@ CONSTRAINT_PATH = os.path.join(os.path.dirname(__file__), CONSTRAINT_BASE = """ # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 + +# This is a old unmaintained library and is replaced with pycryptodome +pycrypto==1000000000.0.0 """ From bbc2f1d808299f637dad8032d3ba1cf26b6b29ee Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Mon, 26 Feb 2018 22:27:20 +0100 Subject: [PATCH 24/29] Unbreak tahoma (#12719) * Update requirements_all.txt * Update tahoma.py --- homeassistant/components/tahoma.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index b288a704d74..7c8d047fbcf 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['tahoma-api==0.0.12'] +REQUIREMENTS = ['tahoma-api==0.0.13'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f99f65ec89f..953e6b2b0f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1156,7 +1156,7 @@ steamodd==4.21 suds-py3==1.3.3.0 # homeassistant.components.tahoma -tahoma-api==0.0.12 +tahoma-api==0.0.13 # homeassistant.components.sensor.tank_utility tank_utility==1.4.0 From ed8eda86e2fee3d90c8504747a73549cb8e9d676 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Tue, 27 Feb 2018 07:42:32 +0100 Subject: [PATCH 25/29] fix for https://github.com/home-assistant/home-assistant/issues/12673 (#12726) --- homeassistant/components/sensor/alpha_vantage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 81c84a7f918..896497a93d5 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -70,8 +70,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from alpha_vantage.foreignexchange import ForeignExchange api_key = config.get(CONF_API_KEY) - symbols = config.get(CONF_SYMBOLS) - conversions = config.get(CONF_FOREIGN_EXCHANGE) + symbols = config.get(CONF_SYMBOLS, []) + conversions = config.get(CONF_FOREIGN_EXCHANGE, []) if not symbols and not conversions: msg = 'Warning: No symbols or currencies configured.' From 802bc322e8e21467df40b35201b290478d37f5f1 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 27 Feb 2018 07:40:46 +0100 Subject: [PATCH 26/29] Fix harmony duplicate detection (#12729) --- homeassistant/components/remote/harmony.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index c84d214d826..842dce087e8 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port) # Ignore hub name when checking if this hub is known - ip and port only - if host and host[1:] in (h.host for h in DEVICES): + if host[1:] in ((h.host, h.port) for h in DEVICES): _LOGGER.debug("Discovered host already known: %s", host) return elif CONF_HOST in config: @@ -139,7 +139,7 @@ class HarmonyRemote(remote.RemoteDevice): _LOGGER.debug("HarmonyRemote device init started for: %s", name) self._name = name self.host = host - self._port = port + self.port = port self._state = None self._current_activity = None self._default_activity = activity From a0997bd2141b28797da8a399841e13324e35913f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 27 Feb 2018 00:05:29 -0800 Subject: [PATCH 27/29] Update ZHA deps (#12737) --- homeassistant/components/zha/__init__.py | 4 ++-- requirements_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index bb29cb28b0f..9a8c88e6f23 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -17,8 +17,8 @@ from homeassistant.helpers import discovery, entity from homeassistant.util import slugify REQUIREMENTS = [ - 'bellows==0.5.0', - 'zigpy==0.0.1', + 'bellows==0.5.1', + 'zigpy==0.0.3', 'zigpy-xbee==0.0.2', ] diff --git a/requirements_all.txt b/requirements_all.txt index 953e6b2b0f0..88d8d19e1c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.5.0 +bellows==0.5.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.3.0 @@ -1298,4 +1298,4 @@ ziggo-mediabox-xl==1.0.0 zigpy-xbee==0.0.2 # homeassistant.components.zha -zigpy==0.0.1 +zigpy==0.0.3 From 484841c89041194c1459de75be714fb712eb4a4f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 27 Feb 2018 20:18:20 +0100 Subject: [PATCH 28/29] Fix MQTT async_add_job in sync context (#12744) --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 0485d82a274..8a5fdb5b86b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -588,7 +588,7 @@ class MQTT(object): def _mqtt_on_message(self, _mqttc, _userdata, msg): """Message received callback.""" - self.hass.async_add_job(self._mqtt_handle_message, msg) + self.hass.add_job(self._mqtt_handle_message, msg) @callback def _mqtt_handle_message(self, msg): From 992516ba8667bee7396bb0bd5e1116588e4011f0 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Tue, 27 Feb 2018 12:19:02 -0700 Subject: [PATCH 29/29] Fix DarkSky floating-point math (#12753) DarkSky delivers relative humidity from 0-100% as a 0-1 decimal value, so we convert it by multiplying by 100.0. Unfortunately, due to floating point math, the display of a raw value of 0.29 ends up looking like 28.999999999999996% relative humidity. This change rounds the value to two decimal places. --- homeassistant/components/weather/darksky.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 21f67ce080a..139f8abfce6 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -96,7 +96,7 @@ class DarkSkyWeather(WeatherEntity): @property def humidity(self): """Return the humidity.""" - return self._ds_currently.get('humidity') * 100.0 + return round(self._ds_currently.get('humidity') * 100.0, 2) @property def wind_speed(self):