diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 5ecf3f55e10..bfbb98ad57e 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -151,6 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device['name'] = '{} {}'.format(device['id'], ipaddr) device[ATTR_MODE] = MODE_RGBW device[CONF_PROTOCOL] = None + device[CONF_CUSTOM_EFFECT] = None light = FluxLight(device) lights.append(light) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 6fb7d42e0ee..f2bca91205c 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -176,7 +176,7 @@ class PersonManager: CONF_ID: uuid.uuid4().hex, CONF_NAME: name, CONF_USER_ID: user_id, - CONF_DEVICE_TRACKERS: device_trackers, + CONF_DEVICE_TRACKERS: device_trackers or [], } self.storage_data[person[CONF_ID]] = person self._async_schedule_save() @@ -337,12 +337,18 @@ class Person(RestoreEntity): if state: self._parse_source_state(state) - @callback - def person_start_hass(now): + if self.hass.is_running: + # Update person now if hass is already running. self.person_updated() + else: + # Wait for hass start to not have race between person + # and device trackers finishing setup. + @callback + def person_start_hass(now): + self.person_updated() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, person_start_hass) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, person_start_hass) @callback def person_updated(self): diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 802512dbf5d..8a7934bd694 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -32,7 +32,12 @@ def _platform_validator(config): 'homeassistant.components.scene.{}'.format( config[CONF_PLATFORM])) except ImportError: - raise vol.Invalid('Invalid platform specified') from None + try: + platform = importlib.import_module( + 'homeassistant.components.{}.scene'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None if not hasattr(platform, 'PLATFORM_SCHEMA'): return config diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index ab7334f1316..f784ed101a7 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,13 +1,14 @@ """Support for climate devices through the SmartThings cloud API.""" import asyncio -from typing import Optional, Sequence +import logging +from typing import Iterable, Optional, Sequence from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) + ClimateDevice, DOMAIN as CLIMATE_DOMAIN) from homeassistant.const import ( ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -38,6 +39,8 @@ UNIT_MAP = { 'F': TEMP_FAHRENHEIT } +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -50,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsThermostat(device) for device in broker.devices.values() - if broker.any_assigned(device.device_id, 'climate')]) + if broker.any_assigned(device.device_id, CLIMATE_DOMAIN)], True) def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: @@ -90,6 +93,8 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): """Init the class.""" super().__init__(device) self._supported_features = self._determine_features() + self._current_operation = None + self._operations = None def _determine_features(self): from pysmartthings import Capability @@ -127,6 +132,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): if operation_state: mode = STATE_TO_MODE[operation_state] await self._device.set_thermostat_mode(mode, set_status=True) + await self.async_update() # Heat/cool setpoint heating_setpoint = None @@ -151,6 +157,33 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) + async def async_update(self): + """Update the attributes of the climate device.""" + thermostat_mode = self._device.status.thermostat_mode + self._current_operation = MODE_TO_STATE.get(thermostat_mode) + if self._current_operation is None: + _LOGGER.debug('Device %s (%s) returned an invalid' + 'thermostat mode: %s', self._device.label, + self._device.device_id, thermostat_mode) + + supported_modes = self._device.status.supported_thermostat_modes + if isinstance(supported_modes, Iterable): + operations = set() + for mode in supported_modes: + state = MODE_TO_STATE.get(mode) + if state is not None: + operations.add(state) + else: + _LOGGER.debug('Device %s (%s) returned an invalid ' + 'supported thermostat mode: %s', + self._device.label, self._device.device_id, + mode) + self._operations = operations + else: + _LOGGER.debug('Device %s (%s) returned invalid supported ' + 'thermostat modes: %s', self._device.label, + self._device.device_id, supported_modes) + @property def current_fan_mode(self): """Return the fan setting.""" @@ -164,7 +197,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return MODE_TO_STATE[self._device.status.thermostat_mode] + return self._current_operation @property def current_temperature(self): @@ -187,8 +220,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): @property def operation_list(self): """Return the list of available operation modes.""" - return {MODE_TO_STATE[mode] for mode in - self._device.status.supported_thermostat_modes} + return self._operations @property def supported_features(self): diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index ce943fb2c93..5e47adc47f9 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.11.1'] +REQUIREMENTS = ['PyXiaomiGateway==0.11.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 8ad0f2522d2..30433ccea3d 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -99,8 +99,10 @@ class XiaomiGatewayLight(XiaomiDevice, Light): if self._write_to_hub(self._sid, **{self._data_key: rgbhex}): self._state = True + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the light off.""" if self._write_to_hub(self._sid, **{self._data_key: 0}): self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant/const.py b/homeassistant/const.py index 28c76366312..e2329a2de26 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 88 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d0a192a9dc7..838c3f31bc5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,3 +26,7 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 + +# Newer version causes pylint to take forever +# https://github.com/timothycrosley/isort/issues/848 +isort==4.3.4 diff --git a/requirements_all.txt b/requirements_all.txt index ac85efe3be7..629dca5cccf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -63,7 +63,7 @@ PySwitchbot==0.5 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.11.1 +PyXiaomiGateway==0.11.2 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 47028ef3530..926aadfc3a5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -157,6 +157,10 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 + +# Newer version causes pylint to take forever +# https://github.com/timothycrosley/isort/issues/848 +isort==4.3.4 """ diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index f2d796fb204..6c8c6ebd0dd 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -101,6 +101,7 @@ async def test_valid_invalid_user_ids(hass, hass_admin_user): async def test_setup_tracker(hass, hass_admin_user): """Test set up person with one device tracker.""" + hass.state = CoreState.not_running user_id = hass_admin_user.id config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -148,6 +149,7 @@ async def test_setup_tracker(hass, hass_admin_user): async def test_setup_two_trackers(hass, hass_admin_user): """Test set up person with two device trackers.""" + hass.state = CoreState.not_running user_id = hass_admin_user.id config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -191,6 +193,7 @@ async def test_setup_two_trackers(hass, hass_admin_user): async def test_ignore_unavailable_states(hass, hass_admin_user): """Test set up person with two device trackers, one unavailable.""" + hass.state = CoreState.not_running user_id = hass_admin_user.id config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -234,7 +237,7 @@ async def test_restore_home_state(hass, hass_admin_user): ATTR_SOURCE: DEVICE_TRACKER, ATTR_USER_ID: user_id} state = State('person.tracked_person', 'home', attrs) mock_restore_cache(hass, (state, )) - hass.state = CoreState.starting + hass.state = CoreState.not_running mock_component(hass, 'recorder') config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -263,6 +266,21 @@ async def test_duplicate_ids(hass, hass_admin_user): assert hass.states.get('person.test_user_2') is None +async def test_create_person_during_run(hass): + """Test that person is updated if created while hass is running.""" + config = {DOMAIN: {}} + assert await async_setup_component(hass, DOMAIN, config) + hass.states.async_set(DEVICE_TRACKER, 'home') + await hass.async_block_till_done() + + await hass.components.person.async_create_person( + 'tracked person', device_trackers=[DEVICE_TRACKER]) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + + async def test_load_person_storage(hass, hass_admin_user, storage_setup): """Test set up person from storage.""" state = hass.states.get('person.tracked_person') diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index c5646fb400f..306bcacdb18 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_UNKNOWN) from .conftest import setup_platform @@ -95,6 +95,25 @@ def thermostat_fixture(device_factory): return device +@pytest.fixture(name="buggy_thermostat") +def buggy_thermostat_fixture(device_factory): + """Fixture returns a buggy thermostat.""" + device = device_factory( + "Buggy Thermostat", + capabilities=[ + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode], + status={ + Attribute.thermostat_mode: 'heating', + Attribute.cooling_setpoint: 74, + Attribute.heating_setpoint: 68} + ) + device.status.attributes[Attribute.temperature] = Status(70, 'F', None) + return device + + async def test_async_setup_platform(): """Test setup platform does nothing (it uses config entries).""" await climate.async_setup_platform(None, None, None) @@ -152,6 +171,29 @@ async def test_thermostat_entity_state(hass, thermostat): assert state.attributes[ATTR_CURRENT_HUMIDITY] == 34 +async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): + """Tests the state attributes properly match the thermostat type.""" + await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + state = hass.states.get('climate.buggy_thermostat') + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | \ + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE + assert ATTR_OPERATION_LIST not in state.attributes + assert state.attributes[ATTR_TEMPERATURE] is None + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + + +async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): + """Tests when an invalid operation mode is included.""" + buggy_thermostat.status.update_attribute_value( + Attribute.supported_thermostat_modes, + ['heat', 'emergency heat', 'other']) + await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + state = hass.states.get('climate.buggy_thermostat') + assert state.attributes[ATTR_OPERATION_LIST] == {'heat'} + + async def test_set_fan_mode(hass, thermostat): """Test the fan mode is set successfully.""" await setup_platform(hass, CLIMATE_DOMAIN, thermostat)