From deb10a1c4de966f4f4bc532043d94d68fc805eca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Jan 2018 23:26:02 -0800 Subject: [PATCH 001/166] Update frontend to 20180126.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 ea8a4d92540..8f5a18ff843 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==20180119.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180126.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 b429818651e..fa872ddb2d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -352,7 +352,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180119.0 +home-assistant-frontend==20180126.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 da8187d23ff..f21a20f7439 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==20180119.0 +home-assistant-frontend==20180126.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 390b7278693ef238adacdb096cc4a6c84b3fafd4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Jan 2018 03:37:06 -0800 Subject: [PATCH 002/166] Allow setting climate devices to AUTO mode via Google Assistant (#11923) * Allow setting climate devices to AUTO mode via Google Assistant * Remove cast to lower * Clarify const name --- homeassistant/components/climate/nest.py | 17 +++++------ .../components/google_assistant/const.py | 3 +- .../components/google_assistant/smart_home.py | 29 ++++++++++++------- tests/components/google_assistant/__init__.py | 2 +- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 3b550c43368..b4492821b1f 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.nest import DATA_NEST from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, @@ -27,8 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1)), }) -STATE_ECO = 'eco' -STATE_HEAT_COOL = 'heat-cool' +NEST_MODE_HEAT_COOL = 'heat-cool' SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE | @@ -118,14 +117,14 @@ class NestThermostat(ClimateDevice): """Return current operation ie. heat, cool, idle.""" if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: return self._mode - elif self._mode == STATE_HEAT_COOL: + elif self._mode == NEST_MODE_HEAT_COOL: return STATE_AUTO return STATE_UNKNOWN @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on: + if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: return self._target_temperature return None @@ -136,7 +135,7 @@ class NestThermostat(ClimateDevice): self._eco_temperature[0]: # eco_temperature is always a low, high tuple return self._eco_temperature[0] - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return self._target_temperature[0] return None @@ -147,7 +146,7 @@ class NestThermostat(ClimateDevice): self._eco_temperature[1]: # eco_temperature is always a low, high tuple return self._eco_temperature[1] - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return self._target_temperature[1] return None @@ -160,7 +159,7 @@ class NestThermostat(ClimateDevice): """Set new target temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) else: @@ -173,7 +172,7 @@ class NestThermostat(ClimateDevice): if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: device_mode = operation_mode elif operation_mode == STATE_AUTO: - device_mode = STATE_HEAT_COOL + device_mode = NEST_MODE_HEAT_COOL self.device.mode = device_mode @property diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index fc250c4b655..0483f424ca3 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -18,7 +18,8 @@ DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ 'switch', 'light', 'group', 'media_player', 'fan', 'cover', 'climate' ] -CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', 'heatcool'} +CLIMATE_MODE_HEATCOOL = 'heatcool' +CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL} PREFIX_TRAITS = 'action.devices.traits.' TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index e2ad609a007..d8e9f668c8e 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -33,7 +33,7 @@ from .const import ( TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP, TRAIT_RGB_COLOR, TRAIT_SCENE, TRAIT_TEMPERATURE_SETTING, TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT, - CONF_ALIASES, CLIMATE_SUPPORTED_MODES + CONF_ALIASES, CLIMATE_SUPPORTED_MODES, CLIMATE_MODE_HEATCOOL ) HANDLERS = Registry() @@ -147,12 +147,15 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem): entity.attributes.get(light.ATTR_MIN_MIREDS)))) if entity.domain == climate.DOMAIN: - modes = ','.join( - m.lower() for m in entity.attributes.get( - climate.ATTR_OPERATION_LIST, []) - if m.lower() in CLIMATE_SUPPORTED_MODES) + modes = [] + for mode in entity.attributes.get(climate.ATTR_OPERATION_LIST, []): + if mode in CLIMATE_SUPPORTED_MODES: + modes.append(mode) + elif mode == climate.STATE_AUTO: + modes.append(CLIMATE_MODE_HEATCOOL) + device['attributes'] = { - 'availableThermostatModes': modes, + 'availableThermostatModes': ','.join(modes), 'thermostatTemperatureUnit': 'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C', } @@ -323,9 +326,9 @@ def determine_service( # special climate handling if domain == climate.DOMAIN: if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - service_data['temperature'] = units.temperature( - params.get('thermostatTemperatureSetpoint', 25), - TEMP_CELSIUS) + service_data['temperature'] = \ + units.temperature( + params['thermostatTemperatureSetpoint'], TEMP_CELSIUS) return (climate.SERVICE_SET_TEMPERATURE, service_data) if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: service_data['target_temp_high'] = units.temperature( @@ -336,8 +339,12 @@ def determine_service( TEMP_CELSIUS) return (climate.SERVICE_SET_TEMPERATURE, service_data) if command == COMMAND_THERMOSTAT_SET_MODE: - service_data['operation_mode'] = params.get( - 'thermostatMode', 'off') + mode = params['thermostatMode'] + + if mode == CLIMATE_MODE_HEATCOOL: + mode = climate.STATE_AUTO + + service_data['operation_mode'] = mode return (climate.SERVICE_SET_OPERATION_MODE, service_data) if command == COMMAND_BRIGHTNESS: diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index db075fb6789..022cf852b88 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -211,7 +211,7 @@ DEMO_DEVICES = [{ 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False, 'attributes': { - 'availableThermostatModes': 'heat,cool,off', + 'availableThermostatModes': 'heat,cool,heatcool,off', 'thermostatTemperatureUnit': 'C', }, }, { From 8332d4e3592af6f73e63bdea4b9986e7ec31934a Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 26 Jan 2018 13:41:52 +0200 Subject: [PATCH 003/166] Add "write" service to system_log (#11901) * Add API to write error log * Move write_error api to system_log.write service call * Restore empty line --- .../components/system_log/__init__.py | 23 ++++++- .../components/system_log/services.yaml | 12 ++++ tests/components/test_system_log.py | 63 ++++++++++++++++--- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 0d478ac9316..5c8fe3109a6 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -18,6 +18,9 @@ from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv CONF_MAX_ENTRIES = 'max_entries' +CONF_MESSAGE = 'message' +CONF_LEVEL = 'level' +CONF_LOGGER = 'logger' DATA_SYSTEM_LOG = 'system_log' DEFAULT_MAX_ENTRIES = 50 @@ -25,6 +28,7 @@ DEPENDENCIES = ['http'] DOMAIN = 'system_log' SERVICE_CLEAR = 'clear' +SERVICE_WRITE = 'write' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -34,6 +38,12 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) SERVICE_CLEAR_SCHEMA = vol.Schema({}) +SERVICE_WRITE_SCHEMA = vol.Schema({ + vol.Required(CONF_MESSAGE): cv.string, + vol.Optional(CONF_LEVEL, default='error'): + vol.In(['debug', 'info', 'warning', 'error', 'critical']), + vol.Optional(CONF_LOGGER): cv.string, +}) class LogErrorHandler(logging.Handler): @@ -78,12 +88,21 @@ def async_setup(hass, config): @asyncio.coroutine def async_service_handler(service): """Handle logger services.""" - # Only one service so far - handler.records.clear() + if service.service == 'clear': + handler.records.clear() + return + if service.service == 'write': + logger = logging.getLogger( + service.data.get(CONF_LOGGER, '{}.external'.format(__name__))) + level = service.data[CONF_LEVEL] + getattr(logger, level)(service.data[CONF_MESSAGE]) hass.services.async_register( DOMAIN, SERVICE_CLEAR, async_service_handler, schema=SERVICE_CLEAR_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_WRITE, async_service_handler, + schema=SERVICE_WRITE_SCHEMA) return True diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index 98f86e12f8c..c168185c9b3 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,3 +1,15 @@ system_log: clear: description: Clear all log entries. + write: + description: Write log entry. + fields: + message: + description: Message to log. [Required] + example: Something went wrong + level: + description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." + example: debug + logger: + description: Logger name under which to log the message. Defaults to 'system_log.external'. + example: mycomponent.myplatform diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index 0f61986cf47..a3e7d662483 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -1,11 +1,12 @@ """Test system log component.""" import asyncio import logging +from unittest.mock import MagicMock, patch + import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log -from unittest.mock import MagicMock, patch _LOGGER = logging.getLogger('test_logger') @@ -117,11 +118,54 @@ def test_clear_logs(hass, test_client): yield from get_error_log(hass, test_client, 0) +@asyncio.coroutine +def test_write_log(hass): + """Test that error propagates to logger.""" + logger = MagicMock() + with patch('logging.getLogger', return_value=logger) as mock_logging: + hass.async_add_job( + hass.services.async_call( + system_log.DOMAIN, system_log.SERVICE_WRITE, + {'message': 'test_message'})) + yield from hass.async_block_till_done() + mock_logging.assert_called_once_with( + 'homeassistant.components.system_log.external') + assert logger.method_calls[0] == ('error', ('test_message',)) + + +@asyncio.coroutine +def test_write_choose_logger(hass): + """Test that correct logger is chosen.""" + with patch('logging.getLogger') as mock_logging: + hass.async_add_job( + hass.services.async_call( + system_log.DOMAIN, system_log.SERVICE_WRITE, + {'message': 'test_message', + 'logger': 'myLogger'})) + yield from hass.async_block_till_done() + mock_logging.assert_called_once_with( + 'myLogger') + + +@asyncio.coroutine +def test_write_choose_level(hass): + """Test that correct logger is chosen.""" + logger = MagicMock() + with patch('logging.getLogger', return_value=logger): + hass.async_add_job( + hass.services.async_call( + system_log.DOMAIN, system_log.SERVICE_WRITE, + {'message': 'test_message', + 'level': 'debug'})) + yield from hass.async_block_till_done() + assert logger.method_calls[0] == ('debug', ('test_message',)) + + @asyncio.coroutine def test_unknown_path(hass, test_client): """Test error logged from unknown path.""" _LOGGER.findCaller = MagicMock( - return_value=('unknown_path', 0, None, None)) + return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') log = (yield from get_error_log(hass, test_client, 1))[0] assert log['source'] == 'unknown_path' @@ -130,16 +174,15 @@ def test_unknown_path(hass, test_client): def log_error_from_test_path(path): """Log error while mocking the path.""" call_path = 'internal_path.py' - with patch.object( - _LOGGER, - 'findCaller', - MagicMock(return_value=(call_path, 0, None, None))): + with patch.object(_LOGGER, + 'findCaller', + MagicMock(return_value=(call_path, 0, None, None))): with patch('traceback.extract_stack', MagicMock(return_value=[ - get_frame('main_path/main.py'), - get_frame(path), - get_frame(call_path), - get_frame('venv_path/logging/log.py')])): + get_frame('main_path/main.py'), + get_frame(path), + get_frame(call_path), + get_frame('venv_path/logging/log.py')])): _LOGGER.error('error message') From 68d2851ecf2dcd05bd5197240a31980d4fee8d2e Mon Sep 17 00:00:00 2001 From: akloeckner Date: Fri, 26 Jan 2018 12:57:54 +0100 Subject: [PATCH 004/166] Map media_stop to idle state (#11819) adresses #11813 --- homeassistant/helpers/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 254a48c3d0a..255f760ebff 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -31,7 +31,7 @@ from homeassistant.components.cover import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, STATE_ALARM_ARMED_AWAY, @@ -78,6 +78,7 @@ SERVICE_TO_STATE = { SERVICE_TURN_OFF: STATE_OFF, SERVICE_MEDIA_PLAY: STATE_PLAYING, SERVICE_MEDIA_PAUSE: STATE_PAUSED, + SERVICE_MEDIA_STOP: STATE_IDLE, SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY, SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME, SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED, From bfe259f7a0634e81ddd76137bc53c65841acc85c Mon Sep 17 00:00:00 2001 From: Bas Schipper Date: Fri, 26 Jan 2018 13:45:02 +0100 Subject: [PATCH 005/166] Fixed rfxtrx binary_sensor KeyError on missing optional device_class (#11925) * Fixed rfxtrx binary_sensor KeyError on missing optional device_class * Fixed rfxtrx binary_sensor KeyError on missing optional device_class --- homeassistant/components/binary_sensor/rfxtrx.py | 5 +++-- homeassistant/components/rfxtrx.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index 763003cab03..2cc0aee2c7b 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) from homeassistant.components.rfxtrx import ( ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY) @@ -29,7 +29,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Optional(CONF_NAME, default=None): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=None): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=None): + DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_OFF_DELAY, default=None): vol.Any(cv.time_period, cv.positive_timedelta), diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 4994e333eda..7d2e428c56b 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -39,7 +39,6 @@ CONF_AUTOMATIC_ADD = 'automatic_add' CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_FIRE_EVENT = 'fire_event' -CONF_DATA_BITS = 'data_bits' CONF_DUMMY = 'dummy' CONF_DEVICE = 'device' CONF_DEBUG = 'debug' From 5af7666a61fe3cf08cfdb940acb26981a05b1e96 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 26 Jan 2018 10:40:02 -0700 Subject: [PATCH 006/166] Adds allergy/disease sensor platform from Pollen.com (#11573) * Base platform in place * Logic in place * Requirements and coverage * Fixed some linting issues * Small attribute reorganization * Collaborator-requested changes round 1 * Updated documentation --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/sensor/pollen.py | 322 ++++++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 327 insertions(+) create mode 100644 homeassistant/components/sensor/pollen.py diff --git a/.coveragerc b/.coveragerc index c1a9fa291fe..16832843e7e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -593,6 +593,7 @@ omit = homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py + homeassistant/components/sensor/pollen.py homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pyload.py diff --git a/CODEOWNERS b/CODEOWNERS index 9ec7ce0742c..d6b0385614a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,6 +61,7 @@ homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel +homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/waqi.py @andrey-git diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py new file mode 100644 index 00000000000..3998af7e32f --- /dev/null +++ b/homeassistant/components/sensor/pollen.py @@ -0,0 +1,322 @@ +""" +Support for Pollen.com allergen and cold/flu sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.pollen/ +""" +import logging +from datetime import timedelta +from statistics import mean + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS +) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pypollencom==1.1.1'] +_LOGGER = logging.getLogger(__name__) + +ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' +ATTR_ALLERGEN_NAME = 'primary_allergen_name' +ATTR_ALLERGEN_TYPE = 'primary_allergen_type' +ATTR_CITY = 'city' +ATTR_OUTLOOK = 'outlook' +ATTR_RATING = 'rating' +ATTR_SEASON = 'season' +ATTR_TREND = 'trend' +ATTR_ZIP_CODE = 'zip_code' + +CONF_ZIP_CODE = 'zip_code' + +DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' + +MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12) +MIN_TIME_UPDATE_INDICES = timedelta(minutes=10) + +CONDITIONS = { + 'allergy_average_forecasted': ( + 'Allergy Index: Forecasted Average', + 'AllergyAverageSensor', + 'allergy_average_data', + {'data_attr': 'extended_data'}, + 'mdi:flower' + ), + 'allergy_average_historical': ( + 'Allergy Index: Historical Average', + 'AllergyAverageSensor', + 'allergy_average_data', + {'data_attr': 'historic_data'}, + 'mdi:flower' + ), + 'allergy_index_today': ( + 'Allergy Index: Today', + 'AllergyIndexSensor', + 'allergy_index_data', + {'key': 'Today'}, + 'mdi:flower' + ), + 'allergy_index_tomorrow': ( + 'Allergy Index: Tomorrow', + 'AllergyIndexSensor', + 'allergy_index_data', + {'key': 'Tomorrow'}, + 'mdi:flower' + ), + 'allergy_index_yesterday': ( + 'Allergy Index: Yesterday', + 'AllergyIndexSensor', + 'allergy_index_data', + {'key': 'Yesterday'}, + 'mdi:flower' + ), + 'disease_average_forecasted': ( + 'Cold & Flu: Forecasted Average', + 'AllergyAverageSensor', + 'disease_average_data', + {'data_attr': 'extended_data'}, + 'mdi:snowflake' + ) +} + +RATING_MAPPING = [{ + 'label': 'Low', + 'minimum': 0.0, + 'maximum': 2.4 +}, { + 'label': 'Low/Medium', + 'minimum': 2.5, + 'maximum': 4.8 +}, { + 'label': 'Medium', + 'minimum': 4.9, + 'maximum': 7.2 +}, { + 'label': 'Medium/High', + 'minimum': 7.3, + 'maximum': 9.6 +}, { + 'label': 'High', + 'minimum': 9.7, + 'maximum': 12 +}] + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ZIP_CODE): cv.positive_int, + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(CONDITIONS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Configure the platform and add the sensors.""" + from pypollencom import Client + + _LOGGER.debug('Configuration data: %s', config) + + client = Client(config[CONF_ZIP_CODE]) + datas = { + 'allergy_average_data': AllergyAveragesData(client), + 'allergy_index_data': AllergyIndexData(client), + 'disease_average_data': DiseaseData(client) + } + + for data in datas.values(): + data.update() + + sensors = [] + for condition in config[CONF_MONITORED_CONDITIONS]: + name, sensor_class, data_key, params, icon = CONDITIONS[condition] + sensors.append(globals()[sensor_class]( + datas[data_key], + params, + name, + icon + )) + + add_devices(sensors, True) + + +def calculate_trend(list_of_nums): + """Calculate the most common rating as a trend.""" + ratings = list( + r['label'] for n in list_of_nums + for r in RATING_MAPPING + if r['minimum'] <= n <= r['maximum']) + return max(set(ratings), key=ratings.count) + + +class BaseSensor(Entity): + """Define a base class for all of our sensors.""" + + def __init__(self, data, data_params, name, icon): + """Initialize the sensor.""" + self._attrs = {} + self._icon = icon + self._name = name + self._data_params = data_params + self._state = None + self._unit = None + self.data = data + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}) + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + +class AllergyAverageSensor(BaseSensor): + """Define a sensor to show allergy average information.""" + + def update(self): + """Update the status of the sensor.""" + self.data.update() + + data_attr = getattr(self.data, self._data_params['data_attr']) + indices = [ + p['Index'] + for p in data_attr['Location']['periods'] + ] + average = round(mean(indices), 1) + + self._attrs[ATTR_TREND] = calculate_trend(indices) + self._attrs[ATTR_CITY] = data_attr['Location']['City'].title() + self._attrs[ATTR_STATE] = data_attr['Location']['State'] + self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP'] + + [rating] = [ + i['label'] for i in RATING_MAPPING + if i['minimum'] <= average <= i['maximum'] + ] + self._attrs[ATTR_RATING] = rating + + self._state = average + self._unit = 'index' + + +class AllergyIndexSensor(BaseSensor): + """Define a sensor to show allergy index information.""" + + def update(self): + """Update the status of the sensor.""" + self.data.update() + + location_data = self.data.current_data['Location'] + [period] = [ + p for p in location_data['periods'] + if p['Type'] == self._data_params['key'] + ] + + self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus'] + self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name'] + self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0]['PlantType'] + self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook'] + self._attrs[ATTR_SEASON] = self.data.outlook_data['Season'] + self._attrs[ATTR_TREND] = self.data.outlook_data[ + 'Trend'].title() + self._attrs[ATTR_CITY] = location_data['City'].title() + self._attrs[ATTR_STATE] = location_data['State'] + self._attrs[ATTR_ZIP_CODE] = location_data['ZIP'] + + [rating] = [ + i['label'] for i in RATING_MAPPING + if i['minimum'] <= period['Index'] <= i['maximum'] + ] + self._attrs[ATTR_RATING] = rating + + self._state = period['Index'] + self._unit = 'index' + + +class DataBase(object): + """Define a generic data object.""" + + def __init__(self, client): + """Initialize.""" + self._client = client + + def _get_client_data(self, module, operation): + """Get data from a particular point in the API.""" + from pypollencom.exceptions import HTTPError + + try: + data = getattr(getattr(self._client, module), operation)() + _LOGGER.debug('Received "%s_%s" data: %s', module, + operation, data) + except HTTPError as exc: + _LOGGER.error('An error occurred while retrieving data') + _LOGGER.debug(exc) + + return data + + +class AllergyAveragesData(DataBase): + """Define an object to averages on future and historical allergy data.""" + + def __init__(self, client): + """Initialize.""" + super().__init__(client) + self.extended_data = None + self.historic_data = None + + @Throttle(MIN_TIME_UPDATE_AVERAGES) + def update(self): + """Update with new data.""" + self.extended_data = self._get_client_data('allergens', 'extended') + self.historic_data = self._get_client_data('allergens', 'historic') + + +class AllergyIndexData(DataBase): + """Define an object to retrieve current allergy index info.""" + + def __init__(self, client): + """Initialize.""" + super().__init__(client) + self.current_data = None + self.outlook_data = None + + @Throttle(MIN_TIME_UPDATE_INDICES) + def update(self): + """Update with new index data.""" + self.current_data = self._get_client_data('allergens', 'current') + self.outlook_data = self._get_client_data('allergens', 'outlook') + + +class DiseaseData(DataBase): + """Define an object to retrieve current disease index info.""" + + def __init__(self, client): + """Initialize.""" + super().__init__(client) + self.extended_data = None + + @Throttle(MIN_TIME_UPDATE_INDICES) + def update(self): + """Update with new cold/flu data.""" + self.extended_data = self._get_client_data('disease', 'extended') diff --git a/requirements_all.txt b/requirements_all.txt index fa872ddb2d1..57f4d64cb3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -830,6 +830,9 @@ pyotp==2.2.6 # homeassistant.components.weather.openweathermap pyowm==2.8.0 +# homeassistant.components.sensor.pollen +pypollencom==1.1.1 + # homeassistant.components.qwikswitch pyqwikswitch==0.4 From 2d8ef36a6c2c928087dc71494bbb3e4cc524aa48 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 26 Jan 2018 19:30:48 +0100 Subject: [PATCH 007/166] fixes #11848 (#11915) Adding tests to check the component after latest patch --- .coveragerc | 1 - .../components/device_tracker/asuswrt.py | 59 ++-- .../components/device_tracker/test_asuswrt.py | 282 +++++++++++++++++- 3 files changed, 295 insertions(+), 47 deletions(-) diff --git a/.coveragerc b/.coveragerc index 16832843e7e..e4b73df1a8f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -341,7 +341,6 @@ omit = homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py - homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/automatic.py homeassistant/components/device_tracker/bbox.py homeassistant/components/device_tracker/bluetooth_le_tracker.py diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index f49f54b3622..0d27c4b5efd 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PUB_KEY = 'pub_key' CONF_SSH_KEY = 'ssh_key' - DEFAULT_SSH_PORT = 22 - SECRET_GROUP = 'Password or SSH Key' PLATFORM_SCHEMA = vol.All( @@ -118,20 +116,10 @@ class AsusWrtDeviceScanner(DeviceScanner): self.port = config[CONF_PORT] if self.protocol == 'ssh': - if not (self.ssh_key or self.password): - _LOGGER.error("No password or private key specified") - self.success_init = False - return - self.connection = SshConnection( self.host, self.port, self.username, self.password, self.ssh_key, self.mode == 'ap') else: - if not self.password: - _LOGGER.error("No password specified") - self.success_init = False - return - self.connection = TelnetConnection( self.host, self.port, self.username, self.password, self.mode == 'ap') @@ -177,11 +165,16 @@ class AsusWrtDeviceScanner(DeviceScanner): """ devices = {} devices.update(self._get_wl()) - devices = self._get_arp(devices) - devices = self._get_neigh(devices) + devices.update(self._get_arp()) + devices.update(self._get_neigh(devices)) if not self.mode == 'ap': devices.update(self._get_leases(devices)) - return devices + + ret_devices = {} + for key in devices: + if devices[key].ip is not None: + ret_devices[key] = devices[key] + return ret_devices def _get_wl(self): lines = self.connection.run_command(_WL_CMD) @@ -219,18 +212,13 @@ class AsusWrtDeviceScanner(DeviceScanner): result = _parse_lines(lines, _IP_NEIGH_REGEX) devices = {} for device in result: - if device['mac']: + if device['mac'] is not None: mac = device['mac'].upper() - devices[mac] = Device(mac, None, None) - else: - cur_devices = { - k: v for k, v in - cur_devices.items() if v.ip != device['ip'] - } - cur_devices.update(devices) - return cur_devices + old_ip = cur_devices.get(mac, {}).ip or None + devices[mac] = Device(mac, device.get('ip', old_ip), None) + return devices - def _get_arp(self, cur_devices): + def _get_arp(self): lines = self.connection.run_command(_ARP_CMD) if not lines: return {} @@ -240,13 +228,7 @@ class AsusWrtDeviceScanner(DeviceScanner): if device['mac']: mac = device['mac'].upper() devices[mac] = Device(mac, device['ip'], None) - else: - cur_devices = { - k: v for k, v in - cur_devices.items() if v.ip != device['ip'] - } - cur_devices.update(devices) - return cur_devices + return devices class _Connection: @@ -272,7 +254,7 @@ class SshConnection(_Connection): def __init__(self, host, port, username, password, ssh_key, ap): """Initialize the SSH connection properties.""" - super(SshConnection, self).__init__() + super().__init__() self._ssh = None self._host = host @@ -322,7 +304,7 @@ class SshConnection(_Connection): self._ssh.login(self._host, self._username, password=self._password, port=self._port) - super(SshConnection, self).connect() + super().connect() def disconnect(self): \ # pylint: disable=broad-except @@ -334,7 +316,7 @@ class SshConnection(_Connection): finally: self._ssh = None - super(SshConnection, self).disconnect() + super().disconnect() class TelnetConnection(_Connection): @@ -342,7 +324,7 @@ class TelnetConnection(_Connection): def __init__(self, host, port, username, password, ap): """Initialize the Telnet connection properties.""" - super(TelnetConnection, self).__init__() + super().__init__() self._telnet = None self._host = host @@ -361,7 +343,6 @@ class TelnetConnection(_Connection): try: if not self.connected: self.connect() - self._telnet.write('{}\n'.format(command).encode('ascii')) data = (self._telnet.read_until(self._prompt_string). split(b'\n')[1:-1]) @@ -392,7 +373,7 @@ class TelnetConnection(_Connection): self._telnet.write((self._password + '\n').encode('ascii')) self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1] - super(TelnetConnection, self).connect() + super().connect() def disconnect(self): \ # pylint: disable=broad-except @@ -402,4 +383,4 @@ class TelnetConnection(_Connection): except Exception: pass - super(TelnetConnection, self).disconnect() + super().disconnect() diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 0159eec2eff..808d3569b8b 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -5,6 +5,7 @@ import unittest from unittest import mock import voluptuous as vol +from future.backports import socket from homeassistant.setup import setup_component from homeassistant.components import device_tracker @@ -12,8 +13,9 @@ from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_TRACK_NEW, CONF_NEW_DEVICE_DEFAULTS, CONF_AWAY_HIDE) from homeassistant.components.device_tracker.asuswrt import ( - CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, - CONF_PORT, PLATFORM_SCHEMA) + CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, _ARP_REGEX, + CONF_PORT, PLATFORM_SCHEMA, Device, get_scanner, AsusWrtDeviceScanner, + _parse_lines, SshConnection, TelnetConnection) from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST) @@ -23,6 +25,84 @@ from tests.common import ( FAKEFILE = None +VALID_CONFIG_ROUTER_SSH = {DOMAIN: { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PROTOCOL: 'ssh', + CONF_MODE: 'router', + CONF_PORT: '22' +} +} + +WL_DATA = [ + 'assoclist 01:02:03:04:06:08\r', + 'assoclist 08:09:10:11:12:14\r', + 'assoclist 08:09:10:11:12:15\r' +] + +WL_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip=None, name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip=None, name=None), + '08:09:10:11:12:15': Device( + mac='08:09:10:11:12:15', ip=None, name=None) +} + +ARP_DATA = [ + '? (123.123.123.125) at 01:02:03:04:06:08 [ether] on eth0\r', + '? (123.123.123.126) at 08:09:10:11:12:14 [ether] on br0\r' + '? (123.123.123.127) at on br0\r' +] + +ARP_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) +} + +NEIGH_DATA = [ + '123.123.123.125 dev eth0 lladdr 01:02:03:04:06:08 REACHABLE\r', + '123.123.123.126 dev br0 lladdr 08:09:10:11:12:14 STALE\r' + '123.123.123.127 dev br0 FAILED\r' +] + +NEIGH_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) +} + +LEASES_DATA = [ + '51910 01:02:03:04:06:08 123.123.123.125 TV 01:02:03:04:06:08\r', + '79986 01:02:03:04:06:10 123.123.123.127 android 01:02:03:04:06:15\r', + '23523 08:09:10:11:12:14 123.123.123.126 * 08:09:10:11:12:14\r', +] + +LEASES_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name='TV'), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name='') +} + +WAKE_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name='TV'), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name='') +} + +WAKE_DEVICES_AP = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) +} + def setup_module(): """Setup the test module.""" @@ -55,6 +135,24 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): except FileNotFoundError: pass + def test_parse_lines_wrong_input(self): + """Testing parse lines.""" + output = _parse_lines("asdf asdfdfsafad", _ARP_REGEX) + self.assertEqual(output, []) + + def test_get_device_name(self): + """Test for getting name.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.last_results = WAKE_DEVICES + self.assertEqual('TV', scanner.get_device_name('01:02:03:04:06:08')) + self.assertEqual(None, scanner.get_device_name('01:02:03:04:08:08')) + + def test_scan_devices(self): + """Test for scan devices.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.last_results = WAKE_DEVICES + self.assertEqual(list(WAKE_DEVICES), scanner.scan_devices()) + def test_password_or_pub_key_required(self): \ # pylint: disable=invalid-name """Test creating an AsusWRT scanner without a pass or pubkey.""" @@ -63,13 +161,14 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): self.hass, DOMAIN, {DOMAIN: { CONF_PLATFORM: 'asuswrt', CONF_HOST: 'fake_host', - CONF_USERNAME: 'fake_user' + CONF_USERNAME: 'fake_user', + CONF_PROTOCOL: 'ssh' }}) @mock.patch( 'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner', return_value=mock.MagicMock()) - def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock): \ + def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock): \ # pylint: disable=invalid-name """Test creating an AsusWRT scanner with a password and no pubkey.""" conf_dict = { @@ -99,7 +198,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): @mock.patch( 'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner', return_value=mock.MagicMock()) - def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): \ + def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): \ # pylint: disable=invalid-name """Test creating an AsusWRT scanner with a pubkey and no password.""" conf_dict = { @@ -178,7 +277,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): password='fake_pass', port=22) ) - def test_ssh_login_without_password_or_pubkey(self): \ + def test_ssh_login_without_password_or_pubkey(self): \ # pylint: disable=invalid-name """Test that login is not called without password or pub_key.""" ssh = mock.MagicMock() @@ -249,7 +348,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): mock.call(b'#') ) - def test_telnet_login_without_password(self): \ + def test_telnet_login_without_password(self): \ # pylint: disable=invalid-name """Test that login is not called without password or pub_key.""" telnet = mock.MagicMock() @@ -277,3 +376,172 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): assert setup_component(self.hass, DOMAIN, {DOMAIN: conf_dict}) telnet.login.assert_not_called() + + def test_get_asuswrt_data(self): + """Test aususwrt data fetch.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner._get_wl = mock.Mock() + scanner._get_arp = mock.Mock() + scanner._get_neigh = mock.Mock() + scanner._get_leases = mock.Mock() + scanner._get_wl.return_value = WL_DEVICES + scanner._get_arp.return_value = ARP_DEVICES + scanner._get_neigh.return_value = NEIGH_DEVICES + scanner._get_leases.return_value = LEASES_DEVICES + self.assertEqual(WAKE_DEVICES, scanner.get_asuswrt_data()) + + def test_get_asuswrt_data_ap(self): + """Test for get asuswrt_data in ap mode.""" + conf = VALID_CONFIG_ROUTER_SSH.copy()[DOMAIN] + conf[CONF_MODE] = 'ap' + scanner = AsusWrtDeviceScanner(conf) + scanner._get_wl = mock.Mock() + scanner._get_arp = mock.Mock() + scanner._get_neigh = mock.Mock() + scanner._get_leases = mock.Mock() + scanner._get_wl.return_value = WL_DEVICES + scanner._get_arp.return_value = ARP_DEVICES + scanner._get_neigh.return_value = NEIGH_DEVICES + scanner._get_leases.return_value = LEASES_DEVICES + self.assertEqual(WAKE_DEVICES_AP, scanner.get_asuswrt_data()) + + def test_update_info(self): + """Test for update info.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.get_asuswrt_data = mock.Mock() + scanner.get_asuswrt_data.return_value = WAKE_DEVICES + self.assertTrue(scanner._update_info()) + self.assertTrue(scanner.last_results, WAKE_DEVICES) + scanner.success_init = False + self.assertFalse(scanner._update_info()) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_wl(self, mocked_ssh): + """Testing wl.""" + mocked_ssh.run_command.return_value = WL_DATA + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual(WL_DEVICES, scanner._get_wl()) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_wl()) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_arp(self, mocked_ssh): + """Testing arp.""" + mocked_ssh.run_command.return_value = ARP_DATA + + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual(ARP_DEVICES, scanner._get_arp()) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_arp()) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_neigh(self, mocked_ssh): + """Testing neigh.""" + mocked_ssh.run_command.return_value = NEIGH_DATA + + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual(NEIGH_DEVICES, scanner._get_neigh(ARP_DEVICES.copy())) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_neigh(ARP_DEVICES.copy())) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_leases(self, mocked_ssh): + """Testing leases.""" + mocked_ssh.run_command.return_value = LEASES_DATA + + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual( + LEASES_DEVICES, scanner._get_leases(NEIGH_DEVICES.copy())) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_leases(NEIGH_DEVICES.copy())) + + +class TestSshConnection(unittest.TestCase): + """Testing SshConnection.""" + + def setUp(self): + """Setup test env.""" + self.connection = SshConnection( + 'fake', 'fake', 'fake', 'fake', 'fake', 'fake') + self.connection._connected = True + + def test_run_command_exception_eof(self): + """Testing exception in run_command.""" + from pexpect import exceptions + self.connection._ssh = mock.Mock() + self.connection._ssh.sendline = mock.Mock() + self.connection._ssh.sendline.side_effect = exceptions.EOF('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + self.assertIsNone(self.connection._ssh) + + def test_run_command_exception_pxssh(self): + """Testing exception in run_command.""" + from pexpect import pxssh + self.connection._ssh = mock.Mock() + self.connection._ssh.sendline = mock.Mock() + self.connection._ssh.sendline.side_effect = pxssh.ExceptionPxssh( + 'except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + self.assertIsNone(self.connection._ssh) + + def test_run_command_assertion_error(self): + """Testing exception in run_command.""" + self.connection._ssh = mock.Mock() + self.connection._ssh.sendline = mock.Mock() + self.connection._ssh.sendline.side_effect = AssertionError('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + self.assertIsNone(self.connection._ssh) + + +class TestTelnetConnection(unittest.TestCase): + """Testing TelnetConnection.""" + + def setUp(self): + """Setup test env.""" + self.connection = TelnetConnection( + 'fake', 'fake', 'fake', 'fake', 'fake') + self.connection._connected = True + + def test_run_command_exception_eof(self): + """Testing EOFException in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = EOFError('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + + def test_run_command_exception_connection_refused(self): + """Testing ConnectionRefusedError in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = ConnectionRefusedError( + 'except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + + def test_run_command_exception_gaierror(self): + """Testing socket.gaierror in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = socket.gaierror('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + + def test_run_command_exception_oserror(self): + """Testing OSError in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = OSError('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) From ffcc41d6ef47b8984b7187c424cba48a27802241 Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Fri, 26 Jan 2018 18:40:39 +0000 Subject: [PATCH 008/166] Implement Alexa temperature sensors (#11930) --- homeassistant/components/alexa/smart_home.py | 84 ++++++++++++++++++-- tests/components/alexa/test_smart_home.py | 56 ++++++++++++- 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2a37fba8b43..2fae0b323a0 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -7,7 +7,7 @@ from uuid import uuid4 from homeassistant.components import ( alert, automation, cover, fan, group, input_boolean, light, lock, - media_player, scene, script, switch, http) + media_player, scene, script, switch, http, sensor) import homeassistant.core as ha import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -16,7 +16,8 @@ from homeassistant.const import ( SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_SET) + SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, + CONF_UNIT_OF_MEASUREMENT) from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -24,9 +25,15 @@ _LOGGER = logging.getLogger(__name__) API_DIRECTIVE = 'directive' API_ENDPOINT = 'endpoint' API_EVENT = 'event' +API_CONTEXT = 'context' API_HEADER = 'header' API_PAYLOAD = 'payload' +API_TEMP_UNITS = { + TEMP_FAHRENHEIT: 'FAHRENHEIT', + TEMP_CELSIUS: 'CELSIUS', +} + SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' CONF_DESCRIPTION = 'description' @@ -94,6 +101,8 @@ class _DisplayCategory(object): def _capability(interface, version=3, supports_deactivation=None, + retrievable=None, + properties_supported=None, cap_type='AlexaInterface'): """Return a Smart Home API capability object. @@ -102,9 +111,7 @@ def _capability(interface, There are some additional fields allowed but not implemented here since we've no use case for them yet: - - properties.supported - proactively_reported - - retrievable `supports_deactivation` applies only to scenes. """ @@ -117,6 +124,12 @@ def _capability(interface, if supports_deactivation is not None: result['supportsDeactivation'] = supports_deactivation + if retrievable is not None: + result['retrievable'] = retrievable + + if properties_supported is not None: + result['properties'] = {'supported': properties_supported} + return result @@ -144,6 +157,8 @@ class _EntityCapabilities(object): def capabilities(self): """Return a list of supported capabilities. + If the returned list is empty, the entity will not be discovered. + You might find _capability() useful. """ raise NotImplementedError @@ -269,6 +284,28 @@ class _GroupCapabilities(_EntityCapabilities): supports_deactivation=True)] +class _SensorCapabilities(_EntityCapabilities): + def default_display_categories(self): + # although there are other kinds of sensors, all but temperature + # sensors are currently ignored. + return [_DisplayCategory.TEMPERATURE_SENSOR] + + def capabilities(self): + capabilities = [] + + attrs = self.entity.attributes + if attrs.get(CONF_UNIT_OF_MEASUREMENT) in ( + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + ): + capabilities.append(_capability( + 'Alexa.TemperatureSensor', + retrievable=True, + properties_supported=[{'name': 'temperature'}])) + + return capabilities + + class _UnknownEntityDomainError(Exception): pass @@ -296,6 +333,7 @@ _CAPABILITIES_FOR_DOMAIN = { scene.DOMAIN: _SceneCapabilities, script.DOMAIN: _ScriptCapabilities, switch.DOMAIN: _SwitchCapabilities, + sensor.DOMAIN: _SensorCapabilities, } @@ -407,7 +445,11 @@ def async_handle_message(hass, config, message): return (yield from funct_ref(hass, config, message)) -def api_message(request, name='Response', namespace='Alexa', payload=None): +def api_message(request, + name='Response', + namespace='Alexa', + payload=None, + context=None): """Create a API formatted response message. Async friendly. @@ -435,6 +477,9 @@ def api_message(request, name='Response', namespace='Alexa', payload=None): if API_ENDPOINT in request: response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy() + if context is not None: + response[API_CONTEXT] = context + return response @@ -490,7 +535,12 @@ def async_api_discovery(hass, config, request): 'manufacturerName': 'Home Assistant', } - endpoint['capabilities'] = entity_capabilities.capabilities() + alexa_capabilities = entity_capabilities.capabilities() + if not alexa_capabilities: + _LOGGER.debug("Not exposing %s because it has no capabilities", + entity.entity_id) + continue + endpoint['capabilities'] = alexa_capabilities discovery_endpoints.append(endpoint) return api_message( @@ -976,3 +1026,25 @@ def async_api_previous(hass, config, request, entity): data, blocking=False) return api_message(request) + + +@HANDLERS.register(('Alexa', 'ReportState')) +@extract_entity +@asyncio.coroutine +def async_api_reportstate(hass, config, request, entity): + """Process a ReportState request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp_property = { + 'namespace': 'Alexa.TemperatureSensor', + 'name': 'temperature', + 'value': { + 'value': float(entity.state), + 'scale': API_TEMP_UNITS[unit], + }, + } + + return api_message( + request, + name='StateReport', + context={'properties': [temp_property]} + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 0f81d687278..3416dfbe367 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,6 +5,7 @@ from uuid import uuid4 import pytest +from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import smart_home @@ -166,13 +167,27 @@ def test_discovery_request(hass): 'position': 85 }) + hass.states.async_set( + 'sensor.test_temp', '59', { + 'friendly_name': "Test Temp Sensor", + 'unit_of_measurement': TEMP_FAHRENHEIT, + }) + + # This sensor measures a quantity not applicable to Alexa, and should not + # be discovered. + hass.states.async_set( + 'sensor.test_sickness', '0.1', { + 'friendly_name': "Test Space Sickness Sensor", + 'unit_of_measurement': 'garn', + }) + msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 16 + assert len(msg['payload']['endpoints']) == 17 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' @@ -334,6 +349,17 @@ def test_discovery_request(hass): assert 'Alexa.PowerController' in caps continue + if appliance['endpointId'] == 'sensor#test_temp': + assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' + assert appliance['friendlyName'] == 'Test Temp Sensor' + assert len(appliance['capabilities']) == 1 + capability = appliance['capabilities'][0] + assert capability['interface'] == 'Alexa.TemperatureSensor' + assert capability['retrievable'] is True + properties = capability['properties'] + assert {'name': 'temperature'} in properties['supported'] + continue + raise AssertionError("Unknown appliance!") @@ -1170,6 +1196,34 @@ def test_api_mute(hass, domain): assert msg['header']['name'] == 'Response' +@asyncio.coroutine +def test_api_report_temperature(hass): + """Test API ReportState response for a temperature sensor.""" + request = get_new_request('Alexa', 'ReportState', 'sensor#test') + + # setup test devices + hass.states.async_set( + 'sensor.test', '42', { + 'friendly_name': 'test sensor', + CONF_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, + }) + + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + header = msg['event']['header'] + assert header['namespace'] == 'Alexa' + assert header['name'] == 'StateReport' + + properties = msg['context']['properties'] + assert len(properties) == 1 + prop = properties[0] + assert prop['namespace'] == 'Alexa.TemperatureSensor' + assert prop['name'] == 'temperature' + assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'} + + @asyncio.coroutine def test_entity_config(hass): """Test that we can configure things via entity config.""" From 2b68bec428d442ee49df6fcba781027a2f8acfdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Fri, 26 Jan 2018 19:43:55 +0100 Subject: [PATCH 009/166] check_config.py: allow colorlog==3.1. (#11927) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 27987a70ce6..f80eafd72cc 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -13,7 +13,7 @@ from homeassistant import bootstrap, loader, setup, config as config_util import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError -REQUIREMENTS = ('colorlog==3.0.1',) +REQUIREMENTS = ('colorlog==3.1.2',) if system() == 'Windows': # Ensure colorama installed for colorlog on Windows REQUIREMENTS += ('colorama<=1',) diff --git a/requirements_all.txt b/requirements_all.txt index 57f4d64cb3e..bc552a83f8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ coinbase==2.0.6 coinmarketcap==4.1.2 # homeassistant.scripts.check_config -colorlog==3.0.1 +colorlog==3.1.2 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 From af5d0b3443d9f531f40ee8d7ffd52ad548e7e9d6 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 26 Jan 2018 21:41:43 +0100 Subject: [PATCH 010/166] Update pyhomematic to 0.1.38 (#11936) --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b2f6384d467..db2a43d8728 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.37'] +REQUIREMENTS = ['pyhomematic==0.1.38'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bc552a83f8c..67fac8e89d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ pyhik==0.1.4 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.37 +pyhomematic==0.1.38 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 From 74b0740e1cfc3f7fd702434926df00073f56c943 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Sat, 27 Jan 2018 07:30:39 +0100 Subject: [PATCH 011/166] Weblink - Allow relative urls in config (#11808) * Allow relative url * Allow absolute urls in config schema * change after pylint build * Add tests and change error message * Change regex to check starting forward slash only * Change error message and const name --- homeassistant/components/weblink.py | 7 +++- tests/components/test_weblink.py | 65 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/weblink.py b/homeassistant/components/weblink.py index f55fe1f0bb5..a20b0fc9b0c 100644 --- a/homeassistant/components/weblink.py +++ b/homeassistant/components/weblink.py @@ -16,12 +16,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_ENTITIES = 'entities' +CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." +CONF_RELATIVE_URL_REGEX = r'\A/' DOMAIN = 'weblink' ENTITIES_SCHEMA = vol.Schema({ - # pylint: disable=no-value-for-parameter - vol.Required(CONF_URL): vol.Url(), + vol.Required(CONF_URL): vol.Any( + vol.Match(CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG), + cv.url), vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, }) diff --git a/tests/components/test_weblink.py b/tests/components/test_weblink.py index e8768342db9..249e81d37af 100644 --- a/tests/components/test_weblink.py +++ b/tests/components/test_weblink.py @@ -26,6 +26,71 @@ class TestComponentWeblink(unittest.TestCase): } })) + def test_bad_config_relative_url(self): + """Test if new entity is created.""" + self.assertFalse(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My router', + weblink.CONF_URL: '../states/group.bla' + }, + ], + } + })) + + def test_bad_config_relative_file(self): + """Test if new entity is created.""" + self.assertFalse(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My group', + weblink.CONF_URL: 'group.bla' + }, + ], + } + })) + + def test_good_config_absolute_path(self): + """Test if new entity is created.""" + self.assertTrue(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My second URL', + weblink.CONF_URL: '/states/group.bla' + }, + ], + } + })) + + def test_good_config_path_short(self): + """Test if new entity is created.""" + self.assertTrue(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My third URL', + weblink.CONF_URL: '/states' + }, + ], + } + })) + + def test_good_config_path_directory(self): + """Test if new entity is created.""" + self.assertTrue(setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [ + { + weblink.CONF_NAME: 'My last URL', + weblink.CONF_URL: '/states/bla/' + }, + ], + } + })) + def test_entities_get_created(self): """Test if new entity is created.""" self.assertTrue(setup_component(self.hass, weblink.DOMAIN, { From cad0bde95b3d4c6b5fca6d64e3b7ec624e7dee51 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Sat, 27 Jan 2018 07:31:40 +0100 Subject: [PATCH 012/166] Panel_Iframe - Allow relative urls in config (#11832) * Panel_Iframe - Allow relative urls in config * change regex to check for starting forward slash only * Change error message and const name --- homeassistant/components/panel_iframe.py | 16 +++++++++++----- tests/components/test_panel_iframe.py | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py index e4be19c53ed..6ddf00cf7d4 100644 --- a/homeassistant/components/panel_iframe.py +++ b/homeassistant/components/panel_iframe.py @@ -8,22 +8,28 @@ import asyncio import voluptuous as vol +from homeassistant.const import (CONF_ICON, CONF_URL) import homeassistant.helpers.config_validation as cv -DOMAIN = 'panel_iframe' DEPENDENCIES = ['frontend'] +DOMAIN = 'panel_iframe' + CONF_TITLE = 'title' -CONF_ICON = 'icon' -CONF_URL = 'url' + +CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." +CONF_RELATIVE_URL_REGEX = r'\A/' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ cv.slug: { vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_ICON): cv.icon, - # pylint: disable=no-value-for-parameter - vol.Required(CONF_URL): vol.Url(), + vol.Required(CONF_URL): vol.Any( + vol.Match( + CONF_RELATIVE_URL_REGEX, + msg=CONF_RELATIVE_URL_ERROR_MSG), + cv.url), }})}, extra=vol.ALLOW_EXTRA) diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 805d73e1820..ef702b96f4b 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -11,11 +11,11 @@ from tests.common import get_test_home_assistant class TestPanelIframe(unittest.TestCase): """Test the panel_iframe component.""" - def setup_method(self, method): + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def teardown_method(self, method): + def tearDown(self): """Stop everything that was started.""" self.hass.stop() @@ -50,6 +50,11 @@ class TestPanelIframe(unittest.TestCase): 'title': 'Weather', 'url': 'https://www.wunderground.com/us/ca/san-diego', }, + 'api': { + 'icon': 'mdi:weather', + 'title': 'Api', + 'url': '/api', + }, }, }) @@ -72,3 +77,12 @@ class TestPanelIframe(unittest.TestCase): 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'weather', } + + assert panels.get('api').to_response(self.hass, None) == { + 'component_name': 'iframe', + 'config': {'url': '/api'}, + 'icon': 'mdi:weather', + 'title': 'Api', + 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', + 'url_path': 'api', + } From b4d682ca751bfc73916e7a2c2d537a5e9f4ad72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 27 Jan 2018 13:16:30 +0200 Subject: [PATCH 013/166] Python 3.6 invalid escape sequence deprecation fixes (#11941) https://docs.python.org/3/whatsnew/3.6.html#deprecated-python-behavior --- tests/components/binary_sensor/test_aurora.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/binary_sensor/test_aurora.py b/tests/components/binary_sensor/test_aurora.py index ed68d23905f..1198aeb1357 100644 --- a/tests/components/binary_sensor/test_aurora.py +++ b/tests/components/binary_sensor/test_aurora.py @@ -28,7 +28,7 @@ class TestAuroraSensorSetUp(unittest.TestCase): def test_setup_and_initial_state(self, mock_req): """Test that the component is created and initialized as expected.""" uri = re.compile( - "http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" + r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" ) mock_req.get(uri, text=load_fixture('aurora.txt')) @@ -66,7 +66,7 @@ class TestAuroraSensorSetUp(unittest.TestCase): def test_custom_threshold_works(self, mock_req): """Test that the config can take a custom forecast threshold.""" uri = re.compile( - "http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" + r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt" ) mock_req.get(uri, text=load_fixture('aurora.txt')) From f1fc3c762a71925258447540351d45eec2d2284e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 27 Jan 2018 13:18:02 +0200 Subject: [PATCH 014/166] tests: Use assertEqual instead of deprecated assertEquals (#11943) --- .../alarm_control_panel/test_manual.py | 2 +- tests/components/light/test_hue.py | 74 +++++++++---------- tests/components/test_hue.py | 14 ++-- tests/components/test_nuheat.py | 4 +- tests/components/test_plant.py | 16 ++-- 5 files changed, 55 insertions(+), 55 deletions(-) diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index c47ed941b65..e4b29d43e48 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -34,7 +34,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): mock = MagicMock() add_devices = mock.MagicMock() demo.setup_platform(self.hass, {}, add_devices) - self.assertEquals(add_devices.call_count, 1) + self.assertEqual(add_devices.call_count, 1) def test_arm_home_no_pending(self): """Test arm home method.""" diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 611f1240d45..659bc4abe16 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -272,36 +272,36 @@ class TestSetup(unittest.TestCase): hue_light.unthrottled_update_lights( self.hass, mock_bridge_two, self.mock_add_devices) - self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2]) - self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3]) + self.assertEqual(sorted(mock_bridge_one.lights.keys()), [1, 2]) + self.assertEqual(sorted(mock_bridge_two.lights.keys()), [1, 3]) - self.assertEquals(len(self.mock_add_devices.mock_calls), 2) + self.assertEqual(len(self.mock_add_devices.mock_calls), 2) # first call name, args, kwargs = self.mock_add_devices.mock_calls[0] - self.assertEquals(len(args), 1) - self.assertEquals(len(kwargs), 0) + self.assertEqual(len(args), 1) + self.assertEqual(len(kwargs), 0) # one argument, a list of lights in bridge one; each of them is an # object of type HueLight so we can't straight up compare them lights = args[0] - self.assertEquals( + self.assertEqual( lights[0].unique_id, '{}.b1l1.Light.1'.format(hue_light.HueLight)) - self.assertEquals( + self.assertEqual( lights[1].unique_id, '{}.b1l2.Light.2'.format(hue_light.HueLight)) # second call works the same name, args, kwargs = self.mock_add_devices.mock_calls[1] - self.assertEquals(len(args), 1) - self.assertEquals(len(kwargs), 0) + self.assertEqual(len(args), 1) + self.assertEqual(len(kwargs), 0) lights = args[0] - self.assertEquals( + self.assertEqual( lights[0].unique_id, '{}.b2l1.Light.1'.format(hue_light.HueLight)) - self.assertEquals( + self.assertEqual( lights[1].unique_id, '{}.b2l3.Light.3'.format(hue_light.HueLight)) @@ -313,8 +313,8 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) - self.assertEquals(self.mock_bridge.lights, {}) + self.assertEqual([], ret) + self.assertEqual(self.mock_bridge.lights, {}) def test_process_lights_no_lights(self): """Test the process_lights function when bridge returns no lights.""" @@ -325,9 +325,9 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) + self.assertEqual([], ret) mock_dispatcher_send.assert_not_called() - self.assertEquals(self.mock_bridge.lights, {}) + self.assertEqual(self.mock_bridge.lights, {}) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_lights_some_lights(self, mock_hue_light): @@ -341,7 +341,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 2) + self.assertEqual(len(ret), 2) mock_hue_light.assert_has_calls([ call( 1, {'state': 'on'}, self.mock_bridge, mock.ANY, @@ -353,7 +353,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge.allow_in_emulated_hue), ]) mock_dispatcher_send.assert_not_called() - self.assertEquals(len(self.mock_bridge.lights), 2) + self.assertEqual(len(self.mock_bridge.lights), 2) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_lights_new_light(self, mock_hue_light): @@ -373,7 +373,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 1) + self.assertEqual(len(ret), 1) mock_hue_light.assert_has_calls([ call( 2, {'state': 'off'}, self.mock_bridge, mock.ANY, @@ -382,7 +382,7 @@ class TestSetup(unittest.TestCase): ]) mock_dispatcher_send.assert_called_once_with( 'hue_light_callback_bridge-id_1') - self.assertEquals(len(self.mock_bridge.lights), 2) + self.assertEqual(len(self.mock_bridge.lights), 2) def test_process_groups_api_error(self): """Test the process_groups function when the bridge errors out.""" @@ -392,8 +392,8 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) - self.assertEquals(self.mock_bridge.lightgroups, {}) + self.assertEqual([], ret) + self.assertEqual(self.mock_bridge.lightgroups, {}) def test_process_groups_no_state(self): """Test the process_groups function when bridge returns no status.""" @@ -405,9 +405,9 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) + self.assertEqual([], ret) mock_dispatcher_send.assert_not_called() - self.assertEquals(self.mock_bridge.lightgroups, {}) + self.assertEqual(self.mock_bridge.lightgroups, {}) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_groups_some_groups(self, mock_hue_light): @@ -421,7 +421,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 2) + self.assertEqual(len(ret), 2) mock_hue_light.assert_has_calls([ call( 1, {'state': 'on'}, self.mock_bridge, mock.ANY, @@ -433,7 +433,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge.allow_in_emulated_hue, True), ]) mock_dispatcher_send.assert_not_called() - self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.assertEqual(len(self.mock_bridge.lightgroups), 2) @patch(HUE_LIGHT_NS + 'HueLight') def test_process_groups_new_group(self, mock_hue_light): @@ -453,7 +453,7 @@ class TestSetup(unittest.TestCase): ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 1) + self.assertEqual(len(ret), 1) mock_hue_light.assert_has_calls([ call( 2, {'state': 'off'}, self.mock_bridge, mock.ANY, @@ -462,7 +462,7 @@ class TestSetup(unittest.TestCase): ]) mock_dispatcher_send.assert_called_once_with( 'hue_light_callback_bridge-id_1') - self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.assertEqual(len(self.mock_bridge.lightgroups), 2) class TestHueLight(unittest.TestCase): @@ -509,27 +509,27 @@ class TestHueLight(unittest.TestCase): class_name = "" light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEquals( + self.assertEqual( class_name+'.foobar', light.unique_id) light = self.buildLight(info={}) - self.assertEquals( + self.assertEqual( class_name+'.Unnamed Device.Light.42', light.unique_id) light = self.buildLight(info={'name': 'my-name'}) - self.assertEquals( + self.assertEqual( class_name+'.my-name.Light.42', light.unique_id) light = self.buildLight(info={'type': 'my-type'}) - self.assertEquals( + self.assertEqual( class_name+'.Unnamed Device.my-type.42', light.unique_id) light = self.buildLight(info={'name': 'a name', 'type': 'my-type'}) - self.assertEquals( + self.assertEqual( class_name+'.a name.my-type.42', light.unique_id) @@ -538,28 +538,28 @@ class TestHueLight(unittest.TestCase): class_name = "" light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEquals( + self.assertEqual( class_name+'.foobar', light.unique_id) light = self.buildLight(info={}, is_group=True) - self.assertEquals( + self.assertEqual( class_name+'.Unnamed Device.Group.42', light.unique_id) light = self.buildLight(info={'name': 'my-name'}, is_group=True) - self.assertEquals( + self.assertEqual( class_name+'.my-name.Group.42', light.unique_id) light = self.buildLight(info={'type': 'my-type'}, is_group=True) - self.assertEquals( + self.assertEqual( class_name+'.Unnamed Device.my-type.42', light.unique_id) light = self.buildLight( info={'name': 'a name', 'type': 'my-type'}, is_group=True) - self.assertEquals( + self.assertEqual( class_name+'.a name.my-type.42', light.unique_id) diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py index b8568f3aaba..30129ec7998 100644 --- a/tests/components/test_hue.py +++ b/tests/components/test_hue.py @@ -36,7 +36,7 @@ class TestSetup(unittest.TestCase): self.assertTrue(setup_component( self.hass, hue.DOMAIN, {})) mock_phue.Bridge.assert_not_called() - self.assertEquals({}, self.hass.data[hue.DOMAIN]) + self.assertEqual({}, self.hass.data[hue.DOMAIN]) @MockDependency('phue') def test_setup_with_host(self, mock_phue): @@ -59,7 +59,7 @@ class TestSetup(unittest.TestCase): {'bridge_id': '127.0.0.1'}) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_setup_with_phue_conf(self, mock_phue): @@ -86,7 +86,7 @@ class TestSetup(unittest.TestCase): {'bridge_id': '127.0.0.1'}) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_setup_with_multiple_hosts(self, mock_phue): @@ -122,7 +122,7 @@ class TestSetup(unittest.TestCase): ], any_order=True) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(2, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(2, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_bridge_discovered(self, mock_phue): @@ -145,7 +145,7 @@ class TestSetup(unittest.TestCase): {'bridge_id': '192.168.0.10'}) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) @MockDependency('phue') def test_bridge_configure_and_discovered(self, mock_phue): @@ -175,7 +175,7 @@ class TestSetup(unittest.TestCase): mock_load.assert_has_calls(calls_to_mock_load) self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) # Then we discover the same bridge hue.bridge_discovered(self.hass, mock_service, discovery_info) @@ -189,7 +189,7 @@ class TestSetup(unittest.TestCase): # Still only one self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEquals(1, len(self.hass.data[hue.DOMAIN])) + self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) class TestHueBridge(unittest.TestCase): diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py index 91a8b326bf9..2b0f249f4c9 100644 --- a/tests/components/test_nuheat.py +++ b/tests/components/test_nuheat.py @@ -35,11 +35,11 @@ class TestNuHeat(unittest.TestCase): mocked_nuheat.NuHeat.assert_called_with("warm", "feet") self.assertIn(nuheat.DOMAIN, self.hass.data) - self.assertEquals(2, len(self.hass.data[nuheat.DOMAIN])) + self.assertEqual(2, len(self.hass.data[nuheat.DOMAIN])) self.assertIsInstance( self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) ) - self.assertEquals(self.hass.data[nuheat.DOMAIN][1], "thermostat123") + self.assertEqual(self.hass.data[nuheat.DOMAIN][1], "thermostat123") mocked_load.assert_called_with( self.hass, "climate", nuheat.DOMAIN, {}, self.config diff --git a/tests/components/test_plant.py b/tests/components/test_plant.py index f5a042ac8c1..14db6689386 100644 --- a/tests/components/test_plant.py +++ b/tests/components/test_plant.py @@ -101,8 +101,8 @@ class TestPlant(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'us/cm'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_PROBLEM, state.state) - self.assertEquals(5, state.attributes[plant.READING_MOISTURE]) + self.assertEqual(STATE_PROBLEM, state.state) + self.assertEqual(5, state.attributes[plant.READING_MOISTURE]) @pytest.mark.skipif(plant.ENABLE_LOAD_HISTORY is False, reason="tests for loading from DB are instable, thus" @@ -132,10 +132,10 @@ class TestPlant(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_UNKNOWN, state.state) + self.assertEqual(STATE_UNKNOWN, state.state) max_brightness = state.attributes.get( plant.ATTR_MAX_BRIGHTNESS_HISTORY) - self.assertEquals(30, max_brightness) + self.assertEqual(30, max_brightness) def test_brightness_history(self): """Test the min_brightness check.""" @@ -149,19 +149,19 @@ class TestPlant(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_PROBLEM, state.state) + self.assertEqual(STATE_PROBLEM, state.state) self.hass.states.set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_OK, state.state) + self.assertEqual(STATE_OK, state.state) self.hass.states.set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) self.hass.block_till_done() state = self.hass.states.get('plant.'+plant_name) - self.assertEquals(STATE_OK, state.state) + self.assertEqual(STATE_OK, state.state) class TestDailyHistory(unittest.TestCase): @@ -195,4 +195,4 @@ class TestDailyHistory(unittest.TestCase): for i in range(len(days)): dh.add_measurement(values[i], days[i]) - self.assertEquals(max_values[i], dh.max) + self.assertEqual(max_values[i], dh.max) From 3af7c67bf1a66b43b982f4598420c34c3d4fcfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 27 Jan 2018 16:20:28 +0200 Subject: [PATCH 015/166] Fix asuswrt AttributeError on neigh for unknown device (#11960) --- homeassistant/components/device_tracker/asuswrt.py | 3 ++- tests/components/device_tracker/test_asuswrt.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 0d27c4b5efd..2196dd78fdb 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -214,7 +214,8 @@ class AsusWrtDeviceScanner(DeviceScanner): for device in result: if device['mac'] is not None: mac = device['mac'].upper() - old_ip = cur_devices.get(mac, {}).ip or None + old_device = cur_devices.get(mac) + old_ip = old_device.ip if old_device else None devices[mac] = Device(mac, device.get('ip', old_ip), None) return devices diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 808d3569b8b..6e646e9862d 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -447,6 +447,9 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) scanner.connection = mocked_ssh self.assertEqual(NEIGH_DEVICES, scanner._get_neigh(ARP_DEVICES.copy())) + self.assertEqual(NEIGH_DEVICES, scanner._get_neigh({ + 'UN:KN:WN:DE:VI:CE': Device('UN:KN:WN:DE:VI:CE', None, None), + })) mocked_ssh.run_command.return_value = '' self.assertEqual({}, scanner._get_neigh(ARP_DEVICES.copy())) From e750428e9d91cd8238001735c79913977fd34e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 27 Jan 2018 17:33:33 +0200 Subject: [PATCH 016/166] huawei_router: Fix documentation link (#11961) --- homeassistant/components/device_tracker/huawei_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py index b78683696cf..abdd829f26c 100644 --- a/homeassistant/components/device_tracker/huawei_router.py +++ b/homeassistant/components/device_tracker/huawei_router.py @@ -2,7 +2,7 @@ Support for HUAWEI routers. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.huawei/ +https://home-assistant.io/components/device_tracker.huawei_router/ """ import base64 import logging From 94316f07a21b35cdd34a34a575e07fae9b97e89a Mon Sep 17 00:00:00 2001 From: tschmidty69 Date: Sat, 27 Jan 2018 14:55:47 -0500 Subject: [PATCH 017/166] Snips - (fix/change) remove response when intent not handled (#11929) * Remove snips endSession response on unknownIntent * Removed snips_response for unknown and error. From 55ee8959ba22011c63dbb3ab84391353e3e21555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 27 Jan 2018 21:58:27 +0200 Subject: [PATCH 018/166] Spelling fixes (#11940) --- CODEOWNERS | 2 +- homeassistant/components/__init__.py | 2 +- homeassistant/components/alexa/smart_home.py | 8 ++++---- homeassistant/components/binary_sensor/hive.py | 2 +- .../components/binary_sensor/insteon_plm.py | 2 +- .../components/binary_sensor/raincloud.py | 2 +- homeassistant/components/binary_sensor/wemo.py | 2 +- homeassistant/components/calendar/caldav.py | 2 +- homeassistant/components/calendar/todoist.py | 2 +- homeassistant/components/camera/__init__.py | 4 ++-- homeassistant/components/climate/demo.py | 2 +- homeassistant/components/climate/eq3btsmart.py | 2 +- homeassistant/components/climate/hive.py | 2 +- homeassistant/components/climate/mqtt.py | 4 ++-- homeassistant/components/climate/tado.py | 4 ++-- homeassistant/components/cover/zwave.py | 2 +- .../components/device_tracker/actiontec.py | 2 +- homeassistant/components/emoncms_history.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/frontend/__init__.py | 2 +- .../components/google_assistant/smart_home.py | 4 ++-- homeassistant/components/homematic/__init__.py | 2 +- homeassistant/components/homematic/services.yaml | 4 ++-- .../components/image_processing/__init__.py | 2 +- homeassistant/components/light/hive.py | 2 +- homeassistant/components/light/insteon_plm.py | 2 +- homeassistant/components/light/mochad.py | 2 +- homeassistant/components/lirc.py | 2 +- homeassistant/components/mailbox/__init__.py | 2 +- .../components/media_player/anthemav.py | 2 +- .../components/media_player/bluesound.py | 4 ++-- homeassistant/components/media_player/cmus.py | 2 +- homeassistant/components/media_player/denon.py | 2 +- .../components/media_player/monoprice.py | 2 +- .../components/media_player/squeezebox.py | 2 +- homeassistant/components/microsoft_face.py | 2 +- homeassistant/components/notify/apns.py | 4 ++-- homeassistant/components/rflink.py | 2 +- homeassistant/components/sensor/bme680.py | 2 +- homeassistant/components/sensor/dsmr.py | 4 ++-- .../components/sensor/eddystone_temperature.py | 4 ++-- homeassistant/components/sensor/eight_sleep.py | 6 +++--- homeassistant/components/sensor/gearbest.py | 2 +- homeassistant/components/sensor/hive.py | 2 +- homeassistant/components/sensor/raincloud.py | 2 +- homeassistant/components/sensor/tado.py | 2 +- homeassistant/components/sensor/waterfurnace.py | 2 +- .../components/switch/acer_projector.py | 2 +- homeassistant/components/switch/broadlink.py | 2 +- .../components/switch/digitalloggers.py | 2 +- homeassistant/components/switch/flux.py | 2 +- homeassistant/components/switch/hive.py | 2 +- homeassistant/components/switch/insteon_plm.py | 2 +- homeassistant/components/switch/raincloud.py | 2 +- homeassistant/components/switch/scsgate.py | 2 +- .../components/telegram_bot/services.yaml | 16 ++++++++-------- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/components/zwave/services.yaml | 6 +++--- homeassistant/core.py | 4 ++-- homeassistant/helpers/entity.py | 4 ++-- homeassistant/helpers/event.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/util/__init__.py | 2 +- homeassistant/util/logging.py | 2 +- tests/components/camera/test_init.py | 2 +- .../device_tracker/test_unifi_direct.py | 4 ++-- .../google_assistant/test_google_assistant.py | 2 +- tests/components/image_processing/test_init.py | 2 +- tests/components/light/test_litejet.py | 2 +- tests/components/media_player/test_sonos.py | 4 ++-- tests/components/media_player/test_yamaha.py | 2 +- tests/components/recorder/test_migrate.py | 2 +- tests/components/sensor/test_dsmr.py | 2 +- tests/components/test_dialogflow.py | 4 ++-- tests/components/tts/test_yandextts.py | 4 ++-- tests/helpers/test_entity_component.py | 4 ++-- tests/test_bootstrap.py | 2 +- tests/test_core.py | 2 +- tests/test_remote.py | 2 +- tests/util/test_yaml.py | 2 +- 80 files changed, 110 insertions(+), 110 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d6b0385614a..6e088a84e5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,7 +41,7 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/hassio.py @home-assistant/hassio -# Indiviudal components +# Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 6db147a5f59..a1c6811afe7 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -133,7 +133,7 @@ def async_setup(hass, config): # have been processed. If a service does not exist it causes a 10 # second delay while we're blocking waiting for a response. # But services can be registered on other HA instances that are - # listening to the bus too. So as a in between solution, we'll + # listening to the bus too. So as an in between solution, we'll # block only if the service is defined in the current HA instance. blocking = hass.services.has_service(domain, service.service) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2fae0b323a0..a24583d8247 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -624,7 +624,7 @@ def async_api_set_brightness(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_adjust_brightness(hass, config, request, entity): - """Process a adjust brightness request.""" + """Process an adjust brightness request.""" brightness_delta = int(request[API_PAYLOAD]['brightnessDelta']) # read current state @@ -812,7 +812,7 @@ def async_api_set_percentage(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_adjust_percentage(hass, config, request, entity): - """Process a adjust percentage request.""" + """Process an adjust percentage request.""" percentage_delta = int(request[API_PAYLOAD]['percentageDelta']) service = None data = {ATTR_ENTITY_ID: entity.entity_id} @@ -873,7 +873,7 @@ def async_api_lock(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_unlock(hass, config, request, entity): - """Process a unlock request.""" + """Process an unlock request.""" yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { ATTR_ENTITY_ID: entity.entity_id }, blocking=False) @@ -904,7 +904,7 @@ def async_api_set_volume(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_adjust_volume(hass, config, request, entity): - """Process a adjust volume request.""" + """Process an adjust volume request.""" volume_delta = int(request[API_PAYLOAD]['volume']) current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py index 6223ebe50a1..2d4cbd8d070 100644 --- a/homeassistant/components/binary_sensor/hive.py +++ b/homeassistant/components/binary_sensor/hive.py @@ -59,5 +59,5 @@ class HiveBinarySensorEntity(BinarySensorDevice): self.node_device_type) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 0702ce8bb9e..1874be6ec41 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -83,5 +83,5 @@ class InsteonPLMBinarySensorDevice(BinarySensorDevice): @callback def async_binarysensor_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update calback from PLM for %s", self._address) + _LOGGER.info("Received update callback from PLM for %s", self._address) self._hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/binary_sensor/raincloud.py index f75f7644c4e..288b46c2370 100644 --- a/homeassistant/components/binary_sensor/raincloud.py +++ b/homeassistant/components/binary_sensor/raincloud.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor_type)) else: - # create an sensor for each zone managed by faucet + # create a sensor for each zone managed by faucet for zone in raincloud.controller.faucet.zones: sensors.append(RainCloudBinarySensor(zone, sensor_type)) diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 1ec9e703eab..857c0c40777 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -62,7 +62,7 @@ class WemoBinarySensor(BinarySensorDevice): @property def name(self): - """Return the name of the sevice if any.""" + """Return the name of the service if any.""" return self.wemo.name @property diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 8b2401aa589..ba798ce7902 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -174,7 +174,7 @@ class WebDavCalendarData(object): @staticmethod def is_matching(vevent, search): - """Return if the event matches the filter critera.""" + """Return if the event matches the filter criteria.""" if search is None: return True diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index 81191e3025e..f1c80612f3b 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -411,7 +411,7 @@ class TodoistProjectData(object): The "best" event is determined by the following criteria: * A proposed event must not be completed - * A proposed event must have a end date (otherwise we go with + * A proposed event must have an end date (otherwise we go with the event at index 0, selected above) * A proposed event must be on the same day or earlier as our current event diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 1bb88050b2f..a531d25841b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -91,13 +91,13 @@ def async_snapshot(hass, filename, entity_id=None): @bind_hass @asyncio.coroutine def async_get_image(hass, entity_id, timeout=10): - """Fetch a image from a camera entity.""" + """Fetch an image from a camera entity.""" websession = async_get_clientsession(hass) state = hass.states.get(entity_id) if state is None: raise HomeAssistantError( - "No entity '{0}' for grab a image".format(entity_id)) + "No entity '{0}' for grab an image".format(entity_id)) url = "{0}{1}".format( hass.config.api.base_url, diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index c3ba523468f..357b1d56200 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -207,7 +207,7 @@ class DemoClimate(ClimateDevice): self.schedule_update_ha_state() def turn_aux_heat_on(self): - """Turn auxillary heater on.""" + """Turn auxiliary heater on.""" self._aux = True self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 9b3b7d650a9..9c712c632e6 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): - """Representation of a eQ-3 Bluetooth Smart thermostat.""" + """Representation of an eQ-3 Bluetooth Smart thermostat.""" def __init__(self, _mac, _name): """Initialize the thermostat.""" diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py index b8ac66d91b3..760ef131049 100644 --- a/homeassistant/components/climate/hive.py +++ b/homeassistant/components/climate/hive.py @@ -174,5 +174,5 @@ class HiveClimateEntity(ClimateDevice): entity.handle_update(self.data_updatesource) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 3656bf7b475..5929cec3b05 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -565,7 +565,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): @asyncio.coroutine def async_turn_aux_heat_on(self): - """Turn auxillary heater on.""" + """Turn auxiliary heater on.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], self._payload_on, self._qos, self._retain) @@ -576,7 +576,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): @asyncio.coroutine def async_turn_aux_heat_off(self): - """Turn auxillary heater off.""" + """Turn auxiliary heater off.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], self._payload_off, self._qos, self._retain) diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 25492cb0895..5b20462c245 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -249,7 +249,7 @@ class TadoClimate(ClimateDevice): data = self._store.get_data(self._data_id) if data is None: - _LOGGER.debug("Recieved no data for zone %s", self.zone_name) + _LOGGER.debug("Received no data for zone %s", self.zone_name) return if 'sensorDataPoints' in data: @@ -317,7 +317,7 @@ class TadoClimate(ClimateDevice): fan_speed = setting_data['fanSpeed'] if self._device_is_active: - # If you set mode manualy to off, there will be an overlay + # If you set mode manually to off, there will be an overlay # and a termination, but we want to see the mode "OFF" self._overlay_mode = termination self._current_operation = termination diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 3c038125616..15100957242 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -158,7 +158,7 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): @property def is_closing(self): - """Return true if cover is in an closing state.""" + """Return true if cover is in a closing state.""" return self._state == "Closing" @property diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 64e1a60ad08..781e486a40e 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -42,7 +42,7 @@ Device = namedtuple('Device', ['mac', 'ip', 'last_update']) class ActiontecDeviceScanner(DeviceScanner): - """This class queries a an actiontec router for connected devices.""" + """This class queries an actiontec router for connected devices.""" def __init__(self, config): """Initialize the scanner.""" diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history.py index 34d9fd0f458..6a92ab64044 100644 --- a/homeassistant/components/emoncms_history.py +++ b/homeassistant/components/emoncms_history.py @@ -59,7 +59,7 @@ def setup(hass, config): payload, fullurl, req.status_code) def update_emoncms(time): - """Send whitelisted entities states reguarly to Emoncms.""" + """Send whitelisted entities states regularly to Emoncms.""" payload_dict = {} for entity_id in whitelist: diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 910e33627a6..97ac382031b 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -200,7 +200,7 @@ class XiaomiAirPurifier(FanEntity): @asyncio.coroutine def _try_command(self, mask_error, func, *args, **kwargs): - """Call a air purifier command handling error messages.""" + """Call an air purifier command handling error messages.""" from miio import DeviceException try: result = yield from self.hass.async_add_job( diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8f5a18ff843..610a531a702 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -407,7 +407,7 @@ def async_setup_themes(hass, themes): @callback def set_theme(call): - """Set backend-prefered theme.""" + """Set backend-preferred theme.""" data = call.data name = data[CONF_NAME] if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]: diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index d8e9f668c8e..ba56e7c3837 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -79,7 +79,7 @@ class SmartHomeError(Exception): """Log error code.""" super(SmartHomeError, self).__init__(msg) _LOGGER.error( - "An error has ocurred in Google SmartHome: %s." + "An error has occurred in Google SmartHome: %s." "Error code: %s", msg, code ) self.code = code @@ -96,7 +96,7 @@ class Config: def entity_to_device(entity: Entity, config: Config, units: UnitSystem): - """Convert a hass entity into an google actions device.""" + """Convert a hass entity into a google actions device.""" entity_config = config.entity_config.get(entity.entity_id, {}) google_domain = entity_config.get(CONF_TYPE) class_data = MAPPING_COMPONENT.get( diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index db2a43d8728..70054e54075 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -466,7 +466,7 @@ def _system_callback_handler(hass, config, src, *args): hass, discovery_type, addresses, interface) # When devices of this type are found - # they are setup in HASS and an discovery event is fired + # they are setup in HASS and a discovery event is fired if found_devices: discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index bf4d99af9e7..c2946b51842 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -13,7 +13,7 @@ virtualkey: description: Event to send i.e. PRESS_LONG, PRESS_SHORT. example: PRESS_LONG interface: - description: (Optional) for set a interface value. + description: (Optional) for set an interface value. example: Interfaces name from config set_variable_value: @@ -42,7 +42,7 @@ set_device_value: description: Event to send i.e. PRESS_LONG, PRESS_SHORT example: PRESS_LONG interface: - description: (Optional) for set a interface value + description: (Optional) for set an interface value example: Interfaces name from config value: description: New value diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 646bfcf421f..2c2b8364823 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -60,7 +60,7 @@ SERVICE_SCAN_SCHEMA = vol.Schema({ @bind_hass def scan(hass, entity_id=None): - """Force process a image.""" + """Force process an image.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.services.call(DOMAIN, SERVICE_SCAN, data) diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index 8fafb88a7db..5ba162a20d2 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -137,5 +137,5 @@ class HiveDeviceLight(Light): return supported_features def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index 51de9f03df5..f0ef0ce1b7e 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -101,7 +101,7 @@ class InsteonPLMDimmerDevice(Light): @callback def async_light_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update calback from PLM for %s", self._address) + _LOGGER.info("Received update callback from PLM for %s", self._address) self._hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/light/mochad.py index efc62b05434..576e244103f 100644 --- a/homeassistant/components/light/mochad.py +++ b/homeassistant/components/light/mochad.py @@ -62,7 +62,7 @@ class MochadLight(Light): @property def brightness(self): - """Return the birghtness of this light between 0..255.""" + """Return the brightness of this light between 0..255.""" return self._brightness def _get_device_status(self): diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py index 8b9ad0209da..ea4df658ef6 100644 --- a/homeassistant/components/lirc.py +++ b/homeassistant/components/lirc.py @@ -1,5 +1,5 @@ """ -LIRC interface to receive signals from a infrared remote control. +LIRC interface to receive signals from an infrared remote control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lirc/ diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index a1e68555649..8ff3746889e 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -133,7 +133,7 @@ class MailboxEntity(Entity): class Mailbox(object): - """Represent an mailbox device.""" + """Represent a mailbox device.""" def __init__(self, hass, name): """Initialize mailbox object.""" diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 293c6e51d52..474751c2574 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -49,7 +49,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update calback from AVR: %s", message) + _LOGGER.info("Received update callback from AVR: %s", message) hass.async_add_job(device.async_update_ha_state()) avr = yield from anthemav.Connection.create( diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index ca6b152a37e..96323579822 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -150,10 +150,10 @@ class BluesoundPlayer(MediaPlayerDevice): self._port = DEFAULT_PORT @staticmethod - def _try_get_index(string, seach_string): + def _try_get_index(string, search_string): """Get the index.""" try: - return string.index(seach_string) + return string.index(search_string) except ValueError: return -1 diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index fe25422360c..bcbee5c4ff7 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -77,7 +77,7 @@ class CmusDevice(MediaPlayerDevice): """Get the latest data and update the state.""" status = self.cmus.get_status_dict() if not status: - _LOGGER.warning("Recieved no status from cmus") + _LOGGER.warning("Received no status from cmus") else: self.status = status diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 572405baa6e..d85bd51e7fb 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -108,7 +108,7 @@ class DenonDevice(MediaPlayerDevice): if not line: break lines.append(line.decode('ASCII').strip()) - _LOGGER.debug("Recived: %s", line) + _LOGGER.debug("Received: %s", line) if all_lines: return lines diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index d26fce0ea88..44d19ac6860 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MonopriceZone(MediaPlayerDevice): - """Representation of a a Monoprice amplifier zone.""" + """Representation of a Monoprice amplifier zone.""" def __init__(self, monoprice, sources, zone_id, zone_name): """Initialize new zone.""" diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 13f05cc59f7..82bd106af8d 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -473,7 +473,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self.async_query('playlist', 'play', media_id) def _add_uri_to_playlist(self, media_id): - """Add a items to the existing playlist.""" + """Add an item to the existing playlist.""" return self.async_query('playlist', 'add', media_id) def async_set_shuffle(self, shuffle): diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index e61ed05ce10..5a0bf2af1c4 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -337,7 +337,7 @@ class MicrosoftFace(object): @asyncio.coroutine def call_api(self, method, function, data=None, binary=False, params=None): - """Make a api call.""" + """Make an api call.""" headers = {"Ocp-Apim-Subscription-Key": self._api_key} url = self._server_url.format(function) diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 6ef758b7bb5..e7a727bc5e2 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -112,13 +112,13 @@ class ApnsDevice(object): self.device_disabled = True def __eq__(self, other): - """Return the comparision.""" + """Return the comparison.""" if isinstance(other, self.__class__): return self.push_id == other.push_id and self.name == other.name return NotImplemented def __ne__(self, other): - """Return the comparision.""" + """Return the comparison.""" return not self.__eq__(other) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 73922d56040..d97d4f38f02 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -390,7 +390,7 @@ class RflinkCommand(RflinkDevice): """Cancel queued signal repetition commands. For example when user changed state while repetitions are still - queued for broadcast. Or when a incoming Rflink command (remote + queued for broadcast. Or when an incoming Rflink command (remote switch) changes the state. """ # cancel any outstanding tasks from the previous state change diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index 09d8ec4659c..081dc6cdc6e 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -238,7 +238,7 @@ class BME680Handler: self._gas_sensor_running = True - # Pause to allow inital data read for device validation. + # Pause to allow initial data read for device validation. sleep(1) start_time = time() diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 5b20ac0f4d0..1c12799549c 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -93,8 +93,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device.telegram = telegram hass.async_add_job(device.async_update_ha_state()) - # Creates a asyncio.Protocol factory for reading DSMR telegrams from serial - # and calls update_entities_telegram to update entities on arrival + # Creates an asyncio.Protocol factory for reading DSMR telegrams from + # serial and calls update_entities_telegram to update entities on arrival if config[CONF_HOST]: reader_factory = partial( create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT], diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fe05da3ccdd..ef06458cd84 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -122,7 +122,7 @@ class EddystoneTemp(Entity): class Monitor(object): - """Continously scan for BLE advertisements.""" + """Continuously scan for BLE advertisements.""" def __init__(self, hass, devices, bt_device_id): """Construct interface object.""" @@ -150,7 +150,7 @@ class Monitor(object): self.scanning = False def start(self): - """Continously scan for BLE advertisements.""" + """Continuously scan for BLE advertisements.""" if not self.scanning: self.scanner.start() self.scanning = True diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py index e6f4addf003..e0a42fdb6a8 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/sensor/eight_sleep.py @@ -65,7 +65,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class EightHeatSensor(EightSleepHeatEntity): - """Representation of a eight sleep heat-based sensor.""" + """Representation of an eight sleep heat-based sensor.""" def __init__(self, name, eight, sensor): """Initialize the sensor.""" @@ -116,7 +116,7 @@ class EightHeatSensor(EightSleepHeatEntity): class EightUserSensor(EightSleepUserEntity): - """Representation of a eight sleep user-based sensor.""" + """Representation of an eight sleep user-based sensor.""" def __init__(self, name, eight, sensor, units): """Initialize the sensor.""" @@ -232,7 +232,7 @@ class EightUserSensor(EightSleepUserEntity): class EightRoomSensor(EightSleepUserEntity): - """Representation of a eight sleep room sensor.""" + """Representation of an eight sleep room sensor.""" def __init__(self, name, eight, sensor, units): """Initialize the sensor.""" diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py index 2bc7e5b3b3a..aa1d2d9eff0 100644 --- a/homeassistant/components/sensor/gearbest.py +++ b/homeassistant/components/sensor/gearbest.py @@ -1,5 +1,5 @@ """ -Parse prices of a item from gearbest. +Parse prices of an item from gearbest. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.gearbest/ diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index af31c14789a..cae2eaf7437 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -48,5 +48,5 @@ class HiveSensorEntity(Entity): return self.session.sensor.hub_online_status(self.node_id) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/sensor/raincloud.py b/homeassistant/components/sensor/raincloud.py index d3b8b7207e3..c03aa0a2aec 100644 --- a/homeassistant/components/sensor/raincloud.py +++ b/homeassistant/components/sensor/raincloud.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): RainCloudSensor(raincloud.controller.faucet, sensor_type)) else: - # create an sensor for each zone managed by a faucet + # create a sensor for each zone managed by a faucet for zone in raincloud.controller.faucet.zones: sensors.append(RainCloudSensor(zone, sensor_type)) diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 8c7259ff800..7acdc1a20bd 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -147,7 +147,7 @@ class TadoSensor(Entity): data = self._store.get_data(self._data_id) if data is None: - _LOGGER.debug("Recieved no data for zone %s", self.zone_name) + _LOGGER.debug("Received no data for zone %s", self.zone_name) return unit = TEMP_CELSIUS diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py index 7d8c71f8d51..24c45ec1ff3 100644 --- a/homeassistant/components/sensor/waterfurnace.py +++ b/homeassistant/components/sensor/waterfurnace.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify class WFSensorConfig(object): """Water Furnace Sensor configuration.""" - def __init__(self, friendly_name, field, icon="mdi:guage", + def __init__(self, friendly_name, field, icon="mdi:gauge", unit_of_measurement=None): """Initialize configuration.""" self.friendly_name = friendly_name diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index 58361b2e8b2..d32c0610b66 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class AcerSwitch(SwitchDevice): - """Represents an Acer Projector as an switch.""" + """Represents an Acer Projector as a switch.""" def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 5841642cc00..8353b4bf8ad 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -100,7 +100,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): packet = yield from hass.async_add_job( broadlink_device.check_data) if packet: - log_msg = "Recieved packet is: {}".\ + log_msg = "Received packet is: {}".\ format(b64encode(packet).decode('utf8')) _LOGGER.info(log_msg) hass.components.persistent_notification.async_create( diff --git a/homeassistant/components/switch/digitalloggers.py b/homeassistant/components/switch/digitalloggers.py index 0625a42f765..f3af70c6222 100644 --- a/homeassistant/components/switch/digitalloggers.py +++ b/homeassistant/components/switch/digitalloggers.py @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DINRelay(SwitchDevice): - """Representation of a individual DIN III relay port.""" + """Representation of an individual DIN III relay port.""" def __init__(self, controller_name, parent_device, outlet): """Initialize the DIN III Relay switch.""" diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index ff432f2efc8..7e3566f17b0 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -210,7 +210,7 @@ class FluxSwitch(SwitchDevice): else: temp = self._start_colortemp + temp_offset else: - # Nightime + # Night time time_state = 'night' if now < stop_time: diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/switch/hive.py index 97d4320280d..67ebe95ba8e 100644 --- a/homeassistant/components/switch/hive.py +++ b/homeassistant/components/switch/hive.py @@ -65,5 +65,5 @@ class HiveDevicePlug(SwitchDevice): entity.handle_update(self.data_updatesource) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index ed7d0ffc479..0b584e14b8d 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -83,7 +83,7 @@ class InsteonPLMSwitchDevice(SwitchDevice): @callback def async_switch_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info('Received update calback from PLM for %s', self._address) + _LOGGER.info('Received update callback from PLM for %s', self._address) self._hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py index f373a6aad84..a18d6544acc 100644 --- a/homeassistant/components/switch/raincloud.py +++ b/homeassistant/components/switch/raincloud.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - # create an sensor for each zone managed by faucet + # create a sensor for each zone managed by faucet for zone in raincloud.controller.faucet.zones: sensors.append( RainCloudSwitch(default_watering_timer, diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index dfcf1816b7b..8b2734612de 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -155,7 +155,7 @@ class SCSGateSwitch(SwitchDevice): class SCSGateScenarioSwitch(object): """Provides a SCSGate scenario switch. - This switch is always in a 'off" state, when toggled it's used to trigger + This switch is always in an 'off" state, when toggled it's used to trigger events. """ diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index dc864c9f61a..4c144fe42db 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -25,7 +25,7 @@ send_message: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text button2:/button2", "Text button3:/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' send_photo: @@ -56,7 +56,7 @@ send_photo: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' send_video: @@ -87,7 +87,7 @@ send_video: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' send_document: @@ -118,7 +118,7 @@ send_document: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' send_location: @@ -140,7 +140,7 @@ send_location: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' edit_message: @@ -165,7 +165,7 @@ edit_message: description: Disables link previews for links in the message. example: true inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' edit_caption: @@ -181,7 +181,7 @@ edit_caption: description: Message body of the notification. example: The garage door has been open for 10 minutes. inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' edit_replymarkup: @@ -194,7 +194,7 @@ edit_replymarkup: description: The chat_id where to edit the reply_markup. example: 12345 inline_keyboard: - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' answer_callback_query: diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 9b80581b85e..abfd353e1f4 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -219,7 +219,7 @@ def get_config_value(node, value_index, tries=5): and value.index == value_index): return value.data except RuntimeError: - # If we get an runtime error the dict has changed while + # If we get a runtime error the dict has changed while # we was looking for a value, just do it again return None if tries <= 0 else get_config_value( node, value_index, tries=tries - 1) diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index ba8e177c9f7..61855143d59 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -4,7 +4,7 @@ change_association: description: Change an association in the Z-Wave network. fields: association: - description: Specify add or remove assosication + description: Specify add or remove association example: add node_id: description: Node id of the node to set association for. @@ -30,14 +30,14 @@ heal_network: description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress. fields: return_routes: - description: Wheter or not to update the return routes from the nodes to the controller. Defaults to False. + description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. example: True heal_node: description: Start a Z-Wave node heal. Refer to OZW.log for progress. fields: return_routes: - description: Wheter or not to update the return routes from the node to the controller. Defaults to False. + description: Whether or not to update the return routes from the node to the controller. Defaults to False. example: True remove_node: diff --git a/homeassistant/core.py b/homeassistant/core.py index 18cf40d3854..b1cf9c51efd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -66,7 +66,7 @@ def valid_entity_id(entity_id: str) -> bool: def valid_state(state: str) -> bool: - """Test if an state is valid.""" + """Test if a state is valid.""" return len(state) < 256 @@ -777,7 +777,7 @@ class ServiceCall(object): self.call_id = call_id def __repr__(self): - """Return the represenation of the service.""" + """Return the representation of the service.""" if self.data: return "".format( self.domain, self.service, util.repr_helper(self.data)) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e3816fdaa6f..5b5c674c32b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -268,7 +268,7 @@ class Entity(object): self.entity_id, state, attr, self.force_update) def schedule_update_ha_state(self, force_refresh=False): - """Schedule a update ha state change task. + """Schedule an update ha state change task. That avoid executor dead looks. """ @@ -276,7 +276,7 @@ class Entity(object): @callback def async_schedule_update_ha_state(self, force_refresh=False): - """Schedule a update ha state change task.""" + """Schedule an update ha state change task.""" self.hass.async_add_job(self.async_update_ha_state(force_refresh)) @asyncio.coroutine diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 6cd1916d4c2..e74881e6e89 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -119,7 +119,7 @@ track_template = threaded_listener_factory(async_track_template) @bind_hass def async_track_same_state(hass, period, action, async_check_same_func, entity_ids=MATCH_ALL): - """Track the state of entities for a period and run a action. + """Track the state of entities for a period and run an action. If async_check_func is None it use the state of orig_value. Without entity_ids we track all state changes. diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ac20f94d243..4b9eec3d540 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -148,7 +148,7 @@ def get_component(comp_name) -> Optional[ModuleType]: # a namespace. We do not care about namespaces. # This prevents that when only # custom_components/switch/some_platform.py exists, - # the import custom_components.switch would succeeed. + # the import custom_components.switch would succeed. if module.__spec__.origin == 'namespace': continue diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index cb3ebeb7ee6..c4fea2846c5 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -227,7 +227,7 @@ class OrderedSet(MutableSet): return '%s(%r)' % (self.__class__.__name__, list(self)) def __eq__(self, other): - """Return the comparision.""" + """Return the comparison.""" if isinstance(other, OrderedSet): return len(self) == len(other) and list(self) == list(other) return set(self) == set(other) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 7daaf937975..8a15c4f6320 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -23,7 +23,7 @@ class HideSensitiveDataFilter(logging.Filter): # pylint: disable=invalid-name class AsyncHandler(object): - """Logging handler wrapper to add a async layer.""" + """Logging handler wrapper to add an async layer.""" def __init__(self, loop, handler): """Initialize async logging handler wrapper.""" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 70e95dd7b93..87612da9faa 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -83,7 +83,7 @@ class TestGetImage(object): @patch('homeassistant.components.camera.demo.DemoCamera.camera_image', autospec=True, return_value=b'Test') def test_get_image_from_camera(self, mock_camera): - """Grab a image from camera entity.""" + """Grab an image from camera entity.""" self.hass.start() image = run_coroutine_threadsafe(camera.async_get_image( diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py index b378118141a..8bc3a60146c 100644 --- a/tests/components/device_tracker/test_unifi_direct.py +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -139,12 +139,12 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): devices = scanner._get_update() # pylint: disable=protected-access self.assertTrue(devices is None) - def test_good_reponse_parses(self): + def test_good_response_parses(self): """Test that the response form the AP parses to JSON correctly.""" response = _response_to_json(load_fixture('unifi_direct.txt')) self.assertTrue(response != {}) - def test_bad_reponse_returns_none(self): + def test_bad_response_returns_none(self): """Test that a bad response form the AP parses to JSON correctly.""" self.assertTrue(_response_to_json("{(}") == {}) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 0d87b491229..9fc35bc17b1 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -304,7 +304,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): @asyncio.coroutine def test_execute_request(hass_fixture, assistant_client): - """Test a execute request.""" + """Test an execute request.""" reqid = '5711642932632160985' data = { 'requestId': diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 0594c436abd..b0bb7d77e3c 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -82,7 +82,7 @@ class TestImageProcessing(object): @patch('homeassistant.components.camera.demo.DemoCamera.camera_image', autospec=True, return_value=b'Test') def test_get_image_from_camera(self, mock_camera): - """Grab a image from camera entity.""" + """Grab an image from camera entity.""" self.hass.start() ip.scan(self.hass, entity_id='image_processing.test') diff --git a/tests/components/light/test_litejet.py b/tests/components/light/test_litejet.py index 001c419066f..dd4b4b4a56e 100644 --- a/tests/components/light/test_litejet.py +++ b/tests/components/light/test_litejet.py @@ -156,7 +156,7 @@ class TestLiteJetLight(unittest.TestCase): # (Requesting the level is not strictly needed with a deactivated # event but the implementation happens to do it. This could be - # changed to a assert_not_called in the future.) + # changed to an assert_not_called in the future.) self.mock_lj.get_load_level.assert_called_with( ENTITY_OTHER_LIGHT_NUMBER) diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 815204e718a..d3ebc67931f 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -152,7 +152,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch('soco.discover') def test_ensure_setup_config_interface_addr(self, discover_mock, *args): - """Test a interface address config'd by the HASS config file.""" + """Test an interface address config'd by the HASS config file.""" discover_mock.return_value = {SoCoMock('192.0.2.1')} config = { @@ -172,7 +172,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('soco.discover') def test_ensure_setup_config_advertise_addr(self, discover_mock, *args): - """Test a advertise address config'd by the HASS config file.""" + """Test an advertise address config'd by the HASS config file.""" discover_mock.return_value = {SoCoMock('192.0.2.1')} config = { diff --git a/tests/components/media_player/test_yamaha.py b/tests/components/media_player/test_yamaha.py index 3322f6021e7..e17241485db 100644 --- a/tests/components/media_player/test_yamaha.py +++ b/tests/components/media_player/test_yamaha.py @@ -44,7 +44,7 @@ class TestYamahaMediaPlayer(unittest.TestCase): self.hass.stop() def enable_output(self, port, enabled): - """Enable ouput on a specific port.""" + """Enable output on a specific port.""" data = { 'entity_id': 'media_player.yamaha_receiver_main_zone', 'port': port, diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 7c558f2803d..5ac9b3adb81 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -25,7 +25,7 @@ def create_engine_test(*args, **kwargs): @asyncio.coroutine def test_schema_update_calls(hass): - """Test that schema migrations occurr in correct order.""" + """Test that schema migrations occur in correct order.""" with patch('sqlalchemy.create_engine', new=create_engine_test), \ patch('homeassistant.components.recorder.migration._apply_update') as \ update: diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 86e637ab1ae..e5fca461a23 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -110,7 +110,7 @@ def test_derivative(): yield from entity.async_update() assert entity.state == STATE_UNKNOWN, \ - 'state after first update shoudl still be unknown' + 'state after first update should still be unknown' entity.telegram = { '1.0.0': MBusObject([ diff --git a/tests/components/test_dialogflow.py b/tests/components/test_dialogflow.py index a52c841e0cc..0acf0833543 100644 --- a/tests/components/test_dialogflow.py +++ b/tests/components/test_dialogflow.py @@ -435,7 +435,7 @@ class TestDialogflow(unittest.TestCase): self.assertEqual("virgo", call.data.get("hello")) def test_intent_with_no_action(self): - """Test a intent with no defined action.""" + """Test an intent with no defined action.""" data = { "id": REQUEST_ID, "timestamp": REQUEST_TIMESTAMP, @@ -480,7 +480,7 @@ class TestDialogflow(unittest.TestCase): "You have not defined an action in your Dialogflow intent.", text) def test_intent_with_unknown_action(self): - """Test a intent with an action not defined in the conf.""" + """Test an intent with an action not defined in the conf.""" data = { "id": REQUEST_ID, "timestamp": REQUEST_TIMESTAMP, diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py index e08229631cf..5b4ef4dcf53 100644 --- a/tests/components/tts/test_yandextts.py +++ b/tests/components/tts/test_yandextts.py @@ -224,7 +224,7 @@ class TestTTSYandexPlatform(object): assert len(calls) == 0 - def test_service_say_specifed_speaker(self, aioclient_mock): + def test_service_say_specified_speaker(self, aioclient_mock): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) @@ -259,7 +259,7 @@ class TestTTSYandexPlatform(object): assert len(aioclient_mock.mock_calls) == 1 assert len(calls) == 1 - def test_service_say_specifed_emotion(self, aioclient_mock): + def test_service_say_specified_emotion(self, aioclient_mock): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3109ea776bc..40a0f8be3b2 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -130,7 +130,7 @@ class TestHelpersEntityComponent(unittest.TestCase): assert poll_ent.async_update.called def test_polling_updates_entities_with_exception(self): - """Test the updated entities that not break with a exception.""" + """Test the updated entities that not break with an exception.""" component = EntityComponent( _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) @@ -663,7 +663,7 @@ def test_raise_error_on_update(hass): entity2 = EntityTest(name='test_2') def _raise(): - """Helper to raise a exception.""" + """Helper to raise an exception.""" raise AssertionError entity1.update = _raise diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 9047f26b2d1..c109ae30aad 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -17,7 +17,7 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) _LOGGER = logging.getLogger(__name__) -# prevent .HA_VERISON file from being written +# prevent .HA_VERSION file from being written @patch( 'homeassistant.bootstrap.conf_util.process_ha_config_upgrade', Mock()) @patch('homeassistant.util.location.detect_location_info', diff --git a/tests/test_core.py b/tests/test_core.py index ea952a7c073..90a72a48a10 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -172,7 +172,7 @@ class TestHomeAssistant(unittest.TestCase): assert len(call_count) == 2 def test_async_add_job_pending_tasks_executor(self): - """Run a executor in pending tasks.""" + """Run an executor in pending tasks.""" call_count = [] def test_executor(): diff --git a/tests/test_remote.py b/tests/test_remote.py index 41011794914..9aa730d6eb6 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -28,7 +28,7 @@ def _url(path=''): # pylint: disable=invalid-name def setUpModule(): - """Initalization of a Home Assistant server instance.""" + """Initialization of a Home Assistant server instance.""" global hass, master_api hass = get_test_home_assistant() diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 38b957ad102..734f4b548b9 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -48,7 +48,7 @@ class TestYaml(unittest.TestCase): load_yaml_config_file(YAML_CONFIG_FILE) def test_no_key(self): - """Test item without an key.""" + """Test item without a key.""" files = {YAML_CONFIG_FILE: 'a: a\nnokeyhere'} with self.assertRaises(HomeAssistantError), \ patch_yaml_files(files): From d8fde947632db90bd95c0a2dc812900f9e626c8a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 27 Jan 2018 20:58:52 +0100 Subject: [PATCH 019/166] Upgrade sqlalchemy to 1.2.2 (#11956) --- homeassistant/components/recorder/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 1c9524223e5..e19bcaaddfc 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -34,7 +34,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.1'] +REQUIREMENTS = ['sqlalchemy==1.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 67fac8e89d8..036c2e445c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1114,7 +1114,7 @@ speedtest-cli==1.0.7 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.2.1 +sqlalchemy==1.2.2 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f21a20f7439..0541ce834f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.2.1 +sqlalchemy==1.2.2 # homeassistant.components.statsd statsd==3.2.1 From 63ae275182e5b50234456f62f8265b743d3bf69e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 27 Jan 2018 20:59:07 +0100 Subject: [PATCH 020/166] Upgrade youtube_dl to 2018.01.21 (#11955) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index de56c5140e9..f712007ccec 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.01.14'] +REQUIREMENTS = ['youtube_dl==2018.01.21'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 036c2e445c0..d49d6957729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ yeelight==0.3.3 yeelightsunflower==0.0.8 # homeassistant.components.media_extractor -youtube_dl==2018.01.14 +youtube_dl==2018.01.21 # homeassistant.components.light.zengge zengge==0.2 From 0c008663adb8fe35ab2e4dd15df8f723ec2b48fc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 27 Jan 2018 20:59:19 +0100 Subject: [PATCH 021/166] Upgrade sphinx-autodoc-typehints to 1.2.4 (#11954) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 04ebb074e03..5041699e03b 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ Sphinx==1.6.6 -sphinx-autodoc-typehints==1.2.3 +sphinx-autodoc-typehints==1.2.4 sphinx-autodoc-annotation==1.0.post1 From f08fd8182ce385a88cbb7cf8f3f45d293a080d40 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 27 Jan 2018 20:59:33 +0100 Subject: [PATCH 022/166] Upgrade coinmarketcap to 4.2.1 (#11953) --- homeassistant/components/sensor/coinmarketcap.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 9d69583f673..cf3f0796df3 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['coinmarketcap==4.1.2'] +REQUIREMENTS = ['coinmarketcap==4.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d49d6957729..f94626e8671 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ ciscosparkapi==0.4.2 coinbase==2.0.6 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.1.2 +coinmarketcap==4.2.1 # homeassistant.scripts.check_config colorlog==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0541ce834f1..9bb41e09c24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -38,7 +38,7 @@ apns2==0.3.0 caldav==0.5.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.1.2 +coinmarketcap==4.2.1 # homeassistant.components.device_tracker.upc_connect defusedxml==0.5.0 From f43234b5330dc2e0c230004f3bf805228e7b7694 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 27 Jan 2018 21:02:55 +0100 Subject: [PATCH 023/166] Bump dev to 0.63.0.dev0 (#11952) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 560c99bb653..a8449bf38c2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 62 +MINOR_VERSION = 63 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 1419005082fb328b56a856aa48331e6a4cd08602 Mon Sep 17 00:00:00 2001 From: MGWGIT Date: Sun, 28 Jan 2018 00:01:48 +0300 Subject: [PATCH 024/166] Update xiaomi_aqara.py (#11969) Sensor can measure temperature below -20, but maybe not so accurate, but no need to discard measurements. --- homeassistant/components/sensor/xiaomi_aqara.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index f5c20fa5a1c..c2498d88822 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -71,7 +71,7 @@ class XiaomiSensor(XiaomiDevice): value /= 100 elif self._data_key in ['illumination']: value = max(value - 300, 0) - if self._data_key == 'temperature' and (value < -20 or value > 60): + if self._data_key == 'temperature' and (value < -50 or value > 60): return False elif self._data_key == 'humidity' and (value <= 0 or value > 100): return False From 2d3034be11eb5cc2ded7d658253da9ba3c195b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 28 Jan 2018 10:15:39 +0200 Subject: [PATCH 025/166] panasonic_viera: Set device name from discovery info (#11990) --- homeassistant/components/media_player/panasonic_viera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 8c946ec0f0f..51d804324d7 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -51,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info: _LOGGER.debug('%s', discovery_info) + name = discovery_info.get('name') host = discovery_info.get('host') port = discovery_info.get('port') remote = RemoteControl(host, port) From a3fc2c7fee5ff10d961a39956952babf1a155d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 28 Jan 2018 10:16:06 +0200 Subject: [PATCH 026/166] Update panasonic_viera to 0.3 (#11989) --- homeassistant/components/media_player/panasonic_viera.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 51d804324d7..3e5e80d7545 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['panasonic_viera==0.2', +REQUIREMENTS = ['panasonic_viera==0.3', 'wakeonlan==0.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f94626e8671..9b4d6ba5461 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -551,7 +551,7 @@ orvibo==1.1.1 paho-mqtt==1.3.1 # homeassistant.components.media_player.panasonic_viera -panasonic_viera==0.2 +panasonic_viera==0.3 # homeassistant.components.media_player.dunehd pdunehd==1.3 From 3c869c6ed6da24f1c5c612106bcd391a45b78234 Mon Sep 17 00:00:00 2001 From: Emil Stjerneman Date: Sun, 28 Jan 2018 10:50:23 +0100 Subject: [PATCH 027/166] Fix 11982 - uvc don't handle null as username (#11984) --- homeassistant/components/camera/uvc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index 8d79fa04a9a..f7dc4cfd973 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -127,6 +127,9 @@ class UnifiVideoCamera(Camera): else: client_cls = uvc_camera.UVCCameraClient + if caminfo['username'] is None: + caminfo['username'] = 'ubnt' + camera = None for addr in addrs: try: From 62d4f23833622939822a34248599d1309d5144b2 Mon Sep 17 00:00:00 2001 From: "Craig J. Ward" Date: Sun, 28 Jan 2018 07:04:40 -0600 Subject: [PATCH 028/166] Add Goalfeed platform (#11098) * add goalfeed * use pysher. begin auth * auth! * update params * cleanup * update library and gen requirements * unused imports * case-sensitive * crazy train * docstrings and some other fixes * remove logging * unused imports * import in setup * move import * Update based on notes * add timeout * It's only a component * Update docstrings --- .coveragerc | 1 + homeassistant/components/goalfeed.py | 62 ++++++++++++++++++++++++++++ requirements_all.txt | 3 ++ 3 files changed, 66 insertions(+) create mode 100644 homeassistant/components/goalfeed.py diff --git a/.coveragerc b/.coveragerc index e4b73df1a8f..3529e7413ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -377,6 +377,7 @@ omit = homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py homeassistant/components/foursquare.py + homeassistant/components/goalfeed.py homeassistant/components/ifttt.py homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py new file mode 100644 index 00000000000..d31a0b5b51a --- /dev/null +++ b/homeassistant/components/goalfeed.py @@ -0,0 +1,62 @@ +""" +Component for the Goalfeed service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/goalfeed/ +""" +import json + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +REQUIREMENTS = ['pysher==0.2.0'] + +DOMAIN = 'goalfeed' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +GOALFEED_HOST = 'feed.goalfeed.ca' +GOALFEED_AUTH_ENDPOINT = 'https://goalfeed.ca/feed/auth' +GOALFEED_APP_ID = 'bfd4ed98c1ff22c04074' + + +def setup(hass, config): + """Set up the Goalfeed component.""" + import pysher + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + def goal_handler(data): + """Handle goal events.""" + goal = json.loads(json.loads(data)) + + hass.bus.fire('goal', event_data=goal) + + def connect_handler(data): + """Handle connection.""" + post_data = { + 'username': username, + 'password': password, + 'connection_info': data} + resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, + timeout=30).json() + + channel = pusher.subscribe('private-goals', resp['auth']) + channel.bind('goalfeed_goal', goal_handler) + + pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080, + custom_host=GOALFEED_HOST, timeout=30) + + pusher.connection.bind('pusher:connection_established', connect_handler) + pusher.connect() + + return True diff --git a/requirements_all.txt b/requirements_all.txt index 9b4d6ba5461..0528871c05b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,6 +851,9 @@ pyserial==3.1.1 # homeassistant.components.lock.sesame pysesame==0.1.0 +# homeassistant.components.goalfeed +pysher==0.2.0 + # homeassistant.components.sensor.sma pysma==0.1.3 From 336bdb18899c46ce2eb847a38d9db3aa92b98bab Mon Sep 17 00:00:00 2001 From: Kevin Goff Date: Sun, 28 Jan 2018 09:50:43 -0500 Subject: [PATCH 029/166] Added support for hourly percent change in coinmarketcap component (#11996) * Added support for hourly percent change (percent_change_1h) * Fixed display of 1h percent change --- homeassistant/components/sensor/coinmarketcap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index cf3f0796df3..f8ada07eec6 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -26,6 +26,7 @@ ATTR_MARKET_CAP = 'market_cap' ATTR_NAME = 'name' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' +ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' ATTR_PRICE = 'price' ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' @@ -105,6 +106,7 @@ class CoinMarketCapSensor(Entity): 'market_cap_{}'.format(self.data.display_currency)), ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), + ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'), ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), } From 96f9a125415fb06a755f3cb4e1517802c92b1c2e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 28 Jan 2018 18:04:40 +0100 Subject: [PATCH 030/166] Upgrade coinbase to 2.0.7 (#11992) --- homeassistant/components/coinbase.py | 16 +++++----- homeassistant/components/sensor/coinbase.py | 33 +++++++++++---------- requirements_all.txt | 2 +- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py index bdb091325cf..10123752c99 100644 --- a/homeassistant/components/coinbase.py +++ b/homeassistant/components/coinbase.py @@ -5,16 +5,17 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/coinbase/ """ from datetime import timedelta - import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY -from homeassistant.util import Throttle +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 = ['coinbase==2.0.6'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'coinbase' @@ -46,14 +47,13 @@ def setup(hass, config): api_secret = config[DOMAIN].get(CONF_API_SECRET) exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES) - hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key, - api_secret) + hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData( + api_key, api_secret) if not hasattr(coinbase_data, 'accounts'): return False for account in coinbase_data.accounts.data: - load_platform(hass, 'sensor', DOMAIN, - {'account': account}, config) + load_platform(hass, 'sensor', DOMAIN, {'account': account}, config) for currency in exchange_currencies: if currency not in coinbase_data.exchange_rates.rates: _LOGGER.warning("Currency %s not found", currency) diff --git a/homeassistant/components/sensor/coinbase.py b/homeassistant/components/sensor/coinbase.py index d66c7d4e4b6..32e1d8f211a 100644 --- a/homeassistant/components/sensor/coinbase.py +++ b/homeassistant/components/sensor/coinbase.py @@ -4,21 +4,22 @@ Support for Coinbase sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.coinbase/ """ -from homeassistant.helpers.entity import Entity from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity - -DEPENDENCIES = ['coinbase'] - -DATA_COINBASE = 'coinbase_cache' - -CONF_ATTRIBUTION = "Data provided by coinbase.com" ATTR_NATIVE_BALANCE = "Balance in native currency" BTC_ICON = 'mdi:currency-btc' -ETH_ICON = 'mdi:currency-eth' + COIN_ICON = 'mdi:coin' +CONF_ATTRIBUTION = "Data provided by coinbase.com" + +DATA_COINBASE = 'coinbase_cache' +DEPENDENCIES = ['coinbase'] + +ETH_ICON = 'mdi:currency-eth' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Coinbase sensors.""" @@ -26,13 +27,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return if 'account' in discovery_info: account = discovery_info['account'] - sensor = AccountSensor(hass.data[DATA_COINBASE], - account['name'], - account['balance']['currency']) + sensor = AccountSensor( + hass.data[DATA_COINBASE], account['name'], + account['balance']['currency']) if 'exchange_currency' in discovery_info: - sensor = ExchangeRateSensor(hass.data[DATA_COINBASE], - discovery_info['exchange_currency'], - discovery_info['native_currency']) + sensor = ExchangeRateSensor( + hass.data[DATA_COINBASE], discovery_info['exchange_currency'], + discovery_info['native_currency']) add_devices([sensor], True) @@ -78,8 +79,8 @@ class AccountSensor(Entity): """Return the state attributes of the sensor.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_NATIVE_BALANCE: "{} {}".format(self._native_balance, - self._native_currency) + ATTR_NATIVE_BALANCE: "{} {}".format( + self._native_balance, self._native_currency), } def update(self): diff --git a/requirements_all.txt b/requirements_all.txt index 0528871c05b..ce83183a89a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ caldav==0.5.0 ciscosparkapi==0.4.2 # homeassistant.components.coinbase -coinbase==2.0.6 +coinbase==2.0.7 # homeassistant.components.sensor.coinmarketcap coinmarketcap==4.2.1 From c7efe5b7dd004240435901d0aa859db659cd0edd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 28 Jan 2018 18:04:54 +0100 Subject: [PATCH 031/166] Upgrade pyota to 2.0.4 (#11991) --- homeassistant/components/iota.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py index 237493c7919..442be6e22e7 100644 --- a/homeassistant/components/iota.py +++ b/homeassistant/components/iota.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyota==2.0.3'] +REQUIREMENTS = ['pyota==2.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ce83183a89a..61dc9ca0681 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -821,7 +821,7 @@ pynut2==2.1.2 pynx584==0.4 # homeassistant.components.iota -pyota==2.0.3 +pyota==2.0.4 # homeassistant.components.sensor.otp pyotp==2.2.6 From b3bf6c4be2dfac32d8b67dbb1c9fc4f040e12d16 Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Sun, 28 Jan 2018 15:30:46 -0800 Subject: [PATCH 032/166] Fixed Canary temperature sensor and remapped air quality value (#11355) * Fixed Canary temperature sensor and remapped air quality value * Addressed review comment * - Fixed canary tests and added more tests - Removed py-canary requirements from tests * Noop to trigger a build again * - Removed py-canary requirements from tests * Addressed PR comment * - Updated tests - Removed py-canary from gen_requirements_all.py * - Fixed hound violation * Added back py-canary to gen_requirements_all.py as it's still need in tests * Added back py-canary to test requirements as it's still need in tests * Address PR comment --- homeassistant/components/canary.py | 7 ++ homeassistant/components/sensor/canary.py | 74 +++++++++---- tests/components/sensor/test_canary.py | 120 +++++++++++++++------- 3 files changed, 145 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py index 8ab7218e201..4d45f31ae59 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary.py @@ -111,6 +111,13 @@ class CanaryData(object): """Return a list of readings based on device_id.""" return self._readings_by_device_id.get(device_id, []) + def get_reading(self, device_id, sensor_type): + """Return reading for device_id and sensor type.""" + readings = self._readings_by_device_id.get(device_id, []) + return next(( + reading.value for reading in readings + if reading.sensor_type == sensor_type), None) + def set_location_mode(self, location_id, mode_name, is_private=False): """Set location mode.""" self._api.set_location_mode(location_id, mode_name, is_private) diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py index b0d2c27ae5d..56da1c4deea 100644 --- a/homeassistant/components/sensor/canary.py +++ b/homeassistant/components/sensor/canary.py @@ -4,13 +4,27 @@ Support for Canary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.canary/ """ + from homeassistant.components.canary import DATA_CANARY -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity DEPENDENCIES = ['canary'] -SENSOR_VALUE_PRECISION = 1 +SENSOR_VALUE_PRECISION = 2 +ATTR_AIR_QUALITY = "air_quality" + +# Sensor types are defined like so: +# sensor type name, unit_of_measurement, icon +SENSOR_TYPES = [ + ["temperature", TEMP_CELSIUS, "mdi:thermometer"], + ["humidity", "%", "mdi:water-percent"], + ["air_quality", None, "mdi:weather-windy"], +] + +STATE_AIR_QUALITY_NORMAL = "normal" +STATE_AIR_QUALITY_ABNORMAL = "abnormal" +STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" def setup_platform(hass, config, add_devices, discovery_info=None): @@ -18,11 +32,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data = hass.data[DATA_CANARY] devices = [] - from canary.api import SensorType for location in data.locations: for device in location.devices: if device.is_online: - for sensor_type in SensorType: + for sensor_type in SENSOR_TYPES: devices.append(CanarySensor(data, sensor_type, location, device)) @@ -37,10 +50,9 @@ class CanarySensor(Entity): self._data = data self._sensor_type = sensor_type self._device_id = device.device_id - self._is_celsius = location.is_celsius self._sensor_value = None - sensor_type_name = sensor_type.value.replace("_", " ").title() + sensor_type_name = sensor_type[0].replace("_", " ").title() self._name = '{} {} {}'.format(location.name, device.name, sensor_type_name) @@ -59,27 +71,51 @@ class CanarySensor(Entity): def unique_id(self): """Return the unique ID of this sensor.""" return "sensor_canary_{}_{}".format(self._device_id, - self._sensor_type.value) + self._sensor_type[0]) @property def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - from canary.api import SensorType - if self._sensor_type == SensorType.TEMPERATURE: - return TEMP_CELSIUS if self._is_celsius else TEMP_FAHRENHEIT - elif self._sensor_type == SensorType.HUMIDITY: - return "%" - elif self._sensor_type == SensorType.AIR_QUALITY: - return "" + """Return the unit of measurement.""" + return self._sensor_type[1] + + @property + def icon(self): + """Icon for the sensor.""" + return self._sensor_type[2] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._sensor_type[0] == "air_quality" \ + and self._sensor_value is not None: + air_quality = None + if self._sensor_value <= .4: + air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL + elif self._sensor_value <= .59: + air_quality = STATE_AIR_QUALITY_ABNORMAL + elif self._sensor_value <= 1.0: + air_quality = STATE_AIR_QUALITY_NORMAL + + return { + ATTR_AIR_QUALITY: air_quality + } + return None def update(self): """Get the latest state of the sensor.""" self._data.update() - readings = self._data.get_readings(self._device_id) - value = next(( - reading.value for reading in readings - if reading.sensor_type == self._sensor_type), None) + from canary.api import SensorType + canary_sensor_type = None + if self._sensor_type[0] == "air_quality": + canary_sensor_type = SensorType.AIR_QUALITY + elif self._sensor_type[0] == "temperature": + canary_sensor_type = SensorType.TEMPERATURE + elif self._sensor_type[0] == "humidity": + canary_sensor_type = SensorType.HUMIDITY + + value = self._data.get_reading(self._device_id, canary_sensor_type) + if value is not None: self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION) diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py index b35b5630d60..79e2bf4ee35 100644 --- a/tests/components/sensor/test_canary.py +++ b/tests/components/sensor/test_canary.py @@ -3,13 +3,13 @@ import copy import unittest from unittest.mock import Mock -from canary.api import SensorType from homeassistant.components.canary import DATA_CANARY from homeassistant.components.sensor import canary -from homeassistant.components.sensor.canary import CanarySensor +from homeassistant.components.sensor.canary import CanarySensor, \ + SENSOR_TYPES, ATTR_AIR_QUALITY, STATE_AIR_QUALITY_NORMAL, \ + STATE_AIR_QUALITY_ABNORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL from tests.common import (get_test_home_assistant) -from tests.components.test_canary import mock_device, mock_reading, \ - mock_location +from tests.components.test_canary import mock_device, mock_location VALID_CONFIG = { "canary": { @@ -55,38 +55,33 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual(6, len(self.DEVICES)) - def test_celsius_temperature_sensor(self): - """Test temperature sensor with celsius.""" - device = mock_device(10, "Family Room") - location = mock_location("Home", True) - - data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.TEMPERATURE, 21.1234)] - - sensor = CanarySensor(data, SensorType.TEMPERATURE, location, device) - sensor.update() - - self.assertEqual("Home Family Room Temperature", sensor.name) - self.assertEqual("sensor_canary_10_temperature", sensor.unique_id) - self.assertEqual("°C", sensor.unit_of_measurement) - self.assertEqual(21.1, sensor.state) - - def test_fahrenheit_temperature_sensor(self): + def test_temperature_sensor(self): """Test temperature sensor with fahrenheit.""" device = mock_device(10, "Family Room") location = mock_location("Home", False) data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.TEMPERATURE, 21.1567)] + data.get_reading.return_value = 21.1234 - sensor = CanarySensor(data, SensorType.TEMPERATURE, location, device) + sensor = CanarySensor(data, SENSOR_TYPES[0], location, device) sensor.update() self.assertEqual("Home Family Room Temperature", sensor.name) - self.assertEqual("°F", sensor.unit_of_measurement) - self.assertEqual(21.2, sensor.state) + self.assertEqual("°C", sensor.unit_of_measurement) + self.assertEqual(21.12, sensor.state) + + def test_temperature_sensor_with_none_sensor_value(self): + """Test temperature sensor with fahrenheit.""" + device = mock_device(10, "Family Room") + location = mock_location("Home", False) + + data = Mock() + data.get_reading.return_value = None + + sensor = CanarySensor(data, SENSOR_TYPES[0], location, device) + sensor.update() + + self.assertEqual(None, sensor.state) def test_humidity_sensor(self): """Test humidity sensor.""" @@ -94,28 +89,79 @@ class TestCanarySensorSetup(unittest.TestCase): location = mock_location("Home") data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.HUMIDITY, 50.4567)] + data.get_reading.return_value = 50.4567 - sensor = CanarySensor(data, SensorType.HUMIDITY, location, device) + sensor = CanarySensor(data, SENSOR_TYPES[1], location, device) sensor.update() self.assertEqual("Home Family Room Humidity", sensor.name) self.assertEqual("%", sensor.unit_of_measurement) - self.assertEqual(50.5, sensor.state) + self.assertEqual(50.46, sensor.state) - def test_air_quality_sensor(self): + def test_air_quality_sensor_with_very_abnormal_reading(self): """Test air quality sensor.""" device = mock_device(10, "Family Room") location = mock_location("Home") data = Mock() - data.get_readings.return_value = [ - mock_reading(SensorType.AIR_QUALITY, 50.4567)] + data.get_reading.return_value = 0.4 - sensor = CanarySensor(data, SensorType.AIR_QUALITY, location, device) + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) sensor.update() self.assertEqual("Home Family Room Air Quality", sensor.name) - self.assertEqual("", sensor.unit_of_measurement) - self.assertEqual(50.5, sensor.state) + self.assertEqual(None, sensor.unit_of_measurement) + self.assertEqual(0.4, sensor.state) + + air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] + self.assertEqual(STATE_AIR_QUALITY_VERY_ABNORMAL, air_quality) + + def test_air_quality_sensor_with_abnormal_reading(self): + """Test air quality sensor.""" + device = mock_device(10, "Family Room") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = 0.59 + + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) + sensor.update() + + self.assertEqual("Home Family Room Air Quality", sensor.name) + self.assertEqual(None, sensor.unit_of_measurement) + self.assertEqual(0.59, sensor.state) + + air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] + self.assertEqual(STATE_AIR_QUALITY_ABNORMAL, air_quality) + + def test_air_quality_sensor_with_normal_reading(self): + """Test air quality sensor.""" + device = mock_device(10, "Family Room") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = 1.0 + + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) + sensor.update() + + self.assertEqual("Home Family Room Air Quality", sensor.name) + self.assertEqual(None, sensor.unit_of_measurement) + self.assertEqual(1.0, sensor.state) + + air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] + self.assertEqual(STATE_AIR_QUALITY_NORMAL, air_quality) + + def test_air_quality_sensor_with_none_sensor_value(self): + """Test air quality sensor.""" + device = mock_device(10, "Family Room") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = None + + sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) + sensor.update() + + self.assertEqual(None, sensor.state) + self.assertEqual(None, sensor.device_state_attributes) From 84711aad90e270ad79e0e3255d530235a3e457cb Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Mon, 29 Jan 2018 00:43:27 +0000 Subject: [PATCH 033/166] Refactor Alexa Smart Home API (#12016) Having an object per interface will make it easier to support properties. Ideally, properties are reported in context in all responses. However current implementation reports them only in response to a ReportState request. This seems to work sufficiently. As long as the device is opened in the Alexa app, Amazon will poll the device state every few seconds with a ReportState request. --- homeassistant/components/alexa/smart_home.py | 379 +++++++++++++------ tests/components/alexa/test_smart_home.py | 28 +- 2 files changed, 283 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index a24583d8247..e09ee751e43 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -40,6 +40,7 @@ CONF_DESCRIPTION = 'description' CONF_DISPLAY_CATEGORIES = 'display_categories' HANDLERS = Registry() +ENTITY_ADAPTERS = Registry() class _DisplayCategory(object): @@ -133,10 +134,36 @@ def _capability(interface, return result -class _EntityCapabilities(object): +class _UnsupportedInterface(Exception): + """This entity does not support the requested Smart Home API interface.""" + + +class _UnsupportedProperty(Exception): + """This entity does not support the requested Smart Home API property.""" + + +class _AlexaEntity(object): + """An adaptation of an entity, expressed in Alexa's terms. + + The API handlers should manipulate entities only through this interface. + """ + def __init__(self, config, entity): self.config = config self.entity = entity + self.entity_conf = config.entity_config.get(entity.entity_id, {}) + + def friendly_name(self): + """Return the Alexa API friendly name.""" + return self.entity_conf.get(CONF_NAME, self.entity.name) + + def description(self): + """Return the Alexa API description.""" + return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id) + + def entity_id(self): + """Return the Alexa API entity id.""" + return self.entity.entity_id.replace('.', '#') def display_categories(self): """Return a list of display categories.""" @@ -154,17 +181,166 @@ class _EntityCapabilities(object): """ raise NotImplementedError - def capabilities(self): - """Return a list of supported capabilities. + def get_interface(self, capability): + """Return the given _AlexaInterface. - If the returned list is empty, the entity will not be discovered. + Raises _UnsupportedInterface. + """ + pass - You might find _capability() useful. + def interfaces(self): + """Return a list of supported interfaces. + + Used for discovery. The list should contain _AlexaInterface instances. + If the list is empty, this entity will not be discovered. """ raise NotImplementedError -class _GenericCapabilities(_EntityCapabilities): +class _AlexaInterface(object): + def __init__(self, entity): + self.entity = entity + + def name(self): + """Return the Alexa API name of this interface.""" + raise NotImplementedError + + @staticmethod + def properties_supported(): + """Return what properties this entity supports.""" + return [] + + @staticmethod + def properties_proactively_reported(): + """Return True if properties asynchronously reported.""" + return False + + @staticmethod + def properties_retrievable(): + """Return True if properties can be retrieved.""" + return False + + @staticmethod + def get_property(name): + """Read and return a property. + + Return value should be a dict, or raise _UnsupportedProperty. + + Properties can also have a timeOfSample and uncertaintyInMilliseconds, + but returning those metadata is not yet implemented. + """ + raise _UnsupportedProperty(name) + + @staticmethod + def supports_deactivation(): + """Applicable only to scenes.""" + return None + + def serialize_discovery(self): + """Serialize according to the Discovery API.""" + result = { + 'type': 'AlexaInterface', + 'interface': self.name(), + 'version': '3', + 'properties': { + 'supported': self.properties_supported(), + 'proactivelyReported': self.properties_proactively_reported(), + }, + # XXX this is incorrect, but the tests assert it + 'retrievable': self.properties_retrievable(), + } + + # pylint: disable=assignment-from-none + supports_deactivation = self.supports_deactivation() + if supports_deactivation is not None: + result['supportsDeactivation'] = supports_deactivation + return result + + def serialize_properties(self): + """Return properties serialized for an API response.""" + for prop in self.properties_supported(): + prop_name = prop['name'] + yield { + 'name': prop_name, + 'namespace': self.name(), + 'value': self.get_property(prop_name), + } + + +class _AlexaPowerController(_AlexaInterface): + def name(self): + return 'Alexa.PowerController' + + +class _AlexaLockController(_AlexaInterface): + def name(self): + return 'Alexa.LockController' + + +class _AlexaSceneController(_AlexaInterface): + def __init__(self, entity, supports_deactivation): + _AlexaInterface.__init__(self, entity) + self.supports_deactivation = lambda: supports_deactivation + + def name(self): + return 'Alexa.SceneController' + + +class _AlexaBrightnessController(_AlexaInterface): + def name(self): + return 'Alexa.BrightnessController' + + +class _AlexaColorController(_AlexaInterface): + def name(self): + return 'Alexa.ColorController' + + +class _AlexaColorTemperatureController(_AlexaInterface): + def name(self): + return 'Alexa.ColorTemperatureController' + + +class _AlexaPercentageController(_AlexaInterface): + def name(self): + return 'Alexa.PercentageController' + + +class _AlexaSpeaker(_AlexaInterface): + def name(self): + return 'Alexa.Speaker' + + +class _AlexaPlaybackController(_AlexaInterface): + def name(self): + return 'Alexa.PlaybackController' + + +class _AlexaTemperatureSensor(_AlexaInterface): + def name(self): + return 'Alexa.TemperatureSensor' + + def properties_supported(self): + return [{'name': 'temperature'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'temperature': + raise _UnsupportedProperty(name) + + unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] + return { + 'value': float(self.entity.state), + 'scale': API_TEMP_UNITS[unit], + } + + +@ENTITY_ADAPTERS.register(alert.DOMAIN) +@ENTITY_ADAPTERS.register(automation.DOMAIN) +@ENTITY_ADAPTERS.register(input_boolean.DOMAIN) +class _GenericCapabilities(_AlexaEntity): """A generic, on/off device. The choice of last resort. @@ -173,78 +349,82 @@ class _GenericCapabilities(_EntityCapabilities): def default_display_categories(self): return [_DisplayCategory.OTHER] - def capabilities(self): - return [_capability('Alexa.PowerController')] + def interfaces(self): + return [_AlexaPowerController(self.entity)] -class _SwitchCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(switch.DOMAIN) +class _SwitchCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.SWITCH] - def capabilities(self): - return [_capability('Alexa.PowerController')] + def interfaces(self): + return [_AlexaPowerController(self.entity)] -class _CoverCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(cover.DOMAIN) +class _CoverCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.DOOR] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: - capabilities.append(_capability('Alexa.PercentageController')) - return capabilities + yield _AlexaPercentageController(self.entity) -class _LightCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(light.DOMAIN) +class _LightCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.LIGHT] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & light.SUPPORT_BRIGHTNESS: - capabilities.append(_capability('Alexa.BrightnessController')) + yield _AlexaBrightnessController(self.entity) if supported & light.SUPPORT_RGB_COLOR: - capabilities.append(_capability('Alexa.ColorController')) + yield _AlexaColorController(self.entity) if supported & light.SUPPORT_XY_COLOR: - capabilities.append(_capability('Alexa.ColorController')) + yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: - capabilities.append( - _capability('Alexa.ColorTemperatureController')) - return capabilities + yield _AlexaColorTemperatureController(self.entity) -class _FanCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(fan.DOMAIN) +class _FanCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.OTHER] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: - capabilities.append(_capability('Alexa.PercentageController')) - return capabilities + yield _AlexaPercentageController(self.entity) -class _LockCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(lock.DOMAIN) +class _LockCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.SMARTLOCK] - def capabilities(self): - return [_capability('Alexa.LockController')] + def interfaces(self): + return [_AlexaLockController(self.entity)] -class _MediaPlayerCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(media_player.DOMAIN) +class _MediaPlayerCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.TV] - def capabilities(self): - capabilities = [_capability('Alexa.PowerController')] + def interfaces(self): + yield _AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.SUPPORT_VOLUME_SET: - capabilities.append(_capability('Alexa.Speaker')) + yield _AlexaSpeaker(self.entity) playback_features = (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE | @@ -252,89 +432,59 @@ class _MediaPlayerCapabilities(_EntityCapabilities): media_player.SUPPORT_NEXT_TRACK | media_player.SUPPORT_PREVIOUS_TRACK) if supported & playback_features: - capabilities.append(_capability('Alexa.PlaybackController')) - - return capabilities + yield _AlexaPlaybackController(self.entity) -class _SceneCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(scene.DOMAIN) +class _SceneCapabilities(_AlexaEntity): + def description(self): + # Required description as per Amazon Scene docs + scene_fmt = '{} (Scene connected via Home Assistant)' + return scene_fmt.format(_AlexaEntity.description(self)) + def default_display_categories(self): return [_DisplayCategory.SCENE_TRIGGER] - def capabilities(self): - return [_capability('Alexa.SceneController')] + def interfaces(self): + return [_AlexaSceneController(self.entity, + supports_deactivation=False)] -class _ScriptCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(script.DOMAIN) +class _ScriptCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.ACTIVITY_TRIGGER] - def capabilities(self): + def interfaces(self): can_cancel = bool(self.entity.attributes.get('can_cancel')) - return [_capability('Alexa.SceneController', - supports_deactivation=can_cancel)] + return [_AlexaSceneController(self.entity, + supports_deactivation=can_cancel)] -class _GroupCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(group.DOMAIN) +class _GroupCapabilities(_AlexaEntity): def default_display_categories(self): return [_DisplayCategory.SCENE_TRIGGER] - def capabilities(self): - return [_capability('Alexa.SceneController', - supports_deactivation=True)] + def interfaces(self): + return [_AlexaSceneController(self.entity, + supports_deactivation=True)] -class _SensorCapabilities(_EntityCapabilities): +@ENTITY_ADAPTERS.register(sensor.DOMAIN) +class _SensorCapabilities(_AlexaEntity): def default_display_categories(self): # although there are other kinds of sensors, all but temperature # sensors are currently ignored. return [_DisplayCategory.TEMPERATURE_SENSOR] - def capabilities(self): - capabilities = [] - + def interfaces(self): attrs = self.entity.attributes if attrs.get(CONF_UNIT_OF_MEASUREMENT) in ( TEMP_FAHRENHEIT, TEMP_CELSIUS, ): - capabilities.append(_capability( - 'Alexa.TemperatureSensor', - retrievable=True, - properties_supported=[{'name': 'temperature'}])) - - return capabilities - - -class _UnknownEntityDomainError(Exception): - pass - - -def _capabilities_for_entity(config, entity): - """Return an _EntityCapabilities appropriate for given entity. - - raises _UnknownEntityDomainError if the given domain is unsupported. - """ - if entity.domain not in _CAPABILITIES_FOR_DOMAIN: - raise _UnknownEntityDomainError() - return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity) - - -_CAPABILITIES_FOR_DOMAIN = { - alert.DOMAIN: _GenericCapabilities, - automation.DOMAIN: _GenericCapabilities, - cover.DOMAIN: _CoverCapabilities, - fan.DOMAIN: _FanCapabilities, - group.DOMAIN: _GroupCapabilities, - input_boolean.DOMAIN: _GenericCapabilities, - light.DOMAIN: _LightCapabilities, - lock.DOMAIN: _LockCapabilities, - media_player.DOMAIN: _MediaPlayerCapabilities, - scene.DOMAIN: _SceneCapabilities, - script.DOMAIN: _ScriptCapabilities, - switch.DOMAIN: _SwitchCapabilities, - sensor.DOMAIN: _SensorCapabilities, -} + yield _AlexaTemperatureSensor(self.entity) class _Cause(object): @@ -511,36 +661,26 @@ def async_api_discovery(hass, config, request): entity.entity_id) continue - try: - entity_capabilities = _capabilities_for_entity(config, entity) - except _UnknownEntityDomainError: + if entity.domain not in ENTITY_ADAPTERS: continue - - entity_conf = config.entity_config.get(entity.entity_id, {}) - - friendly_name = entity_conf.get(CONF_NAME, entity.name) - description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id) - - # Required description as per Amazon Scene docs - if entity.domain == scene.DOMAIN: - scene_fmt = '{} (Scene connected via Home Assistant)' - description = scene_fmt.format(description) + alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity) endpoint = { - 'displayCategories': entity_capabilities.display_categories(), + 'displayCategories': alexa_entity.display_categories(), 'additionalApplianceDetails': {}, - 'endpointId': entity.entity_id.replace('.', '#'), - 'friendlyName': friendly_name, - 'description': description, + 'endpointId': alexa_entity.entity_id(), + 'friendlyName': alexa_entity.friendly_name(), + 'description': alexa_entity.description(), 'manufacturerName': 'Home Assistant', } - alexa_capabilities = entity_capabilities.capabilities() - if not alexa_capabilities: + endpoint['capabilities'] = [ + i.serialize_discovery() for i in alexa_entity.interfaces()] + + if not endpoint['capabilities']: _LOGGER.debug("Not exposing %s because it has no capabilities", entity.entity_id) continue - endpoint['capabilities'] = alexa_capabilities discovery_endpoints.append(endpoint) return api_message( @@ -1033,18 +1173,13 @@ def async_api_previous(hass, config, request, entity): @asyncio.coroutine def async_api_reportstate(hass, config, request, entity): """Process a ReportState request.""" - unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] - temp_property = { - 'namespace': 'Alexa.TemperatureSensor', - 'name': 'temperature', - 'value': { - 'value': float(entity.state), - 'scale': API_TEMP_UNITS[unit], - }, - } + alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity) + properties = [] + for interface in alexa_entity.interfaces(): + properties.extend(interface.serialize_properties()) return api_message( request, name='StateReport', - context={'properties': [temp_property]} + context={'properties': properties} ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3416dfbe367..1b2e98d6558 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -187,10 +187,32 @@ def test_discovery_request(hass): assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 17 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' + endpoint_ids = set( + appliance['endpointId'] + for appliance in msg['payload']['endpoints']) + assert endpoint_ids == { + 'switch#test', + 'light#test_1', + 'light#test_2', + 'light#test_3', + 'script#test', + 'script#test_2', + 'input_boolean#test', + 'scene#test', + 'fan#test_1', + 'fan#test_2', + 'lock#test', + 'media_player#test', + 'alert#test', + 'automation#test', + 'group#test', + 'cover#test', + 'sensor#test_temp', + } + for appliance in msg['payload']['endpoints']: if appliance['endpointId'] == 'switch#test': assert appliance['displayCategories'][0] == "SWITCH" @@ -267,6 +289,8 @@ def test_discovery_request(hass): assert len(appliance['capabilities']) == 1 assert appliance['capabilities'][-1]['interface'] == \ 'Alexa.SceneController' + capability = appliance['capabilities'][-1] + assert not capability['supportsDeactivation'] continue if appliance['endpointId'] == 'fan#test_1': @@ -333,7 +357,7 @@ def test_discovery_request(hass): assert len(appliance['capabilities']) == 1 capability = appliance['capabilities'][-1] assert capability['interface'] == 'Alexa.SceneController' - assert capability['supportsDeactivation'] is True + assert capability['supportsDeactivation'] continue if appliance['endpointId'] == 'cover#test': From 7d6ef4445ee1db07ff9903f7c809d93bdf16c377 Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Mon, 29 Jan 2018 01:00:34 +0000 Subject: [PATCH 034/166] Report states (#11973) * Refactor Alexa Smart Home API Having an object per interface will make it easier to support properties. Ideally, properties are reported in context in all responses. However current implementation reports them only in response to a ReportState request. This seems to work sufficiently. As long as the device is opened in the Alexa app, Amazon will poll the device state every few seconds with a ReportState request. * Report properties for some Alexa interfaces Fixes (mostly) #11874. Other interfaces will need properties implemented as well. Implementing properties for just PowerController seems sufficient to eliminate the "There was a problem." error for any device that supports it, even if other interfaces are supported. Of course the additional properties will be reported incorrectly in the Alexa app. Includes a minor bugfix: `reportable` was previously placed incorrectly in the responses, so Amazon was ignoring it. --- homeassistant/components/alexa/smart_home.py | 47 ++++++++- tests/components/alexa/test_smart_home.py | 101 ++++++++++++++++++- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index e09ee751e43..6721c0038e8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -17,7 +17,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, - CONF_UNIT_OF_MEASUREMENT) + CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -245,9 +245,8 @@ class _AlexaInterface(object): 'properties': { 'supported': self.properties_supported(), 'proactivelyReported': self.properties_proactively_reported(), + 'retrievable': self.properties_retrievable(), }, - # XXX this is incorrect, but the tests assert it - 'retrievable': self.properties_retrievable(), } # pylint: disable=assignment-from-none @@ -271,11 +270,41 @@ class _AlexaPowerController(_AlexaInterface): def name(self): return 'Alexa.PowerController' + def properties_supported(self): + return [{'name': 'powerState'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'powerState': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_ON: + return 'ON' + return 'OFF' + class _AlexaLockController(_AlexaInterface): def name(self): return 'Alexa.LockController' + def properties_supported(self): + return [{'name': 'lockState'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'lockState': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_LOCKED: + return 'LOCKED' + elif self.entity.state == STATE_UNLOCKED: + return 'UNLOCKED' + return 'JAMMED' + class _AlexaSceneController(_AlexaInterface): def __init__(self, entity, supports_deactivation): @@ -290,6 +319,18 @@ class _AlexaBrightnessController(_AlexaInterface): def name(self): return 'Alexa.BrightnessController' + def properties_supported(self): + return [{'name': 'brightness'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'brightness': + raise _UnsupportedProperty(name) + + return round(self.entity.attributes['brightness'] / 255.0 * 100) + class _AlexaColorController(_AlexaInterface): def name(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1b2e98d6558..7795bc85f1a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,9 +5,11 @@ from uuid import uuid4 import pytest -from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, + STATE_UNKNOWN, STATE_ON, STATE_OFF) from homeassistant.setup import async_setup_component -from homeassistant.components import alexa +from homeassistant.components import alexa, light from homeassistant.components.alexa import smart_home from homeassistant.helpers import entityfilter @@ -379,8 +381,8 @@ def test_discovery_request(hass): assert len(appliance['capabilities']) == 1 capability = appliance['capabilities'][0] assert capability['interface'] == 'Alexa.TemperatureSensor' - assert capability['retrievable'] is True properties = capability['properties'] + assert properties['retrievable'] is True assert {'name': 'temperature'} in properties['supported'] continue @@ -1248,6 +1250,99 @@ def test_api_report_temperature(hass): assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'} +@asyncio.coroutine +def test_report_lock_state(hass): + """Test LockController implements lockState property.""" + hass.states.async_set( + 'lock.locked', STATE_LOCKED, {}) + hass.states.async_set( + 'lock.unlocked', STATE_UNLOCKED, {}) + hass.states.async_set( + 'lock.unknown', STATE_UNKNOWN, {}) + + request = get_new_request('Alexa', 'ReportState', 'lock#locked') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + assert len(properties) == 1 + prop = properties[0] + assert prop['namespace'] == 'Alexa.LockController' + assert prop['name'] == 'lockState' + assert prop['value'] == 'LOCKED' + + request = get_new_request('Alexa', 'ReportState', 'lock#unlocked') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + prop = properties[0] + assert prop['value'] == 'UNLOCKED' + + request = get_new_request('Alexa', 'ReportState', 'lock#unknown') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + prop = properties[0] + assert prop['value'] == 'JAMMED' + + +@asyncio.coroutine +def test_report_power_state(hass): + """Test PowerController implements powerState property.""" + hass.states.async_set( + 'switch.on', STATE_ON, {}) + hass.states.async_set( + 'switch.off', STATE_OFF, {}) + + request = get_new_request('Alexa', 'ReportState', 'switch#on') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + assert len(properties) == 1 + prop = properties[0] + assert prop['namespace'] == 'Alexa.PowerController' + assert prop['name'] == 'powerState' + assert prop['value'] == 'ON' + + request = get_new_request('Alexa', 'ReportState', 'switch#off') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + +@asyncio.coroutine +def test_report_brightness(hass): + """Test BrightnessController implements brightness property.""" + hass.states.async_set( + 'light.test', STATE_ON, { + 'brightness': 128, + 'supported_features': light.SUPPORT_BRIGHTNESS, + } + ) + + request = get_new_request('Alexa', 'ReportState', 'light.test') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + for prop in msg['context']['properties']: + if ( + prop['namespace'] == 'Alexa.BrightnessController' + and prop['name'] == 'brightness' + ): + assert prop['value'] == 50 + break + else: + assert False, 'no brightness property present' + + @asyncio.coroutine def test_entity_config(hass): """Test that we can configure things via entity config.""" From 766875f702fb02744854d8ed1a581d4783cd7044 Mon Sep 17 00:00:00 2001 From: Phil Kates Date: Sun, 28 Jan 2018 22:22:04 -0800 Subject: [PATCH 035/166] alexa: Add media_player InputController support (#11946) `media_player`s can support select_source so map that to an InputController. [1]: https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html --- homeassistant/components/alexa/smart_home.py | 43 ++++++++++++++++++ tests/components/alexa/test_smart_home.py | 46 ++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 6721c0038e8..e8ce147472c 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -357,6 +357,11 @@ class _AlexaPlaybackController(_AlexaInterface): return 'Alexa.PlaybackController' +class _AlexaInputController(_AlexaInterface): + def name(self): + return 'Alexa.InputController' + + class _AlexaTemperatureSensor(_AlexaInterface): def name(self): return 'Alexa.TemperatureSensor' @@ -475,6 +480,9 @@ class _MediaPlayerCapabilities(_AlexaEntity): if supported & playback_features: yield _AlexaPlaybackController(self.entity) + if supported & media_player.SUPPORT_SELECT_SOURCE: + yield _AlexaInputController(self.entity) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class _SceneCapabilities(_AlexaEntity): @@ -1081,6 +1089,41 @@ def async_api_set_volume(hass, config, request, entity): return api_message(request) +@HANDLERS.register(('Alexa.InputController', 'SelectInput')) +@extract_entity +@asyncio.coroutine +def async_api_select_input(hass, config, request, entity): + """Process a set input request.""" + media_input = request[API_PAYLOAD]['input'] + + # attempt to map the ALL UPPERCASE payload name to a source + source_list = entity.attributes[media_player.ATTR_INPUT_SOURCE_LIST] or [] + for source in source_list: + # response will always be space separated, so format the source in the + # most likely way to find a match + formatted_source = source.lower().replace('-', ' ').replace('_', ' ') + if formatted_source in media_input.lower(): + media_input = source + break + else: + msg = 'failed to map input {} to a media source on {}'.format( + media_input, entity.entity_id) + _LOGGER.error(msg) + return api_error( + request, error_type='INVALID_VALUE', error_message=msg) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_INPUT_SOURCE: media_input, + } + + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_SELECT_SOURCE, + data, blocking=False) + + return api_message(request) + + @HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) @extract_entity @asyncio.coroutine diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7795bc85f1a..c91965a5396 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1161,6 +1161,52 @@ def test_api_set_volume(hass): assert msg['header']['name'] == 'Response' +@asyncio.coroutine +@pytest.mark.parametrize( + "domain,payload,source_list,idx", [ + ('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1), + ('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0), + ('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0), + ('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None), + ] +) +def test_api_select_input(hass, domain, payload, source_list, idx): + """Test api set input process.""" + request = get_new_request( + 'Alexa.InputController', 'SelectInput', 'media_player#test') + + # add payload + request['directive']['payload']['input'] = payload + + # setup test devices + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", + 'source': 'unknown', + 'source_list': source_list, + }) + + call_media_player = async_mock_service(hass, domain, 'select_source') + + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + assert 'event' in msg + msg = msg['event'] + + # test where no source matches + if idx is None: + assert len(call_media_player) == 0 + assert msg['header']['name'] == 'ErrorResponse' + return + + assert len(call_media_player) == 1 + assert call_media_player[0].data['entity_id'] == 'media_player.test' + assert call_media_player[0].data['source'] == source_list[idx] + assert msg['header']['name'] == 'Response' + + @asyncio.coroutine @pytest.mark.parametrize( "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')]) From 5426e5c875b73cb2d89769199ec80774344d0b10 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Mon, 29 Jan 2018 07:40:00 +0000 Subject: [PATCH 036/166] emulated_hue: allow customization within emulated_hue configuration (#11981) * emulated_hue: add entities configuration * emulated_hue: update tests to include new entities attribute --- .../components/emulated_hue/__init__.py | 29 +++++++++- .../components/emulated_hue/hue_api.py | 13 ++--- tests/components/emulated_hue/test_hue_api.py | 57 ++++++++++++------- 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index b2206f80766..9fba21b81dc 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -39,6 +39,9 @@ CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_TYPE = 'type' +CONF_ENTITIES = 'entities' +CONF_ENTITY_NAME = 'name' +CONF_ENTITY_HIDDEN = 'hidden' TYPE_ALEXA = 'alexa' TYPE_GOOGLE = 'google_home' @@ -52,6 +55,11 @@ DEFAULT_EXPOSED_DOMAINS = [ ] DEFAULT_TYPE = TYPE_GOOGLE +CONFIG_ENTITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_ENTITY_NAME): cv.string, + vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_HOST_IP): cv.string, @@ -63,11 +71,14 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): - vol.Any(TYPE_ALEXA, TYPE_GOOGLE) + vol.Any(TYPE_ALEXA, TYPE_GOOGLE), + vol.Optional(CONF_ENTITIES): + vol.Schema({cv.entity_id: CONFIG_ENTITY_SCHEMA}) }) }, extra=vol.ALLOW_EXTRA) ATTR_EMULATED_HUE = 'emulated_hue' +ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' @@ -183,6 +194,8 @@ class Config(object): self.advertise_port = conf.get( CONF_ADVERTISE_PORT) or self.listen_port + self.entities = conf.get(CONF_ENTITIES, {}) + def entity_id_to_number(self, entity_id): """Get a unique number for the entity id.""" if self.type == TYPE_ALEXA: @@ -215,6 +228,14 @@ class Config(object): assert isinstance(number, str) return self.numbers.get(number) + def get_entity_name(self, entity): + """Get the name of an entity.""" + if entity.entity_id in self.entities and \ + CONF_ENTITY_NAME in self.entities[entity.entity_id]: + return self.entities[entity.entity_id][CONF_ENTITY_NAME] + + return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) + def is_entity_exposed(self, entity): """Determine if an entity should be exposed on the emulated bridge. @@ -227,6 +248,12 @@ class Config(object): domain = entity.domain.lower() explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None) explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None) + + if entity.entity_id in self.entities and \ + CONF_ENTITY_HIDDEN in self.entities[entity.entity_id]: + explicit_hidden = \ + self.entities[entity.entity_id][CONF_ENTITY_HIDDEN] + if explicit_expose is True or explicit_hidden is False: expose = True elif explicit_expose is False or explicit_hidden is True: diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 7b98ca7deaa..5d97ef3cea4 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -24,9 +24,6 @@ from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) -ATTR_EMULATED_HUE = 'emulated_hue' -ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' - HUE_API_STATE_ON = 'on' HUE_API_STATE_BRI = 'bri' @@ -77,7 +74,7 @@ class HueAllLightsStateView(HomeAssistantView): number = self.config.entity_id_to_number(entity.entity_id) json_response[number] = entity_to_json( - entity, state, brightness) + self.config, entity, state, brightness) return self.json(json_response) @@ -110,7 +107,7 @@ class HueOneLightStateView(HomeAssistantView): state, brightness = get_entity_state(self.config, entity) - json_response = entity_to_json(entity, state, brightness) + json_response = entity_to_json(self.config, entity, state, brightness) return self.json(json_response) @@ -344,10 +341,8 @@ def get_entity_state(config, entity): return (final_state, final_brightness) -def entity_to_json(entity, is_on=None, brightness=None): +def entity_to_json(config, entity, is_on=None, brightness=None): """Convert an entity to its Hue bridge JSON representation.""" - name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) - return { 'state': { @@ -356,7 +351,7 @@ def entity_to_json(entity, is_on=None, brightness=None): 'reachable': True }, 'type': 'Dimmable light', - 'name': name, + 'name': config.get_entity_name(entity), 'modelid': 'HASS123', 'uniqueid': entity.entity_id, 'swversion': '123' diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 383b4f7165d..af07da547b7 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -121,7 +121,14 @@ def hass_hue(loop, hass): def hue_client(loop, hass_hue, test_client): """Create web client for emulated hue api.""" web_app = hass_hue.http.app - config = Config(None, {'type': 'alexa'}) + config = Config(None, { + emulated_hue.CONF_TYPE: emulated_hue.TYPE_ALEXA, + emulated_hue.CONF_ENTITIES: { + 'light.bed_light': { + emulated_hue.CONF_ENTITY_HIDDEN: True + } + } + }) HueUsernameView().register(web_app.router) HueAllLightsStateView(config).register(web_app.router) @@ -145,7 +152,7 @@ def test_discover_lights(hue_client): # Make sure the lights we added to the config are there assert 'light.ceiling_lights' in devices - assert 'light.bed_light' in devices + assert 'light.bed_light' not in devices assert 'script.set_kitchen_light' in devices assert 'light.kitchen_lights' not in devices assert 'media_player.living_room' in devices @@ -186,19 +193,23 @@ def test_get_light_state(hass_hue, hue_client): assert result_json['light.ceiling_lights']['state'][HUE_API_STATE_BRI] == \ 127 - # Turn bedroom light off + # Turn office light off yield from hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_OFF, { - const.ATTR_ENTITY_ID: 'light.bed_light' + const.ATTR_ENTITY_ID: 'light.ceiling_lights' }, blocking=True) - bedroom_json = yield from perform_get_light_state( - hue_client, 'light.bed_light', 200) + office_json = yield from perform_get_light_state( + hue_client, 'light.ceiling_lights', 200) - assert bedroom_json['state'][HUE_API_STATE_ON] is False - assert bedroom_json['state'][HUE_API_STATE_BRI] == 0 + assert office_json['state'][HUE_API_STATE_ON] is False + assert office_json['state'][HUE_API_STATE_BRI] == 0 + + # Make sure bedroom light isn't accessible + yield from perform_get_light_state( + hue_client, 'light.bed_light', 404) # Make sure kitchen light isn't accessible yield from perform_get_light_state( @@ -213,29 +224,35 @@ def test_put_light_state(hass_hue, hue_client): # Turn the bedroom light on first yield from hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_ON, - {const.ATTR_ENTITY_ID: 'light.bed_light', + {const.ATTR_ENTITY_ID: 'light.ceiling_lights', light.ATTR_BRIGHTNESS: 153}, blocking=True) - bed_light = hass_hue.states.get('light.bed_light') - assert bed_light.state == STATE_ON - assert bed_light.attributes[light.ATTR_BRIGHTNESS] == 153 + ceiling_lights = hass_hue.states.get('light.ceiling_lights') + assert ceiling_lights.state == STATE_ON + assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 153 # Go through the API to turn it off - bedroom_result = yield from perform_put_light_state( + ceiling_result = yield from perform_put_light_state( hass_hue, hue_client, - 'light.bed_light', False) + 'light.ceiling_lights', False) - bedroom_result_json = yield from bedroom_result.json() + ceiling_result_json = yield from ceiling_result.json() - assert bedroom_result.status == 200 - assert 'application/json' in bedroom_result.headers['content-type'] + assert ceiling_result.status == 200 + assert 'application/json' in ceiling_result.headers['content-type'] - assert len(bedroom_result_json) == 1 + assert len(ceiling_result_json) == 1 # Check to make sure the state changed - bed_light = hass_hue.states.get('light.bed_light') - assert bed_light.state == STATE_OFF + ceiling_lights = hass_hue.states.get('light.ceiling_lights') + assert ceiling_lights.state == STATE_OFF + + # Make sure we can't change the bedroom light state + bedroom_result = yield from perform_put_light_state( + hass_hue, hue_client, + 'light.bed_light', True) + assert bedroom_result.status == 404 # Make sure we can't change the kitchen light state kitchen_result = yield from perform_put_light_state( From 78a3c01f27327593e56279c81114daafb92e9588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 29 Jan 2018 10:23:53 +0200 Subject: [PATCH 037/166] Flake8 35 (#11972) * Upgrade flake8 to 3.5 * Fix flake8 bare except errors * Make flake8 and pylint cooperate --- homeassistant/components/feedreader.py | 6 ++---- homeassistant/components/media_player/bluesound.py | 6 +++--- homeassistant/components/media_player/clementine.py | 2 +- homeassistant/components/sensor/fritzbox_callmonitor.py | 3 +-- homeassistant/components/sensor/qnap.py | 3 +-- homeassistant/components/sensor/synologydsm.py | 3 +-- homeassistant/components/spc.py | 2 +- homeassistant/monkey_patch.py | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/emulated_hue/test_upnp.py | 3 +-- tests/components/test_system_log.py | 2 +- 12 files changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 3d73901b4d8..2c0e146491a 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -153,8 +153,7 @@ class StoredData(object): with self._lock, open(self._data_file, 'rb') as myfile: self._data = pickle.load(myfile) or {} self._cache_outdated = False - # pylint: disable=bare-except - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error("Error loading data from pickled file %s", self._data_file) @@ -172,8 +171,7 @@ class StoredData(object): url, self._data_file) try: pickle.dump(self._data, myfile) - # pylint: disable=bare-except - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error( "Error saving pickled data to %s", self._data_file) self._cache_outdated = True diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index 96323579822..d8a0cd7ebf9 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -165,7 +165,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: resp = yield from self.send_bluesound_command( 'SyncStatus', raise_timeout, raise_timeout) - except: + except Exception: raise if not resp: @@ -202,7 +202,7 @@ class BluesoundPlayer(MediaPlayerDevice): except CancelledError: _LOGGER.debug("Stopping the polling of node %s", self._name) - except: + except Exception: _LOGGER.exception("Unexpected error in %s", self._name) raise @@ -229,7 +229,7 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.info("Node %s is offline, retrying later", self.host) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION) - except: + except Exception: _LOGGER.exception("Unexpected when initiating error in %s", self.host) raise diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index d9688badcd1..057a23579ca 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -97,7 +97,7 @@ class ClementineDevice(MediaPlayerDevice): self._track_artist = client.current_track['track_artist'] self._track_album_name = client.current_track['track_album'] - except: + except Exception: self._state = STATE_OFF raise diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index ea6382ce795..b443bd56f03 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -70,8 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): phonebook = FritzBoxPhonebook( host=host, port=port, username=username, password=password, phonebook_id=phonebook_id, prefixes=prefixes) - # pylint: disable=bare-except - except: + except: # noqa: E722 # pylint: disable=bare-except phonebook = None _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box", phonebook_id) diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 20460f9063c..3caebad2007 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -197,7 +197,6 @@ class QNAPStatsAPI(object): self.data = {} - # pylint: disable=bare-except @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update API information and store locally.""" @@ -207,7 +206,7 @@ class QNAPStatsAPI(object): self.data["smart_drive_health"] = self._api.get_smart_disk_health() self.data["volumes"] = self._api.get_volumes() self.data["bandwidth"] = self._api.get_bandwidth() - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.exception("Failed to fetch QNAP stats from the NAS") diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index cfc868de664..f5a41c7b8ce 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -136,7 +136,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SynoApi(object): """Class to interface with Synology DSM API.""" - # pylint: disable=bare-except def __init__(self, host, port, username, password, temp_unit): """Initialize the API wrapper class.""" from SynologyDSM import SynologyDSM @@ -144,7 +143,7 @@ class SynoApi(object): try: self._api = SynologyDSM(host, port, username, password) - except: + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error("Error setting up Synology DSM") # Will be updated when update() gets called. diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 141d06768e3..72477a5a65f 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -151,7 +151,7 @@ def _ws_process_message(message, async_callback, *args): "Unsuccessful websocket message delivered, ignoring: %s", message) try: yield from async_callback(message['data']['sia'], *args) - except: # pylint: disable=bare-except + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.exception("Exception in callback, ignoring") diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 819d8de48e0..5aa051f2bb5 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -37,7 +37,7 @@ def patch_weakref_tasks(): asyncio.tasks.Task._all_tasks = IgnoreCalls() try: del asyncio.tasks.Task.__del__ - except: + except: # noqa: E722 pass diff --git a/requirements_test.txt b/requirements_test.txt index 22bb6623e16..5ba27b4b290 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -flake8==3.3 +flake8==3.5 pylint==1.6.5 mypy==0.560 pydocstyle==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bb41e09c24..8d3e823a989 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2,7 +2,7 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -flake8==3.3 +flake8==3.5 pylint==1.6.5 mypy==0.560 pydocstyle==1.1.1 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 1cd895954de..b3032954431 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -87,10 +87,9 @@ class TestEmulatedHue(unittest.TestCase): self.assertTrue('text/xml' in result.headers['content-type']) # Make sure the XML is parsable - # pylint: disable=bare-except try: ET.fromstring(result.text) - except: + except: # noqa: E722 # pylint: disable=bare-except self.fail('description.xml is not valid XML!') def test_create_username(self): diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index a3e7d662483..6ad68f2274a 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -34,7 +34,7 @@ def get_error_log(hass, test_client, expected_count): def _generate_and_log_exception(exception, log): try: raise Exception(exception) - except: # pylint: disable=bare-except + except: # noqa: E722 # pylint: disable=bare-except _LOGGER.exception(log) From 384f63dd1d1027771bcfeb82ead0d244c792217f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 29 Jan 2018 10:24:08 +0200 Subject: [PATCH 038/166] Typing fixes (#12015) * .gitignore: Add .mypy_cache * Typing fixes --- .gitignore | 3 +++ homeassistant/components/alert.py | 2 +- homeassistant/components/binary_sensor/ihc.py | 3 ++- homeassistant/components/cover/isy994.py | 2 +- homeassistant/components/device_tracker/unifi.py | 2 +- homeassistant/components/fan/comfoconnect.py | 4 ++-- homeassistant/components/hdmi_cec.py | 2 +- homeassistant/components/ihc/ihcdevice.py | 2 +- homeassistant/components/light/ihc.py | 2 +- homeassistant/components/light/tplink.py | 2 +- homeassistant/components/media_player/hdmi_cec.py | 2 +- homeassistant/components/media_player/onkyo.py | 3 +++ homeassistant/components/media_player/webostv.py | 3 +++ homeassistant/components/pilight.py | 2 +- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/comfoconnect.py | 3 ++- homeassistant/components/sensor/daikin.py | 3 ++- homeassistant/components/sensor/ihc.py | 2 +- homeassistant/components/switch/hdmi_cec.py | 2 +- homeassistant/components/switch/ihc.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/util/dt.py | 2 +- homeassistant/util/unit_system.py | 2 +- 23 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index c8a6fed2ddf..fe26f43e8bc 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,6 @@ desktop.ini /home-assistant.pyproj /home-assistant.sln /.vs/* + +# mypy +/.mypy_cache/* diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 27d1625fd6b..eb941e22877 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -277,7 +277,7 @@ class Alert(ToggleEntity): yield from self.async_update_ha_state() @asyncio.coroutine - def async_toggle(self): + def async_toggle(self, **kwargs): """Async toggle alert.""" if self._ack: return self.async_turn_on() diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/binary_sensor/ihc.py index 14e45f88cf1..04f8c0d00dd 100644 --- a/homeassistant/components/binary_sensor/ihc.py +++ b/homeassistant/components/binary_sensor/ihc.py @@ -69,7 +69,8 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - sensor_type: str, inverting: bool, product: Element=None): + sensor_type: str, inverting: bool, + product: Element=None) -> None: """Initialize the IHC binary sensor.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._state = None diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index b187b8409c2..7d77b1bc3be 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -42,7 +42,7 @@ def setup_platform(hass, config: ConfigType, class ISYCoverDevice(ISYDevice, CoverDevice): """Representation of an ISY994 cover device.""" - def __init__(self, node: object): + def __init__(self, node: object) -> None: """Initialize the ISY994 cover device.""" super().__init__(node) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index a3e81b3ef51..d5b6b044f1f 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -75,7 +75,7 @@ def get_scanner(hass, config): class UnifiScanner(DeviceScanner): """Provide device_tracker support from Unifi WAP client data.""" - def __init__(self, controller, detection_time: timedelta): + def __init__(self, controller, detection_time: timedelta) -> None: """Initialize the scanner.""" self._detection_time = detection_time self._controller = controller diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py index ab32e588c03..c6d1232801f 100644 --- a/homeassistant/components/fan/comfoconnect.py +++ b/homeassistant/components/fan/comfoconnect.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ComfoConnectFan(FanEntity): """Representation of the ComfoConnect fan platform.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge): + def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" from pycomfoconnect import SENSOR_FAN_SPEED_MODE @@ -93,7 +93,7 @@ class ComfoConnectFan(FanEntity): speed = SPEED_LOW self.set_speed(speed) - def turn_off(self) -> None: + def turn_off(self, **kwargs) -> None: """Turn off the fan (to away).""" self.set_speed(SPEED_OFF) diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index f94dd8816a7..8e2464d0922 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -320,7 +320,7 @@ def setup(hass: HomeAssistant, base_config): class CecDevice(Entity): """Representation of a HDMI CEC device entity.""" - def __init__(self, hass: HomeAssistant, device, logical): + def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the device.""" self._device = device self.hass = hass diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 48827851f92..999dda42015 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -14,7 +14,7 @@ class IHCDevice(Entity): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - product: Element=None): + product: Element=None) -> None: """Initialize IHC attributes.""" self.ihc_controller = ihc_controller self._name = name diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/light/ihc.py index f23ae77c8b2..ead0f153562 100644 --- a/homeassistant/components/light/ihc.py +++ b/homeassistant/components/light/ihc.py @@ -64,7 +64,7 @@ class IhcLight(IHCDevice, Light): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - dimmable=False, product: Element=None): + dimmable=False, product: Element=None) -> None: """Initialize the light.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._brightness = 0 diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 30ad3a4d268..6aee02ee914 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -75,7 +75,7 @@ def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" - def __init__(self, smartbulb: 'SmartBulb', name): + def __init__(self, smartbulb: 'SmartBulb', name) -> None: """Initialize the bulb.""" self.smartbulb = smartbulb self._name = name diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py index 7054c83d36a..03e7c6f0c9f 100644 --- a/homeassistant/components/media_player/hdmi_cec.py +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -34,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CecPlayerDevice(CecDevice, MediaPlayerDevice): """Representation of a HDMI device as a Media palyer.""" - def __init__(self, hass: HomeAssistant, device, logical): + def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the HDMI device.""" CecDevice.__init__(self, hass, device, logical) self.entity_id = "%s.%s_%s" % ( diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 97ebe5be92b..432d9ce108f 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -6,6 +6,9 @@ https://home-assistant.io/components/media_player.onkyo/ """ import logging +# pylint: disable=unused-import +from typing import List # noqa: F401 + import voluptuous as vol from homeassistant.components.media_player import ( diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 55179ed60a9..fed442e140e 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -9,6 +9,9 @@ from datetime import timedelta import logging from urllib.parse import urlparse +# pylint: disable=unused-import +from typing import Dict # noqa: F401 + import voluptuous as vol from homeassistant.components.media_player import ( diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 3000820d28c..71e8232e8c2 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -130,7 +130,7 @@ class CallRateDelayThrottle(object): it should not block the mainloop. """ - def __init__(self, hass, delay_seconds: float): + def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) self._queue = [] diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index e19bcaaddfc..1adce50b1aa 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -16,7 +16,7 @@ import queue import threading import time -from typing import Dict, Optional +from typing import Any, Dict, Optional # noqa: F401 import voluptuous as vol diff --git a/homeassistant/components/sensor/comfoconnect.py b/homeassistant/components/sensor/comfoconnect.py index 9df28d861ee..ad6b07fb3da 100644 --- a/homeassistant/components/sensor/comfoconnect.py +++ b/homeassistant/components/sensor/comfoconnect.py @@ -96,7 +96,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ComfoConnectSensor(Entity): """Representation of a ComfoConnect sensor.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type): + def __init__(self, hass, name, ccb: ComfoConnectBridge, + sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py index ad571110e88..3ea3418db4e 100644 --- a/homeassistant/components/sensor/daikin.py +++ b/homeassistant/components/sensor/daikin.py @@ -57,7 +57,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DaikinClimateSensor(Entity): """Representation of a Sensor.""" - def __init__(self, api, monitored_state, units: UnitSystem, name=None): + def __init__(self, api, monitored_state, units: UnitSystem, + name=None) -> None: """Initialize the sensor.""" self._api = api self._sensor = SENSOR_TYPES.get(monitored_state) diff --git a/homeassistant/components/sensor/ihc.py b/homeassistant/components/sensor/ihc.py index 3ad86e51f97..b6440a407a4 100644 --- a/homeassistant/components/sensor/ihc.py +++ b/homeassistant/components/sensor/ihc.py @@ -62,7 +62,7 @@ class IHCSensor(IHCDevice, Entity): """Implementation of the IHC sensor.""" def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - unit, product: Element=None): + unit, product: Element=None) -> None: """Initialize the IHC sensor.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._state = None diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/switch/hdmi_cec.py index a100b582e64..65a7a762c0f 100644 --- a/homeassistant/components/switch/hdmi_cec.py +++ b/homeassistant/components/switch/hdmi_cec.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CecSwitchDevice(CecDevice, SwitchDevice): """Representation of a HDMI device as a Switch.""" - def __init__(self, hass: HomeAssistant, device, logical): + def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the HDMI device.""" CecDevice.__init__(self, hass, device, logical) self.entity_id = "%s.%s_%s" % ( diff --git a/homeassistant/components/switch/ihc.py b/homeassistant/components/switch/ihc.py index 4bab1378acd..eab88035c73 100644 --- a/homeassistant/components/switch/ihc.py +++ b/homeassistant/components/switch/ihc.py @@ -53,7 +53,7 @@ class IHCSwitch(IHCDevice, SwitchDevice): """IHC Switch.""" def __init__(self, ihc_controller, name: str, ihc_id: int, - info: bool, product: Element=None): + info: bool, product: Element=None) -> None: """Initialize the IHC switch.""" super().__init__(ihc_controller, name, ihc_id, product) self._state = False diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 4b9eec3d540..a3ce2a13f56 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -19,7 +19,7 @@ import sys from types import ModuleType # pylint: disable=unused-import -from typing import Dict, Optional, Sequence, Set # NOQA +from typing import Dict, List, Optional, Sequence, Set # NOQA from homeassistant.const import PLATFORM_FORMAT from homeassistant.util import OrderedSet diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 5e8b3382fb1..c3400bac9be 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -3,7 +3,7 @@ import datetime as dt import re # pylint: disable=unused-import -from typing import Any, Union, Optional, Tuple # NOQA +from typing import Any, Dict, Union, Optional, Tuple # NOQA import pytz diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 31b76365da4..ecef1087747 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -105,7 +105,7 @@ class UnitSystem(object): raise TypeError('{} is not a numeric value.'.format(str(length))) return distance_util.convert(length, from_unit, - self.length_unit) # type: float + self.length_unit) def as_dict(self) -> dict: """Convert the unit system to a dictionary.""" From 12a53e27471e4a647b2cb858b4abe5396c4f4a64 Mon Sep 17 00:00:00 2001 From: Andy Castille Date: Mon, 29 Jan 2018 05:15:53 -0600 Subject: [PATCH 039/166] Fix DoorBird push notifications for installations with an API password (#12020) --- homeassistant/components/doorbird.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 56933d198f2..be7adc034a0 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -73,6 +73,7 @@ def setup(hass, config): class DoorbirdRequestView(HomeAssistantView): """Provide a page for the device to call.""" + requires_auth = False url = API_URL name = API_URL[1:].replace('/', ':') extra_urls = [API_URL + '/{sensor}'] From ff0fd71608d8640a3c09825e2ce630f410464fc6 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Mon, 29 Jan 2018 05:35:13 -0600 Subject: [PATCH 040/166] Bump upstream lib version. (#12021) --- homeassistant/components/climate/venstar.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 92e5c71b6c5..6db1d53bc50 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -20,7 +20,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['venstarcolortouch==0.5'] +REQUIREMENTS = ['venstarcolortouch==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 61dc9ca0681..eea3c6e47bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1187,7 +1187,7 @@ user-agents==1.1.0 uvcclient==0.10.1 # homeassistant.components.climate.venstar -venstarcolortouch==0.5 +venstarcolortouch==0.6 # homeassistant.components.volvooncall volvooncall==0.4.0 From 8b9dc71cdec1643b987776c87ced60e0d6406361 Mon Sep 17 00:00:00 2001 From: akloeckner Date: Mon, 29 Jan 2018 23:17:58 +0100 Subject: [PATCH 041/166] sensor.deutsche_bahn: add only_direct option (#11999) * sensor.deutsche_bahn: add only_direct option This adds the `only_direct` option to the HA interface as provided by the schiene class. * fix line length as requested by @houndci-bot and @fabaff * No line breaks needed --- homeassistant/components/sensor/deutsche_bahn.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index c13fc930ed1..c443829a3bb 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -20,6 +20,8 @@ _LOGGER = logging.getLogger(__name__) CONF_DESTINATION = 'to' CONF_START = 'from' +CONF_ONLY_DIRECT = 'only_direct' +DEFAULT_ONLY_DIRECT = False ICON = 'mdi:train' @@ -28,6 +30,7 @@ SCAN_INTERVAL = timedelta(minutes=2) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_START): cv.string, + vol.Optional(CONF_ONLY_DIRECT, default=DEFAULT_ONLY_DIRECT): cv.boolean, }) @@ -35,17 +38,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Deutsche Bahn Sensor.""" start = config.get(CONF_START) destination = config.get(CONF_DESTINATION) + only_direct = config.get(CONF_ONLY_DIRECT) - add_devices([DeutscheBahnSensor(start, destination)], True) + add_devices([DeutscheBahnSensor(start, destination, only_direct)], True) class DeutscheBahnSensor(Entity): """Implementation of a Deutsche Bahn sensor.""" - def __init__(self, start, goal): + def __init__(self, start, goal, only_direct): """Initialize the sensor.""" self._name = '{} to {}'.format(start, goal) - self.data = SchieneData(start, goal) + self.data = SchieneData(start, goal, only_direct) self._state = None @property @@ -82,19 +86,21 @@ class DeutscheBahnSensor(Entity): class SchieneData(object): """Pull data from the bahn.de web page.""" - def __init__(self, start, goal): + def __init__(self, start, goal, only_direct): """Initialize the sensor.""" import schiene self.start = start self.goal = goal + self.only_direct = only_direct self.schiene = schiene.Schiene() self.connections = [{}] def update(self): """Update the connection data.""" self.connections = self.schiene.connections( - self.start, self.goal, dt_util.as_local(dt_util.utcnow())) + self.start, self.goal, dt_util.as_local(dt_util.utcnow()), + self.only_direct) for con in self.connections: # Detail info is not useful. Having a more consistent interface From 105522f03f8244c384a85ef620fca6d010240007 Mon Sep 17 00:00:00 2001 From: c727 Date: Mon, 29 Jan 2018 23:18:33 +0100 Subject: [PATCH 042/166] Fix 404 for Hass.io panel using frontend dev (#12039) * Fix 404 for Hass.io panel using frontend dev * Hound --- homeassistant/components/frontend/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 610a531a702..2da27b7e544 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -300,7 +300,8 @@ def async_setup(hass, config): if is_dev: for subpath in ["src", "build-translations", "build-temp", "build", - "hass_frontend", "bower_components", "panels"]: + "hass_frontend", "bower_components", "panels", + "hassio"]: hass.http.register_static_path( "/home-assistant-polymer/{}".format(subpath), os.path.join(repo_path, subpath), From 38fd9b65bfd2fa5f32b1758e859f83f3a89e2f55 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 29 Jan 2018 23:19:08 +0100 Subject: [PATCH 043/166] Fix MQTT cover availability subscription (#12036) --- homeassistant/components/cover/mqtt.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 9b75f03c232..e55072dbc73 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -214,16 +214,6 @@ class MqttCover(MqttAvailability, CoverDevice): self.async_schedule_update_ha_state() - @callback - def availability_message_received(topic, payload, qos): - """Handle new MQTT availability messages.""" - if payload == self._payload_available: - self._available = True - elif payload == self._payload_not_available: - self._available = False - - self.async_schedule_update_ha_state() - if self._state_topic is None: # Force into optimistic mode. self._optimistic = True @@ -232,11 +222,6 @@ class MqttCover(MqttAvailability, CoverDevice): self.hass, self._state_topic, state_message_received, self._qos) - if self._availability_topic is not None: - yield from mqtt.async_subscribe( - self.hass, self._availability_topic, - availability_message_received, self._qos) - if self._tilt_status_topic is None: self._tilt_optimistic = True else: From 8dcfd35b8b92a40f26131569afe689304d4e9bf8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 29 Jan 2018 23:37:19 +0100 Subject: [PATCH 044/166] Spelling fixes (#12041) * Spelling fixes *Lots* of them. * Spelling breaking changes * Fix lint errors --- homeassistant/components/alexa/smart_home.py | 6 +- homeassistant/components/android_ip_webcam.py | 2 +- homeassistant/components/apcupsd.py | 2 +- homeassistant/components/arlo.py | 2 +- .../components/binary_sensor/ffmpeg_motion.py | 2 +- .../components/binary_sensor/hikvision.py | 2 +- .../components/binary_sensor/isy994.py | 4 +- .../components/binary_sensor/pilight.py | 6 +- homeassistant/components/camera/abode.py | 2 +- homeassistant/components/camera/rpi_camera.py | 6 +- .../components/climate/generic_thermostat.py | 2 +- homeassistant/components/climate/mysensors.py | 3 +- homeassistant/components/climate/netatmo.py | 2 +- homeassistant/components/climate/oem.py | 2 +- homeassistant/components/cover/homematic.py | 2 +- homeassistant/components/cover/knx.py | 2 +- homeassistant/components/cover/lutron.py | 2 +- .../components/device_tracker/asuswrt.py | 2 +- .../device_tracker/huawei_router.py | 2 +- .../components/device_tracker/ubus.py | 8 +- homeassistant/components/dominos.py | 2 +- homeassistant/components/downloader.py | 2 +- homeassistant/components/eight_sleep.py | 4 +- homeassistant/components/fan/__init__.py | 2 +- homeassistant/components/frontend/__init__.py | 4 +- homeassistant/components/google.py | 2 +- .../components/google_assistant/smart_home.py | 2 +- homeassistant/components/group/__init__.py | 2 +- .../components/homematic/__init__.py | 6 +- .../image_processing/microsoft_face_detect.py | 2 +- .../microsoft_face_identify.py | 6 +- .../image_processing/seven_segments.py | 2 +- homeassistant/components/input_boolean.py | 2 +- homeassistant/components/ios.py | 4 +- homeassistant/components/isy994.py | 2 +- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/light/decora.py | 4 +- homeassistant/components/light/flux_led.py | 4 +- homeassistant/components/light/homematic.py | 2 +- homeassistant/components/light/iglo.py | 2 +- homeassistant/components/light/isy994.py | 2 +- homeassistant/components/light/knx.py | 2 +- homeassistant/components/logbook.py | 2 +- homeassistant/components/logger.py | 2 +- .../components/mailbox/asterisk_mbox.py | 2 +- .../components/media_player/bluesound.py | 4 +- homeassistant/components/media_player/cast.py | 2 +- homeassistant/components/media_player/emby.py | 2 +- .../components/media_player/hdmi_cec.py | 2 +- .../components/media_player/squeezebox.py | 2 +- .../components/media_player/volumio.py | 2 +- .../components/media_player/yamaha.py | 2 +- homeassistant/components/mysensors.py | 4 +- homeassistant/components/nest.py | 2 +- homeassistant/components/notify/knx.py | 2 +- homeassistant/components/qwikswitch.py | 2 +- homeassistant/components/rainbird.py | 6 +- homeassistant/components/remote/kira.py | 4 +- homeassistant/components/rfxtrx.py | 2 +- homeassistant/components/scsgate.py | 4 +- homeassistant/components/sensor/bme680.py | 2 +- homeassistant/components/sensor/dsmr.py | 8 +- homeassistant/components/sensor/ebox.py | 2 +- homeassistant/components/sensor/emoncms.py | 2 +- homeassistant/components/sensor/fido.py | 6 +- homeassistant/components/sensor/knx.py | 2 +- homeassistant/components/sensor/metoffice.py | 4 +- .../components/sensor/modem_callerid.py | 2 +- homeassistant/components/sensor/nut.py | 2 +- homeassistant/components/sensor/openevse.py | 2 +- homeassistant/components/sensor/pilight.py | 4 +- homeassistant/components/sensor/pushbullet.py | 2 +- homeassistant/components/sensor/statistics.py | 4 +- homeassistant/components/sensor/ted5000.py | 2 +- homeassistant/components/sensor/travisci.py | 2 +- .../components/sensor/uk_transport.py | 2 +- homeassistant/components/sensor/waqi.py | 2 +- .../components/sensor/wunderground.py | 6 +- homeassistant/components/sensor/zamg.py | 2 +- homeassistant/components/shopping_list.py | 2 +- homeassistant/components/snips.py | 2 +- .../components/switch/acer_projector.py | 6 +- homeassistant/components/switch/flux.py | 6 +- homeassistant/components/switch/knx.py | 2 +- homeassistant/components/switch/neato.py | 4 +- homeassistant/components/switch/pilight.py | 12 +- homeassistant/components/switch/rachio.py | 2 +- homeassistant/components/switch/rainbird.py | 2 +- homeassistant/components/tahoma.py | 2 +- .../components/telegram_bot/__init__.py | 4 +- homeassistant/components/usps.py | 2 +- homeassistant/components/vacuum/neato.py | 2 +- homeassistant/components/volvooncall.py | 2 +- homeassistant/components/weather/darksky.py | 2 +- .../components/weather/openweathermap.py | 2 +- homeassistant/components/zwave/const.py | 4 +- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/event.py | 2 +- homeassistant/helpers/script.py | 2 +- homeassistant/helpers/template.py | 2 +- homeassistant/scripts/influxdb_migrator.py | 2 +- .../automation/test_numeric_state.py | 2 +- tests/components/automation/test_state.py | 6 +- tests/components/automation/test_sun.py | 2 +- tests/components/binary_sensor/test_vultr.py | 2 +- tests/components/cover/test_template.py | 2 +- .../components/device_tracker/test_asuswrt.py | 2 +- tests/components/device_tracker/test_init.py | 2 +- tests/components/device_tracker/test_mqtt.py | 4 +- .../device_tracker/test_owntracks.py | 14 +-- tests/components/emulated_hue/test_hue_api.py | 4 +- tests/components/image_processing/__init__.py | 2 +- .../components/image_processing/test_init.py | 2 +- .../image_processing/test_openalpr_cloud.py | 4 +- tests/components/light/test_rflink.py | 2 +- tests/components/light/test_rfxtrx.py | 2 +- tests/components/light/test_zwave.py | 16 +-- .../media_player/test_soundtouch.py | 114 +++++++++--------- tests/components/notify/test_facebook.py | 2 +- tests/components/sensor/test_mqtt.py | 4 +- tests/components/sensor/test_random.py | 2 +- tests/components/sensor/test_ring.py | 2 +- tests/components/sensor/test_season.py | 16 +-- tests/components/test_alert.py | 2 +- tests/components/test_ffmpeg.py | 8 +- tests/components/test_hassio.py | 2 +- tests/components/test_init.py | 2 +- tests/components/test_pilight.py | 4 +- tests/components/test_plant.py | 2 +- tests/components/test_remember_the_milk.py | 2 +- tests/components/test_rflink.py | 2 +- tests/components/test_rss_feed_template.py | 2 +- tests/components/zwave/test_init.py | 2 +- tests/helpers/test_entity.py | 20 +-- tests/helpers/test_entity_component.py | 10 +- tests/helpers/test_entityfilter.py | 2 +- tests/helpers/test_service.py | 4 +- tests/util/test_distance.py | 2 +- 138 files changed, 285 insertions(+), 286 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index e8ce147472c..f4981046b5b 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -51,8 +51,8 @@ class _DisplayCategory(object): # Describes a combination of devices set to a specific state, when the # state change must occur in a specific order. For example, a "watch - # Neflix" scene might require the: 1. TV to be powered on & 2. Input set to - # HDMI1. Applies to Scenes + # Netflix" scene might require the: 1. TV to be powered on & 2. Input set + # to HDMI1. Applies to Scenes ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER" # Indicates media devices with video or photo capabilities. @@ -667,7 +667,7 @@ def api_message(request, } } - # If a correlation token exsits, add it to header / Need by Async requests + # If a correlation token exists, add it to header / Need by Async requests token = request[API_HEADER].get('correlationToken') if token: response[API_EVENT][API_HEADER]['correlationToken'] = token diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 2883fca9ab6..5fbd5a764e9 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -251,7 +251,7 @@ class AndroidIPCamEntity(Entity): """The Android device running IP Webcam.""" def __init__(self, host, ipcam): - """Initialize the data oject.""" + """Initialize the data object.""" self._host = host self._ipcam = ipcam diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index dd29e7d602f..7e2b4cda28f 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -66,7 +66,7 @@ class APCUPSdData(object): """ def __init__(self, host, port): - """Initialize the data oject.""" + """Initialize the data object.""" from apcaccess import status self._host = host self._port = port diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index a928ed108c9..7e51ec8c045 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -47,7 +47,7 @@ def setup(hass, config): return False hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex)) + _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) hass.components.persistent_notification.create( 'Error: {}
' 'You will need to restart hass after fixing.' diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py index 47b1be988bf..75a9fa1d046 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_motion.py +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the FFmpeg binary moition sensor.""" + """Set up the FFmpeg binary motion sensor.""" manager = hass.data[DATA_FFMPEG] if not manager.async_run_test(config.get(CONF_INPUT)): diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index df488cc0ed6..3ec70896426 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -118,7 +118,7 @@ class HikvisionData(object): """Hikvision device event stream object.""" def __init__(self, hass, url, port, name, username, password): - """Initialize the data oject.""" + """Initialize the data object.""" from pyhik.hikvision import HikCamera self._url = url self._port = port diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 89d9b7e5c8f..4dddb9bdbef 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -67,8 +67,8 @@ def setup_platform(hass, config: ConfigType, elif subnode_id == 2: parent_device.add_negative_node(node) elif device_type == 'moisture': - # Moisure nodes have a subnode 2, but we ignore it because it's - # just the inverse of the primary node. + # Moisture nodes have a subnode 2, but we ignore it because + # it's just the inverse of the primary node. if subnode_id == 4: # Heartbeat node device = ISYBinarySensorHeartbeat(node, parent_device) diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/binary_sensor/pilight.py index c4c26d3a122..d2c46c795a8 100644 --- a/homeassistant/components/binary_sensor/pilight.py +++ b/homeassistant/components/binary_sensor/pilight.py @@ -97,7 +97,7 @@ class PilightBinarySensor(BinarySensorDevice): def _handle_code(self, call): """Handle received code by the pilight-daemon. - If the code matches the defined playload + If the code matches the defined payload of this sensor the sensor state is changed accordingly. """ # Check if received code matches defined playoad @@ -162,10 +162,10 @@ class PilightTriggerSensor(BinarySensorDevice): def _handle_code(self, call): """Handle received code by the pilight-daemon. - If the code matches the defined playload + If the code matches the defined payload of this sensor the sensor state is changed accordingly. """ - # Check if received code matches defined playoad + # Check if received code matches defined payload # True if payload is contained in received code dict payload_ok = True for key in self._payload: diff --git a/homeassistant/components/camera/abode.py b/homeassistant/components/camera/abode.py index 3c0c0a54e0e..ee739810a61 100644 --- a/homeassistant/components/camera/abode.py +++ b/homeassistant/components/camera/abode.py @@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discoveryy_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Abode camera devices.""" import abodepy.helpers.constants as CONST import abodepy.helpers.timeline as TIMELINE diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index 6e3d3622a3f..f37e7778414 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -28,7 +28,7 @@ CONF_VERTICAL_FLIP = 'vertical_flip' DEFAULT_HORIZONTAL_FLIP = 0 DEFAULT_IMAGE_HEIGHT = 480 -DEFAULT_IMAGE_QUALITIY = 7 +DEFAULT_IMAGE_QUALITY = 7 DEFAULT_IMAGE_ROTATION = 0 DEFAULT_IMAGE_WIDTH = 640 DEFAULT_NAME = 'Raspberry Pi Camera' @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT): vol.Coerce(int), - vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY): + vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION): vol.All(vol.Coerce(int), vol.Range(min=0, max=359)), @@ -131,7 +131,7 @@ class RaspberryCamera(Camera): stderr=subprocess.STDOUT) def camera_image(self): - """Return raspstill image response.""" + """Return raspistill image response.""" with open(self._config[CONF_FILE_PATH], 'rb') as file: return file.read() diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 9445fc7cfc9..ba4973aea9e 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -249,7 +249,7 @@ class GenericThermostat(ClimateDevice): else: _LOGGER.error("Unrecognized operation mode: %s", operation_mode) return - # Ensure we updae the current operation after changing the mode + # Ensure we update the current operation after changing the mode self.schedule_update_ha_state() @asyncio.coroutine diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index ff1400a8fae..5553db70f0d 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -139,8 +139,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, value_type, value) if self.gateway.optimistic: - # O - # ptimistically assume that device has changed state + # Optimistically assume that device has changed state self._values[value_type] = value self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 7155aaf5924..5d54b39e773 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -24,7 +24,7 @@ CONF_RELAY = 'relay' CONF_THERMOSTAT = 'thermostat' DEFAULT_AWAY_TEMPERATURE = 14 -# # The default offeset is 2 hours (when you use the thermostat itself) +# # The default offset is 2 hours (when you use the thermostat itself) DEFAULT_TIME_OFFSET = 7200 # # Return cached results if last scan was less then this time ago # # NetAtmo Data is uploaded to server every hour diff --git a/homeassistant/components/climate/oem.py b/homeassistant/components/climate/oem.py index 0cbdc8f2ce6..59f8db03318 100644 --- a/homeassistant/components/climate/oem.py +++ b/homeassistant/components/climate/oem.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ThermostatDevice(ClimateDevice): - """Interface class for the oemthermostat modul.""" + """Interface class for the oemthermostat module.""" def __init__(self, hass, thermostat, name, away_temp): """Initialize the device.""" diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index 1fa215e5fb9..2736b656a15 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -68,7 +68,7 @@ class HMCover(HMDevice, CoverDevice): self._hmdevice.stop(self._channel) def _init_data_struct(self): - """Generate a data dictoinary (self._data) from metadata.""" + """Generate a data dictionary (self._data) from metadata.""" self._state = "LEVEL" self._data.update({self._state: STATE_UNKNOWN}) if "LEVEL_2" in self._hmdevice.WRITENODE: diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index 79c57c41e90..a6cd1263a73 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -74,7 +74,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up cover for KNX platform configured within plattform.""" + """Set up cover for KNX platform configured within platform.""" import xknx cover = xknx.devices.Cover( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py index 08a2ef8c5ad..4e38681a310 100644 --- a/homeassistant/components/cover/lutron.py +++ b/homeassistant/components/cover/lutron.py @@ -63,7 +63,7 @@ class LutronCover(LutronDevice, CoverDevice): def update(self): """Call when forcing a refresh of the device.""" - # Reading the property (rather than last_level()) fetchs value + # Reading the property (rather than last_level()) fetches value level = self._lutron_device.level _LOGGER.debug("Lutron ID: %d updated to %f", self._lutron_device.id, level) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 2196dd78fdb..fb47b26a687 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -242,7 +242,7 @@ class _Connection: return self._connected def connect(self): - """Mark currenct connection state as connected.""" + """Mark current connection state as connected.""" self._connected = True def disconnect(self): diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py index abdd829f26c..357dd0d36cf 100644 --- a/homeassistant/components/device_tracker/huawei_router.py +++ b/homeassistant/components/device_tracker/huawei_router.py @@ -119,7 +119,7 @@ class HuaweiDeviceScanner(DeviceScanner): cnt = requests.post('http://{}/asp/GetRandCount.asp'.format(self.host)) cnt_str = str(cnt.content, cnt.apparent_encoding, errors='replace') - _LOGGER.debug("Loggin in") + _LOGGER.debug("Logging in") cookie = requests.post('http://{}/login.cgi'.format(self.host), data=[('UserName', self.username), ('PassWord', self.password), diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 2306a66070b..dee5044d3a6 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -46,8 +46,8 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -def _refresh_on_acccess_denied(func): - """If remove rebooted, it lost our session so rebuld one and try again.""" +def _refresh_on_access_denied(func): + """If remove rebooted, it lost our session so rebuild one and try again.""" def decorator(self, *args, **kwargs): """Wrap the function to refresh session_id on PermissionError.""" try: @@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner): """Must be implemented depending on the software.""" raise NotImplementedError - @_refresh_on_acccess_denied + @_refresh_on_access_denied def get_device_name(self, mac): """Return the name of the given device or None if we don't know.""" if self.mac2name is None: @@ -104,7 +104,7 @@ class UbusDeviceScanner(DeviceScanner): self.mac2name = None return name - @_refresh_on_acccess_denied + @_refresh_on_access_denied def _update_info(self): """Ensure the information from the router is up to date. diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py index 0d6645f37c1..4bdb4c80add 100644 --- a/homeassistant/components/dominos.py +++ b/homeassistant/components/dominos.py @@ -1,7 +1,7 @@ """ Support for Dominos Pizza ordering. -The Dominos Pizza component ceates a service which can be invoked to order +The Dominos Pizza component creates a service which can be invoked to order from their menu For more details about this platform, please refer to the documentation at diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index d832bbdfdd1..b7354b4f0a7 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -79,7 +79,7 @@ def setup(hass, config): if req.status_code != 200: _LOGGER.warning( - "downloading '%s' failed, stauts_code=%d", + "downloading '%s' failed, status_code=%d", url, req.status_code) diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 88cbf1bd57b..7ae4ec862bb 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -193,7 +193,7 @@ class EightSleepUserEntity(Entity): """The Eight Sleep device entity.""" def __init__(self, eight): - """Initialize the data oject.""" + """Initialize the data object.""" self._eight = eight @asyncio.coroutine @@ -217,7 +217,7 @@ class EightSleepHeatEntity(Entity): """The Eight Sleep device entity.""" def __init__(self, eight): - """Initialize the data oject.""" + """Initialize the data object.""" self._eight = eight @asyncio.coroutine diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index eccc800319c..6e6d377986d 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -205,7 +205,7 @@ def async_setup(hass, config: dict): @asyncio.coroutine def async_handle_fan_service(service): - """Hande service call for fans.""" + """Handle service call for fans.""" method = SERVICE_TO_METHOD.get(service.service) params = service.data.copy() diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2da27b7e544..d699acab25d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -586,9 +586,9 @@ def _is_latest(js_option, request): family_min_version = { 'Chrome': 50, # Probably can reduce this - 'Firefox': 43, # Array.protopype.includes added in 43 + 'Firefox': 43, # Array.prototype.includes added in 43 'Opera': 40, # Probably can reduce this - 'Edge': 14, # Array.protopype.includes added in 14 + 'Edge': 14, # Array.prototype.includes added in 14 'Safari': 10, # many features not supported by 9 } version = family_min_version.get(useragent.browser.family) diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index f7923067270..30151ee1a56 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -128,7 +128,7 @@ def do_authentication(hass, config): """Keep trying to validate the user_code until it expires.""" if now >= dt.as_local(dev_flow.user_code_expiry): hass.components.persistent_notification.create( - 'Authenication code expired, please restart ' + 'Authentication code expired, please restart ' 'Home-Assistant and try again', title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index ba56e7c3837..0cc6f9c3f83 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -429,7 +429,7 @@ def async_devices_query(hass, config, payload): devices = {} for device in payload.get('devices', []): devid = device.get('id') - # In theory this should never happpen + # In theory this should never happen if not devid: _LOGGER.error('Device missing ID: %s', device) continue diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index a8529f18b69..3881b6211c2 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -247,7 +247,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None): @asyncio.coroutine def async_setup(hass, config): - """Set up all groups found definded in the configuration.""" + """Set up all groups found defined in the configuration.""" component = hass.data.get(DOMAIN) if component is None: diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 70054e54075..33df4bfbd17 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -218,7 +218,7 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({ @bind_hass def virtualkey(hass, address, channel, param, interface=None): - """Send virtual keypress to homematic controlller.""" + """Send virtual keypress to homematic controller.""" data = { ATTR_ADDRESS: address, ATTR_CHANNEL: channel, @@ -256,7 +256,7 @@ def set_device_value(hass, address, channel, param, value, interface=None): @bind_hass def set_install_mode(hass, interface, mode=None, time=None, address=None): - """Call setInstallMode XML-RPC method of supplied inteface.""" + """Call setInstallMode XML-RPC method of supplied interface.""" data = { key: value for key, value in ( (ATTR_INTERFACE, interface), @@ -665,7 +665,7 @@ class HMHub(Entity): self.schedule_update_ha_state() def _update_variables(self, now): - """Retrive all variable data and update hmvariable states.""" + """Retrieve all variable data and update hmvariable states.""" variables = self._homematic.getAllSystemVariables(self._name) if variables is None: return diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 40aac61914b..6770ff1bdf6 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -36,7 +36,7 @@ def validate_attributes(list_attributes): """Validate face attributes.""" for attr in list_attributes: if attr not in SUPPORTED_ATTRIBUTES: - raise vol.Invalid("Invalid attribtue {0}".format(attr)) + raise vol.Invalid("Invalid attribute {0}".format(attr)) return list_attributes diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 0cdd1675274..258731326ee 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -201,7 +201,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): return # Parse data - knwon_faces = [] + known_faces = [] total = 0 for face in detect: total += 1 @@ -215,9 +215,9 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): name = s_name break - knwon_faces.append({ + known_faces.append({ ATTR_NAME: name, ATTR_CONFIDENCE: data['confidence'] * 100, }) - self.async_process_faces(knwon_faces, total) + self.async_process_faces(known_faces, total) diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py index 60b2eadee92..1ef8a4bb847 100644 --- a/homeassistant/components/image_processing/seven_segments.py +++ b/homeassistant/components/image_processing/seven_segments.py @@ -66,7 +66,7 @@ class ImageProcessingSsocr(ImageProcessingEntity): if name: self._name = name else: - self._name = "SevenSegement OCR {0}".format( + self._name = "SevenSegment OCR {0}".format( split_entity_id(camera_entity)[1]) self._state = None diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 43feeb8c4f4..56761b5af4e 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -137,7 +137,7 @@ class InputBoolean(ToggleEntity): @property def icon(self): - """Returh the icon to be used for this entity.""" + """Return the icon to be used for this entity.""" return self._icon @property diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index ebabcdb0e79..5e2528d2f0d 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -72,7 +72,7 @@ ATTR_DEVICE_SYSTEM_VERSION = 'systemVersion' ATTR_DEVICE_TYPE = 'type' ATTR_DEVICE_SYSTEM_NAME = 'systemName' -ATTR_APP_BUNDLE_IDENTIFER = 'bundleIdentifer' +ATTR_APP_BUNDLE_IDENTIFIER = 'bundleIdentifier' ATTR_APP_BUILD_NUMBER = 'buildNumber' ATTR_APP_VERSION_NUMBER = 'versionNumber' @@ -136,7 +136,7 @@ IDENTIFY_DEVICE_SCHEMA = vol.Schema({ IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA) IDENTIFY_APP_SCHEMA = vol.Schema({ - vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string, + vol.Required(ATTR_APP_BUNDLE_IDENTIFIER): cv.string, vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int, vol.Optional(ATTR_APP_VERSION_NUMBER): cv.string }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 28cfac39154..d85883e472a 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -123,7 +123,7 @@ SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', 'light', 'switch'] SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch'] -# ISY Scenes are more like Swithes than Hass Scenes +# ISY Scenes are more like Switches than Hass Scenes # (they can turn off, and report their state) SCENE_DOMAIN = 'switch' diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b761b04c705..cfeceb0c991 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -254,7 +254,7 @@ def async_setup(hass, config): @asyncio.coroutine def async_handle_light_service(service): - """Hande a turn light on or off service call.""" + """Handle a turn light on or off service call.""" # Get the validated data params = service.data.copy() diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index 6d502e15d6f..3b6b22faba9 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def retry(method): """Retry bluetooth commands.""" @wraps(method) - def wrapper_retry(device, *args, **kwds): + def wrapper_retry(device, *args, **kwargs): """Try send command and retry on error.""" # pylint: disable=import-error import decora @@ -46,7 +46,7 @@ def retry(method): if time.monotonic() - initial >= 10: return None try: - return method(device, *args, **kwds) + return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, bluepy.btle.BTLEException): _LOGGER.warning("Decora connect error for device %s. " diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index c48de4deaf8..396ddc984fa 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -46,7 +46,7 @@ EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade' EFFECT_COLORSTROBE = 'colorstrobe' EFFECT_RED_STROBE = 'red_strobe' EFFECT_GREEN_STROBE = 'green_strobe' -EFFECT_BLUE_STOBE = 'blue_strobe' +EFFECT_BLUE_STROBE = 'blue_strobe' EFFECT_YELLOW_STROBE = 'yellow_strobe' EFFECT_CYAN_STROBE = 'cyan_strobe' EFFECT_PURPLE_STROBE = 'purple_strobe' @@ -68,7 +68,7 @@ EFFECT_MAP = { EFFECT_COLORSTROBE: 0x30, EFFECT_RED_STROBE: 0x31, EFFECT_GREEN_STROBE: 0x32, - EFFECT_BLUE_STOBE: 0x33, + EFFECT_BLUE_STROBE: 0x33, EFFECT_YELLOW_STROBE: 0x34, EFFECT_CYAN_STROBE: 0x35, EFFECT_PURPLE_STROBE: 0x36, diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 807c19fffdb..a3db1ff30ff 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -1,5 +1,5 @@ """ -Support for Homematic lighs. +Support for Homematic lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.homematic/ diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index a2eed36a089..ba78546cf77 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the iGlo lighs.""" + """Set up the iGlo lights.""" host = config.get(CONF_HOST) name = config.get(CONF_NAME) port = config.get(CONF_PORT) diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index a6191b05c7c..cee8155c322 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -27,7 +27,7 @@ def setup_platform(hass, config: ConfigType, class ISYLightDevice(ISYDevice, Light): - """Representation of an ISY994 light devie.""" + """Representation of an ISY994 light device.""" def __init__(self, node: object) -> None: """Initialize the ISY994 light device.""" diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 732cfe2a644..8c9e78ab2b0 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -57,7 +57,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up light for KNX platform configured within plattform.""" + """Set up light for KNX platform configured within platform.""" import xknx light = xknx.devices.Light( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1dc0861d737..9e1e2e54ad9 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -116,7 +116,7 @@ class LogbookView(HomeAssistantView): extra_urls = ['/api/logbook/{datetime}'] def __init__(self, config): - """Initilalize the logbook view.""" + """Initialize the logbook view.""" self.config = config @asyncio.coroutine diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 21898f7b16d..c2309401977 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -93,7 +93,7 @@ def async_setup(hass, config): if LOGGER_LOGS in logfilter: logs.update(logfilter[LOGGER_LOGS]) - # Add new logpoints mapped to correc severity + # Add new logpoints mapped to correct severity for key, value in logpoints.items(): logs[key] = LOGSEVERITY[value] diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py index a1953839f4f..2e807058edf 100644 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ b/homeassistant/components/mailbox/asterisk_mbox.py @@ -30,7 +30,7 @@ class AsteriskMailbox(Mailbox): """Asterisk VM Sensor.""" def __init__(self, hass, name): - """Initialie Asterisk mailbox.""" + """Initialize Asterisk mailbox.""" super().__init__(hass, name) async_dispatcher_connect( self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index d8a0cd7ebf9..848c6abe91f 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -122,7 +122,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BluesoundPlayer(MediaPlayerDevice): - """Represenatation of a Bluesound Player.""" + """Representation of a Bluesound Player.""" def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" @@ -338,7 +338,7 @@ class BluesoundPlayer(MediaPlayerDevice): @asyncio.coroutine @Throttle(UPDATE_CAPTURE_INTERVAL) def async_update_captures(self): - """Update Capture cources.""" + """Update Capture sources.""" resp = yield from self.send_bluesound_command( 'RadioBrowse?service=Capture') if not resp: diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 2aaff646885..928062cb2dc 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -205,7 +205,7 @@ class CastDevice(MediaPlayerDevice): @property def media_album_artist(self): - """Album arist of current playing media (Music track only).""" + """Album artist of current playing media (Music track only).""" return self.media_status.album_artist if self.media_status else None @property diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index ebb8a670488..35d3ed35095 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -273,7 +273,7 @@ class EmbyDevice(MediaPlayerDevice): @property def media_season(self): - """Season of curent playing media (TV Show only).""" + """Season of current playing media (TV Show only).""" return self.device.media_season @property diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py index 03e7c6f0c9f..e1fffefed18 100644 --- a/homeassistant/components/media_player/hdmi_cec.py +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CecPlayerDevice(CecDevice, MediaPlayerDevice): - """Representation of a HDMI device as a Media palyer.""" + """Representation of a HDMI device as a Media player.""" def __init__(self, hass: HomeAssistant, device, logical) -> None: """Initialize the HDMI device.""" diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 82bd106af8d..769c8951dc8 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -45,7 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ SERVICE_CALL_METHOD = 'squeezebox_call_method' -DATA_SQUEEZEBOX = 'squeexebox' +DATA_SQUEEZEBOX = 'squeezebox' ATTR_PARAMETERS = 'parameters' diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index eda0bc2b326..dab7901111d 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -203,7 +203,7 @@ class Volumio(MediaPlayerDevice): """Send mute command to media player.""" mutecmd = 'mute' if mute else 'unmute' if mute: - # mute is implemenhted as 0 volume, do save last volume level + # mute is implemented as 0 volume, do save last volume level self._lastvol = self._state['volume'] return self.send_volumio_msg( 'commands', params={'cmd': 'volume', 'volume': mutecmd}) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 577988bc58c..8e4729a4409 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -310,7 +310,7 @@ class YamahaDevice(MediaPlayerDevice): NOTE: this might take a while, because the only API interface for setting the net radio station emulates button pressing and - navigating through the net radio menu hiearchy. And each sub + navigating through the net radio menu hierarchy. And each sub menu must be fetched by the receiver from the vtuner service. """ diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 91053b41bf6..390da7ed0e0 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -76,12 +76,12 @@ def is_socket_address(value): def has_parent_dir(value): - """Validate that value is in an existing directory which is writetable.""" + """Validate that value is in an existing directory which is writeable.""" parent = os.path.dirname(os.path.realpath(value)) is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK) if not is_dir_writable: raise vol.Invalid( - '{} directory does not exist or is not writetable'.format(parent)) + '{} directory does not exist or is not writeable'.format(parent)) return value diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 512819b7e74..37028decf71 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -183,7 +183,7 @@ class NestDevice(object): "Connection error logging into the nest web service.") def smoke_co_alarms(self): - """Generate a list of smoke co alarams.""" + """Generate a list of smoke co alarms.""" try: for structure in self.nest.structures: if structure.name in self.local_structure: diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/notify/knx.py index c5dbcb0d4ad..d14d8dcf8ad 100644 --- a/homeassistant/components/notify/knx.py +++ b/homeassistant/components/notify/knx.py @@ -51,7 +51,7 @@ def async_get_service_discovery(hass, discovery_info): @callback def async_get_service_config(hass, config): - """Set up notification for KNX platform configured within plattform.""" + """Set up notification for KNX platform configured within platform.""" import xknx notification = xknx.devices.Notification( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index d5d6f657bc6..4d5f27082de 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -171,7 +171,7 @@ def setup(hass, config): def qs_callback(item): """Typically a button press or update signal.""" if qsusb is None: # Shutting down - _LOGGER.info("Botton press or updating signal done") + _LOGGER.info("Button press or updating signal done") return # If button pressed, fire a hass event diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py index 882731d4f2c..76dda6fd366 100644 --- a/homeassistant/components/rainbird.py +++ b/homeassistant/components/rainbird.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Set up the Rain Bird componenent.""" + """Set up the Rain Bird component.""" conf = config[DOMAIN] server = conf.get(CONF_HOST) password = conf.get(CONF_PASSWORD) @@ -38,8 +38,8 @@ def setup(hass, config): _LOGGER.debug("Rain Bird Controller set to: %s", server) - initialstatus = controller.currentIrrigation() - if initialstatus == -1: + initial_status = controller.currentIrrigation() + if initial_status == -1: _LOGGER.error("Error getting state. Possible configuration issues") return False diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py index 7a04949dbeb..42d4ce77054 100644 --- a/homeassistant/components/remote/kira.py +++ b/homeassistant/components/remote/kira.py @@ -48,8 +48,8 @@ class KiraRemote(Entity): def send_command(self, command, **kwargs): """Send a command to one device.""" - for singel_command in command: - code_tuple = (singel_command, + for single_command in command: + code_tuple = (single_command, kwargs.get(remote.ATTR_DEVICE)) _LOGGER.info("Sending Command: %s to %s", *code_tuple) self._kira.sendCode(code_tuple) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 7d2e428c56b..27bfd1abfbe 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -77,7 +77,7 @@ def setup(hass, config): """Set up the RFXtrx component.""" # Declare the Handle event def handle_receive(event): - """Handle revieved messages from RFXtrx gateway.""" + """Handle received messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: return diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py index 8c5c6570515..a7193b40949 100644 --- a/homeassistant/components/scsgate.py +++ b/homeassistant/components/scsgate.py @@ -87,7 +87,7 @@ class SCSGate(object): self._logger.debug("Received message {}".format(message)) if not isinstance(message, StateMessage) and \ not isinstance(message, ScenarioTriggeredMessage): - msg = "Ignored message {} - not releavant type".format( + msg = "Ignored message {} - not relevant type".format( message) self._logger.debug(msg) return @@ -109,7 +109,7 @@ class SCSGate(object): self._logger.error(msg) else: self._logger.info( - "Ignoring state message for device {} because unknonw".format( + "Ignoring state message for device {} because unknown".format( message.entity)) @property diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index 081dc6cdc6e..470d7749ea2 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -1,7 +1,7 @@ """ Support for BME680 Sensor over SMBus. -Temperature, humidity, pressure and volitile gas support. +Temperature, humidity, pressure and volatile gas support. Air Quality calculation based on humidity and volatile gas. For more details about this platform, please refer to the documentation at diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 1c12799549c..32c888bad3b 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -87,7 +87,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(devices) def update_entities_telegram(telegram): - """Update entities with latests telegram and trigger state update.""" + """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram for device in devices: device.telegram = telegram @@ -122,7 +122,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if transport: # Register listener to close transport on HA shutdown - stop_listerer = hass.bus.async_listen_once( + stop_listener = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, transport.close) # Wait for reader to close @@ -131,8 +131,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if hass.state != CoreState.stopping: # Unexpected disconnect if transport: - # remove listerer - stop_listerer() + # remove listener + stop_listener() # Reflect disconnect state in devices state by setting an # empty telegram resulting in `unknown` states diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index 27d4bdd2f5a..eee959fceba 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ebox_data = EBoxData(username, password) ebox_data.update() except requests.exceptions.HTTPError as error: - _LOGGER.error("Failt login: %s", error) + _LOGGER.error("Failed login: %s", error) return False name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py index fc1daf151c7..cd02137f4d5 100644 --- a/homeassistant/components/sensor/emoncms.py +++ b/homeassistant/components/sensor/emoncms.py @@ -153,7 +153,7 @@ class EmonCmsSensor(Entity): @property def device_state_attributes(self): - """Return the atrributes of the sensor.""" + """Return the attributes of the sensor.""" return { ATTR_FEEDID: self._elem["id"], ATTR_TAG: self._elem["tag"], diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 07c085cd18d..4fc79745b99 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -50,12 +50,12 @@ SENSOR_TYPES = { 'text_int_used': ['International text used', MESSAGES, 'mdi:message-alert'], 'text_int_limit': ['International text limit', - MESSAGES, 'mdi:message-alart'], - 'text_int_remaining': ['Internaltional remaining', + MESSAGES, 'mdi:message-alert'], + 'text_int_remaining': ['International remaining', MESSAGES, 'mdi:message-alert'], 'talk_used': ['Talk used', MINUTES, 'mdi:cellphone'], 'talk_limit': ['Talk limit', MINUTES, 'mdi:cellphone'], - 'talt_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'], + 'talk_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'], 'other_talk_used': ['Other Talk used', MINUTES, 'mdi:cellphone'], 'other_talk_limit': ['Other Talk limit', MINUTES, 'mdi:cellphone'], 'other_talk_remaining': ['Other Talk remaining', MINUTES, 'mdi:cellphone'], diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 0a455099597..70afa6fe1e1 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -52,7 +52,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up sensor for KNX platform configured within plattform.""" + """Set up sensor for KNX platform configured within platform.""" import xknx sensor = xknx.devices.Sensor( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index 772e59f266e..b6366de6432 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -47,7 +47,7 @@ CONDITION_CLASSES = { DEFAULT_NAME = "Met Office" -VISIBILTY_CLASSES = { +VISIBILITY_CLASSES = { 'VP': '<1', 'PO': '1-4', 'MO': '4-10', @@ -144,7 +144,7 @@ class MetOfficeCurrentSensor(Entity): """Return the state of the sensor.""" if (self._condition == 'visibility_distance' and hasattr(self.data.data, 'visibility')): - return VISIBILTY_CLASSES.get(self.data.data.visibility.value) + return VISIBILITY_CLASSES.get(self.data.data.visibility.value) if hasattr(self.data.data, self._condition): variable = getattr(self.data.data, self._condition) if self._condition == 'weather': diff --git a/homeassistant/components/sensor/modem_callerid.py b/homeassistant/components/sensor/modem_callerid.py index 0b71540f346..f80ea5853c8 100644 --- a/homeassistant/components/sensor/modem_callerid.py +++ b/homeassistant/components/sensor/modem_callerid.py @@ -18,7 +18,7 @@ REQUIREMENTS = ['basicmodem==0.7'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Modem CallerID' -ICON = 'mdi:phone-clasic' +ICON = 'mdi:phone-classic' DEFAULT_DEVICE = '/dev/ttyACM0' STATE_RING = 'ring' diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index 2228a8eab60..a9fb3ae7a6f 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -243,7 +243,7 @@ class PyNUTData(object): """ def __init__(self, host, port, alias, username, password): - """Initialize the data oject.""" + """Initialize the data object.""" from pynut2.nut2 import PyNUTClient, PyNUTError self._host = host self._port = port diff --git a/homeassistant/components/sensor/openevse.py b/homeassistant/components/sensor/openevse.py index 6ded982eea1..9086f37f8b2 100644 --- a/homeassistant/components/sensor/openevse.py +++ b/homeassistant/components/sensor/openevse.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'status': ['Charging Status', None], 'charge_time': ['Charge Time Elapsed', 'minutes'], - 'ambient_temp': ['Ambient Termperature', TEMP_CELSIUS], + 'ambient_temp': ['Ambient Temperature', TEMP_CELSIUS], 'ir_temp': ['IR Temperature', TEMP_CELSIUS], 'rtc_temp': ['RTC Temperature', TEMP_CELSIUS], 'usage_session': ['Usage this Session', 'kWh'], diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 86ca496eb62..5b5385f14ef 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -79,10 +79,10 @@ class PilightSensor(Entity): def _handle_code(self, call): """Handle received code by the pilight-daemon. - If the code matches the defined playload + If the code matches the defined payload of this sensor the sensor state is changed accordingly. """ - # Check if received code matches defined playoad + # Check if received code matches defined payload # True if payload is contained in received code dict, not # all items have to match if self._payload.items() <= call.data.items(): diff --git a/homeassistant/components/sensor/pushbullet.py b/homeassistant/components/sensor/pushbullet.py index 086698b06bf..415174ac273 100644 --- a/homeassistant/components/sensor/pushbullet.py +++ b/homeassistant/components/sensor/pushbullet.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Pushbllet Sensor platform.""" + """Set up the Pushbullet Sensor platform.""" from pushbullet import PushBullet from pushbullet import InvalidKeyError try: diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 19281d36d88..46c714d0dbf 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -91,7 +91,7 @@ class StatisticsSensor(Entity): if 'recorder' in self._hass.config.components: # only use the database if it's configured - hass.async_add_job(self._initzialize_from_database) + hass.async_add_job(self._initialize_from_database) @callback # pylint: disable=invalid-name @@ -202,7 +202,7 @@ class StatisticsSensor(Entity): self.average_change = self.change = STATE_UNKNOWN @asyncio.coroutine - def _initzialize_from_database(self): + def _initialize_from_database(self): """Initialize the list of states from the database. The query will get the list of states in DESCENDING order so that we diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index 08681fd37f2..55d520cf6ca 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -1,5 +1,5 @@ """ -Support gahtering ted500 information. +Support gathering ted500 information. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ted5000/ diff --git a/homeassistant/components/sensor/travisci.py b/homeassistant/components/sensor/travisci.py index 5f341760bb6..1ca08e7c0aa 100644 --- a/homeassistant/components/sensor/travisci.py +++ b/homeassistant/components/sensor/travisci.py @@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] - # non specificy repository selected, then show all associated + # non specific repository selected, then show all associated if not repositories: all_repos = travis.repos(member=user.login) repositories = [repo.slug for repo in all_repos] diff --git a/homeassistant/components/sensor/uk_transport.py b/homeassistant/components/sensor/uk_transport.py index 9b35afb418c..72d34411d5c 100644 --- a/homeassistant/components/sensor/uk_transport.py +++ b/homeassistant/components/sensor/uk_transport.py @@ -132,7 +132,7 @@ class UkTransportSensor(Entity): _LOGGER.warning('Invalid response from API') elif 'error' in response.json(): if 'exceeded' in response.json()['error']: - self._state = 'Useage limites exceeded' + self._state = 'Usage limits exceeded' if 'invalid' in response.json()['error']: self._state = 'Credentials invalid' else: diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 318a22cfa2a..bf2e263a0bb 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -84,7 +84,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dev.append(waqi_sensor) except (aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError): - _LOGGER.exception('Failed to connct to WAQI servers.') + _LOGGER.exception('Failed to connect to WAQI servers.') raise PlatformNotReady async_add_devices(dev, True) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 8bb449b2ec1..d0d9758c13a 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -55,7 +55,7 @@ class WUSensorConfig(object): https://www.wunderground.com/weather/api/d/docs?d=data/index value (function(WUndergroundData)): callback that extracts desired value from WUndergroundData object - unit_of_measurement (string): unit of meassurement + unit_of_measurement (string): unit of measurement entity_picture (string): value or callback returning URL of entity picture icon (string): icon name or URL @@ -84,7 +84,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): dictionary. icon (string): icon name or URL, if None sensor will use current weather symbol - unit_of_measurement (string): unit of meassurement + unit_of_measurement (string): unit of measurement """ super().__init__( friendly_name, @@ -230,7 +230,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): value_type (string): "record" or "normal" wu_unit (string): unit name in WU API icon (string): icon name or URL - unit_of_measurement (string): unit of meassurement + unit_of_measurement (string): unit of measurement """ super().__init__( friendly_name=friendly_name, diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py index 4b63d769243..df5ff5e8d37 100644 --- a/homeassistant/components/sensor/zamg.py +++ b/homeassistant/components/sensor/zamg.py @@ -236,7 +236,7 @@ def closest_station(lat, lon, cache_dir): stations = zamg_stations(cache_dir) def comparable_dist(zamg_id): - """Calculate the psudeo-distance from lat/lon.""" + """Calculate the pseudo-distance from lat/lon.""" station_lat, station_lon = stations[zamg_id] return (lat - station_lat) ** 2 + (lon - station_lon) ** 2 diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 8ec023057d1..31259325c04 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -1,4 +1,4 @@ -"""Component to manage a shoppling list.""" +"""Component to manage a shopping list.""" import asyncio import json import logging diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 5c35e43881e..d085b1279cb 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -155,7 +155,7 @@ def async_setup(hass, config): def resolve_slot_values(slot): - """Convert snips builtin types to useable values.""" + """Convert snips builtin types to usable values.""" if 'value' in slot['value']: value = slot['value']['value'] else: diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index d32c0610b66..8fd70ec7ed8 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -103,13 +103,13 @@ class AcerSwitch(SwitchDevice): # need to wait for timeout ret = self.ser.read_until(size=20).decode('utf-8') except serial.SerialException: - _LOGGER.error('Problem comunicating with %s', self._serial_port) + _LOGGER.error('Problem communicating with %s', self._serial_port) self.ser.close() return ret def _write_read_format(self, msg): - """Write msg, obtain awnser and format output.""" - # awnsers are formatted as ***\rawnser\r*** + """Write msg, obtain answer and format output.""" + # answers are formatted as ***\answer\r*** awns = self._write_read(msg) match = re.search(r'\r(.+)\r', awns) if match: diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 7e3566f17b0..a6c57656767 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -33,7 +33,7 @@ CONF_START_CT = 'start_colortemp' CONF_SUNSET_CT = 'sunset_colortemp' CONF_STOP_CT = 'stop_colortemp' CONF_BRIGHTNESS = 'brightness' -CONF_DISABLE_BRIGTNESS_ADJUST = 'disable_brightness_adjust' +CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' CONF_INTERVAL = 'interval' MODE_XY = 'xy' @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), vol.Optional(CONF_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - vol.Optional(CONF_DISABLE_BRIGTNESS_ADJUST): cv.boolean, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Any(MODE_XY, MODE_MIRED, MODE_RGB), vol.Optional(CONF_INTERVAL, default=30): cv.positive_int, @@ -105,7 +105,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sunset_colortemp = config.get(CONF_SUNSET_CT) stop_colortemp = config.get(CONF_STOP_CT) brightness = config.get(CONF_BRIGHTNESS) - disable_brightness_adjust = config.get(CONF_DISABLE_BRIGTNESS_ADJUST) + disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST) mode = config.get(CONF_MODE) interval = config.get(CONF_INTERVAL) transition = config.get(ATTR_TRANSITION) diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index e0b656aafe9..01c08767ca0 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -51,7 +51,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up switch for KNX platform configured within plattform.""" + """Set up switch for KNX platform configured within platform.""" import xknx switch = xknx.devices.Switch( hass.data[DATA_KNX].xknx, diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index 62bc5f99d01..a797abb47fc 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -1,5 +1,5 @@ """ -Support for Neato Connected Vaccums switches. +Support for Neato Connected Vacuums switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.neato/ @@ -68,7 +68,7 @@ class NeatoConnectedSwitch(ToggleEntity): self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF - _LOGGER.debug("Shedule state: %s", self._schedule_state) + _LOGGER.debug("Schedule state: %s", self._schedule_state) @property def name(self): diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 201aee0f58c..1ce599366a1 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -19,9 +19,9 @@ from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) CONF_OFF_CODE = 'off_code' -CONF_OFF_CODE_RECIEVE = 'off_code_receive' +CONF_OFF_CODE_RECEIVE = 'off_code_receive' CONF_ON_CODE = 'on_code' -CONF_ON_CODE_RECIEVE = 'on_code_receive' +CONF_ON_CODE_RECEIVE = 'on_code_receive' CONF_SYSTEMCODE = 'systemcode' CONF_UNIT = 'unit' CONF_UNITCODE = 'unitcode' @@ -48,9 +48,9 @@ SWITCHES_SCHEMA = vol.Schema({ vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFF_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + vol.Optional(CONF_OFF_CODE_RECEIVE, default=[]): vol.All(cv.ensure_list, [COMMAND_SCHEMA]), - vol.Optional(CONF_ON_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + vol.Optional(CONF_ON_CODE_RECEIVE, default=[]): vol.All(cv.ensure_list, [COMMAND_SCHEMA]) }) @@ -72,8 +72,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): properties.get(CONF_NAME, dev_name), properties.get(CONF_ON_CODE), properties.get(CONF_OFF_CODE), - properties.get(CONF_ON_CODE_RECIEVE), - properties.get(CONF_OFF_CODE_RECIEVE) + properties.get(CONF_ON_CODE_RECEIVE), + properties.get(CONF_OFF_CODE_RECEIVE) ) ) diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index a1ce83b597a..d8d424be361 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -134,7 +134,7 @@ class RachioIro(object): return self._running def list_zones(self, include_disabled=False): - """Return alist of the zones connected to the device, incl. data.""" + """Return a list of the zones connected to the device, incl. data.""" if not self._zones: self._zones = [RachioZone(self.rachio, self, zone['id'], self.manual_run_mins) diff --git a/homeassistant/components/switch/rainbird.py b/homeassistant/components/switch/rainbird.py index ee283b3c269..9aa24b9360b 100644 --- a/homeassistant/components/switch/rainbird.py +++ b/homeassistant/components/switch/rainbird.py @@ -52,7 +52,7 @@ class RainBirdSwitch(SwitchDevice): self._devid = dev_id self._zone = int(dev.get(CONF_ZONE)) self._name = dev.get(CONF_FRIENDLY_NAME, - "Sprinker {}".format(self._zone)) + "Sprinkler {}".format(self._zone)) self._state = None self._duration = dev.get(CONF_TRIGGER_TIME) self._attributes = { diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 4eacee08ec1..4488b4e836b 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -65,7 +65,7 @@ def setup(hass, config): api.get_setup() devices = api.get_devices() except RequestException: - _LOGGER.exception("Cannot fetch informations from Tahoma API") + _LOGGER.exception("Cannot fetch information from Tahoma API") return False hass.data[DOMAIN] = { diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index cb314c4a2b4..170e1517a6d 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -330,7 +330,7 @@ class TelegramNotificationService: This can be one of (message_id, inline_message_id) from a msg dict, returning a tuple. **You can use 'last' as message_id** to edit - the last sended message in the chat_id. + the message last sent in the chat_id. """ message_id = inline_message_id = None if ATTR_MESSAGEID in msg_data: @@ -354,7 +354,7 @@ class TelegramNotificationService: chat_ids = [t for t in target if t in self.allowed_chat_ids] if chat_ids: return chat_ids - _LOGGER.warning("Unallowed targets: %s, using default: %s", + _LOGGER.warning("Disallowed targets: %s, using default: %s", target, self._default_user) return [self._default_user] diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps.py index 0c4eba54e35..58f858b0975 100644 --- a/homeassistant/components/usps.py +++ b/homeassistant/components/usps.py @@ -69,7 +69,7 @@ class USPSData(object): """ def __init__(self, session, name): - """Initialize the data oject.""" + """Initialize the data object.""" self.session = session self.name = name self.packages = [] diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 29099db5cd5..2a4eb2d5e7f 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -1,5 +1,5 @@ """ -Support for Neato Connected Vaccums. +Support for Neato Connected Vacuums. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index dcd4ed518d0..3e36d0a3028 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -93,7 +93,7 @@ def setup(hass, config): hass, component, DOMAIN, (vehicle.vin, attr), config) def update_vehicle(vehicle): - """Revieve updated information on vehicle.""" + """Receive updated information on vehicle.""" state.vehicles[vehicle.vin] = vehicle if vehicle.vin not in state.entities: discover_vehicle(vehicle) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 0566cc03662..21f67ce080a 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -1,5 +1,5 @@ """ -Patform for retrieving meteorological data from Dark Sky. +Platform for retrieving meteorological data from Dark Sky. For more details about this platform, please refer to the documentation https://home-assistant.io/components/weather.darksky/ diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 1ff5eeaa535..479831d713e 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -180,7 +180,7 @@ class WeatherData(object): @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) def update_forecast(self): - """Get the lastest forecast from OpenWeatherMap.""" + """Get the latest forecast from OpenWeatherMap.""" from pyowm.exceptions.api_call_error import APICallError try: diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 2815be45df2..e2524aefadf 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -182,8 +182,8 @@ SPECIFIC_TYPE_NOT_USED = 0 # Available in all Generic types GENERIC_TYPE_AV_CONTROL_POINT = 3 SPECIFIC_TYPE_DOORBELL = 18 -SPECIFIC_TYPE_SATELLITE_RECIEVER = 4 -SPECIFIC_TYPE_SATELLITE_RECIEVER_V2 = 17 +SPECIFIC_TYPE_SATELLITE_RECEIVER = 4 +SPECIFIC_TYPE_SATELLITE_RECEIVER_V2 = 17 GENERIC_TYPE_DISPLAY = 4 SPECIFIC_TYPE_SIMPLE_DISPLAY = 1 diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 5b5c674c32b..f9570ac5858 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -77,7 +77,7 @@ class Entity(object): # Protect for multiple updates _update_staged = False - # Process updates pararell + # Process updates in parallel parallel_updates = None @property diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e74881e6e89..f11b2eacf3a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -231,7 +231,7 @@ def async_track_time_interval(hass, action, interval): @callback def interval_listener(now): - """Handle elaspsed intervals.""" + """Handle elapsed intervals.""" nonlocal remove remove = async_track_point_in_utc_time( hass, interval_listener, next_interval()) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index b8b6b29df81..1ef9aa15674 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -220,7 +220,7 @@ class Script(): def async_script_timeout(now): """Call after timeout is retrieve stop script.""" self._async_listener.remove(unsub) - self._log("Timout reach, abort script.") + self._log("Timeout reached, abort script.") self.async_stop() unsub = async_track_point_in_utc_time( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7eb0a602139..b381e1c2b0e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -438,7 +438,7 @@ def multiply(value, amount): def logarithm(value, base=math.e): - """Filter to get logarithm of the value with a spesific base.""" + """Filter to get logarithm of the value with a specific base.""" try: return math.log(float(value), float(base)) except (ValueError, TypeError): diff --git a/homeassistant/scripts/influxdb_migrator.py b/homeassistant/scripts/influxdb_migrator.py index cad8f878ca6..f41240bad74 100644 --- a/homeassistant/scripts/influxdb_migrator.py +++ b/homeassistant/scripts/influxdb_migrator.py @@ -119,7 +119,7 @@ def run(script_args: List) -> int: point_wt_time = 0 print("Migrating from {} to {}".format(old_dbname, args.dbname)) - # Walk into measurenebt + # Walk into measurement for index, measurement in enumerate(measurements): # Get tag list diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 58cfd2cbd70..63ca4b5cd1a 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -736,7 +736,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - def test_if_not_fires_on_entities_change_with_for_afte_stop(self): + def test_if_not_fires_on_entities_change_with_for_after_stop(self): """Test for not firing on entities change with for after stop.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index bf54d24492a..22c84b88935 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -198,7 +198,7 @@ class TestAutomationState(unittest.TestCase): automation.DOMAIN: { 'trigger': { 'platform': 'state', - 'entity_id': 'test.anoter_entity', + 'entity_id': 'test.another_entity', }, 'action': { 'service': 'test.automation' @@ -468,7 +468,7 @@ class TestAutomationState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_for_condition(self): - """Test for firing if contition is on.""" + """Test for firing if condition is on.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: @@ -504,7 +504,7 @@ class TestAutomationState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_for_condition_attribute_change(self): - """Test for firing if contition is on with attribute change.""" + """Test for firing if condition is on with attribute change.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=4) point3 = point1 + timedelta(seconds=8) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index ac1d7bc5acf..355d088719f 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -127,7 +127,7 @@ class TestAutomationSun(unittest.TestCase): self.assertEqual('sun - sunset - 0:30:00', self.calls[0].data['some']) def test_sunrise_trigger_with_offset(self): - """Test the runrise trigger with offset.""" + """Test the sunrise trigger with offset.""" now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) diff --git a/tests/components/binary_sensor/test_vultr.py b/tests/components/binary_sensor/test_vultr.py index 91d5da34901..a13944aef9f 100644 --- a/tests/components/binary_sensor/test_vultr.py +++ b/tests/components/binary_sensor/test_vultr.py @@ -78,7 +78,7 @@ class TestVultrBinarySensorSetup(unittest.TestCase): for device in self.DEVICES: - # Test pre data retieval + # Test pre data retrieval if device.subscription == '555555': self.assertEqual('Vultr {}', device.name) diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index af114135da9..3d7aa3ce618 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -275,7 +275,7 @@ class TestTemplateCover(unittest.TestCase): assert self.hass.states.all() == [] def test_template_open_and_close(self): - """Test that if open_cover is specified, cose_cover is too.""" + """Test that if open_cover is specified, close_cover is too.""" with assert_setup_component(0, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 6e646e9862d..f8d3fdf128b 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -378,7 +378,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): telnet.login.assert_not_called() def test_get_asuswrt_data(self): - """Test aususwrt data fetch.""" + """Test asuswrt data fetch.""" scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) scanner._get_wl = mock.Mock() scanner._get_arp = mock.Mock() diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 78813d9ff0b..84cca1bb843 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -704,7 +704,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): @asyncio.coroutine def test_async_added_to_hass(hass): - """Test resoring state.""" + """Test restoring state.""" attr = { device_tracker.ATTR_LONGITUDE: 18, device_tracker.ATTR_LATITUDE: -33, diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index eb461062971..4905ab4d029 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -56,7 +56,7 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): def test_new_message(self): """Test new message.""" dev_id = 'paulus' - enttiy_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) topic = '/location/paulus' location = 'work' @@ -69,4 +69,4 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): }) fire_mqtt_message(self.hass, topic, location) self.hass.block_till_done() - self.assertEqual(location, self.hass.states.get(enttiy_id).state) + self.assertEqual(location, self.hass.states.get(entity_id).state) diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 44c0e0c6295..2239e13e220 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -433,7 +433,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_gps_entry_exit(self): """Test the entry event.""" - # Entering the owntrack circular region named "inner" + # Entering the owntracks circular region named "inner" self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) # Enter uses the zone's gps co-ords @@ -447,7 +447,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): # note that LOCATION_MESSAGE is actually pretty far # from INNER_ZONE and has good accuracy. I haven't # received a transition message though so I'm still - # asssociated with the inner zone regardless of GPS. + # associated with the inner zone regardless of GPS. self.assert_location_latitude(INNER_ZONE['latitude']) self.assert_location_accuracy(INNER_ZONE['radius']) self.assert_location_state('inner') @@ -624,7 +624,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_entry_zone_loading_dash(self): """Test the event for zone landing.""" # Make sure the leading - is ignored - # Ownracks uses this to switch on hold + # Owntracks uses this to switch on hold message = build_message( {'desc': "-inner"}, REGION_GPS_ENTER_MESSAGE) @@ -673,10 +673,10 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_source_type_entry_exit(self): """Test the entry and exit events of source type.""" - # Entering the owntrack circular region named "inner" + # Entering the owntracks circular region named "inner" self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) - # source_type should be gps when enterings using gps. + # source_type should be gps when entering using gps. self.assert_location_source_type('gps') # owntracks shouldn't send beacon events with acc = 0 @@ -715,7 +715,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): # note that LOCATION_MESSAGE is actually pretty far # from INNER_ZONE and has good accuracy. I haven't # received a transition message though so I'm still - # asssociated with the inner zone regardless of GPS. + # associated with the inner zone regardless of GPS. self.assert_location_latitude(INNER_ZONE['latitude']) self.assert_location_accuracy(INNER_ZONE['radius']) self.assert_location_state('inner') @@ -865,7 +865,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): def test_event_beacon_entry_zone_loading_dash(self): """Test the event for beacon zone landing.""" # Make sure the leading - is ignored - # Ownracks uses this to switch on hold + # Owntracks uses this to switch on hold message = build_message( {'desc': "-inner"}, diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index af07da547b7..cba3c835763 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -218,7 +218,7 @@ def test_get_light_state(hass_hue, hue_client): @asyncio.coroutine def test_put_light_state(hass_hue, hue_client): - """Test the seeting of light states.""" + """Test the setting of light states.""" yield from perform_put_test_on_ceiling_lights(hass_hue, hue_client) # Turn the bedroom light on first @@ -452,7 +452,7 @@ def perform_put_test_on_ceiling_lights(hass_hue, hue_client, @asyncio.coroutine def perform_get_light_state(client, entity_id, expected_status): - """Test the gettting of a light state.""" + """Test the getting of a light state.""" result = yield from client.get('/api/username/lights/{}'.format(entity_id)) assert result.status == expected_status diff --git a/tests/components/image_processing/__init__.py b/tests/components/image_processing/__init__.py index 6e79d49c251..63aee1dfbaf 100644 --- a/tests/components/image_processing/__init__.py +++ b/tests/components/image_processing/__init__.py @@ -1 +1 @@ -"""Test 'image_processing' component plaforms.""" +"""Test 'image_processing' component platforms.""" diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index b0bb7d77e3c..628c5405eaa 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -275,7 +275,7 @@ class TestImageProcessingFace(object): @patch('homeassistant.components.image_processing.demo.' 'DemoImageProcessingFace.confidence', new_callable=PropertyMock(return_value=None)) - def test_face_event_call_no_confidence(self, mock_confi, aioclient_mock): + def test_face_event_call_no_confidence(self, mock_config, aioclient_mock): """Setup and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index e35ac8185d0..40945f932c6 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -1,4 +1,4 @@ -"""The tests for the openalpr clooud platform.""" +"""The tests for the openalpr cloud platform.""" import asyncio from unittest.mock import patch, PropertyMock @@ -13,7 +13,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture) -class TestOpenAlprCloudlSetup(object): +class TestOpenAlprCloudSetup(object): """Test class for image processing.""" def setup_method(self): diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index 25f83b1d123..a6e6d3c1a85 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -254,7 +254,7 @@ def test_signal_repetitions(hass, monkeypatch): assert protocol.send_command_ack.call_count == 2 - # test if default apply to configured devcies + # test if default apply to configured devices hass.async_add_job( hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + '.test1'})) diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py index eef54a6c258..a1f63e45748 100644 --- a/tests/components/light/test_rfxtrx.py +++ b/tests/components/light/test_rfxtrx.py @@ -248,7 +248,7 @@ class TestLightRfxtrx(unittest.TestCase): rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) self.assertEqual(2, len(rfxtrx_core.RFX_DEVICES)) - # trying to add a swicth + # trying to add a switch event = rfxtrx_core.get_rfx_object('0b1100100118cdea02010f70') event.data = bytearray([0x0b, 0x11, 0x00, 0x10, 0x01, 0x18, 0xcd, 0xea, 0x01, 0x01, 0x0f, 0x70]) diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index a260d160bb5..b925b74a7f0 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -208,7 +208,7 @@ def test_set_rgb_color(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB only + # Supports RGB only color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -226,7 +226,7 @@ def test_set_rgbw_color(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGBW + # Supports RGBW color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -245,7 +245,7 @@ def test_zw098_set_color_temp(mock_openzwave): command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, warm white, cold white + # Supports RGB, warm white, cold white color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -267,7 +267,7 @@ def test_rgb_not_supported(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts color temperature only + # Supports color temperature only color_channels = MockValue(data=0x01, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -302,7 +302,7 @@ def test_rgb_value_changed(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB only + # Supports RGB only color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -321,7 +321,7 @@ def test_rgbww_value_changed(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, Warm White + # Supports RGB, Warm White color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -340,7 +340,7 @@ def test_rgbcw_value_changed(mock_openzwave): node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, Cold White + # Supports RGB, Cold White color_channels = MockValue(data=0x1e, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) @@ -360,7 +360,7 @@ def test_ct_value_changed(mock_openzwave): command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) color = MockValue(data='#0000000000', node=node) - # Suppoorts RGB, Cold White + # Supports RGB, Cold White color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py index a8242b39f7f..2da2622e08a 100644 --- a/tests/components/media_player/test_soundtouch.py +++ b/tests/components/media_player/test_soundtouch.py @@ -158,7 +158,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.hass.stop() @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) - def test_ensure_setup_config(self, mocked_sountouch_device): + def test_ensure_setup_config(self, mocked_soundtouch_device): """Test setup OK with custom config.""" soundtouch.setup_platform(self.hass, default_component(), @@ -167,10 +167,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(len(all_devices), 1) self.assertEqual(all_devices[0].name, 'soundtouch') self.assertEqual(all_devices[0].config['port'], 8090) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) - def test_ensure_setup_discovery(self, mocked_sountouch_device): + def test_ensure_setup_discovery(self, mocked_soundtouch_device): """Test setup with discovery.""" new_device = {"port": "8090", "host": "192.168.1.1", @@ -184,11 +184,11 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(len(all_devices), 1) self.assertEqual(all_devices[0].config['port'], 8090) self.assertEqual(all_devices[0].config['host'], '192.168.1.1') - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) def test_ensure_setup_discovery_no_duplicate(self, - mocked_sountouch_device): + mocked_soundtouch_device): """Test setup OK if device already exists.""" soundtouch.setup_platform(self.hass, default_component(), @@ -213,20 +213,20 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock(), existing_device # Existing device ) - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_update(self, mocked_sountouch_device, mocked_status, + def test_update(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test update device state.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) self.hass.data[soundtouch.DATA_SOUNDTOUCH][0].update() @@ -238,13 +238,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusPlaying) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_playing_media(self, mocked_sountouch_device, mocked_status, + def test_playing_media(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test playing media info.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -261,13 +261,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusUnknown) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_playing_unknown_media(self, mocked_sountouch_device, + def test_playing_unknown_media(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test playing media info.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -278,13 +278,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusPlayingRadio) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_playing_radio(self, mocked_sountouch_device, mocked_status, + def test_playing_radio(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test playing radio info.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -301,13 +301,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_get_volume_level(self, mocked_sountouch_device, mocked_status, + def test_get_volume_level(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test volume level.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -318,13 +318,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusStandby) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_get_state_off(self, mocked_sountouch_device, mocked_status, + def test_get_state_off(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test state device is off.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -335,13 +335,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=MockStatusPause) @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_get_state_pause(self, mocked_sountouch_device, mocked_status, + def test_get_state_pause(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test state device is paused.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] @@ -352,25 +352,25 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_is_muted(self, mocked_sountouch_device, mocked_status, + def test_is_muted(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test device volume is muted.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] self.assertEqual(all_devices[0].is_volume_muted, True) @mock.patch('libsoundtouch.soundtouch_device') - def test_media_commands(self, mocked_sountouch_device): + def test_media_commands(self, mocked_soundtouch_device): """Test supported media commands.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] self.assertEqual(all_devices[0].supported_features, 17853) @@ -379,7 +379,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_should_turn_off(self, mocked_sountouch_device, mocked_status, + def test_should_turn_off(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_off): """Test device is turned off.""" soundtouch.setup_platform(self.hass, @@ -387,7 +387,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].turn_off() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_power_off.call_count, 1) @@ -397,7 +397,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_should_turn_on(self, mocked_sountouch_device, mocked_status, + def test_should_turn_on(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_on): """Test device is turned on.""" soundtouch.setup_platform(self.hass, @@ -405,7 +405,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].turn_on() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_power_on.call_count, 1) @@ -415,7 +415,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_volume_up(self, mocked_sountouch_device, mocked_status, + def test_volume_up(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_up): """Test volume up.""" soundtouch.setup_platform(self.hass, @@ -423,7 +423,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].volume_up() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) self.assertEqual(mocked_volume_up.call_count, 1) @@ -433,7 +433,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_volume_down(self, mocked_sountouch_device, mocked_status, + def test_volume_down(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_down): """Test volume down.""" soundtouch.setup_platform(self.hass, @@ -441,7 +441,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].volume_down() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) self.assertEqual(mocked_volume_down.call_count, 1) @@ -451,7 +451,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_set_volume_level(self, mocked_sountouch_device, mocked_status, + def test_set_volume_level(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_set_volume): """Test set volume level.""" soundtouch.setup_platform(self.hass, @@ -459,7 +459,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].set_volume_level(0.17) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) mocked_set_volume.assert_called_with(17) @@ -469,7 +469,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_mute(self, mocked_sountouch_device, mocked_status, mocked_volume, + def test_mute(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_mute): """Test mute volume.""" soundtouch.setup_platform(self.hass, @@ -477,7 +477,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].mute_volume(None) - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) self.assertEqual(mocked_mute.call_count, 1) @@ -487,7 +487,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play(self, mocked_sountouch_device, mocked_status, mocked_volume, + def test_play(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play): """Test play command.""" soundtouch.setup_platform(self.hass, @@ -495,7 +495,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].media_play() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_play.call_count, 1) @@ -505,15 +505,15 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_pause(self, mocked_sountouch_device, mocked_status, mocked_volume, - mocked_pause): + def test_pause(self, mocked_soundtouch_device, mocked_status, + mocked_volume, mocked_pause): """Test pause command.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].media_pause() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_pause.call_count, 1) @@ -523,7 +523,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_pause_play(self, mocked_sountouch_device, mocked_status, + def test_play_pause_play(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_pause): """Test play/pause.""" soundtouch.setup_platform(self.hass, @@ -531,7 +531,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].media_play_pause() - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) self.assertEqual(mocked_play_pause.call_count, 1) @@ -542,7 +542,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_next_previous_track(self, mocked_sountouch_device, mocked_status, + def test_next_previous_track(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_next_track, mocked_previous_track): """Test next/previous track.""" @@ -550,7 +550,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices[0].media_next_track() @@ -567,14 +567,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_media(self, mocked_sountouch_device, mocked_status, + def test_play_media(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_presets, mocked_select_preset): """Test play preset 1.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices[0].play_media('PLAYLIST', 1) @@ -589,14 +589,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_media_url(self, mocked_sountouch_device, mocked_status, + def test_play_media_url(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_url): """Test play preset 1.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_soundtouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) all_devices[0].play_media('MUSIC', "http://fqdn/file.mp3") @@ -607,7 +607,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_play_everywhere(self, mocked_sountouch_device, mocked_status, + def test_play_everywhere(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone): """Test play everywhere.""" soundtouch.setup_platform(self.hass, @@ -619,7 +619,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -647,7 +647,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_create_zone(self, mocked_sountouch_device, mocked_status, + def test_create_zone(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone): """Test creating a zone.""" soundtouch.setup_platform(self.hass, @@ -659,7 +659,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -689,7 +689,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status, + def test_remove_zone_slave(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_remove_zone_slave): """Test adding a slave to an existing zone.""" soundtouch.setup_platform(self.hass, @@ -701,7 +701,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -731,7 +731,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', side_effect=_mock_soundtouch_device) - def test_add_zone_slave(self, mocked_sountouch_device, mocked_status, + def test_add_zone_slave(self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_add_zone_slave): """Test removing a slave from a zone.""" soundtouch.setup_platform(self.hass, @@ -743,7 +743,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices[0].entity_id = "media_player.entity_1" all_devices[1].entity_id = "media_player.entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_soundtouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) diff --git a/tests/components/notify/test_facebook.py b/tests/components/notify/test_facebook.py index 7bc7a55869a..b94a4c38a40 100644 --- a/tests/components/notify/test_facebook.py +++ b/tests/components/notify/test_facebook.py @@ -6,7 +6,7 @@ import homeassistant.components.notify.facebook as facebook class TestFacebook(unittest.TestCase): - """Tests for Facebook notifification service.""" + """Tests for Facebook notification service.""" def setUp(self): """Set up test variables.""" diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index efcd44658c3..b23d89e3057 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -110,7 +110,7 @@ class TestSensorMQTT(unittest.TestCase): self.assertEqual('unknown', state.state) def test_setting_sensor_value_via_mqtt_json_message(self): - """Test the setting of the value via MQTT with JSON playload.""" + """Test the setting of the value via MQTT with JSON payload.""" mock_component(self.hass, 'mqtt') assert setup_component(self.hass, sensor.DOMAIN, { sensor.DOMAIN: { @@ -244,7 +244,7 @@ class TestSensorMQTT(unittest.TestCase): self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) def test_setting_sensor_attribute_via_mqtt_json_message(self): - """Test the setting of attribute via MQTT with JSON playload.""" + """Test the setting of attribute via MQTT with JSON payload.""" mock_component(self.hass, 'mqtt') assert setup_component(self.hass, sensor.DOMAIN, { sensor.DOMAIN: { diff --git a/tests/components/sensor/test_random.py b/tests/components/sensor/test_random.py index eeefef74c02..e04fc31af84 100644 --- a/tests/components/sensor/test_random.py +++ b/tests/components/sensor/test_random.py @@ -18,7 +18,7 @@ class TestRandomSensor(unittest.TestCase): self.hass.stop() def test_random_sensor(self): - """Test the Randowm number sensor.""" + """Test the Random number sensor.""" config = { 'sensor': { 'platform': 'random', diff --git a/tests/components/sensor/test_ring.py b/tests/components/sensor/test_ring.py index fb31dc7c53c..0cce0ea681d 100644 --- a/tests/components/sensor/test_ring.py +++ b/tests/components/sensor/test_ring.py @@ -50,7 +50,7 @@ class TestRingSensorSetup(unittest.TestCase): @requests_mock.Mocker() def test_sensor(self, mock): - """Test the Ring senskor class and methods.""" + """Test the Ring sensor class and methods.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) mock.get('https://api.ring.com/clients_api/ring_devices', diff --git a/tests/components/sensor/test_season.py b/tests/components/sensor/test_season.py index 9dda0d2f2cb..5c071982f7f 100644 --- a/tests/components/sensor/test_season.py +++ b/tests/components/sensor/test_season.py @@ -73,7 +73,7 @@ class TestSeason(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_season_should_be_summer_northern_astonomical(self): + def test_season_should_be_summer_northern_astronomical(self): """Test that season should be summer.""" # A known day in summer summer_day = datetime(2017, 9, 3, 0, 0) @@ -91,7 +91,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SUMMER, current_season) - def test_season_should_be_autumn_northern_astonomical(self): + def test_season_should_be_autumn_northern_astronomical(self): """Test that season should be autumn.""" # A known day in autumn autumn_day = datetime(2017, 9, 23, 0, 0) @@ -109,7 +109,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_AUTUMN, current_season) - def test_season_should_be_winter_northern_astonomical(self): + def test_season_should_be_winter_northern_astronomical(self): """Test that season should be winter.""" # A known day in winter winter_day = datetime(2017, 12, 25, 0, 0) @@ -127,7 +127,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_WINTER, current_season) - def test_season_should_be_spring_northern_astonomical(self): + def test_season_should_be_spring_northern_astronomical(self): """Test that season should be spring.""" # A known day in spring spring_day = datetime(2017, 4, 1, 0, 0) @@ -145,7 +145,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SPRING, current_season) - def test_season_should_be_winter_southern_astonomical(self): + def test_season_should_be_winter_southern_astronomical(self): """Test that season should be winter.""" # A known day in winter winter_day = datetime(2017, 9, 3, 0, 0) @@ -163,7 +163,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_WINTER, current_season) - def test_season_should_be_spring_southern_astonomical(self): + def test_season_should_be_spring_southern_astronomical(self): """Test that season should be spring.""" # A known day in spring spring_day = datetime(2017, 9, 23, 0, 0) @@ -181,7 +181,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SPRING, current_season) - def test_season_should_be_summer_southern_astonomical(self): + def test_season_should_be_summer_southern_astronomical(self): """Test that season should be summer.""" # A known day in summer summer_day = datetime(2017, 12, 25, 0, 0) @@ -199,7 +199,7 @@ class TestSeason(unittest.TestCase): self.assertEqual(season.STATE_SUMMER, current_season) - def test_season_should_be_autumn_southern_astonomical(self): + def test_season_should_be_autumn_southern_astronomical(self): """Test that season should be spring.""" # A known day in spring autumn_day = datetime(2017, 4, 1, 0, 0) diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index a94e5747483..d9eb33be37d 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -107,7 +107,7 @@ class TestAlert(unittest.TestCase): self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) def test_hidden(self): - """Test entity hidding.""" + """Test entity hiding.""" assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') self.assertTrue(hidden) diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py index 9cc706b5690..5a5fdffd5a3 100644 --- a/tests/components/test_ffmpeg.py +++ b/tests/components/test_ffmpeg.py @@ -97,7 +97,7 @@ def test_setup_component_test_register_no_startup(hass): @asyncio.coroutine -def test_setup_component_test_servcie_start(hass): +def test_setup_component_test_service_start(hass): """Setup ffmpeg component test service start.""" with assert_setup_component(2): yield from async_setup_component( @@ -113,7 +113,7 @@ def test_setup_component_test_servcie_start(hass): @asyncio.coroutine -def test_setup_component_test_servcie_stop(hass): +def test_setup_component_test_service_stop(hass): """Setup ffmpeg component test service stop.""" with assert_setup_component(2): yield from async_setup_component( @@ -129,7 +129,7 @@ def test_setup_component_test_servcie_stop(hass): @asyncio.coroutine -def test_setup_component_test_servcie_restart(hass): +def test_setup_component_test_service_restart(hass): """Setup ffmpeg component test service restart.""" with assert_setup_component(2): yield from async_setup_component( @@ -146,7 +146,7 @@ def test_setup_component_test_servcie_restart(hass): @asyncio.coroutine -def test_setup_component_test_servcie_start_with_entity(hass): +def test_setup_component_test_service_start_with_entity(hass): """Setup ffmpeg component test service start.""" with assert_setup_component(2): yield from async_setup_component( diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index fb869569670..8fb017309de 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -211,7 +211,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): @asyncio.coroutine def test_service_register(hassio_env, hass): - """Check if service will be settup.""" + """Check if service will be setup.""" assert (yield from async_setup_component(hass, 'hassio', {})) assert hass.services.has_service('hassio', 'addon_start') assert hass.services.has_service('hassio', 'addon_stop') diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 06ba8a57508..dde141b6495 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -1,4 +1,4 @@ -"""The testd for Core components.""" +"""The tests for Core components.""" # pylint: disable=protected-access import asyncio import unittest diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py index 7bdd44136e8..010136ee0e7 100644 --- a/tests/components/test_pilight.py +++ b/tests/components/test_pilight.py @@ -304,7 +304,7 @@ class TestPilight(unittest.TestCase): with assert_setup_component(4): whitelist = { 'protocol': [PilightDaemonSim.test_message['protocol'], - 'other_protocoll'], + 'other_protocol'], 'id': [PilightDaemonSim.test_message['message']['id']]} self.assertTrue(setup_component( self.hass, pilight.DOMAIN, @@ -330,7 +330,7 @@ class TestPilight(unittest.TestCase): """Check whitelist filter with unmatched data, should not work.""" with assert_setup_component(4): whitelist = { - 'protocol': ['wrong_protocoll'], + 'protocol': ['wrong_protocol'], 'id': [PilightDaemonSim.test_message['message']['id']]} self.assertTrue(setup_component( self.hass, pilight.DOMAIN, diff --git a/tests/components/test_plant.py b/tests/components/test_plant.py index 14db6689386..ee1372509d9 100644 --- a/tests/components/test_plant.py +++ b/tests/components/test_plant.py @@ -105,7 +105,7 @@ class TestPlant(unittest.TestCase): self.assertEqual(5, state.attributes[plant.READING_MOISTURE]) @pytest.mark.skipif(plant.ENABLE_LOAD_HISTORY is False, - reason="tests for loading from DB are instable, thus" + reason="tests for loading from DB are unstable, thus" "this feature is turned of until tests become" "stable") def test_load_from_db(self): diff --git a/tests/components/test_remember_the_milk.py b/tests/components/test_remember_the_milk.py index 1b6619aca9c..d9db61efd40 100644 --- a/tests/components/test_remember_the_milk.py +++ b/tests/components/test_remember_the_milk.py @@ -54,7 +54,7 @@ class TestConfiguration(unittest.TestCase): def test_invalid_data(self): """Test starts with invalid data and should not raise an exception.""" with patch("builtins.open", - mock_open(read_data='random charachters')),\ + mock_open(read_data='random characters')),\ patch("os.path.isfile", Mock(return_value=True)): config = rtm.RememberTheMilkConfiguration(self.hass) self.assertIsNotNone(config) diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py index e7907fc6b54..9f6573920ca 100644 --- a/tests/components/test_rflink.py +++ b/tests/components/test_rflink.py @@ -47,7 +47,7 @@ def mock_rflink(hass, config, domain, monkeypatch, failures=None, 'rflink.protocol.create_rflink_connection', mock_create) - # verify instanstiation of component with given config + # verify instantiation of component with given config with assert_setup_component(platform_count, domain): yield from async_setup_component(hass, domain, config) diff --git a/tests/components/test_rss_feed_template.py b/tests/components/test_rss_feed_template.py index 60eb2530ea1..8b16b5519e9 100644 --- a/tests/components/test_rss_feed_template.py +++ b/tests/components/test_rss_feed_template.py @@ -25,7 +25,7 @@ def mock_http_client(loop, hass, test_client): @asyncio.coroutine -def test_get_noexistant_feed(mock_http_client): +def test_get_nonexistant_feed(mock_http_client): """Test if we can retrieve the correct rss feed.""" resp = yield from mock_http_client.get('/api/rss_template/otherfeed') assert resp.status == 404 diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index e548efb0eb2..77de7d5d1dd 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -899,7 +899,7 @@ class TestZWaveServices(unittest.TestCase): assert value.label == "New Label" def test_set_poll_intensity_enable(self): - """Test zwave set_poll_intensity service, succsessful set.""" + """Test zwave set_poll_intensity service, successful set.""" node = MockNode(node_id=14) value = MockValue(index=12, value_id=123456, poll_intensity=0) node.values = {123456: value} diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index e413fc145ca..4211e3da31b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -232,8 +232,8 @@ def test_async_schedule_update_ha_state(hass): @asyncio.coroutine -def test_async_pararell_updates_with_zero(hass): - """Test pararell updates with 0 (disabled).""" +def test_async_parallel_updates_with_zero(hass): + """Test parallel updates with 0 (disabled).""" updates = [] test_lock = asyncio.Event(loop=hass.loop) @@ -269,11 +269,11 @@ def test_async_pararell_updates_with_zero(hass): @asyncio.coroutine -def test_async_pararell_updates_with_one(hass): - """Test pararell updates with 1 (sequential).""" +def test_async_parallel_updates_with_one(hass): + """Test parallel updates with 1 (sequential).""" updates = [] test_lock = asyncio.Lock(loop=hass.loop) - test_semephore = asyncio.Semaphore(1, loop=hass.loop) + test_semaphore = asyncio.Semaphore(1, loop=hass.loop) yield from test_lock.acquire() @@ -284,7 +284,7 @@ def test_async_pararell_updates_with_one(hass): self.entity_id = entity_id self.hass = hass self._count = count - self.parallel_updates = test_semephore + self.parallel_updates = test_semaphore @asyncio.coroutine def async_update(self): @@ -332,11 +332,11 @@ def test_async_pararell_updates_with_one(hass): @asyncio.coroutine -def test_async_pararell_updates_with_two(hass): - """Test pararell updates with 2 (pararell).""" +def test_async_parallel_updates_with_two(hass): + """Test parallel updates with 2 (parallel).""" updates = [] test_lock = asyncio.Lock(loop=hass.loop) - test_semephore = asyncio.Semaphore(2, loop=hass.loop) + test_semaphore = asyncio.Semaphore(2, loop=hass.loop) yield from test_lock.acquire() @@ -347,7 +347,7 @@ def test_async_pararell_updates_with_two(hass): self.entity_id = entity_id self.hass = hass self._count = count - self.parallel_updates = test_semephore + self.parallel_updates = test_semaphore @asyncio.coroutine def async_update(self): diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 40a0f8be3b2..f2416fc3a31 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -184,7 +184,7 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 2 == len(self.hass.states.entity_ids()) - def test_update_state_adds_entities_with_update_befor_add_true(self): + def test_update_state_adds_entities_with_update_before_add_true(self): """Test if call update before add to state machine.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -197,7 +197,7 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 1 == len(self.hass.states.entity_ids()) assert ent.update.called - def test_update_state_adds_entities_with_update_befor_add_false(self): + def test_update_state_adds_entities_with_update_before_add_false(self): """Test if not call update before add to state machine.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -579,7 +579,7 @@ def test_platform_not_ready(hass): @asyncio.coroutine -def test_pararell_updates_async_platform(hass): +def test_parallel_updates_async_platform(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() @@ -606,7 +606,7 @@ def test_pararell_updates_async_platform(hass): @asyncio.coroutine -def test_pararell_updates_async_platform_with_constant(hass): +def test_parallel_updates_async_platform_with_constant(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() @@ -634,7 +634,7 @@ def test_pararell_updates_async_platform_with_constant(hass): @asyncio.coroutine -def test_pararell_updates_sync_platform(hass): +def test_parallel_updates_sync_platform(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 797cd257833..944224a34d1 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -1,4 +1,4 @@ -"""The tests for the EntityFitler component.""" +"""The tests for the EntityFilter component.""" from homeassistant.helpers.entityfilter import generate_filter diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 31d98633ef8..a5bd6798084 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -28,7 +28,7 @@ class TestServiceHelpers(unittest.TestCase): self.hass.stop() def test_template_service_call(self): - """Test service call with tempating.""" + """Test service call with templating.""" config = { 'service_template': '{{ \'test_domain.test_service\' }}', 'entity_id': 'hello.world', @@ -102,7 +102,7 @@ class TestServiceHelpers(unittest.TestCase): @patch('homeassistant.helpers.service._LOGGER.error') def test_fail_silently_if_no_service(self, mock_log): - """Test failling if service is missing.""" + """Test failing if service is missing.""" service.call_from_config(self.hass, None) self.assertEqual(1, mock_log.call_count) diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 7f04f6f0569..2ad3b42fdb8 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -1,4 +1,4 @@ -"""Test homeasssitant distance utility functions.""" +"""Test homeassistant distance utility functions.""" import unittest import homeassistant.util.distance as distance_util From 89e0b26b7335b84ee3e452a50f82f8df5f90aa66 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Mon, 29 Jan 2018 23:49:38 +0100 Subject: [PATCH 045/166] Error handling, in case no connections are available (#12010) * Error handling, in case no connections are available * Fix elif to if --- homeassistant/components/sensor/deutsche_bahn.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index c443829a3bb..0261288f27e 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -71,15 +71,17 @@ class DeutscheBahnSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" connections = self.data.connections[0] - connections['next'] = self.data.connections[1]['departure'] - connections['next_on'] = self.data.connections[2]['departure'] + if len(self.data.connections) > 1: + connections['next'] = self.data.connections[1]['departure'] + if len(self.data.connections) > 2: + connections['next_on'] = self.data.connections[2]['departure'] return connections def update(self): """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get('departure', 'Unknown') - if self.data.connections[0]['delay'] != 0: + if self.data.connections[0].get('delay', 0) != 0: self._state += " + {}".format(self.data.connections[0]['delay']) @@ -102,6 +104,9 @@ class SchieneData(object): self.start, self.goal, dt_util.as_local(dt_util.utcnow()), self.only_direct) + if not self.connections: + self.connections = [{}] + for con in self.connections: # Detail info is not useful. Having a more consistent interface # simplifies usage of template sensors. From 7ad870c4ffec3471112f32da4f62a371fe3df28d Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Mon, 29 Jan 2018 23:55:39 +0100 Subject: [PATCH 046/166] Online state for samsungtv is jumping when TV is idle (#11998) * Set timeout to offline * Have to rewrite to use ping instead. --- .../components/media_player/samsungtv.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 57f25873ae7..caf458edc69 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -8,6 +8,9 @@ import logging import socket from datetime import timedelta +import sys + +import subprocess import voluptuous as vol from homeassistant.components.media_player import ( @@ -122,12 +125,19 @@ class SamsungTVDevice(MediaPlayerDevice): def update(self): """Update state of device.""" + if sys.platform == 'win32': + _ping_cmd = ['ping', '-n 1', '-w', '1000', self._config['host']] + else: + _ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', + self._config['host']] + + ping = subprocess.Popen( + _ping_cmd, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(self._config[CONF_TIMEOUT]) - sock.connect((self._config['host'], self._config['port'])) - self._state = STATE_ON - except socket.error: + ping.communicate() + self._state = STATE_ON if ping.returncode == 0 else STATE_OFF + except subprocess.CalledProcessError: self._state = STATE_OFF def get_remote(self): From 99c6a10b995c711b79ac1da9ec87a03a7803bd9e Mon Sep 17 00:00:00 2001 From: Frantz Date: Tue, 30 Jan 2018 00:56:55 +0200 Subject: [PATCH 047/166] Set default values for Daikin devices that don't support fan direction and fan speed features (#12000) --- homeassistant/components/climate/daikin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index 1f38fdf3c82..fea1fcee3a3 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -98,10 +98,16 @@ class DaikinClimate(ClimateDevice): daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE] if self._api.device.values.get(daikin_attr) is not None: self._supported_features |= SUPPORT_FAN_MODE + else: + # even devices without support must have a default valid value + self._api.device.values[daikin_attr] = 'A' daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE] if self._api.device.values.get(daikin_attr) is not None: self._supported_features |= SUPPORT_SWING_MODE + else: + # even devices without support must have a default valid value + self._api.device.values[daikin_attr] = '0' def get(self, key): """Retrieve device settings from API library cache.""" From 6d59dad1ce08bde16e3c94c845a7ce790c92c65e Mon Sep 17 00:00:00 2001 From: smoldaner Date: Tue, 30 Jan 2018 00:02:26 +0100 Subject: [PATCH 048/166] Fix parameter escaping (#12008) From rfc3986: The characters slash ("/") and question mark ("?") may represent data within the query component See https://tools.ietf.org/html/rfc3986#section-3.4 --- homeassistant/components/media_player/squeezebox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 769c8951dc8..cc61610b369 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -494,5 +494,5 @@ class SqueezeBoxDevice(MediaPlayerDevice): all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe=':=')) + all_params.append(urllib.parse.quote(parameter, safe=':=/?')) return self.async_query(*all_params) From 24c6285567475af4b916c26b2df501ca1be5921e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Jan 2018 15:51:43 -0800 Subject: [PATCH 049/166] Bump frontend to 20180130.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 d699acab25d..f745458899c 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==20180126.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180130.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 eea3c6e47bb..02d854c4b27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -352,7 +352,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180126.0 +home-assistant-frontend==20180130.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 8d3e823a989..f4ae6d55325 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==20180126.0 +home-assistant-frontend==20180130.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 5609b42863fbac8778dea0ee174a45a5d5f3427a Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 30 Jan 2018 00:54:49 +0100 Subject: [PATCH 050/166] Upgrade pyharmony to 1.0.20 (#12043) --- homeassistant/components/remote/harmony.py | 7 ++++--- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 89cdc7529cb..39f09ea66a2 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -17,9 +17,10 @@ from homeassistant.components.remote import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -REQUIREMENTS = ['pyharmony==1.0.18'] +REQUIREMENTS = ['pyharmony==1.0.20'] _LOGGER = logging.getLogger(__name__) @@ -97,8 +98,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): DEVICES.append(device) add_devices([device]) register_services(hass) - except ValueError: - _LOGGER.warning("Failed to initialize remote: %s", name) + except (ValueError, AttributeError): + raise PlatformNotReady def register_services(hass): diff --git a/requirements_all.txt b/requirements_all.txt index 02d854c4b27..2da73daca70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ pyflexit==0.3 pyfttt==0.3 # homeassistant.components.remote.harmony -pyharmony==1.0.18 +pyharmony==1.0.20 # homeassistant.components.binary_sensor.hikvision pyhik==0.1.4 From 10263230f74e7e7bb0291e85c8c7ba31ca70dab6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 30 Jan 2018 01:08:01 +0100 Subject: [PATCH 051/166] Upgrade astral to 1.5 (#12042) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 243c6d418df..3f34f943ae6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ aiohttp==2.3.7 yarl==0.18.0 async_timeout==2.0.0 chardet==3.0.4 -astral==1.4 +astral==1.5 certifi>=2017.4.17 # Breaks Python 3.6 and is not needed for our supported Pythons diff --git a/requirements_all.txt b/requirements_all.txt index 2da73daca70..93fa176bb63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,7 +10,7 @@ aiohttp==2.3.7 yarl==0.18.0 async_timeout==2.0.0 chardet==3.0.4 -astral==1.4 +astral==1.5 certifi>=2017.4.17 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index 4b19e47fb2c..80458beb25f 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ REQUIRES = [ 'yarl==0.18.0', 'async_timeout==2.0.0', 'chardet==3.0.4', - 'astral==1.4', + 'astral==1.5', 'certifi>=2017.4.17', ] From 8624799c45d6dbb154eca3ac4def0a445f5371d1 Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Tue, 30 Jan 2018 04:33:39 +0000 Subject: [PATCH 052/166] Refactor alexa smart_home tests (#12044) * Refactor alexa smart_home tests The previous tests had a lot of copy pasta due to a lack of expressions for higher-level assertions. I'm hoping this makes it more reasonable to extend all interfaces to support properties. * Lint --- tests/components/alexa/test_smart_home.py | 1558 +++++++++------------ 1 file changed, 651 insertions(+), 907 deletions(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index c91965a5396..35cc610219e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -6,10 +6,10 @@ from uuid import uuid4 import pytest from homeassistant.const import ( - TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, STATE_ON, STATE_OFF) + TEMP_FAHRENHEIT, STATE_LOCKED, STATE_UNLOCKED, + STATE_UNKNOWN) from homeassistant.setup import async_setup_component -from homeassistant.components import alexa, light +from homeassistant.components import alexa from homeassistant.components.alexa import smart_home from homeassistant.helpers import entityfilter @@ -104,84 +104,12 @@ def test_wrong_version(hass): @asyncio.coroutine -def test_discovery_request(hass): +def discovery_test(device, hass, expected_endpoints=1): """Test alexa discovery request.""" request = get_new_request('Alexa.Discovery', 'Discover') # setup test devices - hass.states.async_set( - 'switch.test', 'on', {'friendly_name': "Test switch"}) - - hass.states.async_set( - 'light.test_1', 'on', {'friendly_name': "Test light 1"}) - hass.states.async_set( - 'light.test_2', 'on', { - 'friendly_name': "Test light 2", 'supported_features': 1 - }) - hass.states.async_set( - 'light.test_3', 'on', { - 'friendly_name': "Test light 3", 'supported_features': 19 - }) - - hass.states.async_set( - 'script.test', 'off', {'friendly_name': "Test script"}) - hass.states.async_set( - 'script.test_2', 'off', {'friendly_name': "Test script 2", - 'can_cancel': True}) - - hass.states.async_set( - 'input_boolean.test', 'off', {'friendly_name': "Test input boolean"}) - - hass.states.async_set( - 'scene.test', 'off', {'friendly_name': "Test scene"}) - - hass.states.async_set( - 'fan.test_1', 'off', {'friendly_name': "Test fan 1"}) - - hass.states.async_set( - 'fan.test_2', 'off', { - 'friendly_name': "Test fan 2", 'supported_features': 1, - 'speed_list': ['low', 'medium', 'high'] - }) - - hass.states.async_set( - 'lock.test', 'off', {'friendly_name': "Test lock"}) - - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", - 'supported_features': 20925, - 'volume_level': 1 - }) - - hass.states.async_set( - 'alert.test', 'off', {'friendly_name': "Test alert"}) - - hass.states.async_set( - 'automation.test', 'off', {'friendly_name': "Test automation"}) - - hass.states.async_set( - 'group.test', 'off', {'friendly_name': "Test group"}) - - hass.states.async_set( - 'cover.test', 'off', { - 'friendly_name': "Test cover", 'supported_features': 255, - 'position': 85 - }) - - hass.states.async_set( - 'sensor.test_temp', '59', { - 'friendly_name': "Test Temp Sensor", - 'unit_of_measurement': TEMP_FAHRENHEIT, - }) - - # This sensor measures a quantity not applicable to Alexa, and should not - # be discovered. - hass.states.async_set( - 'sensor.test_sickness', '0.1', { - 'friendly_name': "Test Space Sickness Sensor", - 'unit_of_measurement': 'garn', - }) + hass.states.async_set(*device) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) @@ -191,202 +119,549 @@ def test_discovery_request(hass): assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' + endpoints = msg['payload']['endpoints'] + assert len(endpoints) == expected_endpoints - endpoint_ids = set( - appliance['endpointId'] - for appliance in msg['payload']['endpoints']) - assert endpoint_ids == { + if expected_endpoints == 1: + return endpoints[0] + elif expected_endpoints > 1: + return endpoints + return None + + +def assert_endpoint_capabilities(endpoint, *interfaces): + """Assert the endpoint supports the given interfaces. + + Returns a set of capabilities, in case you want to assert more things about + them. + """ + capabilities = endpoint['capabilities'] + supported = set( + feature['interface'] + for feature in capabilities) + + assert supported == set(interfaces) + return capabilities + + +@asyncio.coroutine +def test_switch(hass): + """Test switch discovery.""" + device = ('switch.test', 'on', {'friendly_name': "Test switch"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'switch#test' + assert appliance['displayCategories'][0] == "SWITCH" + assert appliance['friendlyName'] == "Test switch" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( 'switch#test', + 'switch.turn_on', + 'switch.turn_off', + hass) + + properties = yield from reported_properties(hass, 'switch#test') + properties.assert_equal('Alexa.PowerController', 'powerState', 'ON') + + +@asyncio.coroutine +def test_light(hass): + """Test light discovery.""" + device = ('light.test_1', 'on', {'friendly_name': "Test light 1"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'light#test_1' + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 1" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( 'light#test_1', - 'light#test_2', - 'light#test_3', + 'light.turn_on', + 'light.turn_off', + hass) + + +@asyncio.coroutine +def test_dimmable_light(hass): + """Test dimmable light discovery.""" + device = ( + 'light.test_2', 'on', { + 'brightness': 128, + 'friendly_name': "Test light 2", 'supported_features': 1 + }) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'light#test_2' + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 2" + + assert_endpoint_capabilities( + appliance, + 'Alexa.BrightnessController', + 'Alexa.PowerController', + ) + + properties = yield from reported_properties(hass, 'light#test_2') + properties.assert_equal('Alexa.PowerController', 'powerState', 'ON') + properties.assert_equal('Alexa.BrightnessController', 'brightness', 50) + + call, _ = yield from assert_request_calls_service( + 'Alexa.BrightnessController', 'SetBrightness', 'light#test_2', + 'light.turn_on', + hass, + payload={'brightness': '50'}) + assert call.data['brightness_pct'] == 50 + + +@asyncio.coroutine +def test_color_light(hass): + """Test color light discovery.""" + device = ( + 'light.test_3', + 'on', + { + 'friendly_name': "Test light 3", + 'supported_features': 19, + 'min_mireds': 142, + 'color_temp': '333', + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'light#test_3' + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 3" + + assert_endpoint_capabilities( + appliance, + 'Alexa.BrightnessController', + 'Alexa.PowerController', + 'Alexa.ColorController', + 'Alexa.ColorTemperatureController', + ) + + # IncreaseColorTemperature and DecreaseColorTemperature have their own + # tests + + +@asyncio.coroutine +def test_script(hass): + """Test script discovery.""" + device = ('script.test', 'off', {'friendly_name': "Test script"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'script#test' + assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER" + assert appliance['friendlyName'] == "Test script" + + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert not capability['supportsDeactivation'] + + yield from assert_scene_controller_works( 'script#test', + 'script.turn_on', + None, + hass) + + +@asyncio.coroutine +def test_cancelable_script(hass): + """Test cancalable script discovery.""" + device = ( + 'script.test_2', + 'off', + {'friendly_name': "Test script 2", 'can_cancel': True}, + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'script#test_2' + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert capability['supportsDeactivation'] + + yield from assert_scene_controller_works( 'script#test_2', + 'script.turn_on', + 'script.turn_off', + hass) + + +@asyncio.coroutine +def test_input_boolean(hass): + """Test input boolean discovery.""" + device = ( + 'input_boolean.test', + 'off', + {'friendly_name': "Test input boolean"}, + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'input_boolean#test' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test input boolean" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( 'input_boolean#test', + 'input_boolean.turn_on', + 'input_boolean.turn_off', + hass) + + +@asyncio.coroutine +def test_scene(hass): + """Test scene discovery.""" + device = ('scene.test', 'off', {'friendly_name': "Test scene"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'scene#test' + assert appliance['displayCategories'][0] == "SCENE_TRIGGER" + assert appliance['friendlyName'] == "Test scene" + + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert not capability['supportsDeactivation'] + + yield from assert_scene_controller_works( 'scene#test', - 'fan#test_1', - 'fan#test_2', - 'lock#test', + 'scene.turn_on', + None, + hass) + + +@asyncio.coroutine +def test_fan(hass): + """Test fan discovery.""" + device = ('fan.test_1', 'off', {'friendly_name': "Test fan 1"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'fan#test_1' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 1" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + +@asyncio.coroutine +def test_variable_fan(hass): + """Test fan discovery. + + This one has variable speed. + """ + device = ( + 'fan.test_2', + 'off', { + 'friendly_name': "Test fan 2", + 'supported_features': 1, + 'speed_list': ['low', 'medium', 'high'], + 'speed': 'high', + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'fan#test_2' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 2" + + assert_endpoint_capabilities( + appliance, + 'Alexa.PercentageController', + 'Alexa.PowerController', + ) + + call, _ = yield from assert_request_calls_service( + 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2', + 'fan.set_speed', + hass, + payload={'percentage': '50'}) + assert call.data['speed'] == 'medium' + + yield from assert_percentage_changes( + hass, + [('high', '-5'), ('off', '5'), ('low', '-80')], + 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2', + 'percentageDelta', + 'fan.set_speed', + 'speed') + + +@asyncio.coroutine +def test_lock(hass): + """Test lock discovery.""" + device = ('lock.test', 'off', {'friendly_name': "Test lock"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'lock#test' + assert appliance['displayCategories'][0] == "SMARTLOCK" + assert appliance['friendlyName'] == "Test lock" + assert_endpoint_capabilities(appliance, 'Alexa.LockController') + + yield from assert_request_calls_service( + 'Alexa.LockController', 'Lock', 'lock#test', + 'lock.lock', + hass) + + +@asyncio.coroutine +def test_media_player(hass): + """Test media player discovery.""" + device = ( + 'media_player.test', + 'off', { + 'friendly_name': "Test media player", + 'supported_features': 0x59bd, + 'volume_level': 0.75 + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'media_player#test' + assert appliance['displayCategories'][0] == "TV" + assert appliance['friendlyName'] == "Test media player" + + assert_endpoint_capabilities( + appliance, + 'Alexa.InputController', + 'Alexa.PowerController', + 'Alexa.Speaker', + 'Alexa.PlaybackController', + ) + + yield from assert_power_controller_works( 'media_player#test', + 'media_player.turn_on', + 'media_player.turn_off', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Play', 'media_player#test', + 'media_player.media_play', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Pause', 'media_player#test', + 'media_player.media_pause', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Stop', 'media_player#test', + 'media_player.media_stop', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Next', 'media_player#test', + 'media_player.media_next_track', + hass) + + yield from assert_request_calls_service( + 'Alexa.PlaybackController', 'Previous', 'media_player#test', + 'media_player.media_previous_track', + hass) + + call, _ = yield from assert_request_calls_service( + 'Alexa.Speaker', 'SetVolume', 'media_player#test', + 'media_player.volume_set', + hass, + payload={'volume': 50}) + assert call.data['volume_level'] == 0.5 + + call, _ = yield from assert_request_calls_service( + 'Alexa.Speaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': True}) + assert call.data['is_volume_muted'] + + call, _, = yield from assert_request_calls_service( + 'Alexa.Speaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': False}) + assert not call.data['is_volume_muted'] + + yield from assert_percentage_changes( + hass, + [(0.7, '-5'), (0.8, '5'), (0, '-80')], + 'Alexa.Speaker', 'AdjustVolume', 'media_player#test', + 'volume', + 'media_player.volume_set', + 'volume_level') + + +@asyncio.coroutine +def test_alert(hass): + """Test alert discovery.""" + device = ('alert.test', 'off', {'friendly_name': "Test alert"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'alert#test' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test alert" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( 'alert#test', + 'alert.turn_on', + 'alert.turn_off', + hass) + + +@asyncio.coroutine +def test_automation(hass): + """Test automation discovery.""" + device = ('automation.test', 'off', {'friendly_name': "Test automation"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'automation#test' + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test automation" + assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + + yield from assert_power_controller_works( 'automation#test', + 'automation.turn_on', + 'automation.turn_off', + hass) + + +@asyncio.coroutine +def test_group(hass): + """Test group discovery.""" + device = ('group.test', 'off', {'friendly_name': "Test group"}) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'group#test' + assert appliance['displayCategories'][0] == "SCENE_TRIGGER" + assert appliance['friendlyName'] == "Test group" + + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.SceneController') + assert capability['supportsDeactivation'] + + yield from assert_scene_controller_works( 'group#test', + 'homeassistant.turn_on', + 'homeassistant.turn_off', + hass) + + +@asyncio.coroutine +def test_cover(hass): + """Test cover discovery.""" + device = ( + 'cover.test', + 'off', { + 'friendly_name': "Test cover", + 'supported_features': 255, + 'position': 30, + } + ) + appliance = yield from discovery_test(device, hass) + + assert appliance['endpointId'] == 'cover#test' + assert appliance['displayCategories'][0] == "DOOR" + assert appliance['friendlyName'] == "Test cover" + + assert_endpoint_capabilities( + appliance, + 'Alexa.PercentageController', + 'Alexa.PowerController', + ) + + yield from assert_power_controller_works( 'cover#test', - 'sensor#test_temp', - } + 'cover.open_cover', + 'cover.close_cover', + hass) - for appliance in msg['payload']['endpoints']: - if appliance['endpointId'] == 'switch#test': - assert appliance['displayCategories'][0] == "SWITCH" - assert appliance['friendlyName'] == "Test switch" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue + call, _ = yield from assert_request_calls_service( + 'Alexa.PercentageController', 'SetPercentage', 'cover#test', + 'cover.set_cover_position', + hass, + payload={'percentage': '50'}) + assert call.data['position'] == 50 - if appliance['endpointId'] == 'light#test_1': - assert appliance['displayCategories'][0] == "LIGHT" - assert appliance['friendlyName'] == "Test light 1" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue + yield from assert_percentage_changes( + hass, + [(25, '-5'), (35, '5'), (0, '-80')], + 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test', + 'percentageDelta', + 'cover.set_cover_position', + 'position') - if appliance['endpointId'] == 'light#test_2': - assert appliance['displayCategories'][0] == "LIGHT" - assert appliance['friendlyName'] == "Test light 2" - assert len(appliance['capabilities']) == 2 - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) +@asyncio.coroutine +def assert_percentage_changes( + hass, + adjustments, + namespace, + name, + endpoint, + parameter, + service, + changed_parameter): + """Assert an API request making percentage changes works. - assert 'Alexa.BrightnessController' in caps - assert 'Alexa.PowerController' in caps + AdjustPercentage, AdjustBrightness, etc. are examples of such requests. + """ + for result_volume, adjustment in adjustments: + if parameter: + payload = {parameter: adjustment} + else: + payload = {} - continue + call, _ = yield from assert_request_calls_service( + namespace, name, endpoint, service, + hass, + payload=payload) + assert call.data[changed_parameter] == result_volume - if appliance['endpointId'] == 'light#test_3': - assert appliance['displayCategories'][0] == "LIGHT" - assert appliance['friendlyName'] == "Test light 3" - assert len(appliance['capabilities']) == 4 - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) +@asyncio.coroutine +def test_temp_sensor(hass): + """Test temperature sensor discovery.""" + device = ( + 'sensor.test_temp', + '42', + { + 'friendly_name': "Test Temp Sensor", + 'unit_of_measurement': TEMP_FAHRENHEIT, + } + ) + appliance = yield from discovery_test(device, hass) - assert 'Alexa.BrightnessController' in caps - assert 'Alexa.PowerController' in caps - assert 'Alexa.ColorController' in caps - assert 'Alexa.ColorTemperatureController' in caps + assert appliance['endpointId'] == 'sensor#test_temp' + assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' + assert appliance['friendlyName'] == 'Test Temp Sensor' - continue + (capability,) = assert_endpoint_capabilities( + appliance, + 'Alexa.TemperatureSensor') + assert capability['interface'] == 'Alexa.TemperatureSensor' + properties = capability['properties'] + assert properties['retrievable'] is True + assert {'name': 'temperature'} in properties['supported'] - if appliance['endpointId'] == 'script#test': - assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER" - assert appliance['friendlyName'] == "Test script" - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][-1] - assert capability['interface'] == 'Alexa.SceneController' - assert not capability['supportsDeactivation'] - continue + properties = yield from reported_properties(hass, 'sensor#test_temp') + properties.assert_equal('Alexa.TemperatureSensor', 'temperature', + {'value': 42.0, 'scale': 'FAHRENHEIT'}) - if appliance['endpointId'] == 'script#test_2': - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][-1] - assert capability['supportsDeactivation'] - continue - if appliance['endpointId'] == 'input_boolean#test': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test input boolean" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue - - if appliance['endpointId'] == 'scene#test': - assert appliance['displayCategories'][0] == "SCENE_TRIGGER" - assert appliance['friendlyName'] == "Test scene" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.SceneController' - capability = appliance['capabilities'][-1] - assert not capability['supportsDeactivation'] - continue - - if appliance['endpointId'] == 'fan#test_1': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test fan 1" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue - - if appliance['endpointId'] == 'fan#test_2': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test fan 2" - assert len(appliance['capabilities']) == 2 - - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) - - assert 'Alexa.PercentageController' in caps - assert 'Alexa.PowerController' in caps - continue - - if appliance['endpointId'] == 'lock#test': - assert appliance['displayCategories'][0] == "SMARTLOCK" - assert appliance['friendlyName'] == "Test lock" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.LockController' - continue - - if appliance['endpointId'] == 'media_player#test': - assert appliance['displayCategories'][0] == "TV" - assert appliance['friendlyName'] == "Test media player" - assert len(appliance['capabilities']) == 3 - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) - - assert 'Alexa.PowerController' in caps - assert 'Alexa.Speaker' in caps - assert 'Alexa.PlaybackController' in caps - continue - - if appliance['endpointId'] == 'alert#test': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test alert" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue - - if appliance['endpointId'] == 'automation#test': - assert appliance['displayCategories'][0] == "OTHER" - assert appliance['friendlyName'] == "Test automation" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' - continue - - if appliance['endpointId'] == 'group#test': - assert appliance['displayCategories'][0] == "SCENE_TRIGGER" - assert appliance['friendlyName'] == "Test group" - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][-1] - assert capability['interface'] == 'Alexa.SceneController' - assert capability['supportsDeactivation'] - continue - - if appliance['endpointId'] == 'cover#test': - assert appliance['displayCategories'][0] == "DOOR" - assert appliance['friendlyName'] == "Test cover" - assert len(appliance['capabilities']) == 2 - - caps = set() - for feature in appliance['capabilities']: - caps.add(feature['interface']) - - assert 'Alexa.PercentageController' in caps - assert 'Alexa.PowerController' in caps - continue - - if appliance['endpointId'] == 'sensor#test_temp': - assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' - assert appliance['friendlyName'] == 'Test Temp Sensor' - assert len(appliance['capabilities']) == 1 - capability = appliance['capabilities'][0] - assert capability['interface'] == 'Alexa.TemperatureSensor' - properties = capability['properties'] - assert properties['retrievable'] is True - assert {'name': 'temperature'} in properties['supported'] - continue - - raise AssertionError("Unknown appliance!") +@asyncio.coroutine +def test_unknown_sensor(hass): + """Test sensors of unknown quantities are not discovered.""" + device = ( + 'sensor.test_sickness', '0.1', { + 'friendly_name': "Test Space Sickness Sensor", + 'unit_of_measurement': 'garn', + }) + yield from discovery_test(device, hass, expected_endpoints=0) @asyncio.coroutine @@ -466,7 +741,7 @@ def test_api_entity_not_exists(hass): assert 'event' in msg msg = msg['event'] - assert len(call_switch) == 0 + assert not call_switch assert msg['header']['name'] == 'ErrorResponse' assert msg['header']['namespace'] == 'Alexa' assert msg['payload']['type'] == 'NO_SUCH_ENDPOINT' @@ -488,102 +763,95 @@ def test_api_function_not_implemented(hass): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', - 'input_boolean', 'light', - 'switch']) -def test_api_turn_on(hass, domain): - """Test api turn on process.""" - request = get_new_request( - 'Alexa.PowerController', 'TurnOn', '{}#test'.format(domain)) +def assert_request_fails( + namespace, + name, + endpoint, + service_not_called, + hass, + payload=None): + """Assert an API request returns an ErrorResponse.""" + request = get_new_request(namespace, name, endpoint) + if payload: + request['directive']['payload'] = payload - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call_domain = domain - - if domain == 'cover': - call = async_mock_service(hass, call_domain, 'open_cover') - else: - call = async_mock_service(hass, call_domain, 'turn_on') + domain, service_name = service_not_called.split('.') + call = async_mock_service(hass, domain, service_name) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) yield from hass.async_block_till_done() + assert not call assert 'event' in msg - msg = msg['event'] + assert msg['event']['header']['name'] == 'ErrorResponse' - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' + return msg @asyncio.coroutine -@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', 'group', - 'input_boolean', 'light', 'script', - 'switch']) -def test_api_turn_off(hass, domain): - """Test api turn on process.""" - request = get_new_request( - 'Alexa.PowerController', 'TurnOff', '{}#test'.format(domain)) +def assert_request_calls_service( + namespace, + name, + endpoint, + service, + hass, + response_type='Response', + payload=None): + """Assert an API request calls a hass service.""" + request = get_new_request(namespace, name, endpoint) + if payload: + request['directive']['payload'] = payload - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'on', { - 'friendly_name': "Test {}".format(domain) - }) - - call_domain = domain - - if domain == 'group': - call_domain = 'homeassistant' - - if domain == 'cover': - call = async_mock_service(hass, call_domain, 'close_cover') - else: - call = async_mock_service(hass, call_domain, 'turn_off') + domain, service_name = service.split('.') + call = async_mock_service(hass, domain, service_name) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) yield from hass.async_block_till_done() - assert 'event' in msg - msg = msg['event'] - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' + assert 'event' in msg + assert call[0].data['entity_id'] == endpoint.replace('#', '.') + assert msg['event']['header']['name'] == response_type + + return call[0], msg @asyncio.coroutine -def test_api_set_brightness(hass): - """Test api set brightness process.""" - request = get_new_request( - 'Alexa.BrightnessController', 'SetBrightness', 'light#test') +def assert_power_controller_works(endpoint, on_service, off_service, hass): + """Assert PowerController API requests work.""" + yield from assert_request_calls_service( + 'Alexa.PowerController', 'TurnOn', endpoint, + on_service, hass) - # add payload - request['directive']['payload']['brightness'] = '50' + yield from assert_request_calls_service( + 'Alexa.PowerController', 'TurnOff', endpoint, + off_service, hass) - # setup test devices - hass.states.async_set( - 'light.test', 'off', {'friendly_name': "Test light"}) - call_light = async_mock_service(hass, 'light', 'turn_on') +@asyncio.coroutine +def assert_scene_controller_works( + endpoint, + activate_service, + deactivate_service, + hass): + """Assert SceneController API requests work.""" + _, response = yield from assert_request_calls_service( + 'Alexa.SceneController', 'Activate', endpoint, + activate_service, hass, + response_type='ActivationStarted') + assert response['event']['payload']['cause']['type'] == 'VOICE_INTERACTION' + assert 'timestamp' in response['event']['payload'] - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_light) == 1 - assert call_light[0].data['entity_id'] == 'light.test' - assert call_light[0].data['brightness_pct'] == 50 - assert msg['header']['name'] == 'Response' + if deactivate_service: + yield from assert_request_calls_service( + 'Alexa.SceneController', 'Deactivate', endpoint, + deactivate_service, hass, + response_type='DeactivationStarted') + cause_type = response['event']['payload']['cause']['type'] + assert cause_type == 'VOICE_INTERACTION' + assert 'timestamp' in response['event']['payload'] @asyncio.coroutine @@ -778,524 +1046,6 @@ def test_api_increase_color_temp(hass, result, initial): assert msg['header']['name'] == 'Response' -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['scene', 'group', 'script']) -def test_api_activate(hass, domain): - """Test api activate process.""" - request = get_new_request( - 'Alexa.SceneController', 'Activate', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - if domain == 'group': - call_domain = 'homeassistant' - else: - call_domain = domain - - call = async_mock_service(hass, call_domain, 'turn_on') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'ActivationStarted' - assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION' - assert 'timestamp' in msg['payload'] - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['group', 'script']) -def test_api_deactivate(hass, domain): - """Test api deactivate process.""" - request = get_new_request( - 'Alexa.SceneController', 'Deactivate', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - if domain == 'group': - call_domain = 'homeassistant' - else: - call_domain = domain - - call = async_mock_service(hass, call_domain, 'turn_off') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'DeactivationStarted' - assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION' - assert 'timestamp' in msg['payload'] - - -@asyncio.coroutine -def test_api_set_percentage_fan(hass): - """Test api set percentage for fan process.""" - request = get_new_request( - 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2') - - # add payload - request['directive']['payload']['percentage'] = '50' - - # setup test devices - hass.states.async_set( - 'fan.test_2', 'off', {'friendly_name': "Test fan"}) - - call_fan = async_mock_service(hass, 'fan', 'set_speed') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_fan) == 1 - assert call_fan[0].data['entity_id'] == 'fan.test_2' - assert call_fan[0].data['speed'] == 'medium' - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -def test_api_set_percentage_cover(hass): - """Test api set percentage for cover process.""" - request = get_new_request( - 'Alexa.PercentageController', 'SetPercentage', 'cover#test') - - # add payload - request['directive']['payload']['percentage'] = '50' - - # setup test devices - hass.states.async_set( - 'cover.test', 'closed', { - 'friendly_name': "Test cover" - }) - - call_cover = async_mock_service(hass, 'cover', 'set_cover_position') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_cover) == 1 - assert call_cover[0].data['entity_id'] == 'cover.test' - assert call_cover[0].data['position'] == 50 - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "result,adjust", [('high', '-5'), ('off', '5'), ('low', '-80')]) -def test_api_adjust_percentage_fan(hass, result, adjust): - """Test api adjust percentage for fan process.""" - request = get_new_request( - 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2') - - # add payload - request['directive']['payload']['percentageDelta'] = adjust - - # setup test devices - hass.states.async_set( - 'fan.test_2', 'on', { - 'friendly_name': "Test fan 2", 'speed': 'high' - }) - - call_fan = async_mock_service(hass, 'fan', 'set_speed') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_fan) == 1 - assert call_fan[0].data['entity_id'] == 'fan.test_2' - assert call_fan[0].data['speed'] == result - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')]) -def test_api_adjust_percentage_cover(hass, result, adjust): - """Test api adjust percentage for cover process.""" - request = get_new_request( - 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test') - - # add payload - request['directive']['payload']['percentageDelta'] = adjust - - # setup test devices - hass.states.async_set( - 'cover.test', 'closed', { - 'friendly_name': "Test cover", - 'position': 30 - }) - - call_cover = async_mock_service(hass, 'cover', 'set_cover_position') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_cover) == 1 - assert call_cover[0].data['entity_id'] == 'cover.test' - assert call_cover[0].data['position'] == result - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['lock']) -def test_api_lock(hass, domain): - """Test api lock process.""" - request = get_new_request( - 'Alexa.LockController', 'Lock', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'lock') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_play(hass, domain): - """Test api play process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Play', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_play') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_pause(hass, domain): - """Test api pause process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Pause', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_pause') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_stop(hass, domain): - """Test api stop process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Stop', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_stop') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_next(hass, domain): - """Test api next process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Next', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_next_track') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_previous(hass, domain): - """Test api previous process.""" - request = get_new_request( - 'Alexa.PlaybackController', 'Previous', '{}#test'.format(domain)) - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'media_previous_track') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -def test_api_set_volume(hass): - """Test api set volume process.""" - request = get_new_request( - 'Alexa.Speaker', 'SetVolume', 'media_player#test') - - # add payload - request['directive']['payload']['volume'] = 50 - - # setup test devices - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", 'volume_level': 0 - }) - - call_media_player = async_mock_service(hass, 'media_player', 'volume_set') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_media_player) == 1 - assert call_media_player[0].data['entity_id'] == 'media_player.test' - assert call_media_player[0].data['volume_level'] == 0.5 - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "domain,payload,source_list,idx", [ - ('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1), - ('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0), - ('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0), - ('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None), - ] -) -def test_api_select_input(hass, domain, payload, source_list, idx): - """Test api set input process.""" - request = get_new_request( - 'Alexa.InputController', 'SelectInput', 'media_player#test') - - # add payload - request['directive']['payload']['input'] = payload - - # setup test devices - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", - 'source': 'unknown', - 'source_list': source_list, - }) - - call_media_player = async_mock_service(hass, domain, 'select_source') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - # test where no source matches - if idx is None: - assert len(call_media_player) == 0 - assert msg['header']['name'] == 'ErrorResponse' - return - - assert len(call_media_player) == 1 - assert call_media_player[0].data['entity_id'] == 'media_player.test' - assert call_media_player[0].data['source'] == source_list[idx] - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize( - "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')]) -def test_api_adjust_volume(hass, result, adjust): - """Test api adjust volume process.""" - request = get_new_request( - 'Alexa.Speaker', 'AdjustVolume', 'media_player#test') - - # add payload - request['directive']['payload']['volume'] = adjust - - # setup test devices - hass.states.async_set( - 'media_player.test', 'off', { - 'friendly_name': "Test media player", 'volume_level': 0.75 - }) - - call_media_player = async_mock_service(hass, 'media_player', 'volume_set') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_media_player) == 1 - assert call_media_player[0].data['entity_id'] == 'media_player.test' - assert call_media_player[0].data['volume_level'] == result - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -@pytest.mark.parametrize("domain", ['media_player']) -def test_api_mute(hass, domain): - """Test api mute process.""" - request = get_new_request( - 'Alexa.Speaker', 'SetMute', '{}#test'.format(domain)) - - request['directive']['payload']['mute'] = True - - # setup test devices - hass.states.async_set( - '{}.test'.format(domain), 'off', { - 'friendly_name': "Test {}".format(domain) - }) - - call = async_mock_service(hass, domain, 'volume_mute') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call) == 1 - assert call[0].data['entity_id'] == '{}.test'.format(domain) - assert msg['header']['name'] == 'Response' - - -@asyncio.coroutine -def test_api_report_temperature(hass): - """Test API ReportState response for a temperature sensor.""" - request = get_new_request('Alexa', 'ReportState', 'sensor#test') - - # setup test devices - hass.states.async_set( - 'sensor.test', '42', { - 'friendly_name': 'test sensor', - CONF_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, - }) - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - header = msg['event']['header'] - assert header['namespace'] == 'Alexa' - assert header['name'] == 'StateReport' - - properties = msg['context']['properties'] - assert len(properties) == 1 - prop = properties[0] - assert prop['namespace'] == 'Alexa.TemperatureSensor' - assert prop['name'] == 'temperature' - assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'} - - @asyncio.coroutine def test_report_lock_state(hass): """Test LockController implements lockState property.""" @@ -1306,87 +1056,46 @@ def test_report_lock_state(hass): hass.states.async_set( 'lock.unknown', STATE_UNKNOWN, {}) - request = get_new_request('Alexa', 'ReportState', 'lock#locked') - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() + properties = yield from reported_properties(hass, 'lock.locked') + properties.assert_equal('Alexa.LockController', 'lockState', 'LOCKED') - properties = msg['context']['properties'] - assert len(properties) == 1 - prop = properties[0] - assert prop['namespace'] == 'Alexa.LockController' - assert prop['name'] == 'lockState' - assert prop['value'] == 'LOCKED' + properties = yield from reported_properties(hass, 'lock.unlocked') + properties.assert_equal('Alexa.LockController', 'lockState', 'UNLOCKED') - request = get_new_request('Alexa', 'ReportState', 'lock#unlocked') - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - properties = msg['context']['properties'] - prop = properties[0] - assert prop['value'] == 'UNLOCKED' - - request = get_new_request('Alexa', 'ReportState', 'lock#unknown') - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - properties = msg['context']['properties'] - prop = properties[0] - assert prop['value'] == 'JAMMED' + properties = yield from reported_properties(hass, 'lock.unknown') + properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED') @asyncio.coroutine -def test_report_power_state(hass): - """Test PowerController implements powerState property.""" - hass.states.async_set( - 'switch.on', STATE_ON, {}) - hass.states.async_set( - 'switch.off', STATE_OFF, {}) +def reported_properties(hass, endpoint): + """Use ReportState to get properties and return them. - request = get_new_request('Alexa', 'ReportState', 'switch#on') - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - properties = msg['context']['properties'] - assert len(properties) == 1 - prop = properties[0] - assert prop['namespace'] == 'Alexa.PowerController' - assert prop['name'] == 'powerState' - assert prop['value'] == 'ON' - - request = get_new_request('Alexa', 'ReportState', 'switch#off') + The result is a _ReportedProperties instance, which has methods to make + assertions about the properties. + """ + request = get_new_request('Alexa', 'ReportState', endpoint) msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) yield from hass.async_block_till_done() + return _ReportedProperties(msg['context']['properties']) -@asyncio.coroutine -def test_report_brightness(hass): - """Test BrightnessController implements brightness property.""" - hass.states.async_set( - 'light.test', STATE_ON, { - 'brightness': 128, - 'supported_features': light.SUPPORT_BRIGHTNESS, - } - ) +class _ReportedProperties(object): + def __init__(self, properties): + self.properties = properties - request = get_new_request('Alexa', 'ReportState', 'light.test') - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() + def assert_equal(self, namespace, name, value): + """Assert a property is equal to a given value.""" + for prop in self.properties: + if prop['namespace'] == namespace and prop['name'] == name: + assert prop['value'] == value + return prop - for prop in msg['context']['properties']: - if ( - prop['namespace'] == 'Alexa.BrightnessController' - and prop['name'] == 'brightness' - ): - assert prop['value'] == 50 - break - else: - assert False, 'no brightness property present' + assert False, 'property %s:%s not in %r' % ( + namespace, + name, + self.properties, + ) @asyncio.coroutine @@ -1440,7 +1149,7 @@ def test_unsupported_domain(hass): assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 0 + assert not msg['payload']['endpoints'] @asyncio.coroutine @@ -1483,3 +1192,38 @@ def test_http_api_disabled(hass, test_client): response = yield from do_http_discovery(config, hass, test_client) assert response.status == 404 + + +@asyncio.coroutine +@pytest.mark.parametrize( + "domain,payload,source_list,idx", [ + ('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1), + ('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0), + ('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0), + ('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None), + ] +) +def test_api_select_input(hass, domain, payload, source_list, idx): + """Test api set input process.""" + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", + 'source': 'unknown', + 'source_list': source_list, + }) + + # test where no source matches + if idx is None: + yield from assert_request_fails( + 'Alexa.InputController', 'SelectInput', 'media_player#test', + 'media_player.select_source', + hass, + payload={'input': payload}) + return + + call, _ = yield from assert_request_calls_service( + 'Alexa.InputController', 'SelectInput', 'media_player#test', + 'media_player.select_source', + hass, + payload={'input': payload}) + assert call.data['source'] == source_list[idx] From 5b1c51bdf6f47b55e0f190d282f6569dacd397ef Mon Sep 17 00:00:00 2001 From: freol35241 Date: Tue, 30 Jan 2018 10:18:45 +0100 Subject: [PATCH 053/166] Handling of payload not for this entity. (#11836) * Handling of payload not for this entity. The update state-method should not be called if the payload is not intended for this entity. * Fixing linter errors * Adding warning for case when no matching payload is found --- homeassistant/components/binary_sensor/mqtt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 983c879338d..650179f676b 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -94,6 +94,11 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._state = True elif payload == self._payload_off: self._state = False + else: # Payload is not for this entity + _LOGGER.warning('No matching payload found' + ' for entity: %s with state_topic: %s', + self._name, self._state_topic) + return self.async_schedule_update_ha_state() From 8e441ba03b9006f7a678c8364538473c5e55fea7 Mon Sep 17 00:00:00 2001 From: Phil Kates Date: Tue, 30 Jan 2018 01:19:24 -0800 Subject: [PATCH 054/166] Refactor Google Assistant query_device (#12022) * google_assistant: Refactor query_device The previous code had issues where domains could break out and end up with weird brightness values and we weren't enforcing the `on` and `oneline` keys in the response. * google_assistant: Add media_player to query test --- .../components/google_assistant/smart_home.py | 200 ++++++++++-------- .../google_assistant/test_google_assistant.py | 46 +++- 2 files changed, 157 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0cc6f9c3f83..b718c009160 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -37,6 +37,7 @@ from .const import ( ) HANDLERS = Registry() +QUERY_HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) # Mapping is [actions schema, primary trait, optional features] @@ -177,120 +178,145 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem): return device -def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict: - """Take an entity and return a properly formatted device object.""" - def celsius(deg: Optional[float]) -> Optional[float]: - """Convert a float to Celsius and rounds to one decimal place.""" - if deg is None: - return None - return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1) +def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]: + """Convert a float to Celsius and rounds to one decimal place.""" + if deg is None: + return None + return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1) - if entity.domain == sensor.DOMAIN: - entity_config = config.entity_config.get(entity.entity_id, {}) - google_domain = entity_config.get(CONF_TYPE) - if google_domain == climate.DOMAIN: - # check if we have a string value to convert it to number - value = entity.state - if isinstance(entity.state, str): - try: - value = float(value) - except ValueError: - value = None - - if value is None: - raise SmartHomeError( - ERROR_NOT_SUPPORTED, - "Invalid value {} for the climate sensor" - .format(entity.state) - ) - - # detect if we report temperature or humidity - unit_of_measurement = entity.attributes.get( - ATTR_UNIT_OF_MEASUREMENT, - units.temperature_unit - ) - if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]: - value = celsius(value) - attr = 'thermostatTemperatureAmbient' - elif unit_of_measurement == '%': - attr = 'thermostatHumidityAmbient' - else: - raise SmartHomeError( - ERROR_NOT_SUPPORTED, - "Unit {} is not supported by the climate sensor" - .format(unit_of_measurement) - ) - - return {attr: value} +@QUERY_HANDLERS.register(sensor.DOMAIN) +def query_response_sensor( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a sensor entity to a QUERY response.""" + entity_config = config.entity_config.get(entity.entity_id, {}) + google_domain = entity_config.get(CONF_TYPE) + if google_domain != climate.DOMAIN: raise SmartHomeError( ERROR_NOT_SUPPORTED, "Sensor type {} is not supported".format(google_domain) ) - if entity.domain == climate.DOMAIN: - mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower() - if mode not in CLIMATE_SUPPORTED_MODES: - mode = 'heat' - response = { - 'thermostatMode': mode, - 'thermostatTemperatureSetpoint': - celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)), - 'thermostatTemperatureAmbient': - celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)), - 'thermostatTemperatureSetpointHigh': - celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)), - 'thermostatTemperatureSetpointLow': - celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)), - 'thermostatHumidityAmbient': - entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY), - } - return {k: v for k, v in response.items() if v is not None} + # check if we have a string value to convert it to number + value = entity.state + if isinstance(entity.state, str): + try: + value = float(value) + except ValueError: + value = None - final_state = entity.state != STATE_OFF - final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255 - if final_state else 0) + if value is None: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Invalid value {} for the climate sensor" + .format(entity.state) + ) - if entity.domain == media_player.DOMAIN: - level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL, 1.0 - if final_state else 0.0) - # Convert 0.0-1.0 to 0-255 - final_brightness = round(min(1.0, level) * 255) + # detect if we report temperature or humidity + unit_of_measurement = entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, + units.temperature_unit + ) + if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]: + value = celsius(value, units) + attr = 'thermostatTemperatureAmbient' + elif unit_of_measurement == '%': + attr = 'thermostatHumidityAmbient' + else: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Unit {} is not supported by the climate sensor" + .format(unit_of_measurement) + ) - if final_brightness is None: - final_brightness = 255 if final_state else 0 + return {attr: value} - final_brightness = 100 * (final_brightness / 255) - query_response = { - "on": final_state, - "online": True, - "brightness": int(final_brightness) +@QUERY_HANDLERS.register(climate.DOMAIN) +def query_response_climate( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a climate entity to a QUERY response.""" + mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower() + if mode not in CLIMATE_SUPPORTED_MODES: + mode = 'heat' + attrs = entity.attributes + response = { + 'thermostatMode': mode, + 'thermostatTemperatureSetpoint': + celsius(attrs.get(climate.ATTR_TEMPERATURE), units), + 'thermostatTemperatureAmbient': + celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units), + 'thermostatTemperatureSetpointHigh': + celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units), + 'thermostatTemperatureSetpointLow': + celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units), + 'thermostatHumidityAmbient': + attrs.get(climate.ATTR_CURRENT_HUMIDITY), } + return {k: v for k, v in response.items() if v is not None} + + +@QUERY_HANDLERS.register(media_player.DOMAIN) +def query_response_media_player( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a media_player entity to a QUERY response.""" + level = entity.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL, + 1.0 if entity.state != STATE_OFF else 0.0) + # Convert 0.0-1.0 to 0-255 + brightness = int(level * 100) + + return {'brightness': brightness} + + +@QUERY_HANDLERS.register(light.DOMAIN) +def query_response_light( + entity: Entity, config: Config, units: UnitSystem) -> dict: + """Convert a light entity to a QUERY response.""" + response = {} # type: Dict[str, Any] + + brightness = entity.attributes.get(light.ATTR_BRIGHTNESS) + if brightness is not None: + response['brightness'] = int(100 * (brightness / 255)) supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported_features & \ (light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR): - query_response["color"] = {} + response['color'] = {} if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None: - query_response["color"]["temperature"] = \ + response['color']['temperature'] = \ int(round(color.color_temperature_mired_to_kelvin( entity.attributes.get(light.ATTR_COLOR_TEMP)))) if entity.attributes.get(light.ATTR_COLOR_NAME) is not None: - query_response["color"]["name"] = \ + response['color']['name'] = \ entity.attributes.get(light.ATTR_COLOR_NAME) if entity.attributes.get(light.ATTR_RGB_COLOR) is not None: color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR) if color_rgb is not None: - query_response["color"]["spectrumRGB"] = \ + response['color']['spectrumRGB'] = \ int(color.color_rgb_to_hex( color_rgb[0], color_rgb[1], color_rgb[2]), 16) - return query_response + return response + + +def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict: + """Take an entity and return a properly formatted device object.""" + state = entity.state != STATE_OFF + defaults = { + 'on': state, + 'online': True + } + + handler = QUERY_HANDLERS.get(entity.domain) + if callable(handler): + defaults.update(handler(entity, config, units)) + + return defaults # erroneous bug on old pythons and pylint @@ -438,11 +464,11 @@ def async_devices_query(hass, config, payload): if not state: # If we can't find a state, the device is offline devices[devid] = {'online': False} - - try: - devices[devid] = query_device(state, config, hass.config.units) - except SmartHomeError as error: - devices[devid] = {'errorCode': error.code} + else: + try: + devices[devid] = query_device(state, config, hass.config.units) + except SmartHomeError as error: + devices[devid] = {'errorCode': error.code} return {'devices': devices} diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 9fc35bc17b1..43c36d1ca2a 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -175,6 +175,8 @@ def test_query_request(hass_fixture, assistant_client): 'id': "light.bed_light", }, { 'id': "light.kitchen_lights", + }, { + 'id': 'media_player.lounge_room', }] } }] @@ -187,12 +189,14 @@ def test_query_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid devices = body['payload']['devices'] - assert len(devices) == 3 + assert len(devices) == 4 assert devices['light.bed_light']['on'] is False assert devices['light.ceiling_lights']['on'] is True assert devices['light.ceiling_lights']['brightness'] == 70 assert devices['light.kitchen_lights']['color']['spectrumRGB'] == 16727919 assert devices['light.kitchen_lights']['color']['temperature'] == 4166 + assert devices['media_player.lounge_room']['on'] is True + assert devices['media_player.lounge_room']['brightness'] == 100 @asyncio.coroutine @@ -225,26 +229,36 @@ def test_query_climate_request(hass_fixture, assistant_client): devices = body['payload']['devices'] assert devices == { 'climate.heatpump': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': 20.0, 'thermostatTemperatureAmbient': 25.0, 'thermostatMode': 'heat', }, 'climate.ecobee': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpointHigh': 24, 'thermostatTemperatureAmbient': 23, 'thermostatMode': 'heat', 'thermostatTemperatureSetpointLow': 21 }, 'climate.hvac': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': 21, 'thermostatTemperatureAmbient': 22, 'thermostatMode': 'cool', 'thermostatHumidityAmbient': 54, }, 'sensor.outside_temperature': { + 'on': True, + 'online': True, 'thermostatTemperatureAmbient': 15.6 }, 'sensor.outside_humidity': { + 'on': True, + 'online': True, 'thermostatHumidityAmbient': 54.0 } } @@ -280,23 +294,31 @@ def test_query_climate_request_f(hass_fixture, assistant_client): devices = body['payload']['devices'] assert devices == { 'climate.heatpump': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': -6.7, 'thermostatTemperatureAmbient': -3.9, 'thermostatMode': 'heat', }, 'climate.ecobee': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpointHigh': -4.4, 'thermostatTemperatureAmbient': -5, 'thermostatMode': 'heat', 'thermostatTemperatureSetpointLow': -6.1, }, 'climate.hvac': { + 'on': True, + 'online': True, 'thermostatTemperatureSetpoint': -6.1, 'thermostatTemperatureAmbient': -5.6, 'thermostatMode': 'cool', 'thermostatHumidityAmbient': 54, }, 'sensor.outside_temperature': { + 'on': True, + 'online': True, 'thermostatTemperatureAmbient': -9.1 } } @@ -317,6 +339,8 @@ def test_execute_request(hass_fixture, assistant_client): "id": "light.ceiling_lights", }, { "id": "switch.decorative_lights", + }, { + "id": "media_player.lounge_room", }], "execution": [{ "command": "action.devices.commands.OnOff", @@ -324,6 +348,17 @@ def test_execute_request(hass_fixture, assistant_client): "on": False } }] + }, { + "devices": [{ + "id": "media_player.walkman", + }], + "execution": [{ + "command": + "action.devices.commands.BrightnessAbsolute", + "params": { + "brightness": 70 + } + }] }, { "devices": [{ "id": "light.kitchen_lights", @@ -380,7 +415,7 @@ def test_execute_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid commands = body['payload']['commands'] - assert len(commands) == 6 + assert len(commands) == 8 ceiling = hass_fixture.states.get('light.ceiling_lights') assert ceiling.state == 'off' @@ -394,3 +429,10 @@ def test_execute_request(hass_fixture, assistant_client): assert bed.attributes.get(light.ATTR_RGB_COLOR) == (0, 255, 0) assert hass_fixture.states.get('switch.decorative_lights').state == 'off' + + walkman = hass_fixture.states.get('media_player.walkman') + assert walkman.state == 'playing' + assert walkman.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) == 0.7 + + lounge = hass_fixture.states.get('media_player.lounge_room') + assert lounge.state == 'off' From e51427b28458b86da21f7f317d03c09e5136225a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 30 Jan 2018 01:39:39 -0800 Subject: [PATCH 055/166] Entity registry (#11979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Entity#unique_id defaults to None * Initial commit entity registry * Clean up unique_id property * Lint * Add tests to entity component * Lint * Restore some unique ids * Spelling * Remove use of IP address for unique ID * Add tests * Add tests * Fix tests * Add some docs * Add one more test * Fix new test… --- .../components/binary_sensor/bloomsky.py | 6 - .../components/binary_sensor/ecobee.py | 5 - .../components/binary_sensor/hikvision.py | 2 +- .../components/binary_sensor/netatmo.py | 4 +- .../components/binary_sensor/wemo.py | 2 +- homeassistant/components/camera/netatmo.py | 8 +- homeassistant/components/climate/daikin.py | 5 - homeassistant/components/climate/nest.py | 5 + homeassistant/components/cover/rpi_gpio.py | 5 - homeassistant/components/fan/insteon_local.py | 2 +- homeassistant/components/group/__init__.py | 7 +- homeassistant/components/light/avion.py | 2 +- homeassistant/components/light/decora.py | 2 +- homeassistant/components/light/flux_led.py | 5 - homeassistant/components/light/hue.py | 9 +- .../components/light/insteon_local.py | 2 +- homeassistant/components/light/tikteck.py | 2 +- homeassistant/components/light/wemo.py | 5 +- homeassistant/components/light/yeelight.py | 5 - homeassistant/components/light/zengge.py | 2 +- homeassistant/components/media_player/emby.py | 2 +- homeassistant/components/media_player/plex.py | 3 +- .../components/media_player/yamaha.py | 5 - homeassistant/components/sensor/blink.py | 5 - homeassistant/components/sensor/bloomsky.py | 6 - homeassistant/components/sensor/canary.py | 3 +- homeassistant/components/sensor/daikin.py | 5 - homeassistant/components/sensor/ecobee.py | 7 +- homeassistant/components/sensor/ios.py | 2 +- homeassistant/components/sensor/isy994.py | 5 - homeassistant/components/sensor/netatmo.py | 3 +- .../components/switch/insteon_local.py | 2 +- .../components/switch/rainmachine.py | 3 +- homeassistant/components/switch/wemo.py | 2 +- homeassistant/components/tahoma.py | 8 -- homeassistant/components/zwave/__init__.py | 4 +- homeassistant/helpers/entity.py | 20 ++- homeassistant/helpers/entity_component.py | 60 +++++--- homeassistant/helpers/entity_registry.py | 134 +++++++++++++++++ homeassistant/util/yaml.py | 8 ++ tests/common.py | 9 ++ tests/components/camera/test_local_file.py | 4 + tests/components/light/test_hue.py | 70 +-------- tests/components/sensor/test_file.py | 3 +- tests/components/zwave/test_init.py | 2 +- tests/helpers/test_entity_component.py | 106 ++++++++++---- tests/helpers/test_entity_registry.py | 135 ++++++++++++++++++ 47 files changed, 471 insertions(+), 230 deletions(-) create mode 100644 homeassistant/helpers/entity_registry.py create mode 100644 tests/helpers/test_entity_registry.py diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index 5e69dcc9109..1d0849b255e 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -50,7 +50,6 @@ class BloomSkySensor(BinarySensorDevice): self._device_id = device['DeviceID'] self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name) self._state = None @property @@ -58,11 +57,6 @@ class BloomSkySensor(BinarySensorDevice): """Return the name of the BloomSky device and this sensor.""" return self._name - @property - def unique_id(self): - """Return the unique ID for this sensor.""" - return self._unique_id - @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/binary_sensor/ecobee.py index 214efb870b9..15efa21b226 100644 --- a/homeassistant/components/binary_sensor/ecobee.py +++ b/homeassistant/components/binary_sensor/ecobee.py @@ -50,11 +50,6 @@ class EcobeeBinarySensor(BinarySensorDevice): """Return the status of the sensor.""" return self._state == 'true' - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return "binary_sensor_ecobee_{}_{}".format(self._name, self.index) - @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index 3ec70896426..ec64bdf07b8 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -212,7 +212,7 @@ class HikvisionBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return an unique ID.""" - return '{}.{}'.format(self.__class__, self._id) + return self._id @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index e597f1d0bbe..4d8aaa7d0d9 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -131,10 +131,8 @@ class NetatmoBinarySensor(BinarySensorDevice): self._name += ' / ' + module_name self._sensor_name = sensor self._name += ' ' + sensor - camera_id = data.camera_data.cameraByName( + self._unique_id = data.camera_data.cameraByName( camera=camera_name, home=home)['id'] - self._unique_id = "Netatmo_binary_sensor {0} - {1}".format( - self._name, camera_id) self._cameratype = camera_type self._state = None diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 857c0c40777..cc1f602d871 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -58,7 +58,7 @@ class WemoBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return the id of this WeMo device.""" - return '{}.{}'.format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index c1ec2db0a08..0a9a3fbdca4 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -64,13 +64,11 @@ class NetatmoCamera(Camera): self._name = home + ' / ' + camera_name else: self._name = camera_name - camera_id = data.camera_data.cameraByName( - camera=camera_name, home=home)['id'] - self._unique_id = "Welcome_camera {0} - {1}".format( - self._name, camera_id) self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( camera=camera_name ) + self._unique_id = data.camera_data.cameraByName( + camera=camera_name, home=home)['id'] self._cameratype = camera_type def camera_image(self): @@ -117,5 +115,5 @@ class NetatmoCamera(Camera): @property def unique_id(self): - """Return the unique ID for this sensor.""" + """Return the unique ID for this camera.""" return self._unique_id diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index fea1fcee3a3..0ed4ebe8942 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -183,11 +183,6 @@ class DaikinClimate(ClimateDevice): self._force_refresh = True self._api.device.set(values) - @property - def unique_id(self): - """Return the ID of this AC.""" - return "{}.{}".format(self.__class__, self._api.ip_address) - @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index b4492821b1f..d8d7d6c901a 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -97,6 +97,11 @@ class NestThermostat(ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS + @property + def unique_id(self): + """Unique ID for this device.""" + return self.device.serial + @property def name(self): """Return the name of the nest, if any.""" diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 1ee3ea00476..981312140eb 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -89,11 +89,6 @@ class RPiGPIOCover(CoverDevice): rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) rpi_gpio.write_output(self._relay_pin, not self._invert_relay) - @property - def unique_id(self): - """Return the ID of this cover.""" - return '{}.{}'.format(self.__class__, self._name) - @property def name(self): """Return the name of the cover if any.""" diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index 85e603c8c81..e6f9424d852 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -60,7 +60,7 @@ class InsteonLocalFanDevice(FanEntity): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}_fan'.format(self.node.device_id) + return self.node.device_id @property def speed(self) -> str: diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 3881b6211c2..5e4dfdb0bdc 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -371,7 +371,6 @@ def async_setup(hass, config): @asyncio.coroutine def _async_process_config(hass, config, component): """Process group configuration.""" - groups = [] for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) entity_ids = conf.get(CONF_ENTITIES) or [] @@ -381,13 +380,9 @@ def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. - group = yield from Group.async_create_group( + yield from Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, control=control, object_id=object_id) - groups.append(group) - - if groups: - yield from component.async_add_entities(groups) class Group(Entity): diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index f214d47fa1b..5344c3dce6d 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -83,7 +83,7 @@ class AvionLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index 3b6b22faba9..03441dd8ea6 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -88,7 +88,7 @@ class DecoraLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 396ddc984fa..075b98117f8 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -167,11 +167,6 @@ class FluxLight(Light): """Return True if entity is available.""" return self._bulb is not None - @property - def unique_id(self): - """Return the ID of this light.""" - return '{}.{}'.format(self.__class__, self._ipaddr) - @property def name(self): """Return the name of the device if any.""" diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index cbabaafd3fb..07ba069d831 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -228,14 +228,7 @@ class HueLight(Light): @property def unique_id(self): """Return the ID of this Hue light.""" - lid = self.info.get('uniqueid') - - if lid is None: - default_type = 'Group' if self.is_group else 'Light' - ltype = self.info.get('type', default_type) - lid = '{}.{}.{}'.format(self.name, ltype, self.light_id) - - return '{}.{}'.format(self.__class__, lid) + return self.info.get('uniqueid') @property def name(self): diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index 88d621d4060..bd7814df8f3 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -57,7 +57,7 @@ class InsteonLocalDimmerDevice(Light): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}'.format(self.node.device_id) + return self.node.device_id @property def brightness(self): diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index 07d4b63e99a..c39748e4430 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -70,7 +70,7 @@ class TikteckLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 693e40c0292..540c718b04d 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -76,8 +76,7 @@ class WemoLight(Light): @property def unique_id(self): """Return the ID of this light.""" - deviceid = self.device.uniqueID - return '{}.{}'.format(self.__class__, deviceid) + return self.device.uniqueID @property def name(self): @@ -176,7 +175,7 @@ class WemoDimmer(Light): @property def unique_id(self): """Return the ID of this WeMo dimmer.""" - return "{}.{}".format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index c31bfec4927..33c84df14be 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -175,11 +175,6 @@ class YeelightLight(Light): """Return the list of supported effects.""" return YEELIGHT_EFFECT_LIST - @property - def unique_id(self) -> str: - """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._ipaddr) - @property def color_temp(self) -> int: """Return the color temperature.""" diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index b453218c7c9..7071c8c43bb 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -67,7 +67,7 @@ class ZenggeLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 35d3ed35095..a3fe62c5a42 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -182,7 +182,7 @@ class EmbyDevice(MediaPlayerDevice): @property def unique_id(self): """Return the id of this emby client.""" - return '{}.{}'.format(self.__class__, self.device_id) + return self.device_id @property def supports_remote_control(self): diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index c96b0f3c2ae..38a84053263 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -459,8 +459,7 @@ class PlexClient(MediaPlayerDevice): @property def unique_id(self): """Return the id of this plex client.""" - return '{}.{}'.format(self.__class__, self.machine_identifier or - self.name) + return self.machine_identifier @property def name(self): diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 8e4729a4409..f102d8a490d 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -149,11 +149,6 @@ class YamahaDevice(MediaPlayerDevice): self._name = name self._zone = receiver.zone - @property - def unique_id(self): - """Return an unique ID.""" - return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) - def update(self): """Get the latest details from the device.""" self._play_status = self.receiver.play_status() diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 44557978117..db7ab7c2e9e 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -61,11 +61,6 @@ class BlinkSensor(Entity): """Return the camera's current state.""" return self._state - @property - def unique_id(self): - """Return the unique camera sensor identifier.""" - return "sensor_{}_{}".format(self._name, self.index) - @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index 660cb5ede6e..ce44abdb087 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -64,7 +64,6 @@ class BloomSkySensor(Entity): self._device_id = device['DeviceID'] self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._unique_id = 'bloomsky_sensor {}'.format(self._name) self._state = None @property @@ -72,11 +71,6 @@ class BloomSkySensor(Entity): """Return the name of the BloomSky device and this sensor.""" return self._name - @property - def unique_id(self): - """Return the unique ID for this sensor.""" - return self._unique_id - @property def state(self): """Return the current state, eg. value, of this sensor.""" diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py index 56da1c4deea..ded8f36203e 100644 --- a/homeassistant/components/sensor/canary.py +++ b/homeassistant/components/sensor/canary.py @@ -70,8 +70,7 @@ class CanarySensor(Entity): @property def unique_id(self): """Return the unique ID of this sensor.""" - return "sensor_canary_{}_{}".format(self._device_id, - self._sensor_type[0]) + return "{}_{}".format(self._device_id, self._sensor_type[0]) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py index 3ea3418db4e..0b2f6495b45 100644 --- a/homeassistant/components/sensor/daikin.py +++ b/homeassistant/components/sensor/daikin.py @@ -95,11 +95,6 @@ class DaikinClimateSensor(Entity): return value - @property - def unique_id(self): - """Return the ID of this AC.""" - return "{}.{}".format(self.__class__, self._api.ip_address) - @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index a0c6f7a92e4..dad770d5bab 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -50,18 +50,13 @@ class EcobeeSensor(Entity): @property def name(self): """Return the name of the Ecobee sensor.""" - return self._name.rstrip() + return self._name @property def state(self): """Return the state of the sensor.""" return self._state - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return "sensor_ecobee_{}_{}".format(self._name, self.index) - @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index 9a23da48a6b..398c0b350ee 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -58,7 +58,7 @@ class IOSSensor(Entity): def unique_id(self): """Return the unique ID of this sensor.""" device_id = self._device[ios.ATTR_DEVICE_ID] - return "sensor_ios_battery_{}_{}".format(self.type, device_id) + return "{}_{}".format(self.type, device_id) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 76f026bba10..39c9d8a3b9d 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -317,11 +317,6 @@ class ISYWeatherDevice(ISYDevice): """Initialize the ISY994 weather device.""" super().__init__(node) - @property - def unique_id(self) -> str: - """Return the unique identifier for the node.""" - return self._node.name - @property def raw_units(self) -> str: """Return the raw unit of measurement.""" diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 8ace931a8cc..c20e0a59408 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -113,8 +113,7 @@ class NetAtmoSensor(Entity): module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] self.module_id = module_id[1] - self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format( - self._name, module_id, self.type) + self._unique_id = '{}-{}'.format(self.module_id, self.type) @property def name(self): diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py index c20a638c00f..4456436ea61 100644 --- a/homeassistant/components/switch/insteon_local.py +++ b/homeassistant/components/switch/insteon_local.py @@ -54,7 +54,7 @@ class InsteonLocalSwitchDevice(SwitchDevice): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}'.format(self.node.device_id) + return self.node.device_id @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 9425b61f0e5..3147ded96bd 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -180,8 +180,7 @@ class RainMachineEntity(SwitchDevice): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return '{}.{}.{}'.format(self.__class__, self._device_name, - self.rainmachine_id) + return self.rainmachine_id @aware_throttle('local') def _local_update(self) -> None: diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 7b97ece337b..4339c92bb60 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -81,7 +81,7 @@ class WemoSwitch(SwitchDevice): @property def unique_id(self): """Return the ID of this WeMo switch.""" - return "{}.{}".format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 4488b4e836b..0db055f7d92 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import (slugify) REQUIREMENTS = ['tahoma-api==0.0.10'] @@ -101,15 +100,8 @@ class TahomaDevice(Entity): """Initialize the device.""" self.tahoma_device = tahoma_device self.controller = controller - self._unique_id = TAHOMA_ID_FORMAT.format( - slugify(tahoma_device.label), slugify(tahoma_device.url)) self._name = self.tahoma_device.label - @property - def unique_id(self): - """Return the unique ID for this cover.""" - return self._unique_id - @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index abfd353e1f4..10942de8097 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -865,8 +865,8 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "ZWAVE-{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = "{}-{}".format(self.node.node_id, + self.values.primary.object_id) self._update_attributes() dispatcher.connect( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f9570ac5858..d1e5c0d82a0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -91,7 +91,7 @@ class Entity(object): @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}.{}".format(self.__class__, id(self)) + return None @property def name(self) -> Optional[str]: @@ -338,8 +338,22 @@ class Entity(object): def __eq__(self, other): """Return the comparison.""" - return (isinstance(other, Entity) and - other.unique_id == self.unique_id) + if not isinstance(other, self.__class__): + return False + + # Can only decide equality if both have a unique id + if self.unique_id is None or other.unique_id is None: + return False + + # Ensure they belong to the same platform + if self.platform is not None or other.platform is not None: + if self.platform is None or other.platform is None: + return False + + if self.platform.platform != other.platform.platform: + return False + + return self.unique_id == other.unique_id def __repr__(self): """Return the representation.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4a791d12e52..2c928f184e8 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -8,10 +8,9 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) -from homeassistant.core import callback, valid_entity_id +from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import ( async_track_time_interval, async_track_point_in_time) from homeassistant.helpers.service import extract_entity_ids @@ -19,11 +18,13 @@ from homeassistant.util import slugify from homeassistant.util.async import ( run_callback_threadsafe, run_coroutine_threadsafe) import homeassistant.util.dt as dt_util +from .entity_registry import EntityRegistry DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 PLATFORM_NOT_READY_RETRIES = 10 +DATA_REGISTRY = 'entity_registry' class EntityComponent(object): @@ -357,12 +358,20 @@ class EntityPlatform(object): if not new_entities: return + hass = self.component.hass component_entities = set(entity.entity_id for entity in self.component.entities) + registry = hass.data.get(DATA_REGISTRY) + + if registry is None: + registry = hass.data[DATA_REGISTRY] = EntityRegistry(hass) + + yield from registry.async_ensure_loaded() + tasks = [ self._async_add_entity(entity, update_before_add, - component_entities) + component_entities, registry) for entity in new_entities] yield from asyncio.wait(tasks, loop=self.component.hass.loop) @@ -378,15 +387,12 @@ class EntityPlatform(object): ) @asyncio.coroutine - def _async_add_entity(self, entity, update_before_add, component_entities): + def _async_add_entity(self, entity, update_before_add, component_entities, + registry): """Helper method to add an entity to the platform.""" if entity is None: raise ValueError('Entity cannot be None') - # Do nothing if entity has already been added based on unique id. - if entity in self.component.entities: - return - entity.hass = self.component.hass entity.platform = self entity.parallel_updates = self.parallel_updates @@ -400,17 +406,39 @@ class EntityPlatform(object): "%s: Error on device update!", self.platform) return - # Write entity_id to entity - if getattr(entity, 'entity_id', None) is None: - object_id = entity.name or DEVICE_DEFAULT_NAME + suggested_object_id = None + + # Get entity_id from unique ID registration + if entity.unique_id is not None: + if entity.entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + suggested_object_id = entity.name + + entry = registry.async_get_or_create( + self.component.domain, self.platform, entity.unique_id, + suggested_object_id=suggested_object_id) + entity.entity_id = entry.entity_id + + # We won't generate an entity ID if the platform has already set one + # We will however make sure that platform cannot pick a registered ID + elif (entity.entity_id is not None and + registry.async_is_registered(entity.entity_id)): + # If entity already registered, convert entity id to suggestion + suggested_object_id = split_entity_id(entity.entity_id)[1] + entity.entity_id = None + + # Generate entity ID + if entity.entity_id is None: + suggested_object_id = \ + suggested_object_id or entity.name or DEVICE_DEFAULT_NAME if self.entity_namespace is not None: - object_id = '{} {}'.format(self.entity_namespace, - object_id) + suggested_object_id = '{} {}'.format(self.entity_namespace, + suggested_object_id) - entity.entity_id = async_generate_entity_id( - self.component.entity_id_format, object_id, - component_entities) + entity.entity_id = registry.async_generate_entity_id( + self.component.domain, suggested_object_id) # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py new file mode 100644 index 00000000000..350c8273232 --- /dev/null +++ b/homeassistant/helpers/entity_registry.py @@ -0,0 +1,134 @@ +"""Provide a registry to track entity IDs. + +The Entity Registry keeps a registry of entities. Entities are uniquely +identified by their domain, platform and a unique id provided by that platform. + +The Entity Registry will persist itself 10 seconds after a new entity is +registered. Registering a new entity while a timer is in progress resets the +timer. + +After initializing, call EntityRegistry.async_ensure_loaded to load the data +from disk. +""" +import asyncio +from collections import namedtuple, OrderedDict +from itertools import chain +import logging +import os + +from ..core import callback, split_entity_id +from ..util import ensure_unique_string, slugify +from ..util.yaml import load_yaml, save_yaml + +PATH_REGISTRY = 'entity_registry.yaml' +SAVE_DELAY = 10 +Entry = namedtuple('EntityRegistryEntry', + 'entity_id,unique_id,platform,domain') +_LOGGER = logging.getLogger(__name__) + + +class EntityRegistry: + """Class to hold a registry of entities.""" + + def __init__(self, hass): + """Initialize the registry.""" + self.hass = hass + self.entities = None + self._load_task = None + self._sched_save = None + + @callback + def async_is_registered(self, entity_id): + """Check if an entity_id is currently registered.""" + return entity_id in self.entities + + @callback + def async_generate_entity_id(self, domain, suggested_object_id): + """Generate an entity ID that does not conflict. + + Conflicts checked against registered and currently existing entities. + """ + return ensure_unique_string( + '{}.{}'.format(domain, slugify(suggested_object_id)), + chain(self.entities.keys(), + self.hass.states.async_entity_ids(domain)) + ) + + @callback + def async_get_or_create(self, domain, platform, unique_id, *, + suggested_object_id=None): + """Get entity. Create if it doesn't exist.""" + for entity in self.entities.values(): + if entity.domain == domain and entity.platform == platform and \ + entity.unique_id == unique_id: + return entity + + entity_id = self.async_generate_entity_id( + domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) + entity = Entry( + entity_id=entity_id, + unique_id=unique_id, + platform=platform, + domain=domain, + ) + self.entities[entity_id] = entity + _LOGGER.info('Registered new %s.%s entity: %s', + domain, platform, entity_id) + self.async_schedule_save() + return entity + + @asyncio.coroutine + def async_ensure_loaded(self): + """Load the registry from disk.""" + if self.entities is not None: + return + + if self._load_task is None: + self._load_task = self.hass.async_add_job(self._async_load) + + yield from self._load_task + + @asyncio.coroutine + def _async_load(self): + """Load the entity registry.""" + path = self.hass.config.path(PATH_REGISTRY) + entities = OrderedDict() + + if os.path.isfile(path): + data = yield from self.hass.async_add_job(load_yaml, path) + + for entity_id, info in data.items(): + entities[entity_id] = Entry( + domain=split_entity_id(entity_id)[0], + entity_id=entity_id, + unique_id=info['unique_id'], + platform=info['platform'] + ) + + self.entities = entities + self._load_task = None + + @callback + def async_schedule_save(self): + """Schedule saving the entity registry.""" + if self._sched_save is not None: + self._sched_save.cancel() + + self._sched_save = self.hass.loop.call_later( + SAVE_DELAY, self.hass.async_add_job, self._async_save + ) + + @asyncio.coroutine + def _async_save(self): + """Save the entity registry to a file.""" + self._sched_save = None + data = OrderedDict() + + for entry in self.entities.values(): + data[entry.entity_id] = { + 'unique_id': entry.unique_id, + 'platform': entry.platform, + } + + yield from self.hass.async_add_job( + save_yaml, self.hass.config.path(PATH_REGISTRY), data) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 48d709bc549..d0d5199e0f4 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -83,6 +83,14 @@ def dump(_dict: dict) -> str: .replace(': null\n', ':\n') +def save_yaml(path, data): + """Save YAML to a file.""" + # Dump before writing to not truncate the file if dumping fails + data = dump(data) + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(data) + + def clear_secret_cache() -> None: """Clear the secret cache. diff --git a/tests/common.py b/tests/common.py index 3823a1e2b4e..ed4439c1c49 100644 --- a/tests/common.py +++ b/tests/common.py @@ -22,6 +22,7 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) +from homeassistant.helpers import entity_component, entity_registry from homeassistant.components import mqtt, recorder from homeassistant.components.http.auth import auth_middleware from homeassistant.components.http.const import ( @@ -315,6 +316,14 @@ def mock_component(hass, component): hass.config.components.add(component) +def mock_registry(hass): + """Mock the Entity Registry.""" + registry = entity_registry.EntityRegistry(hass) + registry.entities = {} + hass.data[entity_component.DATA_REGISTRY] = registry + return registry + + class MockModule(object): """Representation of a fake module.""" diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 812dd399a48..42ce7bd7add 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -8,10 +8,14 @@ from mock_open import MockOpen from homeassistant.setup import async_setup_component +from tests.common import mock_registry + @asyncio.coroutine def test_loading_file(hass, test_client): """Test that it loads image from disk.""" + mock_registry(hass) + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ mock.patch('os.access', mock.Mock(return_value=True)): yield from async_setup_component(hass, 'camera', { diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 659bc4abe16..e1d1cdaadec 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -282,29 +282,11 @@ class TestSetup(unittest.TestCase): self.assertEqual(len(args), 1) self.assertEqual(len(kwargs), 0) - # one argument, a list of lights in bridge one; each of them is an - # object of type HueLight so we can't straight up compare them - lights = args[0] - self.assertEqual( - lights[0].unique_id, - '{}.b1l1.Light.1'.format(hue_light.HueLight)) - self.assertEqual( - lights[1].unique_id, - '{}.b1l2.Light.2'.format(hue_light.HueLight)) - # second call works the same name, args, kwargs = self.mock_add_devices.mock_calls[1] self.assertEqual(len(args), 1) self.assertEqual(len(kwargs), 0) - lights = args[0] - self.assertEqual( - lights[0].unique_id, - '{}.b2l1.Light.1'.format(hue_light.HueLight)) - self.assertEqual( - lights[1].unique_id, - '{}.b2l3.Light.3'.format(hue_light.HueLight)) - def test_process_lights_api_error(self): """Test the process_lights function when the bridge errors out.""" self.setup_mocks_for_process_lights() @@ -506,60 +488,16 @@ class TestHueLight(unittest.TestCase): def test_unique_id_for_light(self): """Test the unique_id method with lights.""" - class_name = "" - light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEqual( - class_name+'.foobar', - light.unique_id) + self.assertEqual('foobar', light.unique_id) light = self.buildLight(info={}) - self.assertEqual( - class_name+'.Unnamed Device.Light.42', - light.unique_id) - - light = self.buildLight(info={'name': 'my-name'}) - self.assertEqual( - class_name+'.my-name.Light.42', - light.unique_id) - - light = self.buildLight(info={'type': 'my-type'}) - self.assertEqual( - class_name+'.Unnamed Device.my-type.42', - light.unique_id) - - light = self.buildLight(info={'name': 'a name', 'type': 'my-type'}) - self.assertEqual( - class_name+'.a name.my-type.42', - light.unique_id) + self.assertIsNone(light.unique_id) def test_unique_id_for_group(self): """Test the unique_id method with groups.""" - class_name = "" - light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEqual( - class_name+'.foobar', - light.unique_id) + self.assertEqual('foobar', light.unique_id) light = self.buildLight(info={}, is_group=True) - self.assertEqual( - class_name+'.Unnamed Device.Group.42', - light.unique_id) - - light = self.buildLight(info={'name': 'my-name'}, is_group=True) - self.assertEqual( - class_name+'.my-name.Group.42', - light.unique_id) - - light = self.buildLight(info={'type': 'my-type'}, is_group=True) - self.assertEqual( - class_name+'.Unnamed Device.my-type.42', - light.unique_id) - - light = self.buildLight( - info={'name': 'a name', 'type': 'my-type'}, - is_group=True) - self.assertEqual( - class_name+'.a name.my-type.42', - light.unique_id) + self.assertIsNone(light.unique_id) diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py index 00e8f2ba525..aa048f7a62e 100644 --- a/tests/components/sensor/test_file.py +++ b/tests/components/sensor/test_file.py @@ -9,7 +9,7 @@ from mock_open import MockOpen from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_registry class TestFileSensor(unittest.TestCase): @@ -18,6 +18,7 @@ class TestFileSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + mock_registry(self.hass) def teardown_method(self, method): """Stop everything that was started.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 77de7d5d1dd..828385b9ded 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -198,7 +198,7 @@ def test_device_entity(hass, mock_openzwave): yield from hass.async_block_till_done() assert not device.should_poll - assert device.unique_id == "ZWAVE-10-11" + assert device.unique_id == "10-11" assert device.name == 'Mock Node Sensor' assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123 diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index f2416fc3a31..349766d025e 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, - mock_coro, async_fire_time_changed) + mock_coro, async_fire_time_changed, mock_registry) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -210,30 +210,6 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 1 == len(self.hass.states.entity_ids()) assert not ent.update.called - def test_not_adding_duplicate_entities(self): - """Test for not adding duplicate entities.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert 0 == len(self.hass.states.entity_ids()) - - component.add_entities([EntityTest(unique_id='not_very_unique')]) - - assert 1 == len(self.hass.states.entity_ids()) - - component.add_entities([EntityTest(unique_id='not_very_unique')]) - - assert 1 == len(self.hass.states.entity_ids()) - - def test_not_assigning_entity_id_if_prescribes_one(self): - """Test for not assigning an entity ID.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert 'hello.world' not in self.hass.states.entity_ids() - - component.add_entities([EntityTest(entity_id='hello.world')]) - - assert 'hello.world' in self.hass.states.entity_ids() - def test_extract_from_service_returns_all_if_no_entity_id(self): """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -684,3 +660,83 @@ def test_async_remove_with_platform(hass): assert len(hass.states.async_entity_ids()) == 1 yield from entity1.async_remove() assert len(hass.states.async_entity_ids()) == 0 + + +@asyncio.coroutine +def test_not_adding_duplicate_entities_with_unique_id(hass): + """Test for not adding duplicate entities.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + EntityTest(name='test1', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + yield from component.async_add_entities([ + EntityTest(name='test2', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + +@asyncio.coroutine +def test_using_prescribed_entity_id(hass): + """Test for using predefined entity ID.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + EntityTest(name='bla', entity_id='hello.world')]) + assert 'hello.world' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_with_unique_id(hass): + """Test for ammending predefined entity ID because currently exists.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + EntityTest(entity_id='test_domain.world')]) + yield from component.async_add_entities([ + EntityTest(entity_id='test_domain.world', unique_id='bla')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_which_is_registered(hass): + """Test not allowing predefined entity ID that already registered.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + # This entity_id will be rewritten + yield from component.async_add_entities([ + EntityTest(entity_id='test_domain.world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_name_which_conflict_with_registered(hass): + """Test not generating conflicting entity ID based on name.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + yield from component.async_add_entities([ + EntityTest(name='world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_entity_with_name_and_entity_id_getting_registered(hass): + """Ensure that entity ID is used for registration.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + EntityTest(unique_id='1234', name='bla', + entity_id='test_domain.world')]) + assert 'test_domain.world' in hass.states.async_entity_ids() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py new file mode 100644 index 00000000000..d19a3f3fe49 --- /dev/null +++ b/tests/helpers/test_entity_registry.py @@ -0,0 +1,135 @@ +"""Tests for the Entity Registry.""" +import asyncio +from unittest.mock import patch, mock_open + +import pytest + +from homeassistant.helpers import entity_registry + +from tests.common import mock_registry + + +@pytest.fixture +def registry(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@asyncio.coroutine +def test_get_or_create_returns_same_entry(registry): + """Make sure we do not duplicate entries.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + entry2 = registry.async_get_or_create('light', 'hue', '1234') + + assert len(registry.entities) == 1 + assert entry is entry2 + assert entry.entity_id == 'light.hue_1234' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id(registry): + """Test that suggested_object_id works.""" + entry = registry.async_get_or_create( + 'light', 'hue', '1234', suggested_object_id='beer') + + assert entry.entity_id == 'light.beer' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id_conflict_register(registry): + """Test that we don't generate an entity id that is already registered.""" + entry = registry.async_get_or_create( + 'light', 'hue', '1234', suggested_object_id='beer') + entry2 = registry.async_get_or_create( + 'light', 'hue', '5678', suggested_object_id='beer') + + assert entry.entity_id == 'light.beer' + assert entry2.entity_id == 'light.beer_2' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id_conflict_existing(hass, registry): + """Test that we don't generate an entity id that currently exists.""" + hass.states.async_set('light.hue_1234', 'on') + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234_2' + + +@asyncio.coroutine +def test_create_triggers_save(hass, registry): + """Test that registering entry triggers a save.""" + with patch.object(hass.loop, 'call_later') as mock_call_later: + registry.async_get_or_create('light', 'hue', '1234') + + assert len(mock_call_later.mock_calls) == 1 + + +@asyncio.coroutine +def test_save_timer_reset_on_subsequent_save(hass, registry): + """Test we reset the save timer on a new create.""" + with patch.object(hass.loop, 'call_later') as mock_call_later: + registry.async_get_or_create('light', 'hue', '1234') + + assert len(mock_call_later.mock_calls) == 1 + + with patch.object(hass.loop, 'call_later') as mock_call_later_2: + registry.async_get_or_create('light', 'hue', '5678') + + assert len(mock_call_later().cancel.mock_calls) == 1 + assert len(mock_call_later_2.mock_calls) == 1 + + +@asyncio.coroutine +def test_loading_saving_data(hass, registry): + """Test that we load/save data correctly.""" + yaml_path = 'homeassistant.util.yaml.open' + orig_entry1 = registry.async_get_or_create('light', 'hue', '1234') + orig_entry2 = registry.async_get_or_create('light', 'hue', '5678') + + assert len(registry.entities) == 2 + + with patch(yaml_path, mock_open(), create=True) as mock_write: + yield from registry._async_save() + + # Mock open calls are: open file, context enter, write, context leave + written = mock_write.mock_calls[2][1][0] + + # Now load written data in new registry + registry2 = entity_registry.EntityRegistry(hass) + + with patch('os.path.isfile', return_value=True), \ + patch(yaml_path, mock_open(read_data=written), create=True): + yield from registry2._async_load() + + # Ensure same order + assert list(registry.entities) == list(registry2.entities) + new_entry1 = registry.async_get_or_create('light', 'hue', '1234') + new_entry2 = registry.async_get_or_create('light', 'hue', '5678') + + assert orig_entry1 == new_entry1 + assert orig_entry2 == new_entry2 + + +@asyncio.coroutine +def test_generate_entity_considers_registered_entities(registry): + """Test that we don't create entity id that are already registered.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234' + assert registry.async_generate_entity_id('light', 'hue_1234') == \ + 'light.hue_1234_2' + + +@asyncio.coroutine +def test_generate_entity_considers_existing_entities(hass, registry): + """Test that we don't create entity id that currently exists.""" + hass.states.async_set('light.kitchen', 'on') + assert registry.async_generate_entity_id('light', 'kitchen') == \ + 'light.kitchen_2' + + +@asyncio.coroutine +def test_is_registered(registry): + """Test that is_registered works.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert registry.async_is_registered(entry.entity_id) + assert not registry.async_is_registered('light.non_existing') From 71cb4df817f1cd5e70e22900d35f92928a7a6fab Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Tue, 30 Jan 2018 03:20:20 -0800 Subject: [PATCH 056/166] Return all attributes that are not None in base lock entity class (#12049) * Return all attributes that are not None in base lock entity class * Update __init__.py --- homeassistant/components/lock/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 80abce4ec3e..d03bbebd696 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -41,6 +41,11 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ _LOGGER = logging.getLogger(__name__) +PROP_TO_ATTR = { + 'changed_by': ATTR_CHANGED_BY, + 'code_format': ATTR_CODE_FORMAT, +} + @bind_hass def is_locked(hass, entity_id=None): @@ -156,12 +161,11 @@ class LockDevice(Entity): @property def state_attributes(self): """Return the state attributes.""" - if self.code_format is None: - return None - state_attr = { - ATTR_CODE_FORMAT: self.code_format, - ATTR_CHANGED_BY: self.changed_by - } + state_attr = {} + for prop, attr in PROP_TO_ATTR.items(): + value = getattr(self, prop) + if value is not None: + state_attr[attr] = value return state_attr @property From ec1c395f091e788a6e97ec4e38158419bf2f09d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 30 Jan 2018 03:30:47 -0800 Subject: [PATCH 057/166] Extract requirements (#12051) --- homeassistant/const.py | 1 - homeassistant/requirements.py | 45 +++++++++++ homeassistant/scripts/__init__.py | 18 ++--- homeassistant/setup.py | 124 +++++++++++------------------- tests/test_requirements.py | 61 +++++++++++++++ tests/test_setup.py | 42 +--------- 6 files changed, 160 insertions(+), 131 deletions(-) create mode 100644 homeassistant/requirements.py create mode 100644 tests/test_requirements.py diff --git a/homeassistant/const.py b/homeassistant/const.py index a8449bf38c2..915ee5ac216 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,6 @@ __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) REQUIRED_PYTHON_VER_WIN = (3, 5, 2) -CONSTRAINT_FILE = 'package_constraints.txt' # Format for platforms PLATFORM_FORMAT = '{}.{}' diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py new file mode 100644 index 00000000000..aaf83870147 --- /dev/null +++ b/homeassistant/requirements.py @@ -0,0 +1,45 @@ +"""Module to handle installing requirements.""" +import asyncio +from functools import partial +import logging +import os + +import homeassistant.util.package as pkg_util + +DATA_PIP_LOCK = 'pip_lock' +CONSTRAINT_FILE = 'package_constraints.txt' +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_process_requirements(hass, name, requirements): + """Install the requirements for a component or platform. + + This method is a coroutine. + """ + pip_lock = hass.data.get(DATA_PIP_LOCK) + if pip_lock is None: + pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) + + pip_install = partial(pkg_util.install_package, + **pip_kwargs(hass.config.config_dir)) + + with (yield from pip_lock): + for req in requirements: + ret = yield from hass.async_add_job(pip_install, req) + if not ret: + _LOGGER.error("Not initializing %s because could not install " + "requirement %s", name, req) + return False + + return True + + +def pip_kwargs(config_dir): + """Return keyword arguments for PIP install.""" + kwargs = { + 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) + } + if not pkg_util.running_under_virtualenv(): + kwargs['target'] = os.path.join(config_dir, 'deps') + return kwargs diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index be39540682c..815a5c8e55f 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,9 +9,8 @@ from typing import List from homeassistant.bootstrap import mount_local_lib_path from homeassistant.config import get_default_config_dir -from homeassistant.const import CONSTRAINT_FILE -from homeassistant.util.package import ( - install_package, running_under_virtualenv) +from homeassistant import requirements +from homeassistant.util.package import install_package def run(args: List) -> int: @@ -39,17 +38,14 @@ def run(args: List) -> int: script = importlib.import_module('homeassistant.scripts.' + args[0]) config_dir = extract_config_dir() - deps_dir = mount_local_lib_path(config_dir) + mount_local_lib_path(config_dir) + pip_kwargs = requirements.pip_kwargs(config_dir) logging.basicConfig(stream=sys.stdout, level=logging.INFO) + for req in getattr(script, 'REQUIREMENTS', []): - if running_under_virtualenv(): - returncode = install_package(req, constraints=os.path.join( - os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE)) - else: - returncode = install_package( - req, target=deps_dir, constraints=os.path.join( - os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE)) + returncode = install_package(req, **pip_kwargs) + if not returncode: print('Aborting script, could not install dependency', req) return 1 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 12a39e80517..3221ea35d48 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -1,27 +1,24 @@ """All methods needed to bootstrap a Home Assistant instance.""" import asyncio import logging.handlers -import os from timeit import default_timer as timer from types import ModuleType from typing import Optional, Dict -import homeassistant.config as conf_util -import homeassistant.core as core -import homeassistant.loader as loader -import homeassistant.util.package as pkg_util +from homeassistant import requirements, core, loader, config as conf_util from homeassistant.config import async_notify_setup_error -from homeassistant.const import ( - EVENT_COMPONENT_LOADED, PLATFORM_FORMAT, CONSTRAINT_FILE) +from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async import run_coroutine_threadsafe + _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = 'component' DATA_SETUP = 'setup_tasks' -DATA_PIP_LOCK = 'pip_lock' +DATA_DEPS_REQS = 'deps_reqs_processed' SLOW_SETUP_WARNING = 10 @@ -60,43 +57,6 @@ def async_setup_component(hass: core.HomeAssistant, domain: str, return (yield from task) -@asyncio.coroutine -def _async_process_requirements(hass: core.HomeAssistant, name: str, - requirements) -> bool: - """Install the requirements for a component. - - This method is a coroutine. - """ - if hass.config.skip_pip: - return True - - pip_lock = hass.data.get(DATA_PIP_LOCK) - if pip_lock is None: - pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) - - def pip_install(mod): - """Install packages.""" - if pkg_util.running_under_virtualenv(): - return pkg_util.install_package( - mod, constraints=os.path.join( - os.path.dirname(__file__), CONSTRAINT_FILE)) - return pkg_util.install_package( - mod, target=hass.config.path('deps'), - constraints=os.path.join( - os.path.dirname(__file__), CONSTRAINT_FILE)) - - with (yield from pip_lock): - for req in requirements: - ret = yield from hass.async_add_job(pip_install, req) - if not ret: - _LOGGER.error("Not initializing %s because could not install " - "dependency %s", name, req) - async_notify_setup_error(hass, name) - return False - - return True - - @asyncio.coroutine def _async_process_dependencies(hass, config, name, dependencies): """Ensure all dependencies are set up.""" @@ -162,22 +122,11 @@ def _async_setup_component(hass: core.HomeAssistant, log_error("Invalid config.") return False - if not hass.config.skip_pip and hasattr(component, 'REQUIREMENTS'): - req_success = yield from _async_process_requirements( - hass, domain, component.REQUIREMENTS) - if not req_success: - log_error("Could not install all requirements.") - return False - - if hasattr(component, 'DEPENDENCIES'): - dep_success = yield from _async_process_dependencies( - hass, config, domain, component.DEPENDENCIES) - - if not dep_success: - log_error("Could not setup all dependencies.") - return False - - async_comp = hasattr(component, 'async_setup') + try: + yield from _process_deps_reqs(hass, config, domain, component) + except HomeAssistantError as err: + log_error(str(err)) + return False start = timer() _LOGGER.info("Setting up %s", domain) @@ -192,7 +141,7 @@ def _async_setup_component(hass: core.HomeAssistant, domain, SLOW_SETUP_WARNING) try: - if async_comp: + if hasattr(component, 'async_setup'): result = yield from component.async_setup(hass, processed_config) else: result = yield from hass.async_add_job( @@ -256,21 +205,40 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, elif platform_path in hass.config.components: return platform - # Load dependencies - if hasattr(platform, 'DEPENDENCIES'): - dep_success = yield from _async_process_dependencies( - hass, config, platform_path, platform.DEPENDENCIES) - - if not dep_success: - log_error("Could not setup all dependencies.") - return None - - if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'): - req_success = yield from _async_process_requirements( - hass, platform_path, platform.REQUIREMENTS) - - if not req_success: - log_error("Could not install all requirements.") - return None + try: + yield from _process_deps_reqs(hass, config, platform_name, platform) + except HomeAssistantError as err: + log_error(str(err)) + return None return platform + + +@asyncio.coroutine +def _process_deps_reqs(hass, config, name, module): + """Process all dependencies and requirements for a module. + + Module is a Python module of either a component or platform. + """ + processed = hass.data.get(DATA_DEPS_REQS) + + if processed is None: + processed = hass.data[DATA_DEPS_REQS] = set() + elif name in processed: + return + + if hasattr(module, 'DEPENDENCIES'): + dep_success = yield from _async_process_dependencies( + hass, config, name, module.DEPENDENCIES) + + if not dep_success: + raise HomeAssistantError("Could not setup all dependencies.") + + if not hass.config.skip_pip and hasattr(module, 'REQUIREMENTS'): + req_success = yield from requirements.async_process_requirements( + hass, name, module.REQUIREMENTS) + + if not req_success: + raise HomeAssistantError("Could not install all requirements.") + + processed.add(name) diff --git a/tests/test_requirements.py b/tests/test_requirements.py new file mode 100644 index 00000000000..946e64af847 --- /dev/null +++ b/tests/test_requirements.py @@ -0,0 +1,61 @@ +"""Test requirements module.""" +import os +from unittest import mock + +from homeassistant import loader, setup +from homeassistant.requirements import CONSTRAINT_FILE + +from tests.common import get_test_home_assistant, MockModule + + +class TestRequirements: + """Test the requirements module.""" + + hass = None + backup_cache = None + + # pylint: disable=invalid-name, no-self-use + def setup_method(self, method): + """Setup the test.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Clean up.""" + self.hass.stop() + + @mock.patch('os.path.dirname') + @mock.patch('homeassistant.util.package.running_under_virtualenv', + return_value=True) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_venv( + self, mock_install, mock_venv, mock_dirname): + """Test requirement installed in virtual environment.""" + mock_venv.return_value = True + mock_dirname.return_value = 'ha_package_path' + self.hass.config.skip_pip = False + loader.set_component( + 'comp', MockModule('comp', requirements=['package==0.0.1'])) + assert setup.setup_component(self.hass, 'comp') + assert 'comp' in self.hass.config.components + assert mock_install.call_args == mock.call( + 'package==0.0.1', + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + + @mock.patch('os.path.dirname') + @mock.patch('homeassistant.util.package.running_under_virtualenv', + return_value=False) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_deps( + self, mock_install, mock_venv, mock_dirname): + """Test requirement installed in deps directory.""" + mock_dirname.return_value = 'ha_package_path' + self.hass.config.skip_pip = False + loader.set_component( + 'comp', MockModule('comp', requirements=['package==0.0.1'])) + assert setup.setup_component(self.hass, 'comp') + assert 'comp' in self.hass.config.components + assert mock_install.call_args == mock.call( + 'package==0.0.1', target=self.hass.config.path('deps'), + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) diff --git a/tests/test_setup.py b/tests/test_setup.py index afea30ddcd1..6a94310793c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONSTRAINT_FILE +from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util @@ -41,9 +41,6 @@ class TestSetup: """Clean up.""" self.hass.stop() - # if os.path.isfile(VERSION_PATH): - # os.remove(VERSION_PATH) - def test_validate_component_config(self): """Test validating component configuration.""" config_schema = vol.Schema({ @@ -203,43 +200,6 @@ class TestSetup: assert not setup.setup_component(self.hass, 'comp') assert 'comp' not in self.hass.config.components - @mock.patch('homeassistant.setup.os.path.dirname') - @mock.patch('homeassistant.util.package.running_under_virtualenv', - return_value=True) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) - def test_requirement_installed_in_venv( - self, mock_install, mock_venv, mock_dirname): - """Test requirement installed in virtual environment.""" - mock_venv.return_value = True - mock_dirname.return_value = 'ha_package_path' - self.hass.config.skip_pip = False - loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) - assert setup.setup_component(self.hass, 'comp') - assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( - 'package==0.0.1', - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - - @mock.patch('homeassistant.setup.os.path.dirname') - @mock.patch('homeassistant.util.package.running_under_virtualenv', - return_value=False) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) - def test_requirement_installed_in_deps( - self, mock_install, mock_venv, mock_dirname): - """Test requirement installed in deps directory.""" - mock_dirname.return_value = 'ha_package_path' - self.hass.config.skip_pip = False - loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) - assert setup.setup_component(self.hass, 'comp') - assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( - 'package==0.0.1', target=self.hass.config.path('deps'), - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - def test_component_not_setup_twice_if_loaded_during_other_setup(self): """Test component setup while waiting for lock is not setup twice.""" result = [] From d7017f21381fb0984ad2be93792e3dcefcf63fe8 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 30 Jan 2018 12:41:33 +0100 Subject: [PATCH 058/166] Prepare for recorder purge to be active by default (#11976) --- homeassistant/components/recorder/__init__.py | 11 +++++++++-- homeassistant/components/recorder/purge.py | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 1adce50b1aa..db208dada4f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -76,9 +76,9 @@ FILTER_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: FILTER_SCHEMA.extend({ - vol.Inclusive(CONF_PURGE_KEEP_DAYS, 'purge'): + vol.Optional(CONF_PURGE_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Inclusive(CONF_PURGE_INTERVAL, 'purge'): + vol.Optional(CONF_PURGE_INTERVAL, default=1): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_DB_URL): cv.string, }) @@ -122,6 +122,12 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_days = conf.get(CONF_PURGE_KEEP_DAYS) purge_interval = conf.get(CONF_PURGE_INTERVAL) + if keep_days is None: + _LOGGER.warning( + "From version 0.64.0 the 'recorder' component will by default " + "purge data older than 10 days. To keep data longer you must " + "configure a 'purge_keep_days' value.") + db_url = conf.get(CONF_DB_URL, None) if not db_url: db_url = DEFAULT_URL.format( @@ -162,6 +168,7 @@ class Recorder(threading.Thread): self.hass = hass self.keep_days = keep_days self.purge_interval = purge_interval + self.did_vacuum = False self.queue = queue.Queue() # type: Any self.recording_start = dt_util.utcnow() self.db_url = uri diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index fad6a7de70d..06bd81c2309 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -55,11 +55,12 @@ def purge_old_data(instance, purge_days): # Execute sqlite vacuum command to free up space on disk _LOGGER.debug("DB engine driver: %s", instance.engine.driver) - if instance.engine.driver == 'pysqlite': + if instance.engine.driver == 'pysqlite' and not instance.did_vacuum: from sqlalchemy import exc _LOGGER.info("Vacuuming SQLite to free space") try: instance.engine.execute("VACUUM") + instance.did_vacuum = True except exc.OperationalError as err: _LOGGER.error("Error vacuuming SQLite: %s.", err) From 12182d6e49b04bf75fae0dc8ba33931694338f4a Mon Sep 17 00:00:00 2001 From: Alex Osadchyy <21959540+aosadchyy@users.noreply.github.com> Date: Tue, 30 Jan 2018 05:13:30 -0800 Subject: [PATCH 059/166] Bumped up pymochad requirement to 0.2.0 as a fix for #11928 (#12014) * Bumped up pymochad requirement to 0.2.0 as a fix for #11928 * requirements_all.txt updated to match pymochad requirements --- homeassistant/components/mochad.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad.py index 3cc4eda7675..9f53f84e020 100644 --- a/homeassistant/components/mochad.py +++ b/homeassistant/components/mochad.py @@ -14,7 +14,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.const import (CONF_HOST, CONF_PORT) -REQUIREMENTS = ['pymochad==0.1.1'] +REQUIREMENTS = ['pymochad==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 93fa176bb63..cf603ae1436 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,7 +784,7 @@ pylutron==0.1.0 pymailgunner==1.4 # homeassistant.components.mochad -pymochad==0.1.1 +pymochad==0.2.0 # homeassistant.components.modbus pymodbus==1.3.1 From dfd2d631aeb03f2c603f59a8d4884cac0661f406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 30 Jan 2018 17:25:58 +0100 Subject: [PATCH 060/166] Publish errors on the event bus (#11964) * Publish errors on the event bus * Add block till done to test. * Update test_system_log.py * Remove old logger handlers --- .../components/system_log/__init__.py | 157 ++++++++++-------- tests/components/test_system_log.py | 32 +++- 2 files changed, 111 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 5c8fe3109a6..7d9ebe85130 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -16,6 +16,7 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP CONF_MAX_ENTRIES = 'max_entries' CONF_MESSAGE = 'message' @@ -27,6 +28,8 @@ DEFAULT_MAX_ENTRIES = 50 DEPENDENCIES = ['http'] DOMAIN = 'system_log' +EVENT_SYSTEM_LOG = 'system_log_event' + SERVICE_CLEAR = 'clear' SERVICE_WRITE = 'write' @@ -46,67 +49,6 @@ SERVICE_WRITE_SCHEMA = vol.Schema({ }) -class LogErrorHandler(logging.Handler): - """Log handler for error messages.""" - - def __init__(self, maxlen): - """Initialize a new LogErrorHandler.""" - super().__init__() - self.records = deque(maxlen=maxlen) - - def emit(self, record): - """Save error and warning logs. - - Everything logged with error or warning is saved in local buffer. A - default upper limit is set to 50 (older entries are discarded) but can - be changed if needed. - """ - if record.levelno >= logging.WARN: - stack = [] - if not record.exc_info: - try: - stack = [f for f, _, _, _ in traceback.extract_stack()] - except ValueError: - # On Python 3.4 under py.test getting the stack might fail. - pass - self.records.appendleft([record, stack]) - - -@asyncio.coroutine -def async_setup(hass, config): - """Set up the logger component.""" - conf = config.get(DOMAIN) - - if conf is None: - conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - - handler = LogErrorHandler(conf.get(CONF_MAX_ENTRIES)) - logging.getLogger().addHandler(handler) - - hass.http.register_view(AllErrorsView(handler)) - - @asyncio.coroutine - def async_service_handler(service): - """Handle logger services.""" - if service.service == 'clear': - handler.records.clear() - return - if service.service == 'write': - logger = logging.getLogger( - service.data.get(CONF_LOGGER, '{}.external'.format(__name__))) - level = service.data[CONF_LEVEL] - getattr(logger, level)(service.data[CONF_MESSAGE]) - - hass.services.async_register( - DOMAIN, SERVICE_CLEAR, async_service_handler, - schema=SERVICE_CLEAR_SCHEMA) - hass.services.async_register( - DOMAIN, SERVICE_WRITE, async_service_handler, - schema=SERVICE_WRITE_SCHEMA) - - return True - - def _figure_out_source(record, call_stack, hass): paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] try: @@ -151,14 +93,86 @@ def _exception_as_string(exc_info): return buf.getvalue() -def _convert(record, call_stack, hass): - return { - 'timestamp': record.created, - 'level': record.levelname, - 'message': record.getMessage(), - 'exception': _exception_as_string(record.exc_info), - 'source': _figure_out_source(record, call_stack, hass), - } +class LogErrorHandler(logging.Handler): + """Log handler for error messages.""" + + def __init__(self, hass, maxlen): + """Initialize a new LogErrorHandler.""" + super().__init__() + self.hass = hass + self.records = deque(maxlen=maxlen) + + def _create_entry(self, record, call_stack): + return { + 'timestamp': record.created, + 'level': record.levelname, + 'message': record.getMessage(), + 'exception': _exception_as_string(record.exc_info), + 'source': _figure_out_source(record, call_stack, self.hass), + } + + def emit(self, record): + """Save error and warning logs. + + Everything logged with error or warning is saved in local buffer. A + default upper limit is set to 50 (older entries are discarded) but can + be changed if needed. + """ + if record.levelno >= logging.WARN: + stack = [] + if not record.exc_info: + try: + stack = [f for f, _, _, _ in traceback.extract_stack()] + except ValueError: + # On Python 3.4 under py.test getting the stack might fail. + pass + + entry = self._create_entry(record, stack) + self.records.appendleft(entry) + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the logger component.""" + conf = config.get(DOMAIN) + if conf is None: + conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + + handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) + logging.getLogger().addHandler(handler) + + hass.http.register_view(AllErrorsView(handler)) + + @asyncio.coroutine + def async_service_handler(service): + """Handle logger services.""" + if service.service == 'clear': + handler.records.clear() + return + if service.service == 'write': + logger = logging.getLogger( + service.data.get(CONF_LOGGER, '{}.external'.format(__name__))) + level = service.data[CONF_LEVEL] + getattr(logger, level)(service.data[CONF_MESSAGE]) + + @asyncio.coroutine + def async_shutdown_handler(event): + """Remove logging handler when Home Assistant is shutdown.""" + # This is needed as older logger instances will remain + logging.getLogger().removeHandler(handler) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + async_shutdown_handler) + + hass.services.async_register( + DOMAIN, SERVICE_CLEAR, async_service_handler, + schema=SERVICE_CLEAR_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_WRITE, async_service_handler, + schema=SERVICE_WRITE_SCHEMA) + + return True class AllErrorsView(HomeAssistantView): @@ -174,5 +188,6 @@ class AllErrorsView(HomeAssistantView): @asyncio.coroutine def get(self, request): """Get all errors and warnings.""" - return self.json([_convert(x[0], x[1], request.app['hass']) - for x in self.handler.records]) + # deque is not serializable (it's just "list-like") so it must be + # converted to a list before it can be serialized to json + return self.json(list(self.handler.records)) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index 6ad68f2274a..d119c60dba2 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch import pytest +from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log @@ -13,7 +14,7 @@ _LOGGER = logging.getLogger('test_logger') @pytest.fixture(autouse=True) @asyncio.coroutine -def setup_test_case(hass): +def setup_test_case(hass, test_client): """Setup system_log component before test case.""" config = {'system_log': {'max_entries': 2}} yield from async_setup_component(hass, system_log.DOMAIN, config) @@ -85,6 +86,25 @@ def test_error(hass, test_client): assert_log(log, '', 'error message', 'ERROR') +@asyncio.coroutine +def test_error_posted_as_event(hass, test_client): + """Test that error are posted as events.""" + events = [] + + @callback + def event_listener(event): + """Listen to events of type system_log_event.""" + events.append(event) + + hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) + + _LOGGER.error('error message') + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert_log(events[0].data, '', 'error message', 'ERROR') + + @asyncio.coroutine def test_critical(hass, test_client): """Test that critical are logged and retrieved correctly.""" @@ -189,10 +209,10 @@ def log_error_from_test_path(path): @asyncio.coroutine def test_homeassistant_path(hass, test_client): """Test error logged from homeassistant path.""" - log_error_from_test_path('venv_path/homeassistant/component/component.py') - with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): + log_error_from_test_path( + 'venv_path/homeassistant/component/component.py') log = (yield from get_error_log(hass, test_client, 1))[0] assert log['source'] == 'component/component.py' @@ -200,9 +220,8 @@ def test_homeassistant_path(hass, test_client): @asyncio.coroutine def test_config_path(hass, test_client): """Test error logged from config path.""" - log_error_from_test_path('config/custom_component/test.py') - with patch.object(hass.config, 'config_dir', new='config'): + log_error_from_test_path('config/custom_component/test.py') log = (yield from get_error_log(hass, test_client, 1))[0] assert log['source'] == 'custom_component/test.py' @@ -210,9 +229,8 @@ def test_config_path(hass, test_client): @asyncio.coroutine def test_netdisco_path(hass, test_client): """Test error logged from netdisco path.""" - log_error_from_test_path('venv_path/netdisco/disco_component.py') - with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): + log_error_from_test_path('venv_path/netdisco/disco_component.py') log = (yield from get_error_log(hass, test_client, 1))[0] assert log['source'] == 'disco_component.py' From 990fbdf3ca620b284a924a270921176804814481 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 30 Jan 2018 23:40:44 +0100 Subject: [PATCH 061/166] Unique ID for LIFX lights (#12064) --- homeassistant/components/light/lifx.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 090341e4255..71a261e3806 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -397,6 +397,11 @@ class LIFXLight(Light): """Return the availability of the device.""" return self.registered + @property + def unique_id(self): + """Return a unique ID.""" + return self.device.mac_addr + @property def name(self): """Return the name of the device.""" From 37034a7450f94a16e05283b4799c3774723b0586 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Tue, 30 Jan 2018 23:42:24 +0100 Subject: [PATCH 062/166] Deconz use entity registry (#12067) * Support for entity registry * Not everything is a sensor... --- homeassistant/components/binary_sensor/deconz.py | 5 +++++ homeassistant/components/deconz/__init__.py | 4 ++-- homeassistant/components/light/deconz.py | 5 +++++ homeassistant/components/sensor/deconz.py | 10 ++++++++++ requirements_all.txt | 2 +- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 3c02dfb3508..0d7c3e086bb 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -65,6 +65,11 @@ class DeconzBinarySensor(BinarySensorDevice): """Return the name of the sensor.""" return self._sensor.name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return self._sensor.uniqueid + @property def device_class(self): """Return the class of the sensor.""" diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 269b8136020..9d7d253c328 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==25'] +REQUIREMENTS = ['pydeconz==27'] _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ SERVICE_DATA = 'data' SERVICE_SCHEMA = vol.Schema({ vol.Required(SERVICE_FIELD): cv.string, - vol.Required(SERVICE_DATA): cv.string, + vol.Required(SERVICE_DATA): dict, }) CONFIG_INSTRUCTIONS = """ diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 6b22190dce9..82df352e5af 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -100,6 +100,11 @@ class DeconzLight(Light): """Return the name of the light.""" return self._light.name + @property + def unique_id(self): + """Return a unique identifier for this light.""" + return self._light.uniqueid + @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 7c2c1e0895f..b3adaa412ff 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -74,6 +74,11 @@ class DeconzSensor(Entity): """Return the name of the sensor.""" return self._sensor.name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return self._sensor.uniqueid + @property def device_class(self): """Return the class of the sensor.""" @@ -139,6 +144,11 @@ class DeconzBattery(Entity): """Return the name of the battery.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for the device.""" + return self._device.uniqueid + @property def device_class(self): """Return the class of the sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index cf603ae1436..e91cbaf381f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -687,7 +687,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==25 +pydeconz==27 # homeassistant.components.zwave pydispatcher==2.0.5 From cab6c694c54435c6f4f6734d6eb53997a1d4d238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 31 Jan 2018 00:44:05 +0200 Subject: [PATCH 063/166] Flake8 bugbear fixes (#12072) * Don't use mutable argument defaults (bugbear B006) * Use callable(x) instead of hasattr(x, '__call__') (bugbear B004) * Remove/mark unused loop control variables (bugbear B007) * Fix stripping protocol from kodi host name (bugbear B005) * Fix plant daily history add default date (bugbear B008) --- homeassistant/components/climate/heatmiser.py | 2 +- homeassistant/components/ios.py | 2 +- homeassistant/components/media_player/plex.py | 2 +- homeassistant/components/notify/kodi.py | 2 +- homeassistant/components/plant.py | 4 ++-- homeassistant/components/rfxtrx.py | 2 +- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/scripts/check_config.py | 2 +- homeassistant/util/location.py | 2 +- tests/components/alexa/test_intent.py | 5 +++-- tests/components/light/test_hue.py | 2 +- tests/components/test_canary.py | 4 ++-- tests/components/test_influxdb.py | 2 +- tests/test_core.py | 8 ++++---- tests/test_util/aiohttp.py | 4 ++-- 15 files changed, 23 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index b05c880cc37..19c033a319f 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): serport = connection.connection(ipaddress, port) serport.open() - for thermostat, tstat in tstats.items(): + for tstat in tstats.values(): add_devices([ HeatmiserV3Thermostat( heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport) diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index 5e2528d2f0d..fe3c934659b 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -182,7 +182,7 @@ def enabled_push_ids(): """Return a list of push enabled target push IDs.""" push_ids = list() # pylint: disable=unused-variable - for device_name, device in CONFIG_FILE[ATTR_DEVICES].items(): + for device in CONFIG_FILE[ATTR_DEVICES].values(): if device.get(ATTR_PUSH_ID) is not None: push_ids.append(device.get(ATTR_PUSH_ID)) return push_ids diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 38a84053263..b2a89341cf0 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -175,7 +175,7 @@ def setup_plexserver( else: plex_clients[machine_identifier].refresh(None, session) - for machine_identifier, client in plex_clients.items(): + for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: client.force_idle() diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 05f4c5d17f3..3eb492f7fa6 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -51,7 +51,7 @@ def async_get_service(hass, config, discovery_info=None): encryption = config.get(CONF_PROXY_SSL) if host.startswith('http://') or host.startswith('https://'): - host = host.lstrip('http://').lstrip('https://') + host = host[host.index('://') + 3:] _LOGGER.warning( "Kodi host name should no longer contain http:// See updated " "definitions here: " diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 7df990fa0e5..50bb7c43c72 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -336,9 +336,9 @@ class DailyHistory(object): self._max_dict = dict() self.max = None - def add_measurement(self, value, timestamp=datetime.now()): + def add_measurement(self, value, timestamp=None): """Add a new measurement for a certain day.""" - day = timestamp.date() + day = (timestamp or datetime.now()).date() if value is None: return if self._days is None: diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 27bfd1abfbe..de8a0c00d80 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -171,7 +171,7 @@ def get_pt2262_cmd(device_id, data_bits): # pylint: disable=unused-variable def get_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" - for dev_id, device in RFX_DEVICES.items(): + for device in RFX_DEVICES.values(): if (hasattr(device, 'is_lighting4') and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits)): diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 10942de8097..7b8f471850b 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -170,7 +170,7 @@ def _obj_to_dict(obj): """Convert an object into a hash for debug.""" return {key: getattr(obj, key) for key in dir(obj) - if key[0] != '_' and not hasattr(getattr(obj, key), '__call__')} + if key[0] != '_' and not callable(getattr(obj, key))} def _value_name(value): diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index f80eafd72cc..5cfcf628ec5 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -134,7 +134,7 @@ def run(script_args: List) -> int: for sfn, sdict in res['secret_cache'].items(): sss = [] - for skey, sval in sdict.items(): + for skey in sdict: if skey in flatsecret: _LOGGER.error('Duplicated secrets in files %s and %s', flatsecret[skey], sfn) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 8b07a344148..35b266cb104 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -107,7 +107,7 @@ def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], sinU2 = math.sin(U2) cosU2 = math.cos(U2) - for iteration in range(MAX_ITERATIONS): + for _ in range(MAX_ITERATIONS): sinLambda = math.sin(Lambda) cosLambda = math.cos(Lambda) sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 + diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index a3587622b3d..2c8fafde155 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -98,8 +98,9 @@ def alexa_client(loop, hass, test_client): return loop.run_until_complete(test_client(hass.http.app)) -def _intent_req(client, data={}): - return client.post(intent.INTENTS_API_ENDPOINT, data=json.dumps(data), +def _intent_req(client, data=None): + return client.post(intent.INTENTS_API_ENDPOINT, + data=json.dumps(data or {}), headers={'content-type': 'application/json'}) diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index e1d1cdaadec..5c28ea9988f 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -68,7 +68,7 @@ class TestSetup(unittest.TestCase): """Return a dict suitable for mocking api.get('lights').""" mock_bridge_lights = lights - for light_id, info in mock_bridge_lights.items(): + for info in mock_bridge_lights.values(): if 'state' not in info: info['state'] = {'on': False} diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py index 67122813fb7..2c496c26e11 100644 --- a/tests/components/test_canary.py +++ b/tests/components/test_canary.py @@ -17,12 +17,12 @@ def mock_device(device_id, name, is_online=True): return device -def mock_location(name, is_celsius=True, devices=[]): +def mock_location(name, is_celsius=True, devices=None): """Mock Canary Location class.""" location = MagicMock() type(location).name = PropertyMock(return_value=name) type(location).is_celsius = PropertyMock(return_value=is_celsius) - type(location).devices = PropertyMock(return_value=devices) + type(location).devices = PropertyMock(return_value=devices or []) return location diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index d768136592e..0ec5f973ee4 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -733,7 +733,7 @@ class TestRetryOnErrorDecorator(unittest.TestCase): self.assertEqual(mock_method.call_count, 2) mock_method.assert_called_with(1, 2, test=3) - for cnt in range(3): + for _ in range(3): start = dt_util.utcnow() shifted_time = start + (timedelta(seconds=20 + 1)) self.hass.bus.fire(ha.EVENT_TIME_CHANGED, diff --git a/tests/test_core.py b/tests/test_core.py index 90a72a48a10..77a7872526f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -135,7 +135,7 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - for i in range(3): + for _ in range(3): self.hass.add_job(test_coro()) run_coroutine_threadsafe( @@ -155,7 +155,7 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - for i in range(2): + for _ in range(2): self.hass.add_job(test_coro()) @asyncio.coroutine @@ -185,7 +185,7 @@ class TestHomeAssistant(unittest.TestCase): yield from asyncio.sleep(0, loop=self.hass.loop) yield from asyncio.sleep(0, loop=self.hass.loop) - for i in range(2): + for _ in range(2): self.hass.add_job(test_executor) run_coroutine_threadsafe( @@ -210,7 +210,7 @@ class TestHomeAssistant(unittest.TestCase): yield from asyncio.sleep(0, loop=self.hass.loop) yield from asyncio.sleep(0, loop=self.hass.loop) - for i in range(2): + for _ in range(2): self.hass.add_job(test_callback) run_coroutine_threadsafe( diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d11a71d541f..d7033775a14 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -97,7 +97,7 @@ class AiohttpClientMockResponse: """Mock Aiohttp client response.""" def __init__(self, method, url, status, response, cookies=None, exc=None, - headers={}): + headers=None): """Initialize a fake response.""" self.method = method self._url = url @@ -107,7 +107,7 @@ class AiohttpClientMockResponse: self.response = response self.exc = exc - self._headers = headers + self._headers = headers or {} self._cookies = {} if cookies: From 3e41422caaacdbd7bb0396437effc4cf1991bfb4 Mon Sep 17 00:00:00 2001 From: Marius Date: Wed, 31 Jan 2018 00:59:43 +0200 Subject: [PATCH 064/166] Fix demo platform support (#12070) * Fixing demo platform to use support_flags * Fixed tests as well * Moved humidity low / high as always available based on defaults * Updated demo platform to show more combinations --- homeassistant/components/climate/demo.py | 50 ++++++++++++++++++------ tests/components/climate/test_demo.py | 4 +- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 357b1d56200..102155babea 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -14,23 +14,19 @@ from homeassistant.components.climate import ( SUPPORT_ON_OFF) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | - SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | - SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT | - SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF) +SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo climate devices.""" add_devices([ DemoClimate('HeatPump', 68, TEMP_FAHRENHEIT, None, None, 77, - 'Auto Low', None, None, 'Auto', 'heat', None, None, None), + None, None, None, None, 'heat', None, None, + None, True), DemoClimate('Hvac', 21, TEMP_CELSIUS, True, None, 22, 'On High', - 67, 54, 'Off', 'cool', False, None, None), - DemoClimate('Ecobee', None, TEMP_CELSIUS, None, None, 23, 'Auto Low', - None, None, 'Auto', 'auto', None, 24, 21) + 67, 54, 'Off', 'cool', False, None, None, None), + DemoClimate('Ecobee', None, TEMP_CELSIUS, None, 'home', 23, 'Auto Low', + None, None, 'Auto', 'auto', None, 24, 21, None) ]) @@ -40,9 +36,37 @@ class DemoClimate(ClimateDevice): def __init__(self, name, target_temperature, unit_of_measurement, away, hold, current_temperature, current_fan_mode, target_humidity, current_humidity, current_swing_mode, - current_operation, aux, target_temp_high, target_temp_low): + current_operation, aux, target_temp_high, target_temp_low, + is_on): """Initialize the climate device.""" self._name = name + self._support_flags = SUPPORT_FLAGS + if target_temperature is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE + if away is not None: + self._support_flags = self._support_flags | SUPPORT_AWAY_MODE + if hold is not None: + self._support_flags = self._support_flags | SUPPORT_HOLD_MODE + if current_fan_mode is not None: + self._support_flags = self._support_flags | SUPPORT_FAN_MODE + if target_humidity is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_HUMIDITY + if current_swing_mode is not None: + self._support_flags = self._support_flags | SUPPORT_SWING_MODE + if current_operation is not None: + self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE + if aux is not None: + self._support_flags = self._support_flags | SUPPORT_AUX_HEAT + if target_temp_high is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE_HIGH + if target_temp_low is not None: + self._support_flags = \ + self._support_flags | SUPPORT_TARGET_TEMPERATURE_LOW + if is_on is not None: + self._support_flags = self._support_flags | SUPPORT_ON_OFF self._target_temperature = target_temperature self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement @@ -59,12 +83,12 @@ class DemoClimate(ClimateDevice): self._swing_list = ['Auto', '1', '2', '3', 'Off'] self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low - self._on = True + self._on = is_on @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._support_flags @property def should_poll(self): diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 9098494bf48..b2633a75583 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -224,10 +224,10 @@ class TestDemoClimate(unittest.TestCase): def test_set_hold_mode_none(self): """Test setting the hold mode off/false.""" - climate.set_hold_mode(self.hass, None, ENTITY_ECOBEE) + climate.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) - self.assertEqual(None, state.attributes.get('hold_mode')) + self.assertEqual('off', state.attributes.get('hold_mode')) def test_set_aux_heat_bad_attr(self): """Test setting the auxiliary heater without required attribute.""" From ebfb380449c8c0fe52cb10f2f418a88e61c755cf Mon Sep 17 00:00:00 2001 From: "Craig J. Ward" Date: Tue, 30 Jan 2018 20:46:47 -0600 Subject: [PATCH 065/166] fix event channel name (#12077) --- homeassistant/components/goalfeed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py index d31a0b5b51a..5f9985fd38d 100644 --- a/homeassistant/components/goalfeed.py +++ b/homeassistant/components/goalfeed.py @@ -51,7 +51,7 @@ def setup(hass, config): timeout=30).json() channel = pusher.subscribe('private-goals', resp['auth']) - channel.bind('goalfeed_goal', goal_handler) + channel.bind('goal', goal_handler) pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST, timeout=30) From 4cb1f9301983975c49c3f5d1b04e35f4bfb5f9fe Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 31 Jan 2018 11:26:35 +0100 Subject: [PATCH 066/166] fixed timestamp problem in firefox (#12073) --- homeassistant/components/weather/openweathermap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 479831d713e..408b6968588 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -138,7 +138,7 @@ class OpenWeatherMapWeather(WeatherEntity): def forecast(self): """Return the forecast array.""" return [{ - ATTR_FORECAST_TIME: entry.get_reference_time('iso'), + ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp')} for entry in self.forecast_data.get_weathers()] From 0376cc0917e0576b44509b37194f5081df0b3ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 31 Jan 2018 12:30:48 +0200 Subject: [PATCH 067/166] Handle more file closing using context manager (#11942) --- homeassistant/__main__.py | 6 ++++-- homeassistant/components/light/greenwave.py | 10 ++++------ homeassistant/components/notify/gntp.py | 3 ++- homeassistant/components/sensor/onewire.py | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b7301e13bea..1cf6ecf7b98 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -182,7 +182,8 @@ def check_pid(pid_file: str) -> None: """Check that Home Assistant is not already running.""" # Check pid file try: - pid = int(open(pid_file, 'r').readline()) + with open(pid_file, 'r') as file: + pid = int(file.readline()) except IOError: # PID File does not exist return @@ -204,7 +205,8 @@ def write_pid(pid_file: str) -> None: """Create a PID File.""" pid = os.getpid() try: - open(pid_file, 'w').write(str(pid)) + with open(pid_file, 'w') as file: + file.write(str(pid)) except IOError: print('Fatal Error: Unable to write pid file {}'.format(pid_file)) sys.exit(1) diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py index 5ad7fd4c317..8e9d93657ce 100644 --- a/homeassistant/components/light/greenwave.py +++ b/homeassistant/components/light/greenwave.py @@ -38,18 +38,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): tokenfile = hass.config.path('.greenwave') if config.get(CONF_VERSION) == 3: if os.path.exists(tokenfile): - tokenfile = open(tokenfile) - token = tokenfile.read() - tokenfile.close() + with open(tokenfile) as tokenfile: + token = tokenfile.read() else: try: token = greenwave.grab_token(host, 'hass', 'homeassistant') except PermissionError: _LOGGER.error('The Gateway Is Not In Sync Mode') raise - tokenfile = open(tokenfile, "w+") - tokenfile.write(token) - tokenfile.close() + with open(tokenfile, "w+") as tokenfile: + tokenfile.write(token) else: token = None bulbs = greenwave.grab_bulbs(host, token) diff --git a/homeassistant/components/notify/gntp.py b/homeassistant/components/notify/gntp.py index b7e5b1b813a..1a2b65f958f 100644 --- a/homeassistant/components/notify/gntp.py +++ b/homeassistant/components/notify/gntp.py @@ -44,7 +44,8 @@ def get_service(hass, config, discovery_info=None): if config.get(CONF_APP_ICON) is None: icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend", "www_static", "icons", "favicon-192x192.png") - app_icon = open(icon_file, 'rb').read() + with open(icon_file, 'rb') as file: + app_icon = file.read() else: app_icon = config.get(CONF_APP_ICON) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 1f58eb4c13e..8a07d3484d5 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -66,8 +66,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device_file, 'temperature')) else: for family_file_path in glob(os.path.join(base_dir, '*', 'family')): - family_file = open(family_file_path, "r") - family = family_file.read() + with open(family_file_path, "r") as family_file: + family = family_file.read() if family in DEVICE_SENSORS: for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): sensor_id = os.path.split( From 434d2afbfc9315d3d269f0fa0d1c1412c5e9f4e5 Mon Sep 17 00:00:00 2001 From: Taylor Peet Date: Wed, 31 Jan 2018 05:39:15 -0500 Subject: [PATCH 068/166] Influx import improvements (#11988) * Influx import improvements * fix line length issues * fixing pylint spaces * Added refined except clause * Fix progress bar and exclude issues * fix travis lint too many blank lines * Minor changes --- homeassistant/scripts/influxdb_import.py | 202 +++++++++++++++-------- 1 file changed, 131 insertions(+), 71 deletions(-) diff --git a/homeassistant/scripts/influxdb_import.py b/homeassistant/scripts/influxdb_import.py index c21ac4adad9..e91aeb8a0d7 100644 --- a/homeassistant/scripts/influxdb_import.py +++ b/homeassistant/scripts/influxdb_import.py @@ -1,7 +1,8 @@ -"""Script to import recorded data into influxdb.""" +"""Script to import recorded data into an Influx database.""" import argparse import json import os +import sys from typing import List @@ -11,11 +12,13 @@ import homeassistant.config as config_util def run(script_args: List) -> int: """Run the actual script.""" from sqlalchemy import create_engine + from sqlalchemy import func from sqlalchemy.orm import sessionmaker from influxdb import InfluxDBClient from homeassistant.components.recorder import models from homeassistant.helpers import state as state_helper from homeassistant.core import State + from homeassistant.core import HomeAssistantError parser = argparse.ArgumentParser( description="import data to influxDB.") @@ -99,8 +102,8 @@ def run(script_args: List) -> int: client = None if not simulate: - client = InfluxDBClient(args.host, args.port, - args.username, args.password) + client = InfluxDBClient( + args.host, args.port, args.username, args.password) client.switch_database(args.dbname) config_dir = os.path.join(os.getcwd(), args.config) # type: str @@ -116,105 +119,162 @@ def run(script_args: List) -> int: if not os.path.exists(src_db) and not args.uri: print("Fatal Error: Database '{}' does not exist " - "and no uri given".format(src_db)) + "and no URI given".format(src_db)) return 1 - uri = args.uri or "sqlite:///{}".format(src_db) + uri = args.uri or 'sqlite:///{}'.format(src_db) engine = create_engine(uri, echo=False) session_factory = sessionmaker(bind=engine) session = session_factory() step = int(args.step) + step_start = 0 tags = {} if args.tags: - tags.update(dict(elem.split(":") for elem in args.tags.split(","))) - excl_entities = args.exclude_entities.split(",") - excl_domains = args.exclude_domains.split(",") + tags.update(dict(elem.split(':') for elem in args.tags.split(','))) + excl_entities = args.exclude_entities.split(',') + excl_domains = args.exclude_domains.split(',') override_measurement = args.override_measurement default_measurement = args.default_measurement - query = session.query(models.Events).filter( - models.Events.event_type == "state_changed").order_by( - models.Events.time_fired) + query = session.query(func.count(models.Events.event_type)).filter( + models.Events.event_type == 'state_changed') + + total_events = query.scalar() + prefix_format = '{} of {}' points = [] + invalid_points = [] count = 0 from collections import defaultdict entities = defaultdict(int) + print_progress(0, total_events, prefix_format.format(0, total_events)) - for event in query: - event_data = json.loads(event.event_data) - state = State.from_dict(event_data.get("new_state")) + while True: - if not state or ( - excl_entities and state.entity_id in excl_entities) or ( - excl_domains and state.domain in excl_domains): - session.expunge(event) - continue + step_stop = step_start + step + if step_start > total_events: + print_progress(total_events, total_events, prefix_format.format( + total_events, total_events)) + break + query = session.query(models.Events).filter( + models.Events.event_type == 'state_changed').order_by( + models.Events.time_fired).slice(step_start, step_stop) - try: - _state = float(state_helper.state_as_number(state)) - _state_key = "value" - except ValueError: - _state = state.state - _state_key = "state" + for event in query: + event_data = json.loads(event.event_data) - if override_measurement: - measurement = override_measurement - else: - measurement = state.attributes.get('unit_of_measurement') - if measurement in (None, ''): - if default_measurement: - measurement = default_measurement - else: - measurement = state.entity_id + if not ('entity_id' in event_data) or ( + excl_entities and event_data[ + 'entity_id'] in excl_entities) or ( + excl_domains and event_data[ + 'entity_id'].split('.')[0] in excl_domains): + session.expunge(event) + continue - point = { - 'measurement': measurement, - 'tags': { - 'domain': state.domain, - 'entity_id': state.object_id, - }, - 'time': event.time_fired, - 'fields': { - _state_key: _state, + try: + state = State.from_dict(event_data.get('new_state')) + except HomeAssistantError: + invalid_points.append(event_data) + + if not state: + invalid_points.append(event_data) + continue + + try: + _state = float(state_helper.state_as_number(state)) + _state_key = 'value' + except ValueError: + _state = state.state + _state_key = 'state' + + if override_measurement: + measurement = override_measurement + else: + measurement = state.attributes.get('unit_of_measurement') + if measurement in (None, ''): + if default_measurement: + measurement = default_measurement + else: + measurement = state.entity_id + + point = { + 'measurement': measurement, + 'tags': { + 'domain': state.domain, + 'entity_id': state.object_id, + }, + 'time': event.time_fired, + 'fields': { + _state_key: _state, + } } - } - for key, value in state.attributes.items(): - if key != 'unit_of_measurement': - # If the key is already in fields - if key in point['fields']: - key = key + "_" - # Prevent column data errors in influxDB. - # For each value we try to cast it as float - # But if we can not do it we store the value - # as string add "_str" postfix to the field key - try: - point['fields'][key] = float(value) - except (ValueError, TypeError): - new_key = "{}_str".format(key) - point['fields'][new_key] = str(value) + for key, value in state.attributes.items(): + if key != 'unit_of_measurement': + # If the key is already in fields + if key in point['fields']: + key = key + '_' + # Prevent column data errors in influxDB. + # For each value we try to cast it as float + # But if we can not do it we store the value + # as string add "_str" postfix to the field key + try: + point['fields'][key] = float(value) + except (ValueError, TypeError): + new_key = '{}_str'.format(key) + point['fields'][new_key] = str(value) - entities[state.entity_id] += 1 - point['tags'].update(tags) - points.append(point) - session.expunge(event) - if len(points) >= step: + entities[state.entity_id] += 1 + point['tags'].update(tags) + points.append(point) + session.expunge(event) + + if points: if not simulate: - print("Write {} points to the database".format(len(points))) client.write_points(points) count += len(points) - points = [] + # This prevents the progress bar from going over 100% when + # the last step happens + print_progress((step_start + len( + points)), total_events, prefix_format.format( + step_start, total_events)) + else: + print_progress( + (step_start + step), total_events, prefix_format.format( + step_start, total_events)) - if points: - if not simulate: - print("Write {} points to the database".format(len(points))) - client.write_points(points) - count += len(points) + points = [] + step_start += step print("\nStatistics:") print("\n".join(["{:6}: {}".format(v, k) for k, v in sorted(entities.items(), key=lambda x: x[1])])) - print("\nImport finished {} points written".format(count)) + print("\nInvalid Points: {}".format(len(invalid_points))) + print("\nImport finished: {} points written".format(count)) return 0 + + +# Based on code at +# http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console +def print_progress(iteration: int, total: int, prefix: str='', suffix: str='', + decimals: int=2, bar_length: int=68) -> None: + """Print progress bar. + + Call in a loop to create terminal progress bar + @params: + iteration - Required : current iteration (Int) + total - Required : total iterations (Int) + prefix - Optional : prefix string (Str) + suffix - Optional : suffix string (Str) + decimals - Optional : number of decimals in percent complete (Int) + barLength - Optional : character length of bar (Int) + """ + filled_length = int(round(bar_length * iteration / float(total))) + percents = round(100.00 * (iteration / float(total)), decimals) + line = '#' * filled_length + '-' * (bar_length - filled_length) + sys.stdout.write('%s [%s] %s%s %s\r' % (prefix, line, + percents, '%', suffix)) + sys.stdout.flush() + if iteration == total: + print('\n') From 6ae3fa40cf3cd354a517c342b2c180a3a2d0e0f8 Mon Sep 17 00:00:00 2001 From: Gerben Meijer Date: Wed, 31 Jan 2018 12:00:47 +0100 Subject: [PATCH 069/166] Set flux default stop time to dusk (#12062) This is more in line with how one would expect light temperature transitions to take place, but still allows for a user defined stop_time. --- homeassistant/components/switch/flux.py | 16 ++++++++++++---- tests/components/switch/test_flux.py | 9 ++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index a6c57656767..acc0c3ac423 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_LIGHTS): cv.entity_ids, vol.Optional(CONF_NAME, default="Flux"): cv.string, vol.Optional(CONF_START_TIME): cv.time, - vol.Optional(CONF_STOP_TIME, default=datetime.time(22, 0)): cv.time, + vol.Optional(CONF_STOP_TIME): cv.time, vol.Optional(CONF_START_CT, default=4000): vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), vol.Optional(CONF_SUNSET_CT, default=3000): @@ -184,9 +184,7 @@ class FluxSwitch(SwitchDevice): sunset = get_astral_event_date(self.hass, 'sunset', now.date()) start_time = self.find_start_time(now) - stop_time = now.replace( - hour=self._stop_time.hour, minute=self._stop_time.minute, - second=0) + stop_time = self.find_stop_time(now) if stop_time <= start_time: # stop_time does not happen in the same day as start_time @@ -270,3 +268,13 @@ class FluxSwitch(SwitchDevice): else: sunrise = get_astral_event_date(self.hass, 'sunrise', now.date()) return sunrise + + def find_stop_time(self, now): + """Return dusk or stop_time if given.""" + if self._stop_time: + dusk = now.replace( + hour=self._stop_time.hour, minute=self._stop_time.minute, + second=0) + else: + dusk = get_astral_event_date(self.hass, 'dusk', now.date()) + return dusk diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index 0d2a486cb4f..a1e600860f9 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -238,7 +238,8 @@ class TestSwitchFlux(unittest.TestCase): switch.DOMAIN: { 'platform': 'flux', 'name': 'flux', - 'lights': [dev1.entity_id] + 'lights': [dev1.entity_id], + 'stop_time': '22:00' } }) turn_on_calls = mock_service( @@ -638,7 +639,8 @@ class TestSwitchFlux(unittest.TestCase): 'name': 'flux', 'lights': [dev1.entity_id], 'start_colortemp': '1000', - 'stop_colortemp': '6000' + 'stop_colortemp': '6000', + 'stop_time': '22:00' } }) turn_on_calls = mock_service( @@ -686,7 +688,8 @@ class TestSwitchFlux(unittest.TestCase): 'platform': 'flux', 'name': 'flux', 'lights': [dev1.entity_id], - 'brightness': 255 + 'brightness': 255, + 'stop_time': '22:00' } }) turn_on_calls = mock_service( From e9508405bc1e8cc8df5d9af528bd28ba44b9f568 Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 31 Jan 2018 13:05:15 +0100 Subject: [PATCH 070/166] Add conditions to forecast (#12074) * add conditions to forecast chart * Fix pylint issues --- .../components/weather/openweathermap.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 408b6968588..c8a1bdf8f68 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -21,12 +21,14 @@ REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) +ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = 'Data provided by OpenWeatherMap' DEFAULT_NAME = 'OpenWeatherMap' MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS = 3 CONDITION_CLASSES = { 'cloudy': [804], @@ -137,10 +139,18 @@ class OpenWeatherMapWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return [{ - ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, - ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp')} - for entry in self.forecast_data.get_weathers()] + data = [] + for entry in self.forecast_data.get_weathers(): + data.append({ + ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, + ATTR_FORECAST_TEMP: + entry.get_temperature('celsius').get('temp') + }) + if (len(data) - 1) % MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS == 0: + data[len(data) - 1][ATTR_FORECAST_CONDITION] = \ + [k for k, v in CONDITION_CLASSES.items() + if entry.get_weather_code() in v][0] + return data def update(self): """Get the latest data from OWM and updates the states.""" From 81a6178931093549ae37e08e3836756f0235d4d1 Mon Sep 17 00:00:00 2001 From: John Mihalic Date: Wed, 31 Jan 2018 09:32:08 -0500 Subject: [PATCH 071/166] Squeezebox Fix duplicate server from discovery (#12063) * Fix duplicate server from discovery * Use hass.data instead of global * Simplify --- homeassistant/components/media_player/squeezebox.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index cc61610b369..1fd61b3ead1 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -47,6 +47,8 @@ SERVICE_CALL_METHOD = 'squeezebox_call_method' DATA_SQUEEZEBOX = 'squeezebox' +KNOWN_SERVERS = 'squeezebox_known_servers' + ATTR_PARAMETERS = 'parameters' SQUEEZEBOX_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ @@ -67,6 +69,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the squeezebox platform.""" import socket + known_servers = hass.data.get(KNOWN_SERVERS) + if known_servers is None: + hass.data[KNOWN_SERVERS] = known_servers = set() + if DATA_SQUEEZEBOX not in hass.data: hass.data[DATA_SQUEEZEBOX] = [] @@ -92,6 +98,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): "Could not communicate with %s:%d: %s", host, port, error) return False + if ipaddr in known_servers: + return + + known_servers.add(ipaddr) _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) From 424fe95ce4a35900ca86c0b935da3aa02f49b615 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 31 Jan 2018 18:03:20 +0100 Subject: [PATCH 072/166] Upgrade keyring to 11.0.0 (#12082) * Upgrade keyring to 11.0.0 * Address the removal of 'keyring.__version__' --- homeassistant/scripts/keyring.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index b46d135c107..64ad09bcd70 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==10.6.0', 'keyrings.alt==2.3'] +REQUIREMENTS = ['keyring==11.0.0', 'keyrings.alt==2.3'] def run(args): @@ -29,7 +29,7 @@ def run(args): if args.action == 'info': keyr = keyring.get_keyring() - print('Keyring version {}\n'.format(keyring.__version__)) + print('Keyring version {}\n'.format(REQUIREMENTS[0].split('==')[1])) print('Active keyring : {}'.format(keyr.__module__)) config_name = os.path.join(platform.config_root(), 'keyringrc.cfg') print('Config location : {}'.format(config_name)) diff --git a/requirements_all.txt b/requirements_all.txt index e91cbaf381f..0f48282763a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.5 # homeassistant.scripts.keyring -keyring==10.6.0 +keyring==11.0.0 # homeassistant.scripts.keyring keyrings.alt==2.3 From 40af9f2676928fdb87698f2fb0b472baa01d67e1 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 31 Jan 2018 17:04:32 +0000 Subject: [PATCH 073/166] Correct use of middleware async handling. (#12078) --- homeassistant/components/http/auth.py | 4 ++-- homeassistant/components/http/static.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index ce5bfca3ac1..a6a412b6ba2 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -23,7 +23,7 @@ def auth_middleware(request, handler): # If no password set, just always set authenticated=True if request.app['hass'].http.api_password is None: request[KEY_AUTHENTICATED] = True - return handler(request) + return (yield from handler(request)) # Check authentication authenticated = False @@ -46,7 +46,7 @@ def auth_middleware(request, handler): authenticated = True request[KEY_AUTHENTICATED] = authenticated - return handler(request) + return (yield from handler(request)) def is_trusted_ip(request): diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index c9b094e3f2e..74a5a8818a4 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -67,7 +67,7 @@ def staticresource_middleware(request, handler): """Middleware to strip out fingerprint from fingerprinted assets.""" path = request.path if not path.startswith('/static/') and not path.startswith('/frontend'): - return handler(request) + return (yield from handler(request)) fingerprinted = _FINGERPRINT.match(request.match_info['filename']) @@ -75,4 +75,4 @@ def staticresource_middleware(request, handler): request.match_info['filename'] = \ '{}.{}'.format(*fingerprinted.groups()) - return handler(request) + return (yield from handler(request)) From e11e0666848b9b3ebc493bdd33fc175af5548f7f Mon Sep 17 00:00:00 2001 From: Philip Kleimeyer Date: Wed, 31 Jan 2018 19:10:35 +0100 Subject: [PATCH 074/166] updated sensor name (#12084) * updated sensor name * Lint --- homeassistant/components/sensor/tahoma.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index d0b038fd230..39d1cbc75a3 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -9,7 +9,6 @@ import logging from datetime import timedelta from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.tahoma import ( DOMAIN as TAHOMA_DOMAIN, TahomaDevice) @@ -36,7 +35,6 @@ class TahomaSensor(TahomaDevice, Entity): """Initialize the sensor.""" self.current_value = None super().__init__(tahoma_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id) @property def state(self): From 764343dbf88df2ae9c8212da72e6475c5657cf49 Mon Sep 17 00:00:00 2001 From: c727 Date: Wed, 31 Jan 2018 19:44:33 +0100 Subject: [PATCH 075/166] Fix detection of mobile browsers (#12075) * Fix detection of mobile browsers * Move break * Add Mobile browsers to list `Mobile Safari` is covered by iOS and `Opera Mini` does not support es6 ... And Edge doesn't even work on desktop for me... --- homeassistant/components/frontend/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f745458899c..413f334eb90 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -586,7 +586,9 @@ def _is_latest(js_option, request): family_min_version = { 'Chrome': 50, # Probably can reduce this + 'Chrome Mobile': 50, 'Firefox': 43, # Array.prototype.includes added in 43 + 'Firefox Mobile': 43, 'Opera': 40, # Probably can reduce this 'Edge': 14, # Array.prototype.includes added in 14 'Safari': 10, # many features not supported by 9 From 8991690d53acb91654b0ce8eb9622e81ca3b6b9d Mon Sep 17 00:00:00 2001 From: Philip Kleimeyer Date: Thu, 1 Feb 2018 08:15:13 +0100 Subject: [PATCH 076/166] update tahoma api to version 0.0.11 (#12099) --- 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 0db055f7d92..28a54f40d56 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.10'] +REQUIREMENTS = ['tahoma-api==0.0.11'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0f48282763a..0cbef617116 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ steamodd==4.21 suds-py3==1.3.3.0 # homeassistant.components.tahoma -tahoma-api==0.0.10 +tahoma-api==0.0.11 # homeassistant.components.sensor.tank_utility tank_utility==1.4.0 From 2f07ffc4e46cf378b24b684117e21c4a9db2c6ca Mon Sep 17 00:00:00 2001 From: jodur Date: Thu, 1 Feb 2018 09:49:39 +0100 Subject: [PATCH 077/166] added media_stop (#12100) * added media_stop VLC was missing the media_stop. The pause was present, but starting the same file result in resuming the file instead of start over new * Corrected style issues Style issues and added Support Flag to Support VLC * fixed other style issues * remove trailing whitespace --- homeassistant/components/media_player/vlc.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py index d3346495015..abd8252d813 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -9,8 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC) + SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC) + from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv @@ -24,7 +26,7 @@ CONF_ARGUMENTS = 'arguments' DEFAULT_NAME = 'Vlc' SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, @@ -146,6 +148,11 @@ class VlcDevice(MediaPlayerDevice): self._vlc.pause() self._state = STATE_PAUSED + def media_stop(self): + """Send stop command.""" + self._vlc.stop() + self._state = STATE_IDLE + def play_media(self, media_type, media_id, **kwargs): """Play media from a URL or file.""" if not media_type == MEDIA_TYPE_MUSIC: From 53a99dc9fab72ae1daf297e2e92a281ed955ceca Mon Sep 17 00:00:00 2001 From: "Craig J. Ward" Date: Thu, 1 Feb 2018 04:24:02 -0600 Subject: [PATCH 078/166] Goalfeed channel (#12086) * fix event channel name * I guess accidentally added timeout here too. --- homeassistant/components/goalfeed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py index 5f9985fd38d..f360d4ffba9 100644 --- a/homeassistant/components/goalfeed.py +++ b/homeassistant/components/goalfeed.py @@ -54,7 +54,7 @@ def setup(hass, config): channel.bind('goal', goal_handler) pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080, - custom_host=GOALFEED_HOST, timeout=30) + custom_host=GOALFEED_HOST) pusher.connection.bind('pusher:connection_established', connect_handler) pusher.connect() From be37bb14b7a2d7bf92d47d098cc4219286b45903 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 1 Feb 2018 13:21:15 -0500 Subject: [PATCH 079/166] Update jinja2 to 2.10 (#12118) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f34f943ae6..995002df7e9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ requests==2.18.4 pyyaml>=3.11,<4 pytz>=2017.02 pip>=8.0.3 -jinja2>=2.9.6 +jinja2>=2.10 voluptuous==0.10.5 typing>=3,<4 aiohttp==2.3.7 diff --git a/requirements_all.txt b/requirements_all.txt index 0cbef617116..30aeda2b35d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3,7 +3,7 @@ requests==2.18.4 pyyaml>=3.11,<4 pytz>=2017.02 pip>=8.0.3 -jinja2>=2.9.6 +jinja2>=2.10 voluptuous==0.10.5 typing>=3,<4 aiohttp==2.3.7 diff --git a/setup.py b/setup.py index 80458beb25f..9f28cdd3ef1 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRES = [ 'pyyaml>=3.11,<4', 'pytz>=2017.02', 'pip>=8.0.3', - 'jinja2>=2.9.6', + 'jinja2>=2.10', 'voluptuous==0.10.5', 'typing>=3,<4', 'aiohttp==2.3.7', # If updated, check if yarl also needs an update! From 30ad591a59315eaba4630b4f3a824690319cb4e0 Mon Sep 17 00:00:00 2001 From: Timmo Date: Thu, 1 Feb 2018 19:57:33 +0000 Subject: [PATCH 080/166] Downgrade Sonarr and Radarr 'Host is not avaliable' errors to warnings (#12119) --- homeassistant/components/sensor/radarr.py | 2 +- homeassistant/components/sensor/sonarr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py index 3b2c818a7b3..8adaeb3c71b 100644 --- a/homeassistant/components/sensor/radarr.py +++ b/homeassistant/components/sensor/radarr.py @@ -162,7 +162,7 @@ class RadarrSensor(Entity): self.ssl, self.host, self.port, self.urlbase, start, end), headers={'X-Api-Key': self.apikey}, timeout=10) except OSError: - _LOGGER.error("Host %s is not available", self.host) + _LOGGER.warning("Host %s is not available", self.host) self._available = False self._state = None return diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py index 42460a83d6f..090addb5b6e 100644 --- a/homeassistant/components/sensor/sonarr.py +++ b/homeassistant/components/sensor/sonarr.py @@ -181,7 +181,7 @@ class SonarrSensor(Entity): headers={'X-Api-Key': self.apikey}, timeout=10) except OSError: - _LOGGER.error("Host %s is not available", self.host) + _LOGGER.warning("Host %s is not available", self.host) self._available = False self._state = None return From 6d5a87afb65db1d60ed13477f8935bfbc6261430 Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 2 Feb 2018 10:05:19 +0200 Subject: [PATCH 081/166] Fixes away_mode error on startup (#12121) * Fixes away_mode error on startup * Updated based on feedback --- homeassistant/components/climate/generic_thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index ba4973aea9e..7436053b7d4 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -156,7 +156,7 @@ class GenericThermostat(ClimateDevice): # If we have no initial temperature, restore if self._target_temp is None: # If we have a previously saved temperature - if old_state.attributes[ATTR_TEMPERATURE] is None: + if old_state.attributes.get(ATTR_TEMPERATURE) is None: if self.ac_mode: self._target_temp = self.max_temp else: @@ -166,7 +166,7 @@ class GenericThermostat(ClimateDevice): else: self._target_temp = float( old_state.attributes[ATTR_TEMPERATURE]) - if old_state.attributes[ATTR_AWAY_MODE] is not None: + if old_state.attributes.get(ATTR_AWAY_MODE) is not None: self._is_away = str( old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON if (self._initial_operation_mode is None and From 12dc0db8566133668e1a219592c8ace09bdb253d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 2 Feb 2018 09:05:54 +0100 Subject: [PATCH 082/166] except vol.MultipleInvalid in Broadlink #11795 (#12107) * except vol.MultipleInvalid in Broadlink #11795 * typo --- homeassistant/components/sensor/broadlink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index d23236c2df8..1440e2496fe 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -129,7 +129,7 @@ class BroadlinkData(object): if retry < 1: _LOGGER.error(error) return - except vol.Invalid: + except (vol.Invalid, vol.MultipleInvalid): pass # Continue quietly if device returned malformed data if retry > 0 and self._auth(): self._update(retry-1) From 569f7e2fea57b985839388cc90911ee97f1dcbdb Mon Sep 17 00:00:00 2001 From: hawk259 Date: Fri, 2 Feb 2018 04:11:13 -0500 Subject: [PATCH 083/166] Adds SUPPORT_TARGET_TEMPERATURE_HIGH and SUPPORT_TARGET_TEMPERATURE_LOW support (#12110) Fixes: [Issue 12048] --- homeassistant/components/climate/ecobee.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index b0685b337be..59df7d20687 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -13,7 +13,8 @@ from homeassistant.components.climate import ( DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH) + SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv @@ -46,7 +47,9 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | - SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH) + SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | + SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) def setup_platform(hass, config, add_devices, discovery_info=None): From 65f22b09aefbba53c90edb3db4b77461f25a4109 Mon Sep 17 00:00:00 2001 From: nordlead2005 Date: Fri, 2 Feb 2018 04:23:27 -0500 Subject: [PATCH 084/166] Dark sky precip accumulation (#12127) * Added DarkSky Precipitation Accumulation as an hourly forecast field * fixed spacing --- homeassistant/components/sensor/darksky.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 39a258c5e6a..e224feb7db7 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -56,6 +56,9 @@ SENSOR_TYPES = { 'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%', 'mdi:water-percent', ['currently', 'minutely', 'hourly', 'daily']], + 'precip_accumulation': ['Precip Accumulation', + 'cm', 'in', 'cm', 'cm', 'cm', 'mdi:weather-snowy', + ['hourly', 'daily']], 'temperature': ['Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly']], @@ -269,7 +272,8 @@ class DarkSkySensor(Entity): 'temperature_max', 'apparent_temperature_min', 'apparent_temperature_max', - 'precip_intensity_max']): + 'precip_intensity_max', + 'precip_accumulation']): self.forecast_data.update_daily() daily = self.forecast_data.data_daily if self.type == 'daily_summary': @@ -309,6 +313,7 @@ class DarkSkySensor(Entity): 'temperature_min', 'temperature_max', 'apparent_temperature_min', 'apparent_temperature_max', + 'precip_accumulation', 'pressure', 'ozone', 'uvIndex']): return round(state, 1) return state From ad24cbddccfb960f448806b3a40b16b85c193f11 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Fri, 2 Feb 2018 15:02:11 +0200 Subject: [PATCH 085/166] fixed wrong check for valid range of 'rgb' values (#12132) --- homeassistant/components/light/xiaomi_aqara.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index d1664d13072..efe37d3d577 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -51,13 +51,13 @@ class XiaomiGatewayLight(XiaomiDevice, Light): return True rgbhexstr = "%x" % value - if len(rgbhexstr) == 7: - rgbhexstr = '0' + rgbhexstr - elif len(rgbhexstr) != 8: - _LOGGER.error('Light RGB data error.' - ' Must be 8 characters. Received: %s', rgbhexstr) + if len(rgbhexstr) > 8: + _LOGGER.error("Light RGB data error." + " Can't be more than 8 characters. Received: %s", + rgbhexstr) return False + rgbhexstr = rgbhexstr.zfill(8) rgbhex = bytes.fromhex(rgbhexstr) rgba = struct.unpack('BBBB', rgbhex) brightness = rgba[0] From 1d2e930900dbf78ec6b80359c10088160f0d6291 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Fri, 2 Feb 2018 14:19:13 +0100 Subject: [PATCH 086/166] OpenALPR Cloud API - transfer image in body of POST request (#12112) * Send image in body of POST request * Fix tests * Implement requested change --- homeassistant/components/image_processing/openalpr_cloud.py | 6 ++++-- tests/components/image_processing/test_openalpr_cloud.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 2fdc3d72f2e..dbf36dcd86e 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -109,12 +109,14 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): websession = async_get_clientsession(self.hass) params = self._params.copy() - params['image_bytes'] = str(b64encode(image), 'utf-8') + body = { + 'image_bytes': str(b64encode(image), 'utf-8') + } try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): request = yield from websession.post( - OPENALPR_API_URL, params=params + OPENALPR_API_URL, params=params, data=body ) data = yield from request.json() diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index 40945f932c6..e840bce54f7 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -149,8 +149,7 @@ class TestOpenAlprCloud(object): 'secret_key': "sk_abcxyz123456", 'tasks': "plate", 'return_image': 0, - 'country': 'eu', - 'image_bytes': "aW1hZ2U=" + 'country': 'eu' } def teardown_method(self): From 87c0fd98c755f65f14f067e24c921f24fd7421bd Mon Sep 17 00:00:00 2001 From: Eleftherios Chamakiotis Date: Fri, 2 Feb 2018 16:54:47 +0200 Subject: [PATCH 087/166] Add support for "off" function to iTunes (#12109) fixes #7614 --- homeassistant/components/media_player/itunes.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 575ea414fa3..3291c1ae13d 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -29,7 +29,7 @@ DOMAIN = 'itunes' SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_TURN_OFF SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF @@ -115,6 +115,10 @@ class Itunes(object): """Skip back and returns the current state.""" return self._command('previous') + def stop(self): + """Stop playback and return the current state.""" + return self._command('stop') + def play_playlist(self, playlist_id_or_name): """Set a playlist to be current and returns the current state.""" response = self._request('GET', '/playlists') @@ -346,6 +350,11 @@ class ItunesDevice(MediaPlayerDevice): response = self.client.play_playlist(media_id) self.update_state(response) + def turn_off(self): + """Turn the media player off.""" + response = self.client.stop() + self.update_state(response) + class AirPlayDevice(MediaPlayerDevice): """Representation an AirPlay device via an iTunes API instance.""" From 72c35468b337cfbfd51afdaefc688aada0298d15 Mon Sep 17 00:00:00 2001 From: Marc Khouri Date: Fri, 2 Feb 2018 09:59:05 -0500 Subject: [PATCH 088/166] Remove asyncio.test_utils to fix tests in Docker/Python 3.7 (#12102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module `asyncio.test_utils` has been removed from Python in the 3.7 branch, because it was intended to be a private module for internal testing of asyncio. For more information, see the upstream bug report at https://bugs.python.org/issue32273 and the upstream PR at https://github.com/python/cpython/pull/4785. For this commit, I have migrated the small amount of functionality that was being used from the `asyncio.test_utils` directly into the `RunThreadsafeTests` Class. To see the original `asyncio.test_utils.TestCase` class, which I pulled some functionality from, please see: https://github.com/python/cpython/blob/3.6/Lib/asyncio/test_utils.py#L440 Note: In addition to being broken in 3.7, this test case also seems to be broken in Python 3.6.4 when using Docker. This PR fixes the test when run in docker. To reproduce: `./script/test_docker -- tests/util/test_async.py` failing output (prior to this commit): ``` ... trimmed ... py36 runtests: PYTHONHASHSEED='3262989550' py36 runtests: commands[0] | py.test --timeout=9 --duration=10 --cov --cov-report= tests/util/test_async.py Test session starts (platform: linux, Python 3.6.4, pytest 3.3.1, pytest-sugar 0.9.0) rootdir: /usr/src/app, inifile: setup.cfg plugins: timeout-1.2.1, sugar-0.9.0, cov-2.5.1, aiohttp-0.3.0 timeout: 9.0s method: signal ―――――――――――――――――― ERROR collecting tests/util/test_async.py ―――――――――――――――――――――――― ImportError while importing test module '/usr/src/app/tests/util/test_async.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: tests/util/test_async.py:3: in from asyncio import test_utils /usr/local/lib/python3.6/asyncio/test_utils.py:36: in from test import support E ImportError: cannot import name 'support' ``` --- tests/util/test_async.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index b7a18d00fae..b6ae58a484f 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,7 +1,7 @@ """Tests for async util methods from Python source.""" import asyncio -from asyncio import test_utils from unittest.mock import MagicMock, patch +from unittest import TestCase import pytest @@ -104,14 +104,32 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): assert len(loop.call_soon_threadsafe.mock_calls) == 2 -class RunThreadsafeTests(test_utils.TestCase): - """Test case for asyncio.run_coroutine_threadsafe.""" +class RunThreadsafeTests(TestCase): + """Test case for hasync.run_coroutine_threadsafe.""" def setUp(self): """Test setup method.""" - super().setUp() self.loop = asyncio.new_event_loop() - self.set_event_loop(self.loop) # Will cleanup properly + + def tearDown(self): + """Test teardown method.""" + executor = self.loop._default_executor + if executor is not None: + executor.shutdown(wait=True) + self.loop.close() + + @staticmethod + def run_briefly(loop): + """Momentarily run a coroutine on the given loop.""" + @asyncio.coroutine + def once(): + pass + gen = once() + t = loop.create_task(gen) + try: + loop.run_until_complete(t) + finally: + gen.close() def add_callback(self, a, b, fail, invalid): """Return a + b.""" @@ -185,7 +203,7 @@ class RunThreadsafeTests(test_utils.TestCase): future = self.loop.run_in_executor(None, callback) with self.assertRaises(asyncio.TimeoutError): self.loop.run_until_complete(future) - test_utils.run_briefly(self.loop) + self.run_briefly(self.loop) # Check that there's no pending task (add has been cancelled) for task in asyncio.Task.all_tasks(self.loop): self.assertTrue(task.done()) From ed2d54ab45044e387044d1e60e6ca5967e585ceb Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Fri, 2 Feb 2018 16:56:58 +0100 Subject: [PATCH 089/166] Add Mercedes me component (#11743) * Add Mercedes me component * pump api version to 0.1.2 * Add Mercedes me component * pump api version to 0.1.2 * clean up code * Code cleanup * Remove unneeded return statement * Return statements added again * Implement requested changes * Rework component, move data load to component * lint * remove debug logging * Change RainCloud comments, change from track_utc_time to track_time_interval * Final cleanup for version 1 --- .coveragerc | 3 + .../components/binary_sensor/mercedesme.py | 100 ++++++++++++ .../components/device_tracker/mercedesme.py | 72 +++++++++ homeassistant/components/mercedesme.py | 144 ++++++++++++++++++ homeassistant/components/sensor/mercedesme.py | 92 +++++++++++ requirements_all.txt | 3 + 6 files changed, 414 insertions(+) create mode 100755 homeassistant/components/binary_sensor/mercedesme.py create mode 100755 homeassistant/components/device_tracker/mercedesme.py create mode 100755 homeassistant/components/mercedesme.py create mode 100755 homeassistant/components/sensor/mercedesme.py diff --git a/.coveragerc b/.coveragerc index 3529e7413ca..7287dcb143f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -145,6 +145,9 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py + homeassistant/components/mercedesme.py + homeassistant/components/*/mercedesme.py + homeassistant/components/mochad.py homeassistant/components/*/mochad.py diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py new file mode 100755 index 00000000000..dbbe679e852 --- /dev/null +++ b/homeassistant/components/binary_sensor/mercedesme.py @@ -0,0 +1,100 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import logging +import datetime + +from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, BINARY_SENSORS) + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + data = hass.data[DATA_MME].data + + if not data.cars: + _LOGGER.error("setup_platform data.cars is none") + return + + devices = [] + for car in data.cars: + for dev in BINARY_SENSORS: + devices.append(MercedesMEBinarySensor( + data, dev, dev, car["vin"], None)) + + add_devices(devices, True) + + +class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): + """Representation of a Sensor.""" + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state == "On" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._name == "windowsClosed": + return { + "windowStatusFrontLeft": self._car["windowStatusFrontLeft"], + "windowStatusFrontRight": self._car["windowStatusFrontRight"], + "windowStatusRearLeft": self._car["windowStatusRearLeft"], + "windowStatusRearRight": self._car["windowStatusRearRight"], + "originalValue": self._car[self._name], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + elif self._name == "tireWarningLight": + return { + "frontRightTirePressureKpa": + self._car["frontRightTirePressureKpa"], + "frontLeftTirePressureKpa": + self._car["frontLeftTirePressureKpa"], + "rearRightTirePressureKpa": + self._car["rearRightTirePressureKpa"], + "rearLeftTirePressureKpa": + self._car["rearLeftTirePressureKpa"], + "originalValue": self._car[self._name], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"], + } + return { + "originalValue": self._car[self._name], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + def update(self): + """Fetch new state data for the sensor.""" + _LOGGER.debug("Updating %s", self._name) + + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + result = False + + if self._name == "windowsClosed": + result = bool(self._car[self._name] == "CLOSED") + elif self._name == "tireWarningLight": + result = bool(self._car[self._name] != "INACTIVE") + else: + result = self._car[self._name] is True + + self._state = "On" if result else "Off" + + _LOGGER.debug("Updated %s Value: %s IsOn: %s", + self._name, self._state, self.is_on) diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py new file mode 100755 index 00000000000..c33cc239412 --- /dev/null +++ b/homeassistant/components/device_tracker/mercedesme.py @@ -0,0 +1,72 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import logging +from datetime import timedelta + +from homeassistant.components.mercedesme import DATA_MME +from homeassistant.helpers.event import track_time_interval +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mercedesme'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Mercedes ME tracker.""" + if discovery_info is None: + return False + + data = hass.data[DATA_MME].data + + if not data.cars: + return False + + MercedesMEDeviceTracker(hass, config, see, data) + + return True + + +class MercedesMEDeviceTracker(object): + """A class representing a Mercedes ME device tracker.""" + + def __init__(self, hass, config, see, data): + """Initialize the Mercedes ME device tracker.""" + self.hass = hass + self.see = see + self.data = data + self.update_info() + + track_time_interval( + self.hass, self.update_info, MIN_TIME_BETWEEN_SCANS) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def update_info(self, now=None): + """Update the device info.""" + for device in self.data.cars: + _LOGGER.debug("Updating %s", device["vin"]) + location = self.data.get_location(device["vin"]) + if location is None: + return False + dev_id = device["vin"] + name = device["license"] + + lat = location['positionLat']['value'] + lon = location['positionLong']['value'] + attrs = { + 'trackr_id': dev_id, + 'id': dev_id, + 'name': name + } + self.see( + dev_id=dev_id, host_name=name, + gps=(lat, lon), attributes=attrs + ) + + return True diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py new file mode 100755 index 00000000000..0a5825bb16d --- /dev/null +++ b/homeassistant/components/mercedesme.py @@ -0,0 +1,144 @@ +""" +Support for MercedesME System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSORS = [ + 'doorsClosed', + 'windowsClosed', + 'locked', + 'tireWarningLight' +] + +DATA_MME = 'mercedesme' +DOMAIN = 'mercedesme' + +NOTIFICATION_ID = 'mercedesme_integration_notification' +NOTIFICATION_TITLE = 'Mercedes me integration setup' + +SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=30): + vol.All(cv.positive_int, vol.Clamp(min=10)) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up MercedesMe System.""" + from mercedesmejsonpy.controller import Controller + from mercedesmejsonpy import Exceptions + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + mercedesme_api = Controller(username, password, scan_interval) + if not mercedesme_api.is_valid_session: + raise Exceptions.MercedesMeException(500) + hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) + except Exceptions.MercedesMeException as ex: + if ex.code == 401: + hass.components.persistent_notification.create( + "Error:
Please check username and password." + "You will need to restart Home Assistant after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + else: + hass.components.persistent_notification.create( + "Error:
Can't communicate with Mercedes me API.
" + "Error code: {} Reason: {}" + "You will need to restart Home Assistant after fixing." + "".format(ex.code, ex.message), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + _LOGGER.error("Unable to communicate with Mercedes me API: %s", + ex.message) + return False + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + def hub_refresh(event_time): + """Call Mercedes me API to refresh information.""" + _LOGGER.info("Updating Mercedes me component.") + hass.data[DATA_MME].data.update() + dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) + + track_time_interval( + hass, + hub_refresh, + timedelta(seconds=scan_interval)) + + return True + + +class MercedesMeHub(object): + """Representation of a base MercedesMe device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class MercedesMeEntity(Entity): + """Entity class for MercedesMe devices.""" + + def __init__(self, data, internal_name, sensor_name, vin, unit): + """Initialize the MercedesMe entity.""" + self._car = None + self._data = data + self._state = False + self._name = sensor_name + self._internal_name = internal_name + self._unit = unit + self._vin = vin + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) + + def _update_callback(self): + """Callback update method.""" + self.schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py new file mode 100755 index 00000000000..08183a01ba8 --- /dev/null +++ b/homeassistant/components/sensor/mercedesme.py @@ -0,0 +1,92 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import logging +import datetime + +from homeassistant.const import LENGTH_KILOMETERS +from homeassistant.components.mercedesme import DATA_MME, MercedesMeEntity + + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'fuelLevelPercent': ['Fuel Level', '%'], + 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], + 'latestTrip': ['Latest Trip', None], + 'odometerKm': ['Odometer', LENGTH_KILOMETERS], + 'serviceIntervalDays': ['Next Service', 'days'], + 'doorsClosed': ['doorsClosed', None], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + if discovery_info is None: + return + + data = hass.data[DATA_MME].data + + if not data.cars: + return + + devices = [] + for car in data.cars: + for key, value in sorted(SENSOR_TYPES.items()): + devices.append( + MercedesMESensor(data, key, value[0], car["vin"], value[1])) + + add_devices(devices, True) + + +class MercedesMESensor(MercedesMeEntity): + """Representation of a Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating %s", self._internal_name) + + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + if self._internal_name == "latestTrip": + self._state = self._car["latestTrip"]["id"] + else: + self._state = self._car[self._internal_name] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._internal_name == "latestTrip": + return { + "durationSeconds": + self._car["latestTrip"]["durationSeconds"], + "distanceTraveledKm": + self._car["latestTrip"]["distanceTraveledKm"], + "startedAt": datetime.datetime.fromtimestamp( + self._car["latestTrip"]["startedAt"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "averageSpeedKmPerHr": + self._car["latestTrip"]["averageSpeedKmPerHr"], + "finished": self._car["latestTrip"]["finished"], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + return { + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } diff --git a/requirements_all.txt b/requirements_all.txt index 30aeda2b35d..20dc82cb95a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,6 +477,9 @@ matrix-client==0.0.6 # homeassistant.components.maxcube maxcube-api==0.1.0 +# homeassistant.components.mercedesme +mercedesmejsonpy==0.1.2 + # homeassistant.components.notify.message_bird messagebird==1.2.0 From 13ec8b143dfcf1f7dde66a17f1c55fc4d0284453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 2 Feb 2018 23:35:34 +0200 Subject: [PATCH 090/166] Spelling fixes (#12138) --- homeassistant/components/plant.py | 2 +- homeassistant/components/switch/pulseaudio_loopback.py | 2 +- homeassistant/helpers/deprecation.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 50bb7c43c72..24b8c682d02 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -92,7 +92,7 @@ CONFIG_SCHEMA = vol.Schema({ # Flag for enabling/disabling the loading of the history from the database. -# This feature is turned off right now as it's tests are not 100% stable. +# This feature is turned off right now as its tests are not 100% stable. ENABLE_LOAD_HISTORY = False diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index 03f9e84b3c8..007e74e14fd 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -131,7 +131,7 @@ class PAServer(): self._send_command(str.format(UNLOAD_CMD, module_idx), False) def get_module_idx(self, sink_name, source_name): - """For a sink/source, return it's module id in our cache, if found.""" + """For a sink/source, return its module id in our cache, if found.""" result = re.search(str.format(MOD_REGEX, re.escape(sink_name), re.escape(source_name)), self._current_module_state) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index ee4176a8937..73a09464439 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -8,7 +8,7 @@ def deprecated_substitute(substitute_name): When a property is added to replace an older property, this decorator can be added to the new property, listing the old property as the substitute. - If the old property is defined, it's value will be used instead, and a log + If the old property is defined, its value will be used instead, and a log warning will be issued alerting the user of the impending change. """ def decorator(func): From 730e0a0094b84b3adb4cd6664a84eb07c1bbc75c Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Fri, 2 Feb 2018 17:12:54 -0500 Subject: [PATCH 091/166] Update volumio component (#12045) --- .../components/media_player/volumio.py | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index dab7901111d..06d2304b3e7 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -4,6 +4,7 @@ Volumio Platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.volumio/ """ +from datetime import timedelta import logging import asyncio import aiohttp @@ -13,11 +14,13 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC) + SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST) from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -30,8 +33,10 @@ TIMEOUT = 10 SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | \ + SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE | SUPPORT_CLEAR_PLAYLIST +PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=15) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, @@ -63,6 +68,8 @@ class Volumio(MediaPlayerDevice): self._state = {} self.async_update() self._lastvol = self._state.get('volume', 0) + self._playlists = [] + self._currentplaylist = None @asyncio.coroutine def send_volumio_msg(self, method, params=None): @@ -96,6 +103,7 @@ class Volumio(MediaPlayerDevice): def async_update(self): """Update state.""" resp = yield from self.send_volumio_msg('getState') + yield from self._async_update_playlists() if resp is False: return self._state = resp.copy() @@ -157,7 +165,7 @@ class Volumio(MediaPlayerDevice): def volume_level(self): """Volume level of the media player (0..1).""" volume = self._state.get('volume', None) - if volume is not None: + if volume is not None and volume != "": volume = volume / 100 return volume @@ -171,6 +179,16 @@ class Volumio(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def source_list(self): + """Return the list of available input sources.""" + return self._playlists + + @property + def source(self): + """Name of the current input source.""" + return self._currentplaylist + @property def supported_features(self): """Flag of media commands that are supported.""" @@ -199,6 +217,16 @@ class Volumio(MediaPlayerDevice): return self.send_volumio_msg( 'commands', params={'cmd': 'volume', 'volume': int(volume * 100)}) + def async_volume_up(self): + """Service to send the Volumio the command for volume up.""" + return self.send_volumio_msg( + 'commands', params={'cmd': 'volume', 'volume': 'plus'}) + + def async_volume_down(self): + """Service to send the Volumio the command for volume down.""" + return self.send_volumio_msg( + 'commands', params={'cmd': 'volume', 'volume': 'minus'}) + def async_mute_volume(self, mute): """Send mute command to media player.""" mutecmd = 'mute' if mute else 'unmute' @@ -210,3 +238,21 @@ class Volumio(MediaPlayerDevice): return self.send_volumio_msg( 'commands', params={'cmd': 'volume', 'volume': self._lastvol}) + + def async_select_source(self, source): + """Choose a different available playlist and play it.""" + self._currentplaylist = source + return self.send_volumio_msg( + 'commands', params={'cmd': 'playplaylist', 'name': source}) + + def async_clear_playlist(self): + """Clear players playlist.""" + # FIXME + self._currentplaylist = None + return self.send_volumio_msg('clearQueue') + + @asyncio.coroutine + @Throttle(PLAYLIST_UPDATE_INTERVAL) + def _async_update_playlists(self, **kwargs): + """Update available Volumio playlists.""" + self._playlists = yield from self.send_volumio_msg('listplaylists') From a3e36e6c66dff6eb33bc15847b7666df0b098eeb Mon Sep 17 00:00:00 2001 From: led-spb Date: Sat, 3 Feb 2018 01:54:54 +0300 Subject: [PATCH 092/166] Adding information about current TV channel to WebOS media player (#11339) * Added Channel attribute to webos media pplayer * webostv: Current TV channel display as media_title * Added displaying information about the current TV channel for WebOS platform * Fixed PEP8 requirements --- homeassistant/components/media_player/webostv.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index fed442e140e..2081fc95223 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -174,6 +174,7 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_UNKNOWN self._source_list = {} self._app_list = {} + self._channel = None @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): @@ -189,10 +190,12 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_OFF self._current_source = None self._current_source_id = None + self._channel = None if self._state is not STATE_OFF: self._muted = self._client.get_muted() self._volume = self._client.get_volume() + self._channel = self._client.get_current_channel() self._source_list = {} self._app_list = {} @@ -225,6 +228,7 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_OFF self._current_source = None self._current_source_id = None + self._channel = None @property def name(self): @@ -261,6 +265,14 @@ class LgWebOSDevice(MediaPlayerDevice): """Content type of current playing media.""" return MEDIA_TYPE_CHANNEL + @property + def media_title(self): + """Title of current playing media.""" + if (self._channel is not None) and ('channelName' in self._channel): + return self._channel['channelName'] + else: + return None + @property def media_image_url(self): """Image url of current playing media.""" From 0f879a6c60fe08e8316d1c36eef1777a01b5151f Mon Sep 17 00:00:00 2001 From: Anthony Arnaud Date: Fri, 2 Feb 2018 20:22:14 -0500 Subject: [PATCH 093/166] Fix #8475 device tracker ubus tracks unauthenticated and unassociated devices (#12140) --- homeassistant/components/device_tracker/ubus.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index dee5044d3a6..2490e10be6e 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -122,13 +122,18 @@ class UbusDeviceScanner(DeviceScanner): self.last_results = [] results = 0 + # for each access point for hostapd in self.hostapd: result = _req_json_rpc( self.url, self.session_id, 'call', hostapd, 'get_clients') if result: results = results + 1 - self.last_results.extend(result['clients'].keys()) + # Check for each device is authorized (valid wpa key) + for key in result['clients'].keys(): + device = result['clients'][key] + if device['authorized']: + self.last_results.append(key) return bool(results) From 86daec8c59729e75c344261de4fe0da7d96cb5da Mon Sep 17 00:00:00 2001 From: Anthony Arnaud Date: Fri, 2 Feb 2018 20:22:29 -0500 Subject: [PATCH 094/166] Fix #11875 Ubus broken since upgrade to 0 57 (#12141) --- homeassistant/components/device_tracker/ubus.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 2490e10be6e..e66bb95a11a 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -101,7 +101,6 @@ class UbusDeviceScanner(DeviceScanner): if self.mac2name is None: self._generate_mac2name() name = self.mac2name.get(mac.upper(), None) - self.mac2name = None return name @_refresh_on_access_denied From 2cbab48e1b69a9a1d1720bbedf4dc20ddf43efb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 3 Feb 2018 03:22:44 +0200 Subject: [PATCH 095/166] Update flake8-docstrings to 1.0.3 (#12136) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5ba27b4b290..cddf11a34b8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-timeout>=1.2.1 pytest-sugar==0.9.0 requests_mock==1.4 mock-open==1.3.1 -flake8-docstrings==1.0.2 +flake8-docstrings==1.0.3 asynctest>=0.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4ae6d55325..351175ec858 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-timeout>=1.2.1 pytest-sugar==0.9.0 requests_mock==1.4 mock-open==1.3.1 -flake8-docstrings==1.0.2 +flake8-docstrings==1.0.3 asynctest>=0.11.1 From c204a7c78704d7da723261463f3edc23c516b75d Mon Sep 17 00:00:00 2001 From: Nigel Rook Date: Sat, 3 Feb 2018 01:28:54 +0000 Subject: [PATCH 096/166] Tado fixes (#11294) * Fix tado overlay end state Previously, when tado ended an overlay state itself, say because a timer expired or a scheduled temperature change ocurred, the tado climate component would not return to Smart Schedule mode. This change fixes that issue * Correct tado state after multiple rapid updates Previosuly, making two changes to tado climate within 10 seconds, for example setting operation mode to Tado mode, then changing the temperature, would leave the entity showing the incorrect state for up to a minute. This change forces an unthrottled update after setting the climate state, which fixes the issue * Fix comment formatting --- homeassistant/components/climate/tado.py | 2 +- homeassistant/components/tado.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 5b20462c245..868511c0ac4 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -294,7 +294,7 @@ class TadoClimate(ClimateDevice): overlay = False overlay_data = None - termination = self._current_operation + termination = CONST_MODE_SMART_SCHEDULE cooling = False fan_speed = CONST_MODE_OFF diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py index 1f5125d724e..cfba0a5c0c4 100644 --- a/homeassistant/components/tado.py +++ b/homeassistant/components/tado.py @@ -119,8 +119,10 @@ class TadoDataStore: def reset_zone_overlay(self, zone_id): """Wrap for resetZoneOverlay(..).""" - return self.tado.resetZoneOverlay(zone_id) + self.tado.resetZoneOverlay(zone_id) + self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg def set_zone_overlay(self, zone_id, mode, temperature=None, duration=None): """Wrap for setZoneOverlay(..).""" - return self.tado.setZoneOverlay(zone_id, mode, temperature, duration) + self.tado.setZoneOverlay(zone_id, mode, temperature, duration) + self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg From f7c9787418243ca732241ef21c50117faa45fca7 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Sat, 3 Feb 2018 03:17:01 +0100 Subject: [PATCH 097/166] Add Melissa (HVAC/climate) component (#11503) * Adding component melissa * Adding sensor component melissa * Adding Melissa climate component * Testing component * Tests for Climate component * Testing Melissa sensor * Fixing review Thank you @rytilahti --- homeassistant/components/climate/melissa.py | 274 ++++++++++++++++++++ homeassistant/components/melissa.py | 44 ++++ homeassistant/components/sensor/melissa.py | 98 +++++++ requirements_all.txt | 3 + tests/components/climate/test_melissa.py | 264 +++++++++++++++++++ tests/components/sensor/test_melissa.py | 89 +++++++ tests/components/test_melissa.py | 38 +++ tests/fixtures/melissa_cur_settings.json | 28 ++ tests/fixtures/melissa_fetch_devices.json | 27 ++ tests/fixtures/melissa_status.json | 8 + 10 files changed, 873 insertions(+) create mode 100644 homeassistant/components/climate/melissa.py create mode 100644 homeassistant/components/melissa.py create mode 100644 homeassistant/components/sensor/melissa.py create mode 100644 tests/components/climate/test_melissa.py create mode 100644 tests/components/sensor/test_melissa.py create mode 100644 tests/components/test_melissa.py create mode 100644 tests/fixtures/melissa_cur_settings.json create mode 100644 tests/fixtures/melissa_fetch_devices.json create mode 100644 tests/fixtures/melissa_status.json diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py new file mode 100644 index 00000000000..1e4ceac0edf --- /dev/null +++ b/homeassistant/components/climate/melissa.py @@ -0,0 +1,274 @@ +""" +Support for Melissa Climate A/C. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/climate.melissa/ +""" +import logging + +from homeassistant.components.climate import ClimateDevice, \ + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_ON_OFF, \ + STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, \ + SUPPORT_FAN_MODE +from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH +from homeassistant.components.melissa import DATA_MELISSA, DOMAIN +from homeassistant.const import TEMP_CELSIUS, STATE_ON, STATE_OFF, \ + STATE_UNKNOWN, STATE_IDLE, ATTR_TEMPERATURE, PRECISION_WHOLE + +DEPENDENCIES = [DOMAIN] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_ON_OFF | SUPPORT_FAN_MODE) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Iterate through and add all Melissa devices.""" + api = hass.data[DATA_MELISSA] + devices = api.fetch_devices().values() + + all_devices = [] + + for device in devices: + all_devices.append(MelissaClimate( + api, device['serial_number'], device)) + + add_devices(all_devices) + + +class MelissaClimate(ClimateDevice): + """Representation of a Melissa Climate device.""" + + def __init__(self, api, serial_number, init_data): + """Initialize the climate device.""" + self._name = init_data['name'] + self._api = api + self._serial_number = serial_number + self._data = init_data['controller_log'] + self._state = None + self._cur_settings = None + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self._name + + @property + def is_on(self): + """Return current state.""" + if self._cur_settings is not None: + return self._cur_settings[self._api.STATE] in ( + self._api.STATE_ON, self._api.STATE_IDLE) + else: + _LOGGER.info("Can't determine state of %s", self.entity_id) + return STATE_UNKNOWN + + @property + def current_fan_mode(self): + """Return the current fan mode.""" + if self._cur_settings is not None: + return self.melissa_fan_to_hass( + self._cur_settings[self._api.FAN]) + else: + _LOGGER.info( + "Can't determine current fan mode for %s", self.entity_id) + return STATE_UNKNOWN + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._data: + return self._data[self._api.TEMP] + else: + _LOGGER.info( + "Can't determine current temperature for %s", self.entity_id) + return None + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return PRECISION_WHOLE + + @property + def current_operation(self): + """Return the current operation mode.""" + if self._cur_settings is not None: + return self.melissa_op_to_hass( + self._cur_settings[self._api.MODE]) + else: + _LOGGER.info( + "Can't determine current operation mode of %s", self.entity_id) + return STATE_UNKNOWN + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return [ + STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY + ] + + @property + def fan_list(self): + """List of available fan modes.""" + return [ + STATE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH + ] + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._cur_settings is not None: + return self._cur_settings[self._api.TEMP] + else: + _LOGGER.info( + "Can not determine current target temperature for %s", + self.entity_id) + return STATE_UNKNOWN + + @property + def state(self): + """Return current state.""" + if self._cur_settings is not None: + return self.melissa_state_to_hass( + self._cur_settings[self._api.STATE]) + else: + _LOGGER.info("Cant determine current state for %s", self.entity_id) + return STATE_UNKNOWN + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum supported temperature for the thermostat.""" + return 16 + + @property + def max_temp(self): + """Return the maximum supported temperature for the thermostat.""" + return 30 + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + return self.send({self._api.TEMP: temp}) + + def set_fan_mode(self, fan): + """Set fan mode.""" + fan_mode = self.hass_fan_to_melissa(fan) + return self.send({self._api.FAN: fan_mode}) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + mode = self.hass_mode_to_melissa(operation_mode) + return self.send({self._api.MODE: mode}) + + def turn_on(self): + """Turn on device.""" + return self.send({self._api.STATE: self._api.STATE_ON}) + + def turn_off(self): + """Turn off device.""" + return self.send({self._api.STATE: self._api.STATE_OFF}) + + def send(self, value): + """Sending action to service.""" + try: + old_value = self._cur_settings.copy() + self._cur_settings.update(value) + except AttributeError: + old_value = None + if not self._api.send(self._serial_number, self._cur_settings): + self._cur_settings = old_value + return False + else: + return True + + def update(self): + """Get latest data from Melissa.""" + try: + self._data = self._api.status(cached=True)[self._serial_number] + self._cur_settings = self._api.cur_settings( + self._serial_number + )['controller']['_relation']['command_log'] + except KeyError: + _LOGGER.warning( + 'Unable to update component %s', self.entity_id) + + def melissa_state_to_hass(self, state): + """Translate Melissa states to hass states.""" + if state == self._api.STATE_ON: + return STATE_ON + elif state == self._api.STATE_OFF: + return STATE_OFF + elif state == self._api.STATE_IDLE: + return STATE_IDLE + else: + return STATE_UNKNOWN + + 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: + return STATE_HEAT + elif mode == self._api.MODE_COOL: + return STATE_COOL + elif mode == self._api.MODE_DRY: + return STATE_DRY + elif mode == self._api.MODE_FAN: + return STATE_FAN_ONLY + else: + _LOGGER.warning( + "Operation mode %s could not be mapped to hass", mode) + return STATE_UNKNOWN + + def melissa_fan_to_hass(self, fan): + """Translate Melissa fan modes to hass modes.""" + if fan == self._api.FAN_AUTO: + return STATE_AUTO + elif fan == self._api.FAN_LOW: + return SPEED_LOW + elif fan == self._api.FAN_MEDIUM: + return SPEED_MEDIUM + elif fan == self._api.FAN_HIGH: + return SPEED_HIGH + else: + _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) + return STATE_UNKNOWN + + 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: + return self._api.MODE_HEAT + elif mode == STATE_COOL: + return self._api.MODE_COOL + elif mode == STATE_DRY: + return self._api.MODE_DRY + elif mode == STATE_FAN_ONLY: + return self._api.MODE_FAN + else: + _LOGGER.warning("Melissa have no setting for %s mode", mode) + + def hass_fan_to_melissa(self, fan): + """Translate hass fan modes to melissa modes.""" + if fan == STATE_AUTO: + return self._api.FAN_AUTO + elif fan == SPEED_LOW: + return self._api.FAN_LOW + elif fan == SPEED_MEDIUM: + return self._api.FAN_MEDIUM + elif fan == SPEED_HIGH: + return self._api.FAN_HIGH + else: + _LOGGER.warning("Melissa have no setting for %s fan mode", fan) diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa.py new file mode 100644 index 00000000000..ae82b96222e --- /dev/null +++ b/homeassistant/components/melissa.py @@ -0,0 +1,44 @@ +""" +Support for Melissa climate. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/melissa/ +""" +import logging + +import voluptuous as vol + +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"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "melissa" +DATA_MELISSA = 'MELISSA' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Melissa Climate component.""" + import melissa + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + api = melissa.Melissa(username=username, password=password) + hass.data[DATA_MELISSA] = api + + load_platform(hass, 'sensor', DOMAIN, {}) + load_platform(hass, 'climate', DOMAIN, {}) + return True diff --git a/homeassistant/components/sensor/melissa.py b/homeassistant/components/sensor/melissa.py new file mode 100644 index 00000000000..97645cb9dd4 --- /dev/null +++ b/homeassistant/components/sensor/melissa.py @@ -0,0 +1,98 @@ +""" +Support for Melissa climate Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.melissa/ +""" +import logging + +from homeassistant.components.melissa import DOMAIN, DATA_MELISSA +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = [DOMAIN] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the melissa sensor platform.""" + sensors = [] + api = hass.data[DATA_MELISSA] + devices = api.fetch_devices().values() + + for device in devices: + sensors.append(MelissaTemperatureSensor(device, api)) + sensors.append(MelissaHumiditySensor(device, api)) + add_devices(sensors) + + +class MelissaSensor(Entity): + """Representation of a Melissa Sensor.""" + + _type = 'generic' + + def __init__(self, device, api): + """Initialize the sensor.""" + self._api = api + self._state = STATE_UNKNOWN + self._name = '{0} {1}'.format( + device['name'], + self._type + ) + self._serial = device['serial_number'] + self._data = device['controller_log'] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Fetch status from melissa.""" + self._data = self._api.status(cached=True) + + +class MelissaTemperatureSensor(MelissaSensor): + """Representation of a Melissa temperature Sensor.""" + + _type = 'temperature' + _unit = TEMP_CELSIUS + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data for the sensor.""" + super().update() + try: + self._state = self._data[self._serial]['temp'] + except KeyError: + _LOGGER.warning("Unable to get temperature for %s", self.entity_id) + + +class MelissaHumiditySensor(MelissaSensor): + """Representation of a Melissa humidity Sensor.""" + + _type = 'humidity' + _unit = '%' + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data for the sensor.""" + super().update() + try: + self._state = self._data[self._serial]['humidity'] + except KeyError: + _LOGGER.warning("Unable to get humidity for %s", self.entity_id) diff --git a/requirements_all.txt b/requirements_all.txt index 20dc82cb95a..3bf11f9e27c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -623,6 +623,9 @@ py-canary==0.2.3 # homeassistant.components.sensor.cpuspeed py-cpuinfo==3.3.0 +# homeassistant.components.melissa +py-melissa-climate==1.0.1 + # 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 new file mode 100644 index 00000000000..ef5cbff5087 --- /dev/null +++ b/tests/components/climate/test_melissa.py @@ -0,0 +1,264 @@ +"""Test for Melissa climate component.""" +import unittest +from unittest.mock import Mock, patch +import json + +from asynctest import mock + +from homeassistant.components.climate import melissa, \ + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF, \ + SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY, STATE_COOL, \ + STATE_AUTO +from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.const import TEMP_CELSIUS, STATE_ON, ATTR_TEMPERATURE, \ + STATE_OFF, STATE_IDLE, STATE_UNKNOWN +from tests.common import get_test_home_assistant, load_fixture + + +class TestMelissa(unittest.TestCase): + """Tests for Melissa climate.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up test variables.""" + self.hass = get_test_home_assistant() + self._serial = '12345678' + + self.api = Mock() + self.api.fetch_devices.return_value = json.loads(load_fixture( + 'melissa_fetch_devices.json' + )) + self.api.cur_settings.return_value = json.loads(load_fixture( + 'melissa_cur_settings.json' + )) + self.api.status.return_value = json.loads(load_fixture( + 'melissa_status.json' + )) + self.api.STATE_OFF = 0 + self.api.STATE_ON = 1 + self.api.STATE_IDLE = 2 + + self.api.MODE_AUTO = 0 + self.api.MODE_FAN = 1 + self.api.MODE_HEAT = 2 + self.api.MODE_COOL = 3 + self.api.MODE_DRY = 4 + + self.api.FAN_AUTO = 0 + self.api.FAN_LOW = 1 + self.api.FAN_MEDIUM = 2 + self.api.FAN_HIGH = 3 + + self.api.STATE = 'state' + self.api.MODE = 'mode' + self.api.FAN = 'fan' + self.api.TEMP = 'temp' + + device = self.api.fetch_devices()[self._serial] + self.thermostat = melissa.MelissaClimate( + self.api, device['serial_number'], device) + self.thermostat.update() + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + @patch("homeassistant.components.climate.melissa.MelissaClimate") + def test_setup_platform(self, mocked_thermostat): + """Test setup_platform.""" + device = self.api.fetch_devices()[self._serial] + thermostat = mocked_thermostat(self.api, device['serial_number'], + device) + thermostats = [thermostat] + + self.hass.data[DATA_MELISSA] = self.api + + config = {} + add_devices = Mock() + discovery_info = {} + + melissa.setup_platform(self.hass, config, add_devices, discovery_info) + add_devices.assert_called_once_with(thermostats) + + def test_get_name(self): + """Test name property.""" + self.assertEqual("Melissa 12345678", self.thermostat.name) + + def test_is_on(self): + """Test name property.""" + self.assertEqual(self.thermostat.is_on, True) + self.thermostat._cur_settings = None + self.assertEqual(STATE_UNKNOWN, self.thermostat.is_on) + + def test_current_fan_mode(self): + """Test current_fan_mode property.""" + self.thermostat.update() + self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) + self.thermostat._cur_settings = None + self.assertEqual(STATE_UNKNOWN, self.thermostat.current_fan_mode) + + def test_current_temperature(self): + """Test current temperature.""" + self.assertEqual(27.4, self.thermostat.current_temperature) + + def test_current_temperature_no_data(self): + """Test current temperature without data.""" + self.thermostat._data = None + self.assertIsNone(self.thermostat.current_temperature) + + def test_target_temperature_step(self): + """Test current target_temperature_step.""" + self.assertEqual(1, self.thermostat.target_temperature_step) + + def test_current_operation(self): + """Test current operation.""" + self.thermostat.update() + self.assertEqual(self.thermostat.current_operation, STATE_HEAT) + self.thermostat._cur_settings = None + self.assertEqual(STATE_UNKNOWN, self.thermostat.current_operation) + + def test_operation_list(self): + """Test the operation list.""" + self.assertEqual( + [STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY], + self.thermostat.operation_list + ) + + def test_fan_list(self): + """Test the fan list.""" + self.assertEqual( + [STATE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + self.thermostat.fan_list + ) + + def test_target_temperature(self): + """Test target temperature.""" + self.assertEqual(16, self.thermostat.target_temperature) + self.thermostat._cur_settings = None + self.assertEqual(STATE_UNKNOWN, self.thermostat.target_temperature) + + def test_state(self): + """Test state.""" + self.assertEqual(STATE_ON, self.thermostat.state) + self.thermostat._cur_settings = None + self.assertEqual(STATE_UNKNOWN, self.thermostat.state) + + def test_temperature_unit(self): + """Test temperature unit.""" + self.assertEqual(TEMP_CELSIUS, self.thermostat.temperature_unit) + + def test_min_temp(self): + """Test min temp.""" + self.assertEqual(16, self.thermostat.min_temp) + + def test_max_temp(self): + """Test max temp.""" + self.assertEqual(30, self.thermostat.max_temp) + + def test_supported_features(self): + """Test supported_features property.""" + features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_ON_OFF | SUPPORT_FAN_MODE) + self.assertEqual(features, self.thermostat.supported_features) + + def test_set_temperature(self): + """Test set_temperature.""" + self.api.send.return_value = True + self.thermostat.update() + self.assertTrue(self.thermostat.set_temperature( + **{ATTR_TEMPERATURE: 25})) + self.assertEqual(25, self.thermostat.target_temperature) + + def test_fan_mode(self): + """Test set_fan_mode.""" + self.api.send.return_value = True + self.assertTrue(self.thermostat.set_fan_mode(SPEED_LOW)) + self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) + + def test_set_operation_mode(self): + """Test set_operation_mode.""" + self.api.send.return_value = True + self.assertTrue(self.thermostat.set_operation_mode(STATE_COOL)) + self.assertEqual(STATE_COOL, self.thermostat.current_operation) + + def test_turn_on(self): + """Test turn_on.""" + self.assertTrue(self.thermostat.turn_on()) + + def test_turn_off(self): + """Test turn_off.""" + self.assertTrue(self.thermostat.turn_off()) + + def test_send(self): + """Test send.""" + self.thermostat.update() + self.assertTrue(self.thermostat.send( + {'fan': self.api.FAN_MEDIUM})) + self.assertEqual(SPEED_MEDIUM, self.thermostat.current_fan_mode) + self.api.send.return_value = False + self.thermostat._cur_settings = None + self.assertFalse(self.thermostat.send({ + 'fan': self.api.FAN_LOW})) + self.assertNotEquals(SPEED_LOW, self.thermostat.current_fan_mode) + self.assertIsNone(self.thermostat._cur_settings) + + @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') + def test_update(self, mocked_warning): + """Test update.""" + self.thermostat.update() + self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) + self.assertEqual(STATE_HEAT, self.thermostat.current_operation) + self.thermostat._api.status.side_effect = KeyError('boom') + self.thermostat.update() + mocked_warning.assert_called_once_with( + 'Unable to update component %s', self.thermostat.entity_id) + + def test_melissa_state_to_hass(self): + """Test for translate melissa states to hass.""" + self.assertEqual(STATE_OFF, self.thermostat.melissa_state_to_hass(0)) + self.assertEqual(STATE_ON, self.thermostat.melissa_state_to_hass(1)) + self.assertEqual(STATE_IDLE, self.thermostat.melissa_state_to_hass(2)) + self.assertEqual(STATE_UNKNOWN, + self.thermostat.melissa_state_to_hass(3)) + + 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)) + self.assertEqual(STATE_DRY, self.thermostat.melissa_op_to_hass(4)) + self.assertEqual( + STATE_UNKNOWN, self.thermostat.melissa_op_to_hass(5)) + + def test_melissa_fan_to_hass(self): + """Test for translate melissa fan state to hass.""" + self.assertEqual(STATE_AUTO, self.thermostat.melissa_fan_to_hass(0)) + self.assertEqual(SPEED_LOW, self.thermostat.melissa_fan_to_hass(1)) + self.assertEqual(SPEED_MEDIUM, self.thermostat.melissa_fan_to_hass(2)) + self.assertEqual(SPEED_HIGH, self.thermostat.melissa_fan_to_hass(3)) + self.assertEqual(STATE_UNKNOWN, self.thermostat.melissa_fan_to_hass(4)) + + @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)) + self.assertEqual(3, self.thermostat.hass_mode_to_melissa(STATE_COOL)) + self.assertEqual(4, self.thermostat.hass_mode_to_melissa(STATE_DRY)) + self.thermostat.hass_mode_to_melissa("test") + mocked_warning.assert_called_once_with( + "Melissa have no setting for %s mode", "test") + + @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') + def test_hass_fan_to_melissa(self, mocked_warning): + """Test for translate melissa states to hass.""" + self.assertEqual(0, self.thermostat.hass_fan_to_melissa(STATE_AUTO)) + self.assertEqual(1, self.thermostat.hass_fan_to_melissa(SPEED_LOW)) + self.assertEqual(2, self.thermostat.hass_fan_to_melissa(SPEED_MEDIUM)) + self.assertEqual(3, self.thermostat.hass_fan_to_melissa(SPEED_HIGH)) + self.thermostat.hass_fan_to_melissa("test") + mocked_warning.assert_called_once_with( + "Melissa have no setting for %s fan mode", "test") diff --git a/tests/components/sensor/test_melissa.py b/tests/components/sensor/test_melissa.py new file mode 100644 index 00000000000..3a13020438f --- /dev/null +++ b/tests/components/sensor/test_melissa.py @@ -0,0 +1,89 @@ +"""Test for Melissa climate component.""" +import unittest +import json +from unittest.mock import Mock + +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.components.sensor import melissa +from homeassistant.components.sensor.melissa import MelissaTemperatureSensor, \ + MelissaHumiditySensor +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from tests.common import get_test_home_assistant, load_fixture + + +class TestMelissa(unittest.TestCase): + """Tests for Melissa climate.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up test variables.""" + self.hass = get_test_home_assistant() + self._serial = '12345678' + + self.api = Mock() + self.api.fetch_devices.return_value = json.loads(load_fixture( + 'melissa_fetch_devices.json' + )) + self.api.status.return_value = json.loads(load_fixture( + 'melissa_status.json' + )) + + self.api.TEMP = 'temp' + self.api.HUMIDITY = 'humidity' + device = self.api.fetch_devices()[self._serial] + self.temp = MelissaTemperatureSensor(device, self.api) + self.hum = MelissaHumiditySensor(device, self.api) + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + def test_setup_platform(self): + """Test setup_platform.""" + self.hass.data[DATA_MELISSA] = self.api + + config = {} + add_devices = Mock() + discovery_info = {} + + melissa.setup_platform(self.hass, config, add_devices, discovery_info) + + def test_name(self): + """Test name property.""" + device = self.api.fetch_devices()[self._serial] + self.assertEqual(self.temp.name, '{0} {1}'.format( + device['name'], + self.temp._type + )) + self.assertEqual(self.hum.name, '{0} {1}'.format( + device['name'], + self.hum._type + )) + + def test_state(self): + """Test state property.""" + device = self.api.status()[self._serial] + self.temp.update() + self.assertEqual(self.temp.state, device[self.api.TEMP]) + self.hum.update() + self.assertEqual(self.hum.state, device[self.api.HUMIDITY]) + + def test_unit_of_measurement(self): + """Test unit of measurement property.""" + self.assertEqual(self.temp.unit_of_measurement, TEMP_CELSIUS) + self.assertEqual(self.hum.unit_of_measurement, '%') + + def test_update(self): + """Test for update.""" + self.temp.update() + self.assertEqual(self.temp.state, 27.4) + self.hum.update() + self.assertEqual(self.hum.state, 18.7) + + def test_update_keyerror(self): + """Test for faulty update.""" + self.temp._api.status.return_value = {} + self.temp.update() + self.assertEqual(STATE_UNKNOWN, self.temp.state) + self.hum._api.status.return_value = {} + self.hum.update() + self.assertEqual(STATE_UNKNOWN, self.hum.state) diff --git a/tests/components/test_melissa.py b/tests/components/test_melissa.py new file mode 100644 index 00000000000..e39ceb1add1 --- /dev/null +++ b/tests/components/test_melissa.py @@ -0,0 +1,38 @@ +"""The test for the Melissa Climate component.""" +import unittest +from tests.common import get_test_home_assistant, MockDependency + +from homeassistant.components import melissa + +VALID_CONFIG = { + "melissa": { + "username": "********", + "password": "********", + } +} + + +class TestMelissa(unittest.TestCase): + """Test the Melissa component.""" + + def setUp(self): # pylint: disable=invalid-name + """Initialize the values for this test class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + @MockDependency("melissa") + def test_setup(self, mocked_melissa): + """Test setting up the Melissa component.""" + melissa.setup(self.hass, self.config) + + mocked_melissa.Melissa.assert_called_with( + username="********", password="********") + self.assertIn(melissa.DATA_MELISSA, self.hass.data) + self.assertIsInstance( + self.hass.data[melissa.DATA_MELISSA], type( + mocked_melissa.Melissa()) + ) diff --git a/tests/fixtures/melissa_cur_settings.json b/tests/fixtures/melissa_cur_settings.json new file mode 100644 index 00000000000..9d7fb615330 --- /dev/null +++ b/tests/fixtures/melissa_cur_settings.json @@ -0,0 +1,28 @@ +{ + "controller": { + "id": 1, + "user_id": 1, + "serial_number": "12345678", + "mac": "12345678", + "firmware_version": "V1SHTHF", + "name": "Melissa 12345678", + "type": "melissa", + "room_id": null, + "created": "2016-07-06 18:59:46", + "deleted_at": null, + "online": true, + "_relation": { + "command_log": { + "state": 1, + "mode": 2, + "temp": 16, + "fan": 1 + } + } + }, + "_links": { + "self": { + "href": "/v1/controllers/12345678" + } + } +} diff --git a/tests/fixtures/melissa_fetch_devices.json b/tests/fixtures/melissa_fetch_devices.json new file mode 100644 index 00000000000..4b106a613f7 --- /dev/null +++ b/tests/fixtures/melissa_fetch_devices.json @@ -0,0 +1,27 @@ +{ + "12345678": { + "user_id": 1, + "serial_number": "12345678", + "mac": "12345678", + "firmware_version": "V1SHTHF", + "name": "Melissa 12345678", + "type": "melissa", + "room_id": null, + "created": "2016-07-06 18:59:46", + "id": 1, + "online": true, + "brand_id": 1, + "controller_log": { + "temp": 27.4, + "created": "2018-01-08T21:01:14.281Z", + "raw_temperature": 28928, + "humidity": 18.7, + "raw_humidity": 12946 + }, + "_links": { + "self": { + "href": "/v1/controllers" + } + } + } +} diff --git a/tests/fixtures/melissa_status.json b/tests/fixtures/melissa_status.json new file mode 100644 index 00000000000..ac240b3df12 --- /dev/null +++ b/tests/fixtures/melissa_status.json @@ -0,0 +1,8 @@ +{ + "12345678": { + "temp": 27.4, + "raw_temperature": 28928, + "humidity": 18.7, + "raw_humidity": 12946 + } +} From 880f18a37ef0f076f8645d2d56d9644642177e46 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 3 Feb 2018 13:29:55 +0000 Subject: [PATCH 098/166] Mediaroom (#11864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * make port mapping optional * dependencies + improvements * Added bytes and packets sensors from IGD * flake8 check * new sensor with upnp counters * checks * whitespaces in blank line * requirements update * added sensor.upnp to .coveragerc * downgrade miniupnpc Latest version of miniupnpc is 2.0, but pypi only has 1.9 Fortunately it is enough * revert to non async miniupnpc will do network calls, so this component can’t be moved to coroutine * hof hof forgot to remove import ot asyncio * Add baudrate option * merge * Added Mediaroom media_player component * Updated header Works with MEO and VDF set-top boxes in Portugal * formatting * Development Checklist (done) * fix formatting according to houndci-bot * more format fixing (tks houndci-bot) * more fixes * too much cleanup... * too much * pylint check * Initial commit Basic configuration testing * flake8 and lint --- .coveragerc | 1 + .../components/media_player/mediaroom.py | 199 ++++++++++++++++++ requirements_all.txt | 3 + .../components/media_player/test_mediaroom.py | 32 +++ 4 files changed, 235 insertions(+) create mode 100644 homeassistant/components/media_player/mediaroom.py create mode 100644 tests/components/media_player/test_mediaroom.py diff --git a/.coveragerc b/.coveragerc index 7287dcb143f..bef4f1cf1ee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -439,6 +439,7 @@ omit = homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/liveboxplaytv.py + homeassistant/components/media_player/mediaroom.py homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/nad.py diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py new file mode 100644 index 00000000000..549ad931e35 --- /dev/null +++ b/homeassistant/components/media_player/mediaroom.py @@ -0,0 +1,199 @@ +""" +Support for the Mediaroom Set-up-box. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.mediaroom/ +""" +import logging +import voluptuous as vol + +from homeassistant.components.media_player import ( + MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, + STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, + STATE_ON) +import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['pymediaroom==0.5'] + +_LOGGER = logging.getLogger(__name__) + +NOTIFICATION_TITLE = 'Mediaroom Media Player Setup' +NOTIFICATION_ID = 'mediaroom_notification' +DEFAULT_NAME = 'Mediaroom STB' +DEFAULT_TIMEOUT = 9 + +KNOWN_HOSTS = [] + +SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Mediaroom platform.""" + hosts = [] + + host = config.get(CONF_HOST, None) + if host is None: + _LOGGER.info("Trying to discover Mediaroom STB") + + from pymediaroom import Remote + + host = Remote.discover(KNOWN_HOSTS) + if host is None: + # Can't find any STB + return False + hosts.append(host) + KNOWN_HOSTS.append(host) + + stbs = [] + + try: + for host in hosts: + stbs.append(MediaroomDevice( + config.get(CONF_NAME), + host, + config.get(CONF_OPTIMISTIC), + config.get(CONF_TIMEOUT) + )) + + except ConnectionRefusedError: + hass.components.persistent_notification.create( + 'Error: Unable to initialize mediaroom at {}
' + 'Check its network connection or consider ' + 'using auto discovery.
' + 'You will need to restart hass after fixing.' + ''.format(host), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + add_devices(stbs) + + return True + + +class MediaroomDevice(MediaPlayerDevice): + """Representation of a Mediaroom set-up-box on the network.""" + + def __init__(self, name, host, optimistic=False, timeout=DEFAULT_TIMEOUT): + """Initialize the device.""" + from pymediaroom import Remote + + self.stb = Remote(host, timeout=timeout) + _LOGGER.info( + "Found %s at %s%s", name, host, + " - I'm optimistic" if optimistic else "") + self._name = name + self._is_standby = not optimistic + self._current = None + self._optimistic = optimistic + self._state = STATE_STANDBY + + def update(self): + """Retrieve latest state.""" + if not self._optimistic: + self._is_standby = self.stb.get_standby() + if self._is_standby: + self._state = STATE_STANDBY + elif self._state not in [STATE_PLAYING, STATE_PAUSED]: + self._state = STATE_PLAYING + _LOGGER.debug( + "%s(%s) is [%s]", + self._name, self.stb.stb_ip, self._state) + + def play_media(self, media_type, media_id, **kwargs): + """Play media.""" + _LOGGER.debug( + "%s(%s) Play media: %s (%s)", + self._name, self.stb.stb_ip, media_id, media_type) + if media_type != MEDIA_TYPE_CHANNEL: + _LOGGER.error('invalid media type') + return + if media_id.isdigit(): + media_id = int(media_id) + else: + return + self.stb.send_cmd(media_id) + self._state = STATE_PLAYING + + @property + def name(self): + """Return the name of the device.""" + return self._name + + # MediaPlayerDevice properties and methods + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_MEDIAROOM + + @property + def media_content_type(self): + """Return the content type of current playing media.""" + return MEDIA_TYPE_CHANNEL + + def turn_on(self): + """Turn on the receiver.""" + self.stb.send_cmd('Power') + self._state = STATE_ON + + def turn_off(self): + """Turn off the receiver.""" + self.stb.send_cmd('Power') + self._state = STATE_STANDBY + + def media_play(self): + """Send play command.""" + _LOGGER.debug("media_play()") + self.stb.send_cmd('PlayPause') + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self.stb.send_cmd('PlayPause') + self._state = STATE_PAUSED + + def media_stop(self): + """Send stop command.""" + self.stb.send_cmd('Stop') + self._state = STATE_PAUSED + + def media_previous_track(self): + """Send Program Down command.""" + self.stb.send_cmd('ProgDown') + self._state = STATE_PLAYING + + def media_next_track(self): + """Send Program Up command.""" + self.stb.send_cmd('ProgUp') + self._state = STATE_PLAYING + + def volume_up(self): + """Send volume up command.""" + self.stb.send_cmd('VolUp') + + def volume_down(self): + """Send volume up command.""" + self.stb.send_cmd('VolDown') + + def mute_volume(self, mute): + """Send mute command.""" + self.stb.send_cmd('Mute') diff --git a/requirements_all.txt b/requirements_all.txt index 3bf11f9e27c..9750d3700ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -789,6 +789,9 @@ pylutron==0.1.0 # homeassistant.components.notify.mailgun pymailgunner==1.4 +# homeassistant.components.media_player.mediaroom +pymediaroom==0.5 + # homeassistant.components.mochad pymochad==0.2.0 diff --git a/tests/components/media_player/test_mediaroom.py b/tests/components/media_player/test_mediaroom.py new file mode 100644 index 00000000000..7c7922b87be --- /dev/null +++ b/tests/components/media_player/test_mediaroom.py @@ -0,0 +1,32 @@ +"""The tests for the mediaroom media_player.""" + +import unittest + +from homeassistant.setup import setup_component +import homeassistant.components.media_player as media_player +from tests.common import ( + assert_setup_component, get_test_home_assistant) + + +class TestMediaroom(unittest.TestCase): + """Tests the Mediaroom Component.""" + + def setUp(self): + """Initialize values for this test case class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that we started.""" + self.hass.stop() + + def test_mediaroom_config(self): + """Test set up the platform with basic configuration.""" + config = { + media_player.DOMAIN: { + 'platform': 'mediaroom', + 'name': 'Living Room' + } + } + with assert_setup_component(1, media_player.DOMAIN) as result_config: + assert setup_component(self.hass, media_player.DOMAIN, config) + assert result_config[media_player.DOMAIN] From acc767cdb1d37b669174bd5fe7d55329ec7b451f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 3 Feb 2018 16:55:19 +0100 Subject: [PATCH 099/166] Upgrade mutagen to 1.40.0 (#12152) --- homeassistant/components/tts/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d85b7d189c5..532b4529eca 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['mutagen==1.39'] +REQUIREMENTS = ['mutagen==1.40.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9750d3700ce..f6d5a69fe2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ miniupnpc==2.0.2 motorparts==1.0.2 # homeassistant.components.tts -mutagen==1.39 +mutagen==1.40.0 # homeassistant.components.mychevy mychevy==0.1.1 From c144a3339f45e8800d7f9dddad38696ac59430b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 3 Feb 2018 16:56:35 +0100 Subject: [PATCH 100/166] Upgrade TwitterAPI to 2.4.8 (#12148) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 6cb98e45274..c6f4fa0dd5f 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.4.6'] +REQUIREMENTS = ['TwitterAPI==2.4.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f6d5a69fe2d..bcafa0064fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,7 @@ SoCo==0.13 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.4.6 +TwitterAPI==2.4.8 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 4ac9e7edf4cdecd30d3a25f13deac4902d4bbca0 Mon Sep 17 00:00:00 2001 From: "ruohan.chen" Date: Sat, 3 Feb 2018 23:59:19 +0800 Subject: [PATCH 101/166] fix generic_thermostat bug when restore state from HA start up (#12134) * fix generic_thermostat bug when restore state from HA start up if you don't set "initial_operation_mode" in config, you will get `self._enabled = True` when init GenericThermostat. And then you will miss the `if self._current_operation != STATE_OFF` statement and the self._enabled still keep `True`. That's the problem * add a test to describe the restore case --- .../components/climate/generic_thermostat.py | 4 +- .../climate/test_generic_thermostat.py | 85 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 7436053b7d4..c66e611c8e9 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -173,8 +173,8 @@ class GenericThermostat(ClimateDevice): old_state.attributes[ATTR_OPERATION_MODE] is not None): self._current_operation = \ old_state.attributes[ATTR_OPERATION_MODE] - if self._current_operation != STATE_OFF: - self._enabled = True + self._enabled = self._current_operation != STATE_OFF + else: # No previous state, try and restore defaults if self._target_temp is None: diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 190eb7e8522..abc9e6d74c2 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -14,6 +14,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, STATE_OFF, + STATE_IDLE, TEMP_CELSIUS, ATTR_TEMPERATURE ) @@ -170,7 +171,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_setup_defaults_to_unknown(self): """Test the setting of defaults to unknown.""" - self.assertEqual('idle', self.hass.states.get(ENTITY).state) + self.assertEqual(STATE_IDLE, self.hass.states.get(ENTITY).state) def test_default_setup_params(self): """Test the setup with default parameters.""" @@ -964,6 +965,7 @@ def test_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 20) assert(state.attributes[climate.ATTR_OPERATION_MODE] == "off") + assert(state.state == STATE_OFF) @asyncio.coroutine @@ -990,3 +992,84 @@ def test_no_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 22) + assert(state.state == STATE_OFF) + + +class TestClimateGenericThermostatRestoreState(unittest.TestCase): + """Test generic thermostat when restore state from HA startup.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.temperature_unit = TEMP_CELSIUS + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_restore_state_uncoherence_case(self): + """ + Test restore from a strange state. + + - Turn the generic thermostat off + - Restart HA and restore state from DB + """ + self._mock_restore_cache(temperature=20) + + self._setup_switch(False) + self._setup_sensor(15) + self._setup_climate() + self.hass.block_till_done() + + state = self.hass.states.get(ENTITY) + self.assertEqual(20, state.attributes[ATTR_TEMPERATURE]) + self.assertEqual(STATE_OFF, + state.attributes[climate.ATTR_OPERATION_MODE]) + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(0, len(self.calls)) + + self._setup_switch(False) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(STATE_OFF, + state.attributes[climate.ATTR_OPERATION_MODE]) + self.assertEqual(STATE_OFF, state.state) + + def _setup_climate(self): + assert setup_component(self.hass, climate.DOMAIN, {'climate': { + 'platform': 'generic_thermostat', + 'name': 'test', + 'cold_tolerance': 2, + 'hot_tolerance': 4, + 'away_temp': 30, + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR, + 'ac_mode': True + }}) + + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): + """Setup the test sensor.""" + self.hass.states.set(ENT_SENSOR, temp, { + ATTR_UNIT_OF_MEASUREMENT: unit + }) + + def _setup_switch(self, is_on): + """Setup the test switch.""" + self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) + self.calls = [] + + @callback + def log_call(call): + """Log service calls.""" + self.calls.append(call) + + self.hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, log_call) + self.hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, log_call) + + def _mock_restore_cache(self, temperature=20, operation_mode=STATE_OFF): + mock_restore_cache(self.hass, ( + State(ENTITY, '0', { + ATTR_TEMPERATURE: str(temperature), + climate.ATTR_OPERATION_MODE: operation_mode, + ATTR_AWAY_MODE: "on"}), + )) From 9b0dbf3fbe8a5010f7b070567122246e667c2fe5 Mon Sep 17 00:00:00 2001 From: ErnstEeldert Date: Sat, 3 Feb 2018 17:08:00 +0100 Subject: [PATCH 102/166] Adding xy_color attribute support to deconz lights (#12106) --- homeassistant/components/light/deconz.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 82df352e5af..529917c36e2 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -9,7 +9,7 @@ import asyncio from homeassistant.components.deconz import DOMAIN as DECONZ_DATA from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) from homeassistant.core import callback @@ -134,6 +134,9 @@ class DeconzLight(Light): data['xy'] = xyb[0], xyb[1] data['bri'] = xyb[2] + if ATTR_XY_COLOR in kwargs: + data['xy'] = kwargs[ATTR_XY_COLOR] + if ATTR_BRIGHTNESS in kwargs: data['bri'] = kwargs[ATTR_BRIGHTNESS] From 1aca6f922f751173aecc26a17e1cf8adf4513fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Sat, 3 Feb 2018 17:08:48 +0100 Subject: [PATCH 103/166] update python-openzwave to 4.1.3 (#12057) * update python-openzwave to 4.1.0 * 0.4.1.3 --- homeassistant/components/zwave/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 7b8f471850b..0149bb9287a 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -33,7 +33,7 @@ from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS from .util import check_node_schema, check_value_schema, node_name -REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.0.35'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bcafa0064fc..d0529079204 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -972,7 +972,7 @@ python-wink==1.7.3 python_opendata_transport==0.0.3 # homeassistant.components.zwave -python_openzwave==0.4.0.35 +python_openzwave==0.4.3 # homeassistant.components.alarm_control_panel.egardia pythonegardia==1.0.26 From b33d89326f717063eeacf1626b117a8da95eda6b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 3 Feb 2018 16:51:35 +0000 Subject: [PATCH 104/166] Panasonic viera new services (#11963) * Implemented play_media Panasonic Viera TV has a full blown Web Browser that can play any media * media stop * format checks * added SUPPORT_* * bump version * Tks @rytilahti * one too many --- .../components/media_player/__init__.py | 1 + .../media_player/panasonic_viera.py | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 91bcb4d8af0..06e89548785 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -88,6 +88,7 @@ MEDIA_TYPE_VIDEO = 'movie' MEDIA_TYPE_EPISODE = 'episode' MEDIA_TYPE_CHANNEL = 'channel' MEDIA_TYPE_PLAYLIST = 'playlist' +MEDIA_TYPE_URL = 'url' SUPPORT_PAUSE = 1 SUPPORT_SEEK = 2 diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 3e5e80d7545..1a14ab0289b 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -11,7 +11,8 @@ 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_PLAY, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MEDIA_TYPE_URL, + SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) @@ -30,7 +31,8 @@ DEFAULT_PORT = 55000 SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_TURN_OFF | SUPPORT_PLAY + SUPPORT_TURN_OFF | SUPPORT_PLAY | \ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -184,3 +186,19 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def media_previous_track(self): """Send the previous track command.""" self.send_key('NRC_REW-ONOFF') + + def play_media(self, media_type, media_id, **kwargs): + """Play media.""" + _LOGGER.debug("Play media: %s (%s)", media_id, media_type) + + if media_type == MEDIA_TYPE_URL: + try: + self._remote.open_webpage(media_id) + except (TimeoutError, OSError): + self._state = STATE_OFF + else: + _LOGGER.warning("Unsupported media_type: %s", media_type) + + def media_stop(self): + """Stop playback.""" + self.send_key('NRC_CANCEL-ONOFF') From c209c108873616ba378bf5a53b2fafb56e849e01 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 3 Feb 2018 16:58:34 +0000 Subject: [PATCH 105/166] [Mediaroom media_player] Follow up on PR #11864 (#12155) * addresses @MartinHjelmare on #11864 * as requested by @MartinHjelmare --- .../components/media_player/mediaroom.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index 549ad931e35..3cf0ecdb232 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.mediaroom/ """ import logging + import voluptuous as vol from homeassistant.components.media_player import ( @@ -26,8 +27,7 @@ NOTIFICATION_TITLE = 'Mediaroom Media Player Setup' NOTIFICATION_ID = 'mediaroom_notification' DEFAULT_NAME = 'Mediaroom STB' DEFAULT_TIMEOUT = 9 - -KNOWN_HOSTS = [] +DATA_MEDIAROOM = "mediaroom_known_stb" SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ @@ -46,18 +46,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Mediaroom platform.""" hosts = [] + known_hosts = hass.data.get(DATA_MEDIAROOM) + if known_hosts is None: + known_hosts = hass.data[DATA_MEDIAROOM] = [] + host = config.get(CONF_HOST, None) if host is None: _LOGGER.info("Trying to discover Mediaroom STB") from pymediaroom import Remote - host = Remote.discover(KNOWN_HOSTS) + host = Remote.discover(known_hosts) if host is None: - # Can't find any STB - return False + _LOGGER.warning("Can't find any STB") + return hosts.append(host) - KNOWN_HOSTS.append(host) + known_hosts.append(host) stbs = [] @@ -82,8 +86,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(stbs) - return True - class MediaroomDevice(MediaPlayerDevice): """Representation of a Mediaroom set-up-box on the network.""" From 8fe339d2a8e7da92b3311a0254c0ed0f97fada2a Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Sat, 3 Feb 2018 19:09:16 +0100 Subject: [PATCH 106/166] Tests for samsungtv (#11933) * Testing samsungtv * Remove samsungtv from .coveragerc --- .coveragerc | 1 - .../components/media_player/test_samsungtv.py | 271 ++++++++++++++++++ 2 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 tests/components/media_player/test_samsungtv.py diff --git a/.coveragerc b/.coveragerc index bef4f1cf1ee..e30e3025d16 100644 --- a/.coveragerc +++ b/.coveragerc @@ -454,7 +454,6 @@ omit = homeassistant/components/media_player/roku.py homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rnet.py - homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/spotify.py diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py new file mode 100644 index 00000000000..c3753eb53b5 --- /dev/null +++ b/tests/components/media_player/test_samsungtv.py @@ -0,0 +1,271 @@ +"""Tests for samsungtv Components.""" +import unittest +from subprocess import CalledProcessError + +from asynctest import mock + +import tests.common +from homeassistant.components.media_player import SUPPORT_TURN_ON +from homeassistant.components.media_player.samsungtv import setup_platform, \ + CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \ + CONF_MAC, STATE_OFF +from tests.common import MockDependency +from homeassistant.util import dt as dt_util +from datetime import timedelta + +WORKING_CONFIG = { + CONF_HOST: 'fake', + CONF_NAME: 'fake', + CONF_PORT: 8001, + CONF_TIMEOUT: 10, + CONF_MAC: 'fake' +} + +DISCOVERY_INFO = { + 'name': 'fake', + 'model_name': 'fake', + 'host': 'fake' +} + + +class PackageException(Exception): + """Dummy Exception.""" + + +class TestSamsungTv(unittest.TestCase): + """Testing Samsungtv component.""" + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + def setUp(self, samsung_mock, wol_mock): + """Setting up test environment.""" + self.hass = tests.common.get_test_home_assistant() + self.hass.start() + self.hass.block_till_done() + self.device = SamsungTVDevice(**WORKING_CONFIG) + self.device._exceptions_class = mock.Mock() + self.device._exceptions_class.UnhandledResponse = PackageException + self.device._exceptions_class.AccessDenied = PackageException + self.device._exceptions_class.ConnectionClosed = PackageException + + def tearDown(self): + """Tear down test data.""" + self.hass.stop() + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + def test_setup(self, samsung_mock, wol_mock): + """Testing setup of platform.""" + with mock.patch( + 'homeassistant.components.media_player.samsungtv.socket'): + add_devices = mock.Mock() + setup_platform( + self.hass, WORKING_CONFIG, add_devices) + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + def test_setup_discovery(self, samsung_mock, wol_mock): + """Testing setup of platform with discovery.""" + with mock.patch( + 'homeassistant.components.media_player.samsungtv.socket'): + add_devices = mock.Mock() + setup_platform(self.hass, {}, add_devices, + discovery_info=DISCOVERY_INFO) + + @MockDependency('samsungctl') + @MockDependency('wakeonlan') + @mock.patch( + 'homeassistant.components.media_player.samsungtv._LOGGER.warning') + def test_setup_none(self, samsung_mock, wol_mock, mocked_warn): + """Testing setup of platform with no data.""" + with mock.patch( + 'homeassistant.components.media_player.samsungtv.socket'): + add_devices = mock.Mock() + setup_platform(self.hass, {}, add_devices, + discovery_info=None) + mocked_warn.assert_called_once_with("Cannot determine device") + add_devices.assert_not_called() + + @mock.patch( + 'homeassistant.components.media_player.samsungtv.subprocess.Popen' + ) + def test_update_on(self, mocked_popen): + """Testing update tv on.""" + ping = mock.Mock() + mocked_popen.return_value = ping + ping.returncode = 0 + self.device.update() + self.assertEqual(STATE_ON, self.device._state) + + @mock.patch( + 'homeassistant.components.media_player.samsungtv.subprocess.Popen' + ) + def test_update_off(self, mocked_popen): + """Testing update tv off.""" + ping = mock.Mock() + mocked_popen.return_value = ping + ping.returncode = 1 + self.device.update() + self.assertEqual(STATE_OFF, self.device._state) + ping = mock.Mock() + ping.communicate = mock.Mock( + side_effect=CalledProcessError("BOOM", None)) + mocked_popen.return_value = ping + self.device.update() + self.assertEqual(STATE_OFF, self.device._state) + + def test_send_key(self): + """Test for send key.""" + self.device.send_key('KEY_POWER') + self.assertEqual(STATE_ON, self.device._state) + + def test_send_key_broken_pipe(self): + """Testing broken pipe Exception.""" + _remote = mock.Mock() + self.device.get_remote = mock.Mock() + _remote.control = mock.Mock( + side_effect=BrokenPipeError("Boom")) + self.device.get_remote.return_value = _remote + self.device.send_key("HELLO") + self.assertIsNone(self.device._remote) + self.assertEqual(STATE_ON, self.device._state) + + def test_send_key_os_error(self): + """Testing broken pipe Exception.""" + _remote = mock.Mock() + self.device.get_remote = mock.Mock() + _remote.control = mock.Mock( + side_effect=OSError("Boom")) + self.device.get_remote.return_value = _remote + self.device.send_key("HELLO") + self.assertIsNone(self.device._remote) + self.assertEqual(STATE_OFF, self.device._state) + + def test_power_off_in_progress(self): + """Test for power_off_in_progress.""" + self.assertFalse(self.device._power_off_in_progress()) + self.device._end_of_power_off = dt_util.utcnow() + timedelta( + seconds=15) + self.assertTrue(self.device._power_off_in_progress()) + + def test_name(self): + """Test for name property.""" + self.assertEqual('fake', self.device.name) + + def test_state(self): + """Test for state property.""" + self.device._state = STATE_ON + self.assertEqual(STATE_ON, self.device.state) + self.device._state = STATE_OFF + self.assertEqual(STATE_OFF, self.device.state) + + def test_is_volume_muted(self): + """Test for is_volume_muted property.""" + self.device._muted = False + self.assertFalse(self.device.is_volume_muted) + self.device._muted = True + self.assertTrue(self.device.is_volume_muted) + + def test_supported_features(self): + """Test for supported_features property.""" + self.device._mac = None + self.assertEqual(SUPPORT_SAMSUNGTV, self.device.supported_features) + self.device._mac = "fake" + self.assertEqual( + SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON, + self.device.supported_features) + + def test_turn_off(self): + """Test for turn_off.""" + self.device.send_key = mock.Mock() + _remote = mock.Mock() + _remote.close = mock.Mock() + self.get_remote = mock.Mock(return_value=_remote) + self.device._end_of_power_off = None + self.device.turn_off() + self.assertIsNotNone(self.device._end_of_power_off) + self.device.send_key.assert_called_once_with('KEY_POWER') + self.device.send_key = mock.Mock() + self.device._config['method'] = 'legacy' + self.device.turn_off() + self.device.send_key.assert_called_once_with('KEY_POWEROFF') + + @mock.patch( + 'homeassistant.components.media_player.samsungtv._LOGGER.debug') + def test_turn_off_os_error(self, mocked_debug): + """Test for turn_off with OSError.""" + _remote = mock.Mock() + _remote.close = mock.Mock(side_effect=OSError("BOOM")) + self.device.get_remote = mock.Mock(return_value=_remote) + self.device.turn_off() + mocked_debug.assert_called_once_with("Could not establish connection.") + + def test_volume_up(self): + """Test for volume_up.""" + self.device.send_key = mock.Mock() + self.device.volume_up() + self.device.send_key.assert_called_once_with("KEY_VOLUP") + + def test_volume_down(self): + """Test for volume_down.""" + self.device.send_key = mock.Mock() + self.device.volume_down() + self.device.send_key.assert_called_once_with("KEY_VOLDOWN") + + def test_mute_volume(self): + """Test for mute_volume.""" + self.device.send_key = mock.Mock() + self.device.mute_volume(True) + self.device.send_key.assert_called_once_with("KEY_MUTE") + + def test_media_play_pause(self): + """Test for media_next_track.""" + self.device.send_key = mock.Mock() + self.device._playing = False + self.device.media_play_pause() + self.device.send_key.assert_called_once_with("KEY_PLAY") + self.assertTrue(self.device._playing) + self.device.send_key = mock.Mock() + self.device.media_play_pause() + self.device.send_key.assert_called_once_with("KEY_PAUSE") + self.assertFalse(self.device._playing) + + def test_media_play(self): + """Test for media_play.""" + self.device.send_key = mock.Mock() + self.device._playing = False + self.device.media_play() + self.device.send_key.assert_called_once_with("KEY_PLAY") + self.assertTrue(self.device._playing) + + def test_media_pause(self): + """Test for media_pause.""" + self.device.send_key = mock.Mock() + self.device._playing = True + self.device.media_pause() + self.device.send_key.assert_called_once_with("KEY_PAUSE") + self.assertFalse(self.device._playing) + + def test_media_next_track(self): + """Test for media_next_track.""" + self.device.send_key = mock.Mock() + self.device.media_next_track() + self.device.send_key.assert_called_once_with("KEY_FF") + + def test_media_previous_track(self): + """Test for media_previous_track.""" + self.device.send_key = mock.Mock() + self.device.media_previous_track() + self.device.send_key.assert_called_once_with("KEY_REWIND") + + def test_turn_on(self): + """Test turn on.""" + self.device.send_key = mock.Mock() + self.device._mac = None + self.device.turn_on() + self.device.send_key.assert_called_once_with('KEY_POWERON') + self.device._wol.send_magic_packet = mock.Mock() + self.device._mac = "fake" + self.device.turn_on() + self.device._wol.send_magic_packet.assert_called_once_with("fake") From 64cbfdfd772537cce1645c7d33c22727e4a32f20 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 4 Feb 2018 18:23:26 +0100 Subject: [PATCH 107/166] Upgrade influxdb to 5.0.0 (#12156) * Upgrade influxdb to 5.0.0 * UPdate sensor as well --- homeassistant/components/influxdb.py | 16 ++++++------ homeassistant/components/sensor/influxdb.py | 27 +++++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 82f98449411..30a47828cc7 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -21,7 +21,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues from homeassistant.util import utcnow -REQUIREMENTS = ['influxdb==4.1.1'] +REQUIREMENTS = ['influxdb==5.0.0'] _LOGGER = logging.getLogger(__name__) @@ -39,6 +39,7 @@ CONF_RETRY_QUEUE = 'retry_queue_limit' DEFAULT_DATABASE = 'home_assistant' DEFAULT_VERIFY_SSL = True DOMAIN = 'influxdb' + TIMEOUT = 5 COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({ @@ -137,7 +138,7 @@ def setup(hass, config): _LOGGER.error("Database host is not accessible due to '%s', please " "check your entries in the configuration file (host, " "port, etc.) and verify that the database exists and is " - "READ/WRITE.", exc) + "READ/WRITE", exc) return False def influx_event_listener(event): @@ -145,8 +146,7 @@ def setup(hass, config): state = event.data.get('new_state') if state is None or state.state in ( STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ - state.entity_id in blacklist_e or \ - state.domain in blacklist_d: + state.entity_id in blacklist_e or state.domain in blacklist_d: return try: @@ -301,11 +301,9 @@ class RetryOnError(object): target = utcnow() + self.retry_delay tracking = {'target': target} - remove = track_point_in_utc_time(self.hass, - partial(scheduled, - retry + 1, - tracking), - target) + remove = track_point_in_utc_time( + self.hass, partial(scheduled, retry + 1, tracking), + target) tracking['remove'] = remove tracking["exc"] = ex wrapper._retry_queue.append(tracking) diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 8adf85f0a2e..c0d492984e0 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -4,25 +4,25 @@ InfluxDB component which allows you to get data from an Influx database. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.influxdb/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL, - CONF_NAME, CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE) -from homeassistant.const import STATE_UNKNOWN -from homeassistant.util import Throttle +from homeassistant.components.influxdb import CONF_DB_NAME +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, STATE_UNKNOWN) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['influxdb==4.1.1'] +REQUIREMENTS = ['influxdb==5.0.0'] DEFAULT_HOST = 'localhost' DEFAULT_PORT = 8086 @@ -32,13 +32,13 @@ DEFAULT_VERIFY_SSL = False DEFAULT_GROUP_FUNCTION = 'mean' DEFAULT_FIELD = 'value' -CONF_DB_NAME = 'database' CONF_QUERIES = 'queries' CONF_GROUP_FUNCTION = 'group_function' CONF_FIELD = 'field' CONF_MEASUREMENT_NAME = 'measurement' CONF_WHERE = 'where' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _QUERY_SCHEME = vol.Schema({ vol.Required(CONF_NAME): cv.string, @@ -62,9 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean }) -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the InfluxDB component.""" @@ -122,7 +119,7 @@ class InfluxSensor(Entity): except exceptions.InfluxDBClientError as exc: _LOGGER.error("Database host is not accessible due to '%s', please" " check your entries in the configuration file and" - " that the database exists and is READ/WRITE.", exc) + " that the database exists and is READ/WRITE", exc) self.connected = False @property diff --git a/requirements_all.txt b/requirements_all.txt index d0529079204..481ab45bb40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -406,7 +406,7 @@ ihcsdk==2.1.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb -influxdb==4.1.1 +influxdb==5.0.0 # homeassistant.components.insteon_local insteonlocal==0.53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 351175ec858..b9a43eea025 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -79,7 +79,7 @@ home-assistant-frontend==20180130.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb -influxdb==4.1.1 +influxdb==5.0.0 # homeassistant.components.dyson libpurecoollink==0.4.2 From cff4f8ec9abc7adae77ed706cf0ec30ca85016c4 Mon Sep 17 00:00:00 2001 From: akloeckner Date: Sun, 4 Feb 2018 18:30:03 +0100 Subject: [PATCH 108/166] add delay_arrival (#12169) This adds the delay_arrival field from the schiene interface. This field sometimes explains an ontime=false with delay=0... --- homeassistant/components/sensor/deutsche_bahn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 0261288f27e..0325f796617 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -116,4 +116,5 @@ class SchieneData(object): 'delay_arrival': 0}) # IMHO only delay_departure is useful con['delay'] = delay['delay_departure'] + con['delay_arrival'] = delay['delay_arrival'] con['ontime'] = con.get('ontime', False) From ec201f345899907d23f5873f4ff5f5e2b9b4687c Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Sun, 4 Feb 2018 18:20:06 +0000 Subject: [PATCH 109/166] Move TP-Link socket LED state setting to update() (#12170) * Add error handling to TP-LInk LED state set Handles errors when setting the LED state of TP-Link sockets. If the socket is unavailable then the raised exception will cause the compoent to not be added to HA. * Move LED state setting out of __init__ --- homeassistant/components/switch/tplink.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index f67aaec9796..14faa98fb59 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -50,8 +50,7 @@ class SmartPlugSwitch(SwitchDevice): """Initialize the switch.""" self.smartplug = smartplug self._name = name - if leds_on is not None: - self.smartplug.led = leds_on + self._leds_on = leds_on self._state = None self._available = True # Set up emeter cache @@ -94,6 +93,10 @@ class SmartPlugSwitch(SwitchDevice): self._state = self.smartplug.state == \ self.smartplug.SWITCH_STATE_ON + if self._leds_on is not None: + self.smartplug.led = self._leds_on + self._leds_on = None + # Pull the name from the device if a name was not specified if self._name == DEFAULT_NAME: self._name = self.smartplug.alias From 905a9949724e310465136ce8a0da6cdaf51fd89f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 4 Feb 2018 21:33:41 +0100 Subject: [PATCH 110/166] Upgrade schiene to 0.21 (#12176) --- homeassistant/components/sensor/deutsche_bahn.py | 3 +-- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 0325f796617..2b125155892 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['schiene==0.20'] +REQUIREMENTS = ['schiene==0.21'] _LOGGER = logging.getLogger(__name__) @@ -114,7 +114,6 @@ class SchieneData(object): con.pop('details') delay = con.get('delay', {'delay_departure': 0, 'delay_arrival': 0}) - # IMHO only delay_departure is useful con['delay'] = delay['delay_departure'] con['delay_arrival'] = delay['delay_arrival'] con['ontime'] = con.get('ontime', False) diff --git a/requirements_all.txt b/requirements_all.txt index 481ab45bb40..f6a882aa49a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1071,7 +1071,7 @@ samsungctl==0.6.0 satel_integra==0.1.0 # homeassistant.components.sensor.deutsche_bahn -schiene==0.20 +schiene==0.21 # homeassistant.components.scsgate scsgate==0.1.0 From 137933a774625ef901904f14335dca6eb957915d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 5 Feb 2018 09:14:09 +0100 Subject: [PATCH 111/166] python-miio version bumped. Fixes all xiaomi_miio components. (Closes: #12017, Closes: #11948, Closes: #11200) (#12188) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 97ac382031b..942aff4ec57 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.4'] +REQUIREMENTS = ['python-miio==0.3.5'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 43c8860e77b..afaafe6c971 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.4'] +REQUIREMENTS = ['python-miio==0.3.5'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index f2fdf3177aa..87871079a9c 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.4'] +REQUIREMENTS = ['python-miio==0.3.5'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 06690a0909f..700b31cb86a 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.4'] +REQUIREMENTS = ['python-miio==0.3.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f6a882aa49a..ca956523f27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -920,7 +920,7 @@ python-juicenet==0.0.5 # homeassistant.components.light.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.4 +python-miio==0.3.5 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 From f5030d9ebf025d82690fa752814857f8d8ec9db5 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Mon, 5 Feb 2018 03:19:56 -0500 Subject: [PATCH 112/166] Services (small_pr)(fix): Added missing return on data template error (#12184) * Added return on data template error * Rebased so not sure why spelling errors returned... --- homeassistant/helpers/service.py | 1 + tests/helpers/test_service.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f5b626c8828..b89b1689c9e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -74,6 +74,7 @@ def async_call_from_config(hass, config, blocking=False, variables=None, config[CONF_SERVICE_DATA_TEMPLATE], variables)) except TemplateError as ex: _LOGGER.error('Error rendering data template: %s', ex) + return if CONF_SERVICE_ENTITY_ID in config: service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a5bd6798084..a987f5130f1 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -68,6 +68,24 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual('goodbye', self.calls[0].data['hello']) + def test_bad_template(self): + """Test passing bad template.""" + config = { + 'service_template': '{{ var_service }}', + 'entity_id': 'hello.world', + 'data_template': { + 'hello': '{{ states + unknown_var }}' + } + } + + service.call_from_config(self.hass, config, variables={ + 'var_service': 'test_domain.test_service', + 'var_data': 'goodbye', + }) + self.hass.block_till_done() + + self.assertEqual(len(self.calls), 0) + def test_split_entity_string(self): """Test splitting of entity string.""" service.call_from_config(self.hass, { From 44cfd2999c86d75bb05286c1bc0ed8ef33301af1 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Mon, 5 Feb 2018 03:21:20 -0500 Subject: [PATCH 113/166] fix ecobee is_aux_heat_on property (#12186) --- homeassistant/components/climate/ecobee.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 59df7d20687..6a4253ceca7 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -14,7 +14,8 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv @@ -48,7 +49,7 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | - SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW) From 86e89b7c2610832bbfe005fe898f0b4688b1ee3b Mon Sep 17 00:00:00 2001 From: tbergo Date: Mon, 5 Feb 2018 09:33:07 +0100 Subject: [PATCH 114/166] Upgrade pytouchline to 0.7 (#12179) --- homeassistant/components/climate/touchline.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/touchline.py b/homeassistant/components/climate/touchline.py index cc45e26a1cf..f9c5676629b 100644 --- a/homeassistant/components/climate/touchline.py +++ b/homeassistant/components/climate/touchline.py @@ -13,7 +13,7 @@ from homeassistant.components.climate import ( from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pytouchline==0.6'] +REQUIREMENTS = ['pytouchline==0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ca956523f27..7270a4bf103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -984,7 +984,7 @@ pythonwhois==2.4.3 pytile==1.1.0 # homeassistant.components.climate.touchline -pytouchline==0.6 +pytouchline==0.7 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 From e35d4f0a2c95051cf39ea31445c2707f01caf275 Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Mon, 5 Feb 2018 05:02:43 -0800 Subject: [PATCH 115/166] Canary live stream (#11949) * Added support for Canary live stream view * Updated requirements * - Fixed lint error * Addressed PR comment * - Disabled polling for Canary camera as suggested in PR comment - Live session is now renewed every time camera is retrieved and min time between session renewal is 90 seconds --- homeassistant/components/camera/canary.py | 113 +++++++++++++--------- homeassistant/components/canary.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py index 302758eee94..a230e0f6d4a 100644 --- a/homeassistant/components/camera/canary.py +++ b/homeassistant/components/camera/canary.py @@ -4,19 +4,30 @@ Support for Canary camera. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.canary/ """ +import asyncio import logging +from datetime import timedelta -import requests +import voluptuous as vol -from homeassistant.components.camera import Camera +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.util import Throttle -DEPENDENCIES = ['canary'] +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' + +DEPENDENCIES = ['canary', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) -ATTR_MOTION_START_TIME = "motion_start_time" -ATTR_MOTION_END_TIME = "motion_end_time" +MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, +}) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -25,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for location in data.locations: - entries = data.get_motion_entries(location.location_id) - if entries: - devices.append(CanaryCamera(data, location.location_id, - DEFAULT_TIMEOUT)) + for device in location.devices: + if device.is_online: + devices.append( + CanaryCamera(hass, data, location, device, DEFAULT_TIMEOUT, + config.get(CONF_FFMPEG_ARGUMENTS))) add_devices(devices, True) @@ -36,60 +48,65 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CanaryCamera(Camera): """An implementation of a Canary security camera.""" - def __init__(self, data, location_id, timeout): + def __init__(self, hass, data, location, device, timeout, ffmpeg_args): """Initialize a Canary security camera.""" super().__init__() + + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = ffmpeg_args self._data = data - self._location_id = location_id + self._location = location + self._device = device self._timeout = timeout - - self._location = None - self._motion_entry = None - self._image_content = None - - def camera_image(self): - """Update the status of the camera and return bytes of camera image.""" - self.update() - return self._image_content + self._live_stream_session = None @property def name(self): """Return the name of this device.""" - return self._location.name + return self._device.name @property def is_recording(self): """Return true if the device is recording.""" return self._location.is_recording - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - if self._motion_entry is None: - return None - - return { - ATTR_MOTION_START_TIME: self._motion_entry.start_time, - ATTR_MOTION_END_TIME: self._motion_entry.end_time, - } - - def update(self): - """Update the status of the camera.""" - self._data.update() - self._location = self._data.get_location(self._location_id) - - entries = self._data.get_motion_entries(self._location_id) - if entries: - current = entries[0] - previous = self._motion_entry - - if previous is None or previous.entry_id != current.entry_id: - self._motion_entry = current - self._image_content = requests.get( - current.thumbnails[0].image_url, - timeout=self._timeout).content - @property def motion_detection_enabled(self): """Return the camera motion detection status.""" return not self._location.is_recording + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + self.renew_live_stream_session() + + from haffmpeg import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) + image = yield from asyncio.shield(ffmpeg.get_image( + self._live_stream_session.live_stream_url, + output_format=IMAGE_JPEG, + extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) + return image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + if self._live_stream_session is None: + return + + from haffmpeg import CameraMjpeg + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + yield from stream.open_camera( + self._live_stream_session.live_stream_url, + extra_cmd=self._ffmpeg_arguments) + + yield from async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + yield from stream.close() + + @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) + def renew_live_stream_session(self): + """Renew live stream session.""" + self._live_stream_session = self._data.get_live_stream_session( + self._device) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py index 4d45f31ae59..dfef4976eb8 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ['py-canary==0.2.3'] +REQUIREMENTS = ['py-canary==0.4.0'] _LOGGER = logging.getLogger(__name__) @@ -122,3 +122,7 @@ class CanaryData(object): """Set location mode.""" self._api.set_location_mode(location_id, mode_name, is_private) self.update(no_throttle=True) + + def get_live_stream_session(self, device): + """Return live stream session.""" + return self._api.get_live_stream_session(device) diff --git a/requirements_all.txt b/requirements_all.txt index 7270a4bf103..72bbee3e860 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -618,7 +618,7 @@ pushetta==1.0.15 pwmled==1.2.1 # homeassistant.components.canary -py-canary==0.2.3 +py-canary==0.4.0 # homeassistant.components.sensor.cpuspeed py-cpuinfo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9a43eea025..b4116a8b44c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -121,7 +121,7 @@ prometheus_client==0.1.0 pushbullet.py==0.11.0 # homeassistant.components.canary -py-canary==0.2.3 +py-canary==0.4.0 # homeassistant.components.zwave pydispatcher==2.0.5 From 49343c9b025918070f0707fde4de3a053f525a67 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 5 Feb 2018 21:25:37 +0100 Subject: [PATCH 116/166] Replace Gitter with Discord (#12199) --- docs/source/_templates/links.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html index 272809d1920..53a8d1e425d 100644 --- a/docs/source/_templates/links.html +++ b/docs/source/_templates/links.html @@ -2,5 +2,5 @@
  • Homepage
  • Community Forums
  • GitHub
  • -
  • Gitter
  • +
  • Discord
  • From c72460ccf0f3e0fc5cae674f92df9ed28a717db0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 5 Feb 2018 22:11:20 +0100 Subject: [PATCH 117/166] Upgrade Sphinx to 1.6.7 (#12200) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 5041699e03b..c5c48e0bc73 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.6.6 +Sphinx==1.6.7 sphinx-autodoc-typehints==1.2.4 sphinx-autodoc-annotation==1.0.post1 From 323fe87b57d973979b03c6bbd8b988cc94e8e8e7 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Mon, 5 Feb 2018 23:29:19 +0100 Subject: [PATCH 118/166] Change attributes in new Mercedes Me component (#12147) * Fix wrong component doc URL * Change attributes to lowercase snakecase * pylint fix * Remove test comments --- .../components/binary_sensor/mercedesme.py | 60 +++++++++---------- .../components/device_tracker/mercedesme.py | 5 +- homeassistant/components/mercedesme.py | 24 +++++--- homeassistant/components/sensor/mercedesme.py | 29 ++++----- 4 files changed, 56 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py index dbbe679e852..c817447f181 100755 --- a/homeassistant/components/binary_sensor/mercedesme.py +++ b/homeassistant/components/binary_sensor/mercedesme.py @@ -2,7 +2,7 @@ Support for Mercedes cars with Mercedes ME. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mercedesme/ +https://home-assistant.io/components/binary_sensor.mercedesme/ """ import logging import datetime @@ -21,14 +21,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data = hass.data[DATA_MME].data if not data.cars: - _LOGGER.error("setup_platform data.cars is none") + _LOGGER.error("No cars found. Check component log.") return devices = [] for car in data.cars: - for dev in BINARY_SENSORS: + for key, value in sorted(BINARY_SENSORS.items()): devices.append(MercedesMEBinarySensor( - data, dev, dev, car["vin"], None)) + data, key, value[0], car["vin"], None)) add_devices(devices, True) @@ -39,62 +39,56 @@ class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): @property def is_on(self): """Return the state of the binary sensor.""" - return self._state == "On" + return self._state @property def device_state_attributes(self): """Return the state attributes.""" - if self._name == "windowsClosed": + if self._internal_name == "windowsClosed": return { - "windowStatusFrontLeft": self._car["windowStatusFrontLeft"], - "windowStatusFrontRight": self._car["windowStatusFrontRight"], - "windowStatusRearLeft": self._car["windowStatusRearLeft"], - "windowStatusRearRight": self._car["windowStatusRearRight"], - "originalValue": self._car[self._name], - "lastUpdate": datetime.datetime.fromtimestamp( + "window_front_left": self._car["windowStatusFrontLeft"], + "window_front_right": self._car["windowStatusFrontRight"], + "window_rear_left": self._car["windowStatusRearLeft"], + "window_rear_right": self._car["windowStatusRearRight"], + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), "car": self._car["license"] } - elif self._name == "tireWarningLight": + elif self._internal_name == "tireWarningLight": return { - "frontRightTirePressureKpa": + "front_right_tire_pressure_kpa": self._car["frontRightTirePressureKpa"], - "frontLeftTirePressureKpa": + "front_left_tire_pressure_kpa": self._car["frontLeftTirePressureKpa"], - "rearRightTirePressureKpa": + "rear_right_tire_pressure_kpa": self._car["rearRightTirePressureKpa"], - "rearLeftTirePressureKpa": + "rear_left_tire_pressure_kpa": self._car["rearLeftTirePressureKpa"], - "originalValue": self._car[self._name], - "lastUpdate": datetime.datetime.fromtimestamp( + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( self._car["lastUpdate"] ).strftime('%Y-%m-%d %H:%M:%S'), "car": self._car["license"], } return { - "originalValue": self._car[self._name], - "lastUpdate": datetime.datetime.fromtimestamp( + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), "car": self._car["license"] } def update(self): """Fetch new state data for the sensor.""" - _LOGGER.debug("Updating %s", self._name) - self._car = next( car for car in self._data.cars if car["vin"] == self._vin) - result = False - - if self._name == "windowsClosed": - result = bool(self._car[self._name] == "CLOSED") - elif self._name == "tireWarningLight": - result = bool(self._car[self._name] != "INACTIVE") + if self._internal_name == "windowsClosed": + self._state = bool(self._car[self._internal_name] == "CLOSED") + elif self._internal_name == "tireWarningLight": + self._state = bool(self._car[self._internal_name] != "INACTIVE") else: - result = self._car[self._name] is True - - self._state = "On" if result else "Off" + self._state = self._car[self._internal_name] is True _LOGGER.debug("Updated %s Value: %s IsOn: %s", - self._name, self._state, self.is_on) + self._internal_name, self._state, self.is_on) diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py index c33cc239412..ed516b738cc 100755 --- a/homeassistant/components/device_tracker/mercedesme.py +++ b/homeassistant/components/device_tracker/mercedesme.py @@ -2,7 +2,7 @@ Support for Mercedes cars with Mercedes ME. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mercedesme/ +https://home-assistant.io/components/device_tracker.mercedesme/ """ import logging from datetime import timedelta @@ -38,13 +38,12 @@ class MercedesMEDeviceTracker(object): def __init__(self, hass, config, see, data): """Initialize the Mercedes ME device tracker.""" - self.hass = hass self.see = see self.data = data self.update_info() track_time_interval( - self.hass, self.update_info, MIN_TIME_BETWEEN_SCANS) + hass, self.update_info, MIN_TIME_BETWEEN_SCANS) @Throttle(MIN_TIME_BETWEEN_SCANS) def update_info(self, now=None): diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py index 0a5825bb16d..0ac58e9c62e 100755 --- a/homeassistant/components/mercedesme.py +++ b/homeassistant/components/mercedesme.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS) from homeassistant.helpers import discovery from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) @@ -23,12 +23,20 @@ REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] _LOGGER = logging.getLogger(__name__) -BINARY_SENSORS = [ - 'doorsClosed', - 'windowsClosed', - 'locked', - 'tireWarningLight' -] +BINARY_SENSORS = { + 'doorsClosed': ['Doors closed'], + 'windowsClosed': ['Windows closed'], + 'locked': ['Doors locked'], + 'tireWarningLight': ['Tire Warning'] +} + +SENSORS = { + 'fuelLevelPercent': ['Fuel Level', '%'], + 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], + 'latestTrip': ['Latest Trip', None], + 'odometerKm': ['Odometer', LENGTH_KILOMETERS], + 'serviceIntervalDays': ['Next Service', 'days'] +} DATA_MME = 'mercedesme' DOMAIN = 'mercedesme' @@ -136,6 +144,8 @@ class MercedesMeEntity(Entity): def _update_callback(self): """Callback update method.""" + # If the method is made a callback this should be changed + # to the async version. Check core.callback self.schedule_update_ha_state(True) @property diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py index 08183a01ba8..21a63dd562d 100755 --- a/homeassistant/components/sensor/mercedesme.py +++ b/homeassistant/components/sensor/mercedesme.py @@ -2,28 +2,19 @@ Support for Mercedes cars with Mercedes ME. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mercedesme/ +https://home-assistant.io/components/sensor.mercedesme/ """ import logging import datetime -from homeassistant.const import LENGTH_KILOMETERS -from homeassistant.components.mercedesme import DATA_MME, MercedesMeEntity +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, SENSORS) DEPENDENCIES = ['mercedesme'] _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = { - 'fuelLevelPercent': ['Fuel Level', '%'], - 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], - 'latestTrip': ['Latest Trip', None], - 'odometerKm': ['Odometer', LENGTH_KILOMETERS], - 'serviceIntervalDays': ['Next Service', 'days'], - 'doorsClosed': ['doorsClosed', None], -} - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the sensor platform.""" @@ -37,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for car in data.cars: - for key, value in sorted(SENSOR_TYPES.items()): + for key, value in sorted(SENSORS.items()): devices.append( MercedesMESensor(data, key, value[0], car["vin"], value[1])) @@ -69,24 +60,24 @@ class MercedesMESensor(MercedesMeEntity): """Return the state attributes.""" if self._internal_name == "latestTrip": return { - "durationSeconds": + "duration_seconds": self._car["latestTrip"]["durationSeconds"], - "distanceTraveledKm": + "distance_traveled_km": self._car["latestTrip"]["distanceTraveledKm"], - "startedAt": datetime.datetime.fromtimestamp( + "started_at": datetime.datetime.fromtimestamp( self._car["latestTrip"]["startedAt"] ).strftime('%Y-%m-%d %H:%M:%S'), - "averageSpeedKmPerHr": + "average_speed_km_per_hr": self._car["latestTrip"]["averageSpeedKmPerHr"], "finished": self._car["latestTrip"]["finished"], - "lastUpdate": datetime.datetime.fromtimestamp( + "last_update": datetime.datetime.fromtimestamp( self._car["lastUpdate"] ).strftime('%Y-%m-%d %H:%M:%S'), "car": self._car["license"] } return { - "lastUpdate": datetime.datetime.fromtimestamp( + "last_update": datetime.datetime.fromtimestamp( self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), "car": self._car["license"] } From 4f0776de1376fb87d665569b1b6ba7271d44cd13 Mon Sep 17 00:00:00 2001 From: hawk259 Date: Mon, 5 Feb 2018 17:30:56 -0500 Subject: [PATCH 119/166] Binary Sensor Template: Add icon_template and entity_picture_template support (#12158) * Binary Sensor Template: Add icon_template and entity_picture_template support * fix white space * Added else logging and return state --- .../components/binary_sensor/template.py | 58 ++++++++++++++- .../components/binary_sensor/test_template.py | 70 +++++++++++++++++-- 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 92213a9b590..5bce1243fd0 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, + CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -29,6 +30,8 @@ CONF_DELAY_OFF = 'delay_off' SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -55,6 +58,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for device, device_config in config[CONF_SENSORS].items(): value_template = device_config[CONF_VALUE_TEMPLATE] + icon_template = device_config.get(CONF_ICON_TEMPLATE) + entity_picture_template = device_config.get( + CONF_ENTITY_PICTURE_TEMPLATE) entity_ids = (device_config.get(ATTR_ENTITY_ID) or value_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) @@ -65,10 +71,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass + if icon_template is not None: + icon_template.hass = hass + + if entity_picture_template is not None: + entity_picture_template.hass = hass + sensors.append( BinarySensorTemplate( hass, device, friendly_name, device_class, value_template, - entity_ids, delay_on, delay_off) + icon_template, entity_picture_template, entity_ids, + delay_on, delay_off) ) if not sensors: _LOGGER.error("No sensors added") @@ -82,7 +95,8 @@ class BinarySensorTemplate(BinarySensorDevice): """A virtual binary sensor that triggers from another sensor.""" def __init__(self, hass, device, friendly_name, device_class, - value_template, entity_ids, delay_on, delay_off): + value_template, icon_template, entity_picture_template, + entity_ids, delay_on, delay_off): """Initialize the Template binary sensor.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -91,6 +105,10 @@ class BinarySensorTemplate(BinarySensorDevice): self._device_class = device_class self._template = value_template self._state = None + self._icon_template = icon_template + self._entity_picture_template = entity_picture_template + self._icon = None + self._entity_picture = None self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off @@ -119,6 +137,16 @@ class BinarySensorTemplate(BinarySensorDevice): """Return the name of the sensor.""" return self._name + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + @property + def entity_picture(self): + """Return the entity_picture to use in the frontend, if any.""" + return self._entity_picture + @property def is_on(self): """Return true if sensor is on.""" @@ -137,8 +165,9 @@ class BinarySensorTemplate(BinarySensorDevice): @callback def _async_render(self): """Get the state of template.""" + state = None try: - return self._template.async_render().lower() == 'true' + state = (self._template.async_render().lower() == 'true') except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): @@ -148,6 +177,29 @@ class BinarySensorTemplate(BinarySensorDevice): return _LOGGER.error("Could not render template %s: %s", self._name, ex) + for property_name, template in ( + ('_icon', self._icon_template), + ('_entity_picture', self._entity_picture_template)): + if template is None: + continue + + try: + setattr(self, property_name, template.async_render()) + except TemplateError as ex: + friendly_property_name = property_name[1:].replace('_', ' ') + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute"): + # Common during HA startup - so just a warning + _LOGGER.warning('Could not render %s template %s,' + ' the state is unknown.', + friendly_property_name, self._name) + else: + _LOGGER.error('Could not render %s template %s: %s', + friendly_property_name, self._name, ex) + return state + + return state + @callback def async_check_state(self): """Update the state from the template.""" diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 481226c4f73..c47f23bf902 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -98,13 +98,75 @@ class TestBinarySensorTemplate(unittest.TestCase): } }) + def test_icon_template(self): + """Test icon template.""" + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "State", + 'icon_template': + "{% if " + "states.binary_sensor.test_state.state == " + "'Works' %}" + "mdi:check" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes.get('icon') == '' + + self.hass.states.set('binary_sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes['icon'] == 'mdi:check' + + def test_entity_picture_template(self): + """Test entity_picture template.""" + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "State", + 'entity_picture_template': + "{% if " + "states.binary_sensor.test_state.state == " + "'Works' %}" + "/local/sensor.png" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes.get('entity_picture') == '' + + self.hass.states.set('binary_sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test_template_sensor') + assert state.attributes['entity_picture'] == '/local/sensor.png' + def test_attributes(self): """"Test the attributes.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', - template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL, - None, None + template_hlpr.Template('{{ 1 > 1 }}', self.hass), + None, None, MATCH_ALL, None, None ).result() self.assertFalse(vs.should_poll) self.assertEqual('motion', vs.device_class) @@ -156,8 +218,8 @@ class TestBinarySensorTemplate(unittest.TestCase): vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', - template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL, - None, None + template_hlpr.Template('{{ 1 > 1 }}', self.hass), + None, None, MATCH_ALL, None, None ).result() mock_render.side_effect = TemplateError('foo') run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() From bdaf9cfae2839fe8f62feff25eec8883259682fb Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 5 Feb 2018 18:59:17 -0500 Subject: [PATCH 120/166] Bump pyeconet version to fix JSONDecodeError (#12204) --- homeassistant/components/climate/econet.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py index bb92a92467a..0591178391a 100644 --- a/homeassistant/components/climate/econet.py +++ b/homeassistant/components/climate/econet.py @@ -18,7 +18,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyeconet==0.0.4'] +REQUIREMENTS = ['pyeconet==0.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 72bbee3e860..de31e639102 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -705,7 +705,7 @@ pydroid-ipcam==0.8 pyebox==0.1.0 # homeassistant.components.climate.econet -pyeconet==0.0.4 +pyeconet==0.0.5 # homeassistant.components.eight_sleep pyeight==0.0.7 From e7a0759e1c1af0524e1b84f28e4c2fa5f8f89677 Mon Sep 17 00:00:00 2001 From: Richard Lucas Date: Mon, 5 Feb 2018 16:02:08 -0800 Subject: [PATCH 121/166] Add support for Alexa.StepSpeaker (#12183) --- homeassistant/components/alexa/smart_home.py | 34 ++++++++++++++++++++ tests/components/alexa/test_smart_home.py | 29 +++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index f4981046b5b..354a612c4b8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -352,6 +352,11 @@ class _AlexaSpeaker(_AlexaInterface): return 'Alexa.Speaker' +class _AlexaStepSpeaker(_AlexaInterface): + def name(self): + return 'Alexa.StepSpeaker' + + class _AlexaPlaybackController(_AlexaInterface): def name(self): return 'Alexa.PlaybackController' @@ -472,6 +477,11 @@ class _MediaPlayerCapabilities(_AlexaEntity): if supported & media_player.SUPPORT_VOLUME_SET: yield _AlexaSpeaker(self.entity) + step_volume_features = (media_player.SUPPORT_VOLUME_MUTE | + media_player.SUPPORT_VOLUME_STEP) + if supported & step_volume_features: + yield _AlexaStepSpeaker(self.entity) + playback_features = (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE | media_player.SUPPORT_STOP | @@ -1153,6 +1163,30 @@ def async_api_adjust_volume(hass, config, request, entity): return api_message(request) +@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_volume_step(hass, config, request, entity): + """Process an adjust volume step request.""" + volume_step = round(float(request[API_PAYLOAD]['volume'] / 100), 2) + + current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + + volume = current_level + volume_step + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_VOLUME_SET, + data, blocking=False) + + return api_message(request) + + +@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute')) @HANDLERS.register(('Alexa.Speaker', 'SetMute')) @extract_entity @asyncio.coroutine diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 35cc610219e..71485231150 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -429,6 +429,7 @@ def test_media_player(hass): 'Alexa.InputController', 'Alexa.PowerController', 'Alexa.Speaker', + 'Alexa.StepSpeaker', 'Alexa.PlaybackController', ) @@ -492,6 +493,34 @@ def test_media_player(hass): 'media_player.volume_set', 'volume_level') + call, _ = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': True}) + assert call.data['is_volume_muted'] + + call, _, = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'SetMute', 'media_player#test', + 'media_player.volume_mute', + hass, + payload={'mute': False}) + assert not call.data['is_volume_muted'] + + call, _ = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test', + 'media_player.volume_set', + hass, + payload={'volume': 20}) + assert call.data['volume_level'] == 0.95 + + call, _ = yield from assert_request_calls_service( + 'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test', + 'media_player.volume_set', + hass, + payload={'volume': -20}) + assert call.data['volume_level'] == 0.55 + @asyncio.coroutine def test_alert(hass): From 98b47cecbd211a3790e4b894a419640d01252292 Mon Sep 17 00:00:00 2001 From: tadly Date: Tue, 6 Feb 2018 01:04:31 +0100 Subject: [PATCH 122/166] Upgrade wakeonlan to 1.0.0 (#12190) --- .../components/media_player/panasonic_viera.py | 6 +++--- homeassistant/components/media_player/samsungtv.py | 6 +++--- homeassistant/components/media_player/webostv.py | 2 +- homeassistant/components/switch/wake_on_lan.py | 6 +++--- homeassistant/components/wake_on_lan.py | 8 ++++---- requirements_all.txt | 3 +-- requirements_test_all.txt | 3 +-- tests/components/switch/test_wake_on_lan.py | 12 ++++++------ 8 files changed, 22 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 1a14ab0289b..21a897f4d35 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['panasonic_viera==0.3', - 'wakeonlan==0.2.2'] + 'wakeonlan==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -72,9 +72,9 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def __init__(self, mac, name, remote): """Initialize the Panasonic device.""" - from wakeonlan import wol + import wakeonlan # Save a reference to the imported class - self._wol = wol + self._wol = wakeonlan self._mac = mac self._name = name self._muted = False diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index caf458edc69..4afd578211e 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -23,7 +23,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util -REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==0.2.2'] +REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -92,13 +92,13 @@ class SamsungTVDevice(MediaPlayerDevice): """Initialize the Samsung device.""" from samsungctl import exceptions from samsungctl import Remote - from wakeonlan import wol + import wakeonlan # Save a reference to the imported classes self._exceptions_class = exceptions self._remote_class = Remote self._name = name self._mac = mac - self._wol = wol + self._wol = wakeonlan # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 2081fc95223..3ccd3c7dbe9 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -26,7 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script import homeassistant.util as util -REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2', 'wakeonlan==0.2.2'] +REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2'] _CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index d94ff8c268b..ecaff14e2e2 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script from homeassistant.const import (CONF_HOST, CONF_NAME) -REQUIREMENTS = ['wakeonlan==0.2.2'] +REQUIREMENTS = ['wakeonlan==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -53,7 +53,7 @@ class WOLSwitch(SwitchDevice): def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): """Initialize the WOL switch.""" - from wakeonlan import wol + import wakeonlan self._hass = hass self._name = name self._host = host @@ -61,7 +61,7 @@ class WOLSwitch(SwitchDevice): self._broadcast_address = broadcast_address self._off_script = Script(hass, off_action) if off_action else None self._state = False - self._wol = wol + self._wol = wakeonlan @property def should_poll(self): diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py index 7da0f3054f3..4e729c7ccc7 100644 --- a/homeassistant/components/wake_on_lan.py +++ b/homeassistant/components/wake_on_lan.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_MAC import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['wakeonlan==0.2.2'] +REQUIREMENTS = ['wakeonlan==1.0.0'] DOMAIN = "wake_on_lan" _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): """Set up the wake on LAN component.""" - from wakeonlan import wol + import wakeonlan @asyncio.coroutine def send_magic_packet(call): @@ -42,11 +42,11 @@ def async_setup(hass, config): mac_address, broadcast_address) if broadcast_address is not None: yield from hass.async_add_job( - partial(wol.send_magic_packet, mac_address, + partial(wakeonlan.send_magic_packet, mac_address, ip_address=broadcast_address)) else: yield from hass.async_add_job( - partial(wol.send_magic_packet, mac_address)) + partial(wakeonlan.send_magic_packet, mac_address)) hass.services.async_register( DOMAIN, SERVICE_SEND_MAGIC_PACKET, send_magic_packet, diff --git a/requirements_all.txt b/requirements_all.txt index de31e639102..935ba2c8fa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1213,9 +1213,8 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan # homeassistant.components.media_player.panasonic_viera # homeassistant.components.media_player.samsungtv -# homeassistant.components.media_player.webostv # homeassistant.components.switch.wake_on_lan -wakeonlan==0.2.2 +wakeonlan==1.0.0 # homeassistant.components.sensor.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4116a8b44c..f4a4e09b124 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,9 +183,8 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan # homeassistant.components.media_player.panasonic_viera # homeassistant.components.media_player.samsungtv -# homeassistant.components.media_player.webostv # homeassistant.components.switch.wake_on_lan -wakeonlan==0.2.2 +wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index 2c4be648c8c..d7945218e73 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -40,7 +40,7 @@ class TestWOLSwitch(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @patch('subprocess.call', new=call) def test_valid_hostname(self): """Test with valid hostname.""" @@ -71,7 +71,7 @@ class TestWOLSwitch(unittest.TestCase): state = self.hass.states.get('switch.wake_on_lan') self.assertEqual(STATE_ON, state.state) - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @patch('subprocess.call', new=call) @patch('platform.system', new=system) def test_valid_hostname_windows(self): @@ -97,7 +97,7 @@ class TestWOLSwitch(unittest.TestCase): state = self.hass.states.get('switch.wake_on_lan') self.assertEqual(STATE_ON, state.state) - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @patch('subprocess.call', new=call) def test_minimal_config(self): """Test with minimal config.""" @@ -108,7 +108,7 @@ class TestWOLSwitch(unittest.TestCase): } })) - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @patch('subprocess.call', new=call) def test_broadcast_config(self): """Test with broadcast address config.""" @@ -126,7 +126,7 @@ class TestWOLSwitch(unittest.TestCase): switch.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @patch('subprocess.call', new=call) def test_off_script(self): """Test with turn off script.""" @@ -165,7 +165,7 @@ class TestWOLSwitch(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) assert len(calls) == 1 - @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet) + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @patch('subprocess.call', new=call) @patch('platform.system', new=system) def test_invalid_hostname_windows(self): From f58e5f442dfb3f4897275d94896e1e7f4fbf8e22 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Mon, 5 Feb 2018 16:05:19 -0800 Subject: [PATCH 123/166] zha: Update to bellows 0.5.0+zigpy (#12187) --- homeassistant/components/binary_sensor/zha.py | 4 ++-- homeassistant/components/light/zha.py | 2 +- homeassistant/components/sensor/zha.py | 2 +- homeassistant/components/zha/__init__.py | 11 +++++++---- homeassistant/components/zha/const.py | 4 ++-- requirements_all.txt | 5 ++++- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index ad7c29badf9..80549037ee1 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if discovery_info is None: return - from bellows.zigbee.zcl.clusters.security import IasZone + from zigpy.zcl.clusters.security import IasZone in_clusters = discovery_info['in_clusters'] @@ -63,7 +63,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) self._device_class = device_class - from bellows.zigbee.zcl.clusters.security import IasZone + from zigpy.zcl.clusters.security import IasZone self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] @property diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index c468d50ce6d..f50b3d7689b 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -61,7 +61,7 @@ class Light(zha.Entity, light.Light): self._xy_color = None self._brightness = None - import bellows.zigbee.zcl.clusters as zcl_clusters + import zigpy.zcl.clusters as zcl_clusters if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters: self._supported_features |= light.SUPPORT_BRIGHTNESS self._supported_features |= light.SUPPORT_TRANSITION diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index cd2847c1fa6..a1820f7d7dd 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -31,7 +31,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @asyncio.coroutine def make_sensor(discovery_info): """Create ZHA sensors factory.""" - from bellows.zigbee.zcl.clusters.measurement import TemperatureMeasurement + from zigpy.zcl.clusters.measurement import TemperatureMeasurement in_clusters = discovery_info['in_clusters'] if TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index f87537a1938..cf44dea8c1d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -14,7 +14,10 @@ from homeassistant import const as ha_const from homeassistant.helpers import discovery, entity from homeassistant.util import slugify -REQUIREMENTS = ['bellows==0.4.0'] +REQUIREMENTS = [ + 'bellows==0.5.0', + 'zigpy==0.0.1', +] DOMAIN = 'zha' @@ -130,7 +133,7 @@ class ApplicationListener: @asyncio.coroutine def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" - import bellows.zigbee.profiles + import zigpy.profiles import homeassistant.components.zha.const as zha_const zha_const.populate_data() @@ -146,8 +149,8 @@ class ApplicationListener: node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get( device_key, {}) - if endpoint.profile_id in bellows.zigbee.profiles.PROFILES: - profile = bellows.zigbee.profiles.PROFILES[endpoint.profile_id] + if endpoint.profile_id in zigpy.profiles.PROFILES: + profile = zigpy.profiles.PROFILES[endpoint.profile_id] if zha_const.DEVICE_CLASS.get(endpoint.profile_id, {}).get(endpoint.device_type, None): diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index b1659536e32..a8d4671ebf7 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -11,8 +11,8 @@ def populate_data(): These cannot be module level, as importing bellows must be done in a in a function. """ - from bellows.zigbee import zcl - from bellows.zigbee.profiles import PROFILES, zha, zll + from zigpy import zcl + from zigpy.profiles import PROFILES, zha, zll DEVICE_CLASS[zha.PROFILE_ID] = { zha.DeviceType.ON_OFF_SWITCH: 'switch', diff --git a/requirements_all.txt b/requirements_all.txt index 935ba2c8fa3..cd718810f44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -126,7 +126,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.4.0 +bellows==0.5.0 # homeassistant.components.blink blinkpy==0.6.0 @@ -1271,3 +1271,6 @@ zeroconf==0.19.1 # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.0.0 + +# homeassistant.components.zha +zigpy==0.0.1 From 6f74b672a356ce0fa020e1baefda8ec3fd7e21e8 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Tue, 6 Feb 2018 06:12:35 -0500 Subject: [PATCH 124/166] Make waterfurnace recovery more robust (#12202) This makes waterfurnace recovery more robust by catching any understood exceptions by the library, and always doing another login. --- homeassistant/components/waterfurnace.py | 15 ++++++++------- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/waterfurnace.py b/homeassistant/components/waterfurnace.py index 346bdcdfb97..a587285e0ba 100644 --- a/homeassistant/components/waterfurnace.py +++ b/homeassistant/components/waterfurnace.py @@ -18,7 +18,7 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery -REQUIREMENTS = ["waterfurnace==0.3.0"] +REQUIREMENTS = ["waterfurnace==0.4.0"] _LOGGER = logging.getLogger(__name__) @@ -83,6 +83,8 @@ class WaterFurnaceData(threading.Thread): def run(self): """Thread run loop.""" + import waterfurnace.waterfurnace as wf + @callback def register(): """Connect to hass for shutdown.""" @@ -110,8 +112,11 @@ class WaterFurnaceData(threading.Thread): try: self.data = self.client.read() - except ConnectionError: - # attempt to log back in if there was a session expiration. + except wf.WFException: + # WFExceptions are things the WF library understands + # that pretty much can all be solved by logging in and + # back out again. + _LOGGER.exception("Failed to read data, attempting to recover") try: self.client.login() except Exception: # pylint: disable=broad-except @@ -127,10 +132,6 @@ class WaterFurnaceData(threading.Thread): "Lost our connection to websocket, trying again") time.sleep(SCAN_INTERVAL.seconds) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error updating waterfurnace data.") - time.sleep(SCAN_INTERVAL.seconds) - else: self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) time.sleep(SCAN_INTERVAL.seconds) diff --git a/requirements_all.txt b/requirements_all.txt index cd718810f44..85ca4e89bd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1223,7 +1223,7 @@ waqiasync==1.0.0 warrant==0.6.1 # homeassistant.components.waterfurnace -waterfurnace==0.3.0 +waterfurnace==0.4.0 # homeassistant.components.media_player.gpmdp websocket-client==0.37.0 From 0fd17a7c358ed68a7c00fc2149d6ff9d982ea9df Mon Sep 17 00:00:00 2001 From: lance36 Date: Tue, 6 Feb 2018 18:52:55 +0100 Subject: [PATCH 125/166] Much nicer icon (#12212) mdi:robot-vacuum will be renamed mdi:roomba https://github.com/Templarian/MaterialDesign/issues/2368 --- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 700b31cb86a..d64f7a754ee 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -24,7 +24,7 @@ REQUIREMENTS = ['python-miio==0.3.5'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Vacuum cleaner' -ICON = 'mdi:google-circles-group' +ICON = 'mdi:roomba' PLATFORM = 'xiaomi_miio' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From 844337ca42fc197175ed036cdcb693d252c35991 Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Tue, 6 Feb 2018 18:32:56 +0000 Subject: [PATCH 126/166] Properly handle thresholds of zero (#12175) Explicitly test for thresholds to be None rather than truth value testing (which for number types returns False for zero values). --- .../components/binary_sensor/threshold.py | 7 ++- .../binary_sensor/test_threshold.py | 60 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 36e8868661d..79c36fb2ef2 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -126,11 +126,12 @@ class ThresholdSensor(BinarySensorDevice): @property def threshold_type(self): """Return the type of threshold this sensor represents.""" - if self._threshold_lower and self._threshold_upper: + if self._threshold_lower is not None and \ + self._threshold_upper is not None: return TYPE_RANGE - elif self._threshold_lower: + elif self._threshold_lower is not None: return TYPE_LOWER - elif self._threshold_upper: + elif self._threshold_upper is not None: return TYPE_UPPER @property diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py index 38573b295d3..926b3c67983 100644 --- a/tests/components/binary_sensor/test_threshold.py +++ b/tests/components/binary_sensor/test_threshold.py @@ -333,3 +333,63 @@ class TestThresholdSensor(unittest.TestCase): self.assertEqual('unknown', state.attributes.get('position')) assert state.state == 'off' + + def test_sensor_lower_zero_threshold(self): + """Test if a lower threshold of zero is set.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'lower': '0', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('lower', state.attributes.get('type')) + self.assertEqual(float(config['binary_sensor']['lower']), + state.attributes.get('lower')) + + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', -3) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + assert state.state == 'on' + + def test_sensor_upper_zero_threshold(self): + """Test if an upper threshold of zero is set.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'upper': '0', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', -10) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('upper', state.attributes.get('type')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 2) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + assert state.state == 'on' From c7dad113d9cfcdb0febef576a73ff0e9397d277b Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Tue, 6 Feb 2018 10:46:28 -0800 Subject: [PATCH 127/166] zha: Add support for XBee radios (#12205) * zha: Add support for xbee radios * Lint --- homeassistant/components/binary_sensor/zha.py | 2 +- homeassistant/components/zha/__init__.py | 33 +++++++++++++++---- requirements_all.txt | 3 ++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 80549037ee1..de7896e595b 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -78,7 +78,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): """Return the class of this device, from component DEVICE_CLASSES.""" return self._device_class - def cluster_command(self, aps_frame, tsn, command_id, args): + def cluster_command(self, tsn, command_id, args): """Handle commands received to this cluster.""" if command_id == 0: self._state = args[0] & 3 diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index cf44dea8c1d..58d44b31994 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import asyncio +import enum import logging import voluptuous as vol @@ -17,13 +18,23 @@ from homeassistant.util import slugify REQUIREMENTS = [ 'bellows==0.5.0', 'zigpy==0.0.1', + 'zigpy-xbee==0.0.1', ] DOMAIN = 'zha' + +class RadioType(enum.Enum): + """Possible options for radio type in config.""" + + ezsp = 'ezsp' + xbee = 'xbee' + + CONF_BAUDRATE = 'baudrate' CONF_DATABASE = 'database_path' CONF_DEVICE_CONFIG = 'device_config' +CONF_RADIO_TYPE = 'radio_type' CONF_USB_PATH = 'usb_path' DATA_DEVICE_CONFIG = 'zha_device_config' @@ -33,6 +44,8 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ + vol.Optional(CONF_RADIO_TYPE, default=RadioType.ezsp): + cv.enum(RadioType), CONF_USB_PATH: cv.string, vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int, CONF_DATABASE: cv.string, @@ -70,16 +83,22 @@ def async_setup(hass, config): """ global APPLICATION_CONTROLLER - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - - ezsp_ = bellows.ezsp.EZSP() usb_path = config[DOMAIN].get(CONF_USB_PATH) baudrate = config[DOMAIN].get(CONF_BAUDRATE) - yield from ezsp_.connect(usb_path, baudrate) + radio_type = config[DOMAIN].get(CONF_RADIO_TYPE) + if radio_type == RadioType.ezsp: + import bellows.ezsp + from bellows.zigbee.application import ControllerApplication + radio = bellows.ezsp.EZSP() + elif radio_type == RadioType.xbee: + import zigpy_xbee.api + from zigpy_xbee.zigbee.application import ControllerApplication + radio = zigpy_xbee.api.XBee() + + yield from radio.connect(usb_path, baudrate) database = config[DOMAIN].get(CONF_DATABASE) - APPLICATION_CONTROLLER = ControllerApplication(ezsp_, database) + APPLICATION_CONTROLLER = ControllerApplication(radio, database) listener = ApplicationListener(hass, config) APPLICATION_CONTROLLER.add_listener(listener) yield from APPLICATION_CONTROLLER.startup(auto_form=True) @@ -256,7 +275,7 @@ class Entity(entity.Entity): """Handle an attribute updated on this cluster.""" pass - def zdo_command(self, aps_frame, tsn, command_id, args): + def zdo_command(self, tsn, command_id, args): """Handle a ZDO command received on this cluster.""" pass diff --git a/requirements_all.txt b/requirements_all.txt index 85ca4e89bd5..ad86e129ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1272,5 +1272,8 @@ zeroconf==0.19.1 # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.0.0 +# homeassistant.components.zha +zigpy-xbee==0.0.1 + # homeassistant.components.zha zigpy==0.0.1 From a1d586c79369ade1e5a3573d78f3f1b6b5ecbe53 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Tue, 6 Feb 2018 13:46:44 -0500 Subject: [PATCH 128/166] Fix clear playlist in Volumio component (#12173) --- homeassistant/components/media_player/volumio.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 06d2304b3e7..84b957533fe 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -3,6 +3,8 @@ Volumio Platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.volumio/ + +Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ from datetime import timedelta import logging @@ -247,9 +249,9 @@ class Volumio(MediaPlayerDevice): def async_clear_playlist(self): """Clear players playlist.""" - # FIXME self._currentplaylist = None - return self.send_volumio_msg('clearQueue') + return self.send_volumio_msg('commands', + params={'cmd': 'clearQueue'}) @asyncio.coroutine @Throttle(PLAYLIST_UPDATE_INTERVAL) From 49c7b422f2a4f2c89c368a94b67c249b1cae044c Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Tue, 6 Feb 2018 19:47:24 +0100 Subject: [PATCH 129/166] Add Xiaomi Universal IR Remote (Chuangmi IR) (#11891) * First version of remote xiaomi-miio * added to coveragerc * fixed pylint error * misc fixes and input validation * address syssi's requests except device and async_service_handler * forgot to run linter * implemented async_service_handler * fixed delay == None, honor timeout given by user, pythonic compare of None * Added some whitespace for readability, added error message to turn_on and turn_off, fixed services.yaml examples * fixed pylint errors * readd pass for readability * fixed small stuff * Use RemoteDevice, Make send_command non-async * Ready code for next version of python-miio (Support for pronto hex codes) * cast command_optional to int, better input validation, fixed index out of bounds error. * revert code now in python-miio * ready for python-miio 0.3.5 * Removed unneccary return statements * require 0.3.5 * Rebase and update requirements_all.txt --- .coveragerc | 1 + homeassistant/components/remote/services.yaml | 13 + .../components/remote/xiaomi_miio.py | 255 ++++++++++++++++++ requirements_all.txt | 1 + 4 files changed, 270 insertions(+) create mode 100755 homeassistant/components/remote/xiaomi_miio.py diff --git a/.coveragerc b/.coveragerc index e30e3025d16..4b19519038f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -510,6 +510,7 @@ omit = homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py + homeassistant/components/remote/xiaomi_miio.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/airvisual.py diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 2a1deebdc7b..25ad626f96d 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -49,3 +49,16 @@ harmony_sync: entity_id: description: Name(s) of entities to sync. example: 'remote.family_room' + +xiaomi_miio_learn_command: + description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + fields: + entity_id: + description: 'Name of the entity to learn command from.' + example: 'remote.xiaomi_miio' + slot: + description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' + example: '1' + timeout: + description: 'Define the timeout in seconds, before which the command must be learned.' + example: '30' diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py new file mode 100755 index 00000000000..aa05246c9cd --- /dev/null +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -0,0 +1,255 @@ +""" +Support for the Xiaomi IR Remote (Chuangmi IR). + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/remote.xiaomi_miio/ +""" +import asyncio +import logging +import time + +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.remote import ( + PLATFORM_SCHEMA, DOMAIN, ATTR_NUM_REPEATS, ATTR_DELAY_SECS, + DEFAULT_DELAY_SECS, RemoteDevice) +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, + ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND) +import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import utcnow + +REQUIREMENTS = ['python-miio==0.3.5'] + +_LOGGER = logging.getLogger(__name__) + +SERVICE_LEARN = 'xiaomi_miio_learn_command' +PLATFORM = 'xiaomi_miio' + +CONF_SLOT = 'slot' +CONF_COMMANDS = 'commands' + +DEFAULT_TIMEOUT = 10 +DEFAULT_SLOT = 1 + +LEARN_COMMAND_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): vol.All(str), + vol.Optional(CONF_TIMEOUT, default=10): + vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_SLOT, default=1): + vol.All(int, vol.Range(min=1, max=1000000)), +}) + +COMMAND_SCHEMA = vol.Schema({ + vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string]) + }) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): + vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): + vol.All(int, vol.Range(min=1, max=1000000)), + vol.Optional(ATTR_HIDDEN, default=True): cv.boolean, + vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), + vol.Optional(CONF_COMMANDS, default={}): + vol.Schema({cv.slug: COMMAND_SCHEMA}), +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" + from miio import ChuangmiIr, DeviceException + + host = config.get(CONF_HOST) + token = config.get(CONF_TOKEN) + + # Create handler + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + device = ChuangmiIr(host, token) + + # Check that we can communicate with device. + try: + device.info() + except DeviceException as ex: + _LOGGER.error("Token not accepted by device : %s", ex) + return + + if PLATFORM not in hass.data: + hass.data[PLATFORM] = {} + + friendly_name = config.get(CONF_NAME, "xiaomi_miio_" + + host.replace('.', '_')) + slot = config.get(CONF_SLOT) + timeout = config.get(CONF_TIMEOUT) + + hidden = config.get(ATTR_HIDDEN) + + xiaomi_miio_remote = XiaomiMiioRemote( + friendly_name, device, slot, timeout, + hidden, config.get(CONF_COMMANDS)) + + hass.data[PLATFORM][host] = xiaomi_miio_remote + + async_add_devices([xiaomi_miio_remote]) + + @asyncio.coroutine + def async_service_handler(service): + """Handle a learn command.""" + if service.service != SERVICE_LEARN: + _LOGGER.error("We should not handle service: %s", service.service) + return + + entity_id = service.data.get(ATTR_ENTITY_ID) + entity = None + for remote in hass.data[PLATFORM].values(): + if remote.entity_id == entity_id: + entity = remote + + if not entity: + _LOGGER.error("entity_id: '%s' not found", entity_id) + return + + device = entity.device + + slot = service.data.get(CONF_SLOT, entity.slot) + + yield from hass.async_add_job(device.learn, slot) + + timeout = service.data.get(CONF_TIMEOUT, entity.timeout) + + _LOGGER.info("Press the key you want Home Assistant to learn") + start_time = utcnow() + while (utcnow() - start_time) < timedelta(seconds=timeout): + message = yield from hass.async_add_job( + device.read, slot) + _LOGGER.debug("Message recieved from device: '%s'", message) + + if 'code' in message and message['code']: + log_msg = "Received command is: {}".format(message['code']) + _LOGGER.info(log_msg) + hass.components.persistent_notification.async_create( + log_msg, title='Xiaomi Miio Remote') + return + + if ('error' in message and + message['error']['message'] == "learn timeout"): + yield from hass.async_add_job(device.learn, slot) + + yield from asyncio.sleep(1, loop=hass.loop) + + _LOGGER.error("Timeout. No infrared command captured") + hass.components.persistent_notification.async_create( + "Timeout. No infrared command captured", + title='Xiaomi Miio Remote') + + hass.services.async_register(DOMAIN, SERVICE_LEARN, async_service_handler, + schema=LEARN_COMMAND_SCHEMA) + + +class XiaomiMiioRemote(RemoteDevice): + """Representation of a Xiaomi Miio Remote device.""" + + def __init__(self, friendly_name, device, + slot, timeout, hidden, commands): + """Initialize the remote.""" + self._name = friendly_name + self._device = device + self._is_hidden = hidden + self._slot = slot + self._timeout = timeout + self._state = False + self._commands = commands + + @property + def name(self): + """Return the name of the remote.""" + return self._name + + @property + def device(self): + """Return the remote object.""" + return self._device + + @property + def hidden(self): + """Return if we should hide entity.""" + return self._is_hidden + + @property + def slot(self): + """Return the slot to save learned command.""" + return self._slot + + @property + def timeout(self): + """Return the timeout for learning command.""" + return self._timeout + + @property + def is_on(self): + """Return False if device is unreachable, else True.""" + from miio import DeviceException + try: + self.device.info() + return True + except DeviceException: + return False + + @property + def should_poll(self): + """We should not be polled for device up state.""" + return False + + @property + def device_state_attributes(self): + """Hide remote by default.""" + if self._is_hidden: + return {'hidden': 'true'} + else: + return + + # pylint: disable=R0201 + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on.""" + _LOGGER.error("Device does not support turn_on, " + + "please use 'remote.send_command' to send commands.") + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn the device off.""" + _LOGGER.error("Device does not support turn_off, " + + "please use 'remote.send_command' to send commands.") + + # pylint: enable=R0201 + def _send_command(self, payload): + """Send a command.""" + from miio import DeviceException + + _LOGGER.debug("Sending payload: '%s'", payload) + try: + self.device.play(payload) + except DeviceException as ex: + _LOGGER.error( + "Transmit of IR command failed, %s, exception: %s", + payload, ex) + + def send_command(self, command, **kwargs): + """Wrapper for _send_command.""" + num_repeats = kwargs.get(ATTR_NUM_REPEATS) + + delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + + for _ in range(num_repeats): + for payload in command: + if payload in self._commands: + for local_payload in self._commands[payload][CONF_COMMAND]: + self._send_command(local_payload) + else: + self._send_command(payload) + time.sleep(delay) diff --git a/requirements_all.txt b/requirements_all.txt index ad86e129ae0..804409c946f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -918,6 +918,7 @@ python-juicenet==0.0.5 # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio python-miio==0.3.5 From a2916a9c477294a83a9dc634f2f391185edb7ca1 Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Tue, 6 Feb 2018 10:47:38 -0800 Subject: [PATCH 130/166] Fix Xeoma camera platform to allow different admin/viewer credentials (#12161) --- .gitignore | 2 ++ homeassistant/components/camera/xeoma.py | 26 ++++++++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index fe26f43e8bc..0d55cae3c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,9 @@ Icon # Thumbnails ._* +# IntelliJ IDEA .idea +*.iml # pytest .cache diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index 72f40cb83a4..b4bcad0064d 100755 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['pyxeoma==1.2'] +REQUIREMENTS = ['pyxeoma==1.3'] _LOGGER = logging.getLogger(__name__) @@ -22,6 +22,8 @@ CONF_CAMERAS = 'cameras' CONF_HIDE = 'hide' CONF_IMAGE_NAME = 'image_name' CONF_NEW_VERSION = 'new_version' +CONF_VIEWER_PASSWORD = 'viewer_password' +CONF_VIEWER_USERNAME = 'viewer_username' CAMERAS_SCHEMA = vol.Schema({ vol.Required(CONF_IMAGE_NAME): cv.string, @@ -48,9 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): host = config[CONF_HOST] login = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - new_version = config[CONF_NEW_VERSION] - xeoma = Xeoma(host, new_version, login, password) + xeoma = Xeoma(host, login, password) try: yield from xeoma.async_test_connection() @@ -59,9 +60,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): { CONF_IMAGE_NAME: image_name, CONF_HIDE: False, - CONF_NAME: image_name + CONF_NAME: image_name, + CONF_VIEWER_USERNAME: username, + CONF_VIEWER_PASSWORD: pw + } - for image_name in discovered_image_names + for image_name, username, pw in discovered_image_names ] for cam in config[CONF_CAMERAS]: @@ -77,8 +81,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras)) async_add_devices( - [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME]) - for camera in cameras]) + [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME], + camera[CONF_VIEWER_USERNAME], + camera[CONF_VIEWER_PASSWORD]) for camera in cameras]) except XeomaError as err: _LOGGER.error("Error: %s", err.message) return @@ -87,12 +92,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class XeomaCamera(Camera): """Implementation of a Xeoma camera.""" - def __init__(self, xeoma, image, name): + def __init__(self, xeoma, image, name, username, password): """Initialize a Xeoma camera.""" super().__init__() self._xeoma = xeoma self._name = name self._image = image + self._username = username + self._password = password self._last_image = None @asyncio.coroutine @@ -100,7 +107,8 @@ class XeomaCamera(Camera): """Return a still image response from the camera.""" from pyxeoma.xeoma import XeomaError try: - image = yield from self._xeoma.async_get_camera_image(self._image) + image = yield from self._xeoma.async_get_camera_image( + self._image, self._username, self._password) self._last_image = image except XeomaError as err: _LOGGER.error("Error fetching image: %s", err.message) diff --git a/requirements_all.txt b/requirements_all.txt index 804409c946f..6a1d1eeb59f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1015,7 +1015,7 @@ pywebpush==1.5.0 pywemo==0.4.25 # homeassistant.components.camera.xeoma -pyxeoma==1.2 +pyxeoma==1.3 # homeassistant.components.zabbix pyzabbix==0.7.4 From cee57aab24007448ded767ebedbfd1119303361d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 6 Feb 2018 19:59:49 +0100 Subject: [PATCH 131/166] Xiaomi MiIO Light: Brightness mapping improved (#12203) * Mapping ([1,100] <-> [1,255]) of the brightness improved. * The cast to int isn't needed for python3. --- homeassistant/components/light/xiaomi_miio.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index afaafe6c971..a3c5fa9f62e 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/light.xiaomi_miio/ import asyncio from functools import partial import logging +from math import ceil import voluptuous as vol @@ -204,11 +205,11 @@ class XiaomiPhilipsGenericLight(Light): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = int(100 * brightness / 255) + percent_brightness = ceil(100 * brightness / 255.0) _LOGGER.debug( "Setting brightness: %s %s%%", - self.brightness, percent_brightness) + brightness, percent_brightness) result = yield from self._try_command( "Setting brightness failed: %s", @@ -235,7 +236,7 @@ class XiaomiPhilipsGenericLight(Light): _LOGGER.debug("Got new state: %s", state) self._state = state.is_on - self._brightness = int(255 * 0.01 * state.brightness) + self._brightness = ceil((255/100.0) * state.brightness) except DeviceException as ex: _LOGGER.error("Got exception while fetching the state: %s", ex) @@ -306,11 +307,11 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] - percent_brightness = int(100 * brightness / 255) + percent_brightness = ceil(100 * brightness / 255.0) _LOGGER.debug( "Setting brightness: %s %s%%", - self.brightness, percent_brightness) + brightness, percent_brightness) result = yield from self._try_command( "Setting brightness failed: %s", @@ -331,7 +332,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): _LOGGER.debug("Got new state: %s", state) self._state = state.is_on - self._brightness = int(255 * 0.01 * state.brightness) + self._brightness = ceil((255/100.0) * state.brightness) self._color_temp = self.translate( state.color_temperature, CCT_MIN, CCT_MAX, From bd29cd2ba2e00dbe7f63dbc12889093a906267c8 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Tue, 6 Feb 2018 21:27:35 +0100 Subject: [PATCH 132/166] Fixes according to review from @MartinHjelmare Thank you. (#12171) --- homeassistant/components/climate/melissa.py | 81 +++++++++------------ homeassistant/components/sensor/melissa.py | 8 +- tests/components/climate/test_melissa.py | 53 +++++++------- tests/components/sensor/test_melissa.py | 6 +- 4 files changed, 68 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py index 1e4ceac0edf..2b3b3bfbab1 100644 --- a/homeassistant/components/climate/melissa.py +++ b/homeassistant/components/climate/melissa.py @@ -6,21 +6,32 @@ https://home-assistant.io/components/climate.melissa/ """ import logging -from homeassistant.components.climate import ClimateDevice, \ - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_ON_OFF, \ - STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, \ - SUPPORT_FAN_MODE +from homeassistant.components.climate import ( + ClimateDevice, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_ON_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, SUPPORT_FAN_MODE +) from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH -from homeassistant.components.melissa import DATA_MELISSA, DOMAIN -from homeassistant.const import TEMP_CELSIUS, STATE_ON, STATE_OFF, \ - STATE_UNKNOWN, STATE_IDLE, ATTR_TEMPERATURE, PRECISION_WHOLE +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, STATE_OFF, STATE_IDLE, ATTR_TEMPERATURE, + PRECISION_WHOLE +) -DEPENDENCIES = [DOMAIN] +DEPENDENCIES = ['melissa'] _LOGGER = logging.getLogger(__name__) -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_ON_OFF | SUPPORT_FAN_MODE) +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 +] + +FAN_MODES = [ + STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM +] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -60,9 +71,7 @@ class MelissaClimate(ClimateDevice): if self._cur_settings is not None: return self._cur_settings[self._api.STATE] in ( self._api.STATE_ON, self._api.STATE_IDLE) - else: - _LOGGER.info("Can't determine state of %s", self.entity_id) - return STATE_UNKNOWN + return None @property def current_fan_mode(self): @@ -70,20 +79,12 @@ class MelissaClimate(ClimateDevice): if self._cur_settings is not None: return self.melissa_fan_to_hass( self._cur_settings[self._api.FAN]) - else: - _LOGGER.info( - "Can't determine current fan mode for %s", self.entity_id) - return STATE_UNKNOWN @property def current_temperature(self): """Return the current temperature.""" if self._data: return self._data[self._api.TEMP] - else: - _LOGGER.info( - "Can't determine current temperature for %s", self.entity_id) - return None @property def target_temperature_step(self): @@ -96,35 +97,22 @@ class MelissaClimate(ClimateDevice): if self._cur_settings is not None: return self.melissa_op_to_hass( self._cur_settings[self._api.MODE]) - else: - _LOGGER.info( - "Can't determine current operation mode of %s", self.entity_id) - return STATE_UNKNOWN @property def operation_list(self): """Return the list of available operation modes.""" - return [ - STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY - ] + return OP_MODES @property def fan_list(self): """List of available fan modes.""" - return [ - STATE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH - ] + return FAN_MODES @property def target_temperature(self): """Return the temperature we try to reach.""" if self._cur_settings is not None: return self._cur_settings[self._api.TEMP] - else: - _LOGGER.info( - "Can not determine current target temperature for %s", - self.entity_id) - return STATE_UNKNOWN @property def state(self): @@ -132,9 +120,6 @@ class MelissaClimate(ClimateDevice): if self._cur_settings is not None: return self.melissa_state_to_hass( self._cur_settings[self._api.STATE]) - else: - _LOGGER.info("Cant determine current state for %s", self.entity_id) - return STATE_UNKNOWN @property def temperature_unit(self): @@ -159,25 +144,25 @@ class MelissaClimate(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - return self.send({self._api.TEMP: temp}) + self.send({self._api.TEMP: temp}) def set_fan_mode(self, fan): """Set fan mode.""" fan_mode = self.hass_fan_to_melissa(fan) - return self.send({self._api.FAN: fan_mode}) + self.send({self._api.FAN: fan_mode}) def set_operation_mode(self, operation_mode): """Set operation mode.""" mode = self.hass_mode_to_melissa(operation_mode) - return self.send({self._api.MODE: mode}) + self.send({self._api.MODE: mode}) def turn_on(self): """Turn on device.""" - return self.send({self._api.STATE: self._api.STATE_ON}) + self.send({self._api.STATE: self._api.STATE_ON}) def turn_off(self): """Turn off device.""" - return self.send({self._api.STATE: self._api.STATE_OFF}) + self.send({self._api.STATE: self._api.STATE_OFF}) def send(self, value): """Sending action to service.""" @@ -201,7 +186,7 @@ class MelissaClimate(ClimateDevice): )['controller']['_relation']['command_log'] except KeyError: _LOGGER.warning( - 'Unable to update component %s', self.entity_id) + 'Unable to update entity %s', self.entity_id) def melissa_state_to_hass(self, state): """Translate Melissa states to hass states.""" @@ -212,7 +197,7 @@ class MelissaClimate(ClimateDevice): elif state == self._api.STATE_IDLE: return STATE_IDLE else: - return STATE_UNKNOWN + return None def melissa_op_to_hass(self, mode): """Translate Melissa modes to hass states.""" @@ -229,7 +214,7 @@ class MelissaClimate(ClimateDevice): else: _LOGGER.warning( "Operation mode %s could not be mapped to hass", mode) - return STATE_UNKNOWN + return None def melissa_fan_to_hass(self, fan): """Translate Melissa fan modes to hass modes.""" @@ -243,7 +228,7 @@ class MelissaClimate(ClimateDevice): return SPEED_HIGH else: _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) - return STATE_UNKNOWN + return None def hass_mode_to_melissa(self, mode): """Translate hass states to melissa modes.""" diff --git a/homeassistant/components/sensor/melissa.py b/homeassistant/components/sensor/melissa.py index 97645cb9dd4..58313428861 100644 --- a/homeassistant/components/sensor/melissa.py +++ b/homeassistant/components/sensor/melissa.py @@ -6,11 +6,11 @@ https://home-assistant.io/components/sensor.melissa/ """ import logging -from homeassistant.components.melissa import DOMAIN, DATA_MELISSA -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.components.melissa import DATA_MELISSA +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -DEPENDENCIES = [DOMAIN] +DEPENDENCIES = ['melissa'] _LOGGER = logging.getLogger(__name__) @@ -35,7 +35,7 @@ class MelissaSensor(Entity): def __init__(self, device, api): """Initialize the sensor.""" self._api = api - self._state = STATE_UNKNOWN + self._state = None self._name = '{0} {1}'.format( device['name'], self._type diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py index ef5cbff5087..f8a044c2f4b 100644 --- a/tests/components/climate/test_melissa.py +++ b/tests/components/climate/test_melissa.py @@ -5,14 +5,16 @@ import json from asynctest import mock -from homeassistant.components.climate import melissa, \ - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF, \ - SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY, STATE_COOL, \ - STATE_AUTO +from homeassistant.components.climate import ( + melissa, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + SUPPORT_ON_OFF, SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY, + STATE_COOL, STATE_AUTO +) from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH from homeassistant.components.melissa import DATA_MELISSA -from homeassistant.const import TEMP_CELSIUS, STATE_ON, ATTR_TEMPERATURE, \ - STATE_OFF, STATE_IDLE, STATE_UNKNOWN +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, ATTR_TEMPERATURE, STATE_OFF, STATE_IDLE +) from tests.common import get_test_home_assistant, load_fixture @@ -86,16 +88,16 @@ class TestMelissa(unittest.TestCase): def test_is_on(self): """Test name property.""" - self.assertEqual(self.thermostat.is_on, True) + self.assertTrue(self.thermostat.is_on) self.thermostat._cur_settings = None - self.assertEqual(STATE_UNKNOWN, self.thermostat.is_on) + self.assertFalse(self.thermostat.is_on) def test_current_fan_mode(self): """Test current_fan_mode property.""" self.thermostat.update() self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) self.thermostat._cur_settings = None - self.assertEqual(STATE_UNKNOWN, self.thermostat.current_fan_mode) + self.assertEqual(None, self.thermostat.current_fan_mode) def test_current_temperature(self): """Test current temperature.""" @@ -115,19 +117,19 @@ class TestMelissa(unittest.TestCase): self.thermostat.update() self.assertEqual(self.thermostat.current_operation, STATE_HEAT) self.thermostat._cur_settings = None - self.assertEqual(STATE_UNKNOWN, self.thermostat.current_operation) + self.assertEqual(None, self.thermostat.current_operation) def test_operation_list(self): """Test the operation list.""" self.assertEqual( - [STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY], + [STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT], self.thermostat.operation_list ) def test_fan_list(self): """Test the fan list.""" self.assertEqual( - [STATE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + [STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM], self.thermostat.fan_list ) @@ -135,13 +137,13 @@ class TestMelissa(unittest.TestCase): """Test target temperature.""" self.assertEqual(16, self.thermostat.target_temperature) self.thermostat._cur_settings = None - self.assertEqual(STATE_UNKNOWN, self.thermostat.target_temperature) + self.assertEqual(None, self.thermostat.target_temperature) def test_state(self): """Test state.""" self.assertEqual(STATE_ON, self.thermostat.state) self.thermostat._cur_settings = None - self.assertEqual(STATE_UNKNOWN, self.thermostat.state) + self.assertEqual(None, self.thermostat.state) def test_temperature_unit(self): """Test temperature unit.""" @@ -165,29 +167,30 @@ class TestMelissa(unittest.TestCase): """Test set_temperature.""" self.api.send.return_value = True self.thermostat.update() - self.assertTrue(self.thermostat.set_temperature( - **{ATTR_TEMPERATURE: 25})) + self.thermostat.set_temperature(**{ATTR_TEMPERATURE: 25}) self.assertEqual(25, self.thermostat.target_temperature) def test_fan_mode(self): """Test set_fan_mode.""" self.api.send.return_value = True - self.assertTrue(self.thermostat.set_fan_mode(SPEED_LOW)) - self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode) + self.thermostat.set_fan_mode(SPEED_HIGH) + self.assertEqual(SPEED_HIGH, self.thermostat.current_fan_mode) def test_set_operation_mode(self): """Test set_operation_mode.""" self.api.send.return_value = True - self.assertTrue(self.thermostat.set_operation_mode(STATE_COOL)) + self.thermostat.set_operation_mode(STATE_COOL) self.assertEqual(STATE_COOL, self.thermostat.current_operation) def test_turn_on(self): """Test turn_on.""" - self.assertTrue(self.thermostat.turn_on()) + self.thermostat.turn_on() + self.assertTrue(self.thermostat.state) def test_turn_off(self): """Test turn_off.""" - self.assertTrue(self.thermostat.turn_off()) + self.thermostat.turn_off() + self.assertEqual(STATE_OFF, self.thermostat.state) def test_send(self): """Test send.""" @@ -211,14 +214,14 @@ class TestMelissa(unittest.TestCase): self.thermostat._api.status.side_effect = KeyError('boom') self.thermostat.update() mocked_warning.assert_called_once_with( - 'Unable to update component %s', self.thermostat.entity_id) + 'Unable to update entity %s', self.thermostat.entity_id) def test_melissa_state_to_hass(self): """Test for translate melissa states to hass.""" self.assertEqual(STATE_OFF, self.thermostat.melissa_state_to_hass(0)) self.assertEqual(STATE_ON, self.thermostat.melissa_state_to_hass(1)) self.assertEqual(STATE_IDLE, self.thermostat.melissa_state_to_hass(2)) - self.assertEqual(STATE_UNKNOWN, + self.assertEqual(None, self.thermostat.melissa_state_to_hass(3)) def test_melissa_op_to_hass(self): @@ -229,7 +232,7 @@ class TestMelissa(unittest.TestCase): self.assertEqual(STATE_COOL, self.thermostat.melissa_op_to_hass(3)) self.assertEqual(STATE_DRY, self.thermostat.melissa_op_to_hass(4)) self.assertEqual( - STATE_UNKNOWN, self.thermostat.melissa_op_to_hass(5)) + None, self.thermostat.melissa_op_to_hass(5)) def test_melissa_fan_to_hass(self): """Test for translate melissa fan state to hass.""" @@ -237,7 +240,7 @@ class TestMelissa(unittest.TestCase): self.assertEqual(SPEED_LOW, self.thermostat.melissa_fan_to_hass(1)) self.assertEqual(SPEED_MEDIUM, self.thermostat.melissa_fan_to_hass(2)) self.assertEqual(SPEED_HIGH, self.thermostat.melissa_fan_to_hass(3)) - self.assertEqual(STATE_UNKNOWN, self.thermostat.melissa_fan_to_hass(4)) + self.assertEqual(None, self.thermostat.melissa_fan_to_hass(4)) @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning') def test_hass_mode_to_melissa(self, mocked_warning): diff --git a/tests/components/sensor/test_melissa.py b/tests/components/sensor/test_melissa.py index 3a13020438f..55b3e7f70f4 100644 --- a/tests/components/sensor/test_melissa.py +++ b/tests/components/sensor/test_melissa.py @@ -7,7 +7,7 @@ from homeassistant.components.melissa import DATA_MELISSA from homeassistant.components.sensor import melissa from homeassistant.components.sensor.melissa import MelissaTemperatureSensor, \ MelissaHumiditySensor -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.const import TEMP_CELSIUS from tests.common import get_test_home_assistant, load_fixture @@ -83,7 +83,7 @@ class TestMelissa(unittest.TestCase): """Test for faulty update.""" self.temp._api.status.return_value = {} self.temp.update() - self.assertEqual(STATE_UNKNOWN, self.temp.state) + self.assertEqual(None, self.temp.state) self.hum._api.status.return_value = {} self.hum.update() - self.assertEqual(STATE_UNKNOWN, self.hum.state) + self.assertEqual(None, self.hum.state) From 7e246e4680d71fe9913678ede731e8c6b51aa7ae Mon Sep 17 00:00:00 2001 From: Boyi C Date: Wed, 7 Feb 2018 04:56:31 +0800 Subject: [PATCH 133/166] Fix logger bug on Windows: path contains '\'. (#12197) * Fix logger bug on Windows: path contains '\'. * update * Update __init__.py --- homeassistant/components/system_log/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 7d9ebe85130..1dad1f3a1eb 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -76,10 +76,11 @@ def _figure_out_source(record, call_stack, hass): # Iterate through the stack call (in reverse) and find the last call from # a file in Home Assistant. Try to figure out where error happened. + paths_re = r'(?:{})/(.*)'.format('|'.join([re.escape(x) for x in paths])) for pathname in reversed(stack): # Try to match with a file within Home Assistant - match = re.match(r'(?:{})/(.*)'.format('|'.join(paths)), pathname) + match = re.match(paths_re, pathname) if match: return match.group(1) # Ok, we don't know what this is From 5ba02c531e4d7e68dc8b811ba2ebc7438afe6f16 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 7 Feb 2018 05:30:18 +0000 Subject: [PATCH 134/166] Catch concurrent.futures.CancelledError in websocket code. (#12150) * Catch concurrent.futures.CancelledError in websocket code. * Added a comment about the use of futures.CancelledError --- homeassistant/components/websocket_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index a4bfc46bf83..030d1bee579 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/developers/websocket_api/ """ import asyncio +from concurrent import futures from contextlib import suppress from functools import partial import json @@ -120,6 +121,11 @@ BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ TYPE_PING) }, extra=vol.ALLOW_EXTRA) +# Define the possible errors that occur when connections are cancelled. +# Originally, this was just asyncio.CancelledError, but issue #9546 showed +# that futures.CancelledErrors can also occur in some situations. +CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) + def auth_ok_message(): """Return an auth_ok message.""" @@ -231,7 +237,7 @@ class ActiveConnection: def _writer(self): """Write outgoing messages.""" # Exceptions if Socket disconnected or cancelled by connection handler - with suppress(RuntimeError, asyncio.CancelledError): + with suppress(RuntimeError, *CANCELLATION_ERRORS): while not self.wsock.closed: message = yield from self.to_write.get() if message is None: @@ -363,7 +369,7 @@ class ActiveConnection: self.log_error(msg) self._writer_task.cancel() - except asyncio.CancelledError: + except CANCELLATION_ERRORS: self.debug("Connection cancelled by server") except asyncio.QueueFull: From d05a1e35fcfcfd96397a0224a20f39ccfa3fbf60 Mon Sep 17 00:00:00 2001 From: Kevin Siml Date: Wed, 7 Feb 2018 17:23:10 +0100 Subject: [PATCH 135/166] Update pushsafer.py (#11466) * Update pushsafer.py Now you can setup your pushsafer notification, and change the following parameters: sound, vibration, icon, devices (targets), icon/led color, url, url title, time2live, picture * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * Update pushsafer.py * fix lint --- homeassistant/components/notify/pushsafer.py | 138 +++++++++++++++++-- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/notify/pushsafer.py b/homeassistant/components/notify/pushsafer.py index 78a600ab8d6..30068854f2e 100644 --- a/homeassistant/components/notify/pushsafer.py +++ b/homeassistant/components/notify/pushsafer.py @@ -5,20 +5,41 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.pushsafer/ """ import logging +import base64 +import mimetypes import requests +from requests.auth import HTTPBasicAuth import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA, + PLATFORM_SCHEMA, BaseNotificationService) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://www.pushsafer.com/api' +_ALLOWED_IMAGES = ['image/gif', 'image/jpeg', 'image/png'] CONF_DEVICE_KEY = 'private_key' +CONF_TIMEOUT = 15 -DEFAULT_TIMEOUT = 10 +# Top level attributes in 'data' +ATTR_SOUND = 'sound' +ATTR_VIBRATION = 'vibration' +ATTR_ICON = 'icon' +ATTR_ICONCOLOR = 'iconcolor' +ATTR_URL = 'url' +ATTR_URLTITLE = 'urltitle' +ATTR_TIME2LIVE = 'time2live' +ATTR_PICTURE1 = 'picture1' + +# Attributes contained in picture1 +ATTR_PICTURE1_URL = 'url' +ATTR_PICTURE1_PATH = 'path' +ATTR_PICTURE1_USERNAME = 'username' +ATTR_PICTURE1_PASSWORD = 'password' +ATTR_PICTURE1_AUTH = 'auth' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DEVICE_KEY): cv.string, @@ -27,21 +48,118 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the Pushsafer.com notification service.""" - return PushsaferNotificationService(config.get(CONF_DEVICE_KEY)) + return PushsaferNotificationService(config.get(CONF_DEVICE_KEY), + hass.config.is_allowed_path) class PushsaferNotificationService(BaseNotificationService): """Implementation of the notification service for Pushsafer.com.""" - def __init__(self, private_key): + def __init__(self, private_key, is_allowed_path): """Initialize the service.""" self._private_key = private_key + self.is_allowed_path = is_allowed_path def send_message(self, message='', **kwargs): - """Send a message to a user.""" + """Send a message to specified target.""" + if kwargs.get(ATTR_TARGET) is None: + targets = ["a"] + _LOGGER.debug("No target specified. Sending push to all") + else: + targets = kwargs.get(ATTR_TARGET) + _LOGGER.debug("%s target(s) specified", len(targets)) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - payload = {'k': self._private_key, 't': title, 'm': message} - response = requests.get(_RESOURCE, params=payload, - timeout=DEFAULT_TIMEOUT) - if response.status_code != 200: - _LOGGER.error("Not possible to send notification") + data = kwargs.get(ATTR_DATA, {}) + + # Converting the specified image to base64 + picture1 = data.get(ATTR_PICTURE1) + picture1_encoded = "" + if picture1 is not None: + _LOGGER.debug("picture1 is available") + url = picture1.get(ATTR_PICTURE1_URL, None) + local_path = picture1.get(ATTR_PICTURE1_PATH, None) + username = picture1.get(ATTR_PICTURE1_USERNAME) + password = picture1.get(ATTR_PICTURE1_PASSWORD) + auth = picture1.get(ATTR_PICTURE1_AUTH) + + if url is not None: + _LOGGER.debug("Loading image from url %s", url) + picture1_encoded = self.load_from_url(url, username, + password, auth) + elif local_path is not None: + _LOGGER.debug("Loading image from file %s", local_path) + picture1_encoded = self.load_from_file(local_path) + else: + _LOGGER.warning("missing url or local_path for picture1") + else: + _LOGGER.debug("picture1 is not specified") + + payload = { + 'k': self._private_key, + 't': title, + 'm': message, + 's': data.get(ATTR_SOUND, ""), + 'v': data.get(ATTR_VIBRATION, ""), + 'i': data.get(ATTR_ICON, ""), + 'c': data.get(ATTR_ICONCOLOR, ""), + 'u': data.get(ATTR_URL, ""), + 'ut': data.get(ATTR_URLTITLE, ""), + 'l': data.get(ATTR_TIME2LIVE, ""), + 'p': picture1_encoded + } + + for target in targets: + payload['d'] = target + response = requests.post(_RESOURCE, data=payload, + timeout=CONF_TIMEOUT) + if response.status_code != 200: + _LOGGER.error("Pushsafer failed with: %s", response.text) + else: + _LOGGER.debug("Push send: %s", response.json()) + + @classmethod + def get_base64(cls, filebyte, mimetype): + """Convert the image to the expected base64 string of pushsafer.""" + if mimetype not in _ALLOWED_IMAGES: + _LOGGER.warning("%s is a not supported mimetype for images", + mimetype) + return None + + base64_image = base64.b64encode(filebyte).decode('utf8') + return "data:{};base64,{}".format(mimetype, base64_image) + + def load_from_url(self, url=None, username=None, password=None, auth=None): + """Load image/document/etc from URL.""" + if url is not None: + _LOGGER.debug("Downloading image from %s", url) + if username is not None and password is not None: + auth_ = HTTPBasicAuth(username, password) + response = requests.get(url, auth=auth_, + timeout=CONF_TIMEOUT) + else: + response = requests.get(url, timeout=CONF_TIMEOUT) + return self.get_base64(response.content, + response.headers['content-type']) + else: + _LOGGER.warning("url not found in param") + + return None + + def load_from_file(self, local_path=None): + """Load image/document/etc from a local path.""" + try: + if local_path is not None: + _LOGGER.debug("Loading image from local path") + if self.is_allowed_path(local_path): + file_mimetype = mimetypes.guess_type(local_path) + _LOGGER.debug("Detected mimetype %s", file_mimetype) + with open(local_path, "rb") as binary_file: + data = binary_file.read() + return self.get_base64(data, file_mimetype[0]) + else: + _LOGGER.warning("Local path not found in params!") + except OSError as error: + _LOGGER.error("Can't load from local path: %s", error) + + return None From ea35ffbc819ba305a2a1b9dfe92c5378f0c9544b Mon Sep 17 00:00:00 2001 From: Marc Egli Date: Wed, 7 Feb 2018 18:35:08 +0100 Subject: [PATCH 136/166] Add wake on lan capability to philips TV (#12065) * Add wake on lan capability to philips TV * Update requirements_all.txt * Fix line length issues. * Replace wake on lan with turn on script for philips TV * rerun requirements script --- .../components/media_player/philips_js.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index d8450d31ea4..24981555007 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -12,10 +12,11 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +from homeassistant.helpers.script import Script from homeassistant.util import Throttle REQUIREMENTS = ['ha-philipsjs==0.0.1'] @@ -30,14 +31,16 @@ SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY +CONF_ON_ACTION = 'turn_on_action' + DEFAULT_DEVICE = 'default' DEFAULT_HOST = '127.0.0.1' DEFAULT_NAME = 'Philips TV' -BASE_URL = 'http://{0}:1925/1/{1}' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, }) @@ -48,16 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) host = config.get(CONF_HOST) + turn_on_action = config.get(CONF_ON_ACTION) tvapi = haphilipsjs.PhilipsTV(host) + on_script = Script(hass, turn_on_action) if turn_on_action else None - add_devices([PhilipsTV(tvapi, name)]) + add_devices([PhilipsTV(tvapi, name, on_script)]) class PhilipsTV(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" - def __init__(self, tv, name): + def __init__(self, tv, name, on_script): """Initialize the Philips TV.""" self._tv = tv self._name = name @@ -74,6 +79,7 @@ class PhilipsTV(MediaPlayerDevice): self._source_mapping = {} self._watching_tv = None self._channel_name = None + self._on_script = on_script @property def name(self): @@ -88,9 +94,10 @@ class PhilipsTV(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" + is_supporting_turn_on = SUPPORT_TURN_ON if self._on_script else 0 if self._watching_tv: - return SUPPORT_PHILIPS_JS_TV - return SUPPORT_PHILIPS_JS + return SUPPORT_PHILIPS_JS_TV | is_supporting_turn_on + return SUPPORT_PHILIPS_JS | is_supporting_turn_on @property def state(self): @@ -126,6 +133,11 @@ class PhilipsTV(MediaPlayerDevice): """Boolean if volume is currently muted.""" return self._muted + def turn_on(self): + """Turn on the device.""" + if self._on_script: + self._on_script.run() + def turn_off(self): """Turn off the device.""" self._tv.sendKey('Standby') From 9d5dee574a07297e6017a34c722be481be01936f Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 7 Feb 2018 20:38:06 +0000 Subject: [PATCH 137/166] Specify the minimum python version in the setup.py. (#12144) * Specify the minimum python version in the setup.py. * Used the minimum python version defined in homeassistant.const. --- setup.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 9f28cdd3ef1..f250dd739f7 100755 --- a/setup.py +++ b/setup.py @@ -2,14 +2,16 @@ """Home Assistant setup script.""" import os from setuptools import setup, find_packages +import sys + +import homeassistant.const as hass_const -from homeassistant.const import __version__ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-2017, {}'.format(PROJECT_AUTHOR) +PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR) PROJECT_URL = 'https://home-assistant.io/' PROJECT_EMAIL = 'hello@home-assistant.io' PROJECT_DESCRIPTION = ('Open-source home automation platform ' @@ -41,7 +43,7 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) HERE = os.path.abspath(os.path.dirname(__file__)) -DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, __version__) +DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) @@ -61,9 +63,15 @@ REQUIRES = [ 'certifi>=2017.4.17', ] +MIN_PY_VERSION = '.'.join(map( + str, + hass_const.REQUIRED_PYTHON_VER_WIN + if sys.platform.startswith('win') + else hass_const.REQUIRED_PYTHON_VER)) + setup( name=PROJECT_PACKAGE_NAME, - version=__version__, + version=hass_const.__version__, license=PROJECT_LICENSE, url=PROJECT_URL, download_url=DOWNLOAD_URL, @@ -75,6 +83,7 @@ setup( zip_safe=False, platforms='any', install_requires=REQUIRES, + python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', keywords=['home', 'automation'], entry_points={ From aa9b5e6ea58f9452bffe849d381d52a0ede04731 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 8 Feb 2018 07:46:14 +0100 Subject: [PATCH 138/166] Return of entity_id in template platforms (#12234) --- homeassistant/components/binary_sensor/template.py | 5 ----- homeassistant/components/cover/template.py | 5 ----- homeassistant/components/light/template.py | 5 ----- homeassistant/components/sensor/template.py | 5 ----- homeassistant/components/switch/template.py | 5 ----- 5 files changed, 25 deletions(-) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 5bce1243fd0..68ffbf77af2 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -41,11 +41,6 @@ SENSOR_SCHEMA = vol.Schema({ vol.All(cv.time_period, cv.positive_timedelta), }) -SENSOR_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - SENSOR_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index a7db472f191..f4728a12a3b 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -67,11 +67,6 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_ENTITY_ID): cv.entity_ids }) -COVER_SCHEMA = vol.All( - cv.deprecated(CONF_ENTITY_ID), - COVER_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), }) diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index ed7ba1978cc..d4f2b93e6b5 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -44,11 +44,6 @@ LIGHT_SCHEMA = vol.Schema({ vol.Optional(CONF_ENTITY_ID): cv.entity_ids }) -LIGHT_SCHEMA = vol.All( - cv.deprecated(CONF_ENTITY_ID), - LIGHT_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}), }) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 1d9bf0b7a9a..b347439e08d 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -31,11 +31,6 @@ SENSOR_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) -SENSOR_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - SENSOR_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 64dafdcadef..93ebf98e9ac 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -38,11 +38,6 @@ SWITCH_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) -SWITCH_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - SWITCH_SCHEMA, -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), }) From 945606238c12ff957fa5c52aae6a1c3ab1819c04 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 8 Feb 2018 07:59:09 +0100 Subject: [PATCH 139/166] Allow zero purge_interval to disable recorder purge (#12220) --- homeassistant/components/recorder/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index db208dada4f..b2628f954fc 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -79,7 +79,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PURGE_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_PURGE_INTERVAL, default=1): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(CONF_DB_URL): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -122,11 +122,11 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_days = conf.get(CONF_PURGE_KEEP_DAYS) purge_interval = conf.get(CONF_PURGE_INTERVAL) - if keep_days is None: + if keep_days is None and purge_interval != 0: _LOGGER.warning( "From version 0.64.0 the 'recorder' component will by default " "purge data older than 10 days. To keep data longer you must " - "configure a 'purge_keep_days' value.") + "configure 'purge_keep_days' or 'purge_interval'.") db_url = conf.get(CONF_DB_URL, None) if not db_url: From 2b9bb7963d7d004a85e24b9905b6808528b12675 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 8 Feb 2018 09:14:31 +0200 Subject: [PATCH 140/166] Update min js=latest version (#12091) --- homeassistant/components/frontend/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 413f334eb90..c3158731022 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -585,13 +585,13 @@ def _is_latest(js_option, request): return useragent.os.version[0] >= 12 family_min_version = { - 'Chrome': 50, # Probably can reduce this - 'Chrome Mobile': 50, - 'Firefox': 43, # Array.prototype.includes added in 43 - 'Firefox Mobile': 43, - 'Opera': 40, # Probably can reduce this - 'Edge': 14, # Array.prototype.includes added in 14 - 'Safari': 10, # many features not supported by 9 + 'Chrome': 54, # Object.values + 'Chrome Mobile': 54, + 'Firefox': 47, # Object.values + 'Firefox Mobile': 47, + 'Opera': 41, # Object.values + 'Edge': 14, # Array.prototype.includes added in 14 + 'Safari': 10, # Many features not supported by 9 } version = family_min_version.get(useragent.browser.family) return version and useragent.browser.version[0] >= version From d0ffb1bc52a71030223053a79a9b9118b5fdf4c1 Mon Sep 17 00:00:00 2001 From: Sergey Isachenko Date: Thu, 8 Feb 2018 10:15:02 +0300 Subject: [PATCH 141/166] librouteros version bump (#12227) --- homeassistant/components/device_tracker/mikrotik.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 7ac84125863..1805559c252 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) -REQUIREMENTS = ['librouteros==1.0.4'] +REQUIREMENTS = ['librouteros==1.0.5'] MTK_DEFAULT_API_PORT = '8728' diff --git a/requirements_all.txt b/requirements_all.txt index 6a1d1eeb59f..79d87b2d9e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -441,7 +441,7 @@ libpurecoollink==0.4.2 libpyfoscam==1.0 # homeassistant.components.device_tracker.mikrotik -librouteros==1.0.4 +librouteros==1.0.5 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 From 0300229085882edaf7e275e0086e271cfdb19c65 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 8 Feb 2018 07:32:39 +0000 Subject: [PATCH 142/166] SQL sensor (#12142) * Initial Commit * Passed all checks * Make DB_URL required * addresses review comments from @fabaff * unused variable --- homeassistant/components/sensor/sql.py | 145 +++++++++++++++++++++++++ requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/sensor/test_sql.py | 37 +++++++ 4 files changed, 184 insertions(+) create mode 100644 homeassistant/components/sensor/sql.py create mode 100644 tests/components/sensor/test_sql.py diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py new file mode 100644 index 00000000000..a0648d4851a --- /dev/null +++ b/homeassistant/components/sensor/sql.py @@ -0,0 +1,145 @@ +""" +Sensor from an SQL Query. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sql/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE) +from homeassistant.components.recorder import ( + CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['sqlalchemy==1.2.2'] + +CONF_QUERIES = 'queries' +CONF_QUERY = 'query' +CONF_COLUMN_NAME = 'column' + +_QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_QUERY): cv.string, + vol.Required(CONF_COLUMN_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_QUERIES): [_QUERY_SCHEME], + vol.Optional(CONF_DB_URL): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + db_url = config.get(CONF_DB_URL, None) + if not db_url: + db_url = DEFAULT_URL.format( + hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + + import sqlalchemy + from sqlalchemy.orm import sessionmaker, scoped_session + + try: + engine = sqlalchemy.create_engine(db_url) + sessionmaker = scoped_session(sessionmaker(bind=engine)) + + # run a dummy query just to test the db_url + sess = sessionmaker() + sess.execute("SELECT 1;") + + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) + return + + queries = [] + + for query in config.get(CONF_QUERIES): + name = query.get(CONF_NAME) + query_str = query.get(CONF_QUERY) + unit = query.get(CONF_UNIT_OF_MEASUREMENT) + value_template = query.get(CONF_VALUE_TEMPLATE) + column_name = query.get(CONF_COLUMN_NAME) + + if value_template is not None: + value_template.hass = hass + + sensor = SQLSensor( + name, sessionmaker, query_str, column_name, unit, value_template + ) + queries.append(sensor) + + add_devices(queries, True) + + +class SQLSensor(Entity): + """An SQL sensor.""" + + def __init__(self, name, sessmaker, query, column, unit, value_template): + """Initialize SQL sensor.""" + self._name = name + if "LIMIT" in query: + self._query = query + else: + self._query = query.replace(";", " LIMIT 1;") + self._unit_of_measurement = unit + self._template = value_template + self._column_name = column + self.sessionmaker = sessmaker + self._state = None + self._attributes = None + + @property + def name(self): + """Return the name of the query.""" + return self._name + + @property + def state(self): + """Return the query's current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + def update(self): + """Retrieve sensor data from the query.""" + import sqlalchemy + try: + sess = self.sessionmaker() + result = sess.execute(self._query) + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Error executing query %s: %s", self._query, err) + return + + for res in result: + _LOGGER.debug(res.items()) + data = res[self._column_name] + self._attributes = {k: str(v) for k, v in res.items()} + + if data is None: + _LOGGER.error("%s returned no results", self._query) + return False + + if self._template is not None: + self._state = self._template.async_render_with_possible_json_value( + data, None) + else: + self._state = data + + sess.close() diff --git a/requirements_all.txt b/requirements_all.txt index 79d87b2d9e5..149bf6d7154 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,6 +1127,7 @@ speedtest-cli==1.0.7 # homeassistant.components.recorder # homeassistant.scripts.db_migrator +# homeassistant.components.sensor.sql sqlalchemy==1.2.2 # homeassistant.components.statsd diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4a4e09b124..acddad7e942 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,6 +169,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator +# homeassistant.components.sensor.sql sqlalchemy==1.2.2 # homeassistant.components.statsd diff --git a/tests/components/sensor/test_sql.py b/tests/components/sensor/test_sql.py new file mode 100644 index 00000000000..ebf2d749e67 --- /dev/null +++ b/tests/components/sensor/test_sql.py @@ -0,0 +1,37 @@ +"""The test for the sql sensor platform.""" +import unittest + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + + +class TestSQLSensor(unittest.TestCase): + """Test the SQL sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_query(self): + """Test the SQL sensor.""" + config = { + 'sensor': { + 'platform': 'sql', + 'db_url': 'sqlite://', + 'queries': [{ + 'name': 'count_tables', + 'query': 'SELECT count(*) value FROM sqlite_master;', + 'column': 'value', + }] + } + } + + assert setup_component(self.hass, 'sensor', config) + + state = self.hass.states.get('sensor.count_tables') + self.assertEqual(state.state, '0') From 85239336054eb2a2fb64d43563e70edc4f134da5 Mon Sep 17 00:00:00 2001 From: Sergey Isachenko Date: Thu, 8 Feb 2018 10:34:26 +0300 Subject: [PATCH 143/166] Fixes for tesla. New sensors. (#12225) * fixes #11970 * long line fix --- homeassistant/components/sensor/tesla.py | 20 +++++++++++++------- homeassistant/components/tesla.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py index 4534c8d6203..74e74262710 100644 --- a/homeassistant/components/sensor/tesla.py +++ b/homeassistant/components/sensor/tesla.py @@ -10,7 +10,8 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN from homeassistant.components.tesla import TeslaDevice -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_KILOMETERS, LENGTH_MILES) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -66,18 +67,23 @@ class TeslaSensor(TeslaDevice, Entity): """Update the state from the sensor.""" _LOGGER.debug("Updating sensor: %s", self._name) self.tesla_device.update() + units = self.tesla_device.measurement + if self.tesla_device.bin_type == 0x4: if self.type == 'outside': self.current_value = self.tesla_device.get_outside_temp() else: self.current_value = self.tesla_device.get_inside_temp() - - tesla_temp_units = self.tesla_device.measurement - - if tesla_temp_units == 'F': + if units == 'F': self._unit = TEMP_FAHRENHEIT else: self._unit = TEMP_CELSIUS else: - self.current_value = self.tesla_device.battery_level() - self._unit = "%" + self.current_value = self.tesla_device.get_value() + if self.tesla_device.bin_type == 0x5: + self._unit = units + elif self.tesla_device.bin_type in (0xA, 0xB): + if units == 'LENGTH_MILES': + self._unit = LENGTH_MILES + else: + self._unit = LENGTH_KILOMETERS diff --git a/homeassistant/components/tesla.py b/homeassistant/components/tesla.py index 4f76b70432e..76b5c00d9d4 100644 --- a/homeassistant/components/tesla.py +++ b/homeassistant/components/tesla.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -REQUIREMENTS = ['teslajsonpy==0.0.19'] +REQUIREMENTS = ['teslajsonpy==0.0.23'] DOMAIN = 'tesla' diff --git a/requirements_all.txt b/requirements_all.txt index 149bf6d7154..166f835c029 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ tellduslive==0.10.4 temperusb==1.5.3 # homeassistant.components.tesla -teslajsonpy==0.0.19 +teslajsonpy==0.0.23 # homeassistant.components.thingspeak thingspeak==0.4.1 From 5601fbdc7a94230c54b438272b527531a53dc595 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Feb 2018 03:16:51 -0800 Subject: [PATCH 144/166] Entity layer cleanup (#12237) * Simplify entity update * Split entity platform from entity component * Decouple entity platform from entity component * Always include unit of measurement again * Lint * Fix test --- homeassistant/helpers/entity.py | 63 +-- homeassistant/helpers/entity_component.py | 356 ++------------ homeassistant/helpers/entity_platform.py | 317 +++++++++++++ tests/common.py | 44 +- tests/helpers/test_entity_component.py | 535 ++-------------------- tests/helpers/test_entity_platform.py | 435 ++++++++++++++++++ tests/test_config.py | 12 - 7 files changed, 905 insertions(+), 857 deletions(-) create mode 100644 homeassistant/helpers/entity_platform.py create mode 100644 tests/helpers/test_entity_platform.py diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d1e5c0d82a0..c7653d5d5b9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -152,7 +152,7 @@ class Entity(object): @property def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" - return None + return False @property def force_update(self) -> bool: @@ -221,21 +221,41 @@ class Entity(object): if device_attr is not None: attr.update(device_attr) - self._attr_setter('unit_of_measurement', str, ATTR_UNIT_OF_MEASUREMENT, - attr) + unit_of_measurement = self.unit_of_measurement + if unit_of_measurement is not None: + attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement - self._attr_setter('name', str, ATTR_FRIENDLY_NAME, attr) - self._attr_setter('icon', str, ATTR_ICON, attr) - self._attr_setter('entity_picture', str, ATTR_ENTITY_PICTURE, attr) - self._attr_setter('hidden', bool, ATTR_HIDDEN, attr) - self._attr_setter('assumed_state', bool, ATTR_ASSUMED_STATE, attr) - self._attr_setter('supported_features', int, ATTR_SUPPORTED_FEATURES, - attr) - self._attr_setter('device_class', str, ATTR_DEVICE_CLASS, attr) + name = self.name + if name is not None: + attr[ATTR_FRIENDLY_NAME] = name + + icon = self.icon + if icon is not None: + attr[ATTR_ICON] = icon + + entity_picture = self.entity_picture + if entity_picture is not None: + attr[ATTR_ENTITY_PICTURE] = entity_picture + + hidden = self.hidden + if hidden: + attr[ATTR_HIDDEN] = hidden + + assumed_state = self.assumed_state + if assumed_state: + attr[ATTR_ASSUMED_STATE] = assumed_state + + supported_features = self.supported_features + if supported_features is not None: + attr[ATTR_SUPPORTED_FEATURES] = supported_features + + device_class = self.device_class + if device_class is not None: + attr[ATTR_DEVICE_CLASS] = str(device_class) end = timer() - if not self._slow_reported and end - start > 0.4: + if end - start > 0.4 and not self._slow_reported: self._slow_reported = True _LOGGER.warning("Updating state for %s (%s) took %.3f seconds. " "Please report platform to the developers at " @@ -246,10 +266,6 @@ class Entity(object): if DATA_CUSTOMIZE in self.hass.data: attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)) - # Remove hidden property if false so it won't show up. - if not attr.get(ATTR_HIDDEN, True): - attr.pop(ATTR_HIDDEN) - # Convert temperature if we detect one try: unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) @@ -321,21 +337,6 @@ class Entity(object): else: self.hass.states.async_remove(self.entity_id) - def _attr_setter(self, name, typ, attr, attrs): - """Populate attributes based on properties.""" - if attr in attrs: - return - - value = getattr(self, name) - - if value is None: - return - - try: - attrs[attr] = typ(value) - except (TypeError, ValueError): - pass - def __eq__(self, other): """Return the comparison.""" if not isinstance(other, self.__class__): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 2c928f184e8..9dfbe580c16 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -6,25 +6,15 @@ from itertools import chain from homeassistant import config as conf_util from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, - DEVICE_DEFAULT_NAME) -from homeassistant.core import callback, valid_entity_id, split_entity_id -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady + ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.event import ( - async_track_time_interval, async_track_point_in_time) from homeassistant.helpers.service import extract_entity_ids from homeassistant.util import slugify -from homeassistant.util.async import ( - run_callback_threadsafe, run_coroutine_threadsafe) -import homeassistant.util.dt as dt_util -from .entity_registry import EntityRegistry +from .entity_platform import EntityPlatform DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) -SLOW_SETUP_WARNING = 10 -SLOW_SETUP_MAX_WAIT = 60 -PLATFORM_NOT_READY_RETRIES = 10 -DATA_REGISTRY = 'entity_registry' class EntityComponent(object): @@ -43,16 +33,23 @@ class EntityComponent(object): """Initialize an entity component.""" self.logger = logger self.hass = hass - self.domain = domain - self.entity_id_format = domain + '.{}' self.scan_interval = scan_interval self.group_name = group_name self.config = None self._platforms = { - 'core': EntityPlatform(self, domain, self.scan_interval, 0, None), + 'core': EntityPlatform( + hass=hass, + logger=logger, + domain=domain, + platform_name='core', + scan_interval=self.scan_interval, + parallel_updates=0, + entity_namespace=None, + async_entities_added_callback=self._async_update_group, + ) } self.async_add_entities = self._platforms['core'].async_add_entities self.add_entities = self._platforms['core'].add_entities @@ -107,17 +104,6 @@ class EntityComponent(object): discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered) - def extract_from_service(self, service, expand_group=True): - """Extract all known entities from a service call. - - Will return all entities if no entities specified in call. - Will return an empty list if entities specified but unknown. - """ - return run_callback_threadsafe( - self.hass.loop, self.async_extract_from_service, service, - expand_group - ).result() - @callback def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. @@ -136,11 +122,8 @@ class EntityComponent(object): @asyncio.coroutine def _async_setup_platform(self, platform_type, platform_config, - discovery_info=None, tries=0): - """Set up a platform for this component. - - This method must be run in the event loop. - """ + discovery_info=None): + """Set up a platform for this component.""" platform = yield from async_prepare_setup_platform( self.hass, self.config, self.domain, platform_type) @@ -161,59 +144,23 @@ class EntityComponent(object): if key not in self._platforms: entity_platform = self._platforms[key] = EntityPlatform( - self, platform_type, scan_interval, parallel_updates, - entity_namespace) + hass=self.hass, + logger=self.logger, + domain=self.domain, + platform_name=platform_type, + scan_interval=scan_interval, + parallel_updates=parallel_updates, + entity_namespace=entity_namespace, + async_entities_added_callback=self._async_update_group, + ) else: entity_platform = self._platforms[key] - self.logger.info("Setting up %s.%s", self.domain, platform_type) - warn_task = self.hass.loop.call_later( - SLOW_SETUP_WARNING, self.logger.warning, - "Setup of platform %s is taking over %s seconds.", platform_type, - SLOW_SETUP_WARNING) - - try: - if getattr(platform, 'async_setup_platform', None): - task = platform.async_setup_platform( - self.hass, platform_config, - entity_platform.async_schedule_add_entities, discovery_info - ) - else: - # This should not be replaced with hass.async_add_job because - # we don't want to track this task in case it blocks startup. - task = self.hass.loop.run_in_executor( - None, platform.setup_platform, self.hass, platform_config, - entity_platform.schedule_add_entities, discovery_info - ) - yield from asyncio.wait_for( - asyncio.shield(task, loop=self.hass.loop), - SLOW_SETUP_MAX_WAIT, loop=self.hass.loop) - yield from entity_platform.async_block_entities_done() - self.hass.config.components.add( - '{}.{}'.format(self.domain, platform_type)) - except PlatformNotReady: - tries += 1 - wait_time = min(tries, 6) * 30 - self.logger.warning( - 'Platform %s not ready yet. Retrying in %d seconds.', - platform_type, wait_time) - async_track_point_in_time( - self.hass, self._async_setup_platform( - platform_type, platform_config, discovery_info, tries), - dt_util.utcnow() + timedelta(seconds=wait_time)) - except asyncio.TimeoutError: - self.logger.error( - "Setup of platform %s is taking longer than %s seconds." - " Startup will proceed without waiting any longer.", - platform_type, SLOW_SETUP_MAX_WAIT) - except Exception: # pylint: disable=broad-except - self.logger.exception( - "Error while setting up platform %s", platform_type) - finally: - warn_task.cancel() + yield from entity_platform.async_setup( + platform, platform_config, discovery_info) @callback - def async_update_group(self): + def _async_update_group(self): """Set up and/or update component group. This method must be run in the event loop. @@ -230,12 +177,8 @@ class EntityComponent(object): visible=False, entity_ids=ids ) - def reset(self): - """Remove entities and reset the entity component to initial values.""" - run_coroutine_threadsafe(self.async_reset(), self.hass.loop).result() - @asyncio.coroutine - def async_reset(self): + def _async_reset(self): """Remove entities and reset the entity component to initial values. This method must be run in the event loop. @@ -261,11 +204,6 @@ class EntityComponent(object): if entity_id in platform.entities: yield from platform.async_remove_entity(entity_id) - def prepare_reload(self): - """Prepare reloading this entity component.""" - return run_coroutine_threadsafe( - self.async_prepare_reload(), loop=self.hass.loop).result() - @asyncio.coroutine def async_prepare_reload(self): """Prepare reloading this entity component. @@ -285,239 +223,5 @@ class EntityComponent(object): if conf is None: return None - yield from self.async_reset() + yield from self._async_reset() return conf - - -class EntityPlatform(object): - """Manage the entities for a single platform.""" - - def __init__(self, component, platform, scan_interval, parallel_updates, - entity_namespace): - """Initialize the entity platform.""" - self.component = component - self.platform = platform - self.scan_interval = scan_interval - self.parallel_updates = None - self.entity_namespace = entity_namespace - self.entities = {} - self._tasks = [] - self._async_unsub_polling = None - self._process_updates = asyncio.Lock(loop=component.hass.loop) - - if parallel_updates: - self.parallel_updates = asyncio.Semaphore( - parallel_updates, loop=component.hass.loop) - - @asyncio.coroutine - def async_block_entities_done(self): - """Wait until all entities add to hass.""" - if self._tasks: - pending = [task for task in self._tasks if not task.done()] - self._tasks.clear() - - if pending: - yield from asyncio.wait(pending, loop=self.component.hass.loop) - - def schedule_add_entities(self, new_entities, update_before_add=False): - """Add entities for a single platform.""" - run_callback_threadsafe( - self.component.hass.loop, - self.async_schedule_add_entities, list(new_entities), - update_before_add - ).result() - - @callback - def async_schedule_add_entities(self, new_entities, - update_before_add=False): - """Add entities for a single platform async.""" - self._tasks.append(self.component.hass.async_add_job( - self.async_add_entities( - new_entities, update_before_add=update_before_add) - )) - - def add_entities(self, new_entities, update_before_add=False): - """Add entities for a single platform.""" - # That avoid deadlocks - if update_before_add: - self.component.logger.warning( - "Call 'add_entities' with update_before_add=True " - "only inside tests or you can run into a deadlock!") - - run_coroutine_threadsafe( - self.async_add_entities(list(new_entities), update_before_add), - self.component.hass.loop).result() - - @asyncio.coroutine - def async_add_entities(self, new_entities, update_before_add=False): - """Add entities for a single platform async. - - This method must be run in the event loop. - """ - # handle empty list from component/platform - if not new_entities: - return - - hass = self.component.hass - component_entities = set(entity.entity_id for entity - in self.component.entities) - - registry = hass.data.get(DATA_REGISTRY) - - if registry is None: - registry = hass.data[DATA_REGISTRY] = EntityRegistry(hass) - - yield from registry.async_ensure_loaded() - - tasks = [ - self._async_add_entity(entity, update_before_add, - component_entities, registry) - for entity in new_entities] - - yield from asyncio.wait(tasks, loop=self.component.hass.loop) - self.component.async_update_group() - - if self._async_unsub_polling is not None or \ - not any(entity.should_poll for entity - in self.entities.values()): - return - - self._async_unsub_polling = async_track_time_interval( - self.component.hass, self._update_entity_states, self.scan_interval - ) - - @asyncio.coroutine - def _async_add_entity(self, entity, update_before_add, component_entities, - registry): - """Helper method to add an entity to the platform.""" - if entity is None: - raise ValueError('Entity cannot be None') - - entity.hass = self.component.hass - entity.platform = self - entity.parallel_updates = self.parallel_updates - - # Update properties before we generate the entity_id - if update_before_add: - try: - yield from entity.async_device_update(warning=False) - except Exception: # pylint: disable=broad-except - self.component.logger.exception( - "%s: Error on device update!", self.platform) - return - - suggested_object_id = None - - # Get entity_id from unique ID registration - if entity.unique_id is not None: - if entity.entity_id is not None: - suggested_object_id = split_entity_id(entity.entity_id)[1] - else: - suggested_object_id = entity.name - - entry = registry.async_get_or_create( - self.component.domain, self.platform, entity.unique_id, - suggested_object_id=suggested_object_id) - entity.entity_id = entry.entity_id - - # We won't generate an entity ID if the platform has already set one - # We will however make sure that platform cannot pick a registered ID - elif (entity.entity_id is not None and - registry.async_is_registered(entity.entity_id)): - # If entity already registered, convert entity id to suggestion - suggested_object_id = split_entity_id(entity.entity_id)[1] - entity.entity_id = None - - # Generate entity ID - if entity.entity_id is None: - suggested_object_id = \ - suggested_object_id or entity.name or DEVICE_DEFAULT_NAME - - if self.entity_namespace is not None: - suggested_object_id = '{} {}'.format(self.entity_namespace, - suggested_object_id) - - entity.entity_id = registry.async_generate_entity_id( - self.component.domain, suggested_object_id) - - # Make sure it is valid in case an entity set the value themselves - if not valid_entity_id(entity.entity_id): - raise HomeAssistantError( - 'Invalid entity id: {}'.format(entity.entity_id)) - elif entity.entity_id in component_entities: - raise HomeAssistantError( - 'Entity id already exists: {}'.format(entity.entity_id)) - - self.entities[entity.entity_id] = entity - component_entities.add(entity.entity_id) - - if hasattr(entity, 'async_added_to_hass'): - yield from entity.async_added_to_hass() - - yield from entity.async_update_ha_state() - - @asyncio.coroutine - def async_reset(self): - """Remove all entities and reset data. - - This method must be run in the event loop. - """ - if not self.entities: - return - - tasks = [self._async_remove_entity(entity_id) - for entity_id in self.entities] - - yield from asyncio.wait(tasks, loop=self.component.hass.loop) - - if self._async_unsub_polling is not None: - self._async_unsub_polling() - self._async_unsub_polling = None - - @asyncio.coroutine - def async_remove_entity(self, entity_id): - """Remove entity id from platform.""" - yield from self._async_remove_entity(entity_id) - - # Clean up polling job if no longer needed - if (self._async_unsub_polling is not None and - not any(entity.should_poll for entity - in self.entities.values())): - self._async_unsub_polling() - self._async_unsub_polling = None - - @asyncio.coroutine - def _async_remove_entity(self, entity_id): - """Remove entity id from platform.""" - entity = self.entities.pop(entity_id) - - if hasattr(entity, 'async_will_remove_from_hass'): - yield from entity.async_will_remove_from_hass() - - self.component.hass.states.async_remove(entity_id) - - @asyncio.coroutine - def _update_entity_states(self, now): - """Update the states of all the polling entities. - - To protect from flooding the executor, we will update async entities - in parallel and other entities sequential. - - This method must be run in the event loop. - """ - if self._process_updates.locked(): - self.component.logger.warning( - "Updating %s %s took longer than the scheduled update " - "interval %s", self.platform, self.component.domain, - self.scan_interval) - return - - with (yield from self._process_updates): - tasks = [] - for entity in self.entities.values(): - if not entity.should_poll: - continue - tasks.append(entity.async_update_ha_state(True)) - - if tasks: - yield from asyncio.wait(tasks, loop=self.component.hass.loop) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py new file mode 100644 index 00000000000..3362f1e3b3f --- /dev/null +++ b/homeassistant/helpers/entity_platform.py @@ -0,0 +1,317 @@ +"""Class to manage the entities for a single platform.""" +import asyncio +from datetime import timedelta + +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import callback, valid_entity_id, split_entity_id +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.util.async import ( + run_callback_threadsafe, run_coroutine_threadsafe) +import homeassistant.util.dt as dt_util + +from .event import async_track_time_interval, async_track_point_in_time +from .entity_registry import EntityRegistry + +SLOW_SETUP_WARNING = 10 +SLOW_SETUP_MAX_WAIT = 60 +PLATFORM_NOT_READY_RETRIES = 10 +DATA_REGISTRY = 'entity_registry' + + +class EntityPlatform(object): + """Manage the entities for a single platform.""" + + def __init__(self, *, hass, logger, domain, platform_name, scan_interval, + parallel_updates, entity_namespace, + async_entities_added_callback): + """Initialize the entity platform. + + hass: HomeAssistant + logger: Logger + domain: str + platform_name: str + scan_interval: timedelta + parallel_updates: int + entity_namespace: str + async_entities_added_callback: @callback method + """ + self.hass = hass + self.logger = logger + self.domain = domain + self.platform_name = platform_name + self.scan_interval = scan_interval + self.parallel_updates = None + self.entity_namespace = entity_namespace + self.async_entities_added_callback = async_entities_added_callback + self.entities = {} + self._tasks = [] + self._async_unsub_polling = None + self._process_updates = asyncio.Lock(loop=hass.loop) + + if parallel_updates: + self.parallel_updates = asyncio.Semaphore( + parallel_updates, loop=hass.loop) + + @asyncio.coroutine + def async_setup(self, platform, platform_config, discovery_info=None, + tries=0): + """Setup the platform.""" + logger = self.logger + hass = self.hass + full_name = '{}.{}'.format(self.domain, self.platform_name) + + logger.info("Setting up %s", full_name) + warn_task = hass.loop.call_later( + SLOW_SETUP_WARNING, logger.warning, + "Setup of platform %s is taking over %s seconds.", + self.platform_name, SLOW_SETUP_WARNING) + + try: + if getattr(platform, 'async_setup_platform', None): + task = platform.async_setup_platform( + hass, platform_config, + self._async_schedule_add_entities, discovery_info + ) + else: + # This should not be replaced with hass.async_add_job because + # we don't want to track this task in case it blocks startup. + task = hass.loop.run_in_executor( + None, platform.setup_platform, hass, platform_config, + self._schedule_add_entities, discovery_info + ) + yield from asyncio.wait_for( + asyncio.shield(task, loop=hass.loop), + SLOW_SETUP_MAX_WAIT, loop=hass.loop) + + # Block till all entities are done + if self._tasks: + pending = [task for task in self._tasks if not task.done()] + self._tasks.clear() + + if pending: + yield from asyncio.wait( + pending, loop=self.hass.loop) + + hass.config.components.add(full_name) + except PlatformNotReady: + tries += 1 + wait_time = min(tries, 6) * 30 + logger.warning( + 'Platform %s not ready yet. Retrying in %d seconds.', + self.platform_name, wait_time) + async_track_point_in_time( + hass, self.async_setup( + platform, platform_config, discovery_info, tries), + dt_util.utcnow() + timedelta(seconds=wait_time)) + except asyncio.TimeoutError: + logger.error( + "Setup of platform %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer.", + self.platform_name, SLOW_SETUP_MAX_WAIT) + except Exception: # pylint: disable=broad-except + logger.exception( + "Error while setting up platform %s", self.platform_name) + finally: + warn_task.cancel() + + def _schedule_add_entities(self, new_entities, update_before_add=False): + """Synchronously schedule adding entities for a single platform.""" + run_callback_threadsafe( + self.hass.loop, + self._async_schedule_add_entities, list(new_entities), + update_before_add + ).result() + + @callback + def _async_schedule_add_entities(self, new_entities, + update_before_add=False): + """Schedule adding entities for a single platform async.""" + self._tasks.append(self.hass.async_add_job( + self.async_add_entities( + new_entities, update_before_add=update_before_add) + )) + + def add_entities(self, new_entities, update_before_add=False): + """Add entities for a single platform.""" + # That avoid deadlocks + if update_before_add: + self.logger.warning( + "Call 'add_entities' with update_before_add=True " + "only inside tests or you can run into a deadlock!") + + run_coroutine_threadsafe( + self.async_add_entities(list(new_entities), update_before_add), + self.hass.loop).result() + + @asyncio.coroutine + def async_add_entities(self, new_entities, update_before_add=False): + """Add entities for a single platform async. + + This method must be run in the event loop. + """ + # handle empty list from component/platform + if not new_entities: + return + + hass = self.hass + component_entities = set(hass.states.async_entity_ids(self.domain)) + + registry = hass.data.get(DATA_REGISTRY) + + if registry is None: + registry = hass.data[DATA_REGISTRY] = EntityRegistry(hass) + + yield from registry.async_ensure_loaded() + + tasks = [ + self._async_add_entity(entity, update_before_add, + component_entities, registry) + for entity in new_entities] + + yield from asyncio.wait(tasks, loop=self.hass.loop) + self.async_entities_added_callback() + + if self._async_unsub_polling is not None or \ + not any(entity.should_poll for entity + in self.entities.values()): + return + + self._async_unsub_polling = async_track_time_interval( + self.hass, self._update_entity_states, self.scan_interval + ) + + @asyncio.coroutine + def _async_add_entity(self, entity, update_before_add, component_entities, + registry): + """Helper method to add an entity to the platform.""" + if entity is None: + raise ValueError('Entity cannot be None') + + entity.hass = self.hass + entity.platform = self + entity.parallel_updates = self.parallel_updates + + # Update properties before we generate the entity_id + if update_before_add: + try: + yield from entity.async_device_update(warning=False) + except Exception: # pylint: disable=broad-except + self.logger.exception( + "%s: Error on device update!", self.platform_name) + return + + suggested_object_id = None + + # Get entity_id from unique ID registration + if entity.unique_id is not None: + if entity.entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + suggested_object_id = entity.name + + entry = registry.async_get_or_create( + self.domain, self.platform_name, entity.unique_id, + suggested_object_id=suggested_object_id) + entity.entity_id = entry.entity_id + + # We won't generate an entity ID if the platform has already set one + # We will however make sure that platform cannot pick a registered ID + elif (entity.entity_id is not None and + registry.async_is_registered(entity.entity_id)): + # If entity already registered, convert entity id to suggestion + suggested_object_id = split_entity_id(entity.entity_id)[1] + entity.entity_id = None + + # Generate entity ID + if entity.entity_id is None: + suggested_object_id = \ + suggested_object_id or entity.name or DEVICE_DEFAULT_NAME + + if self.entity_namespace is not None: + suggested_object_id = '{} {}'.format(self.entity_namespace, + suggested_object_id) + + entity.entity_id = registry.async_generate_entity_id( + self.domain, suggested_object_id) + + # Make sure it is valid in case an entity set the value themselves + if not valid_entity_id(entity.entity_id): + raise HomeAssistantError( + 'Invalid entity id: {}'.format(entity.entity_id)) + elif entity.entity_id in component_entities: + raise HomeAssistantError( + 'Entity id already exists: {}'.format(entity.entity_id)) + + self.entities[entity.entity_id] = entity + component_entities.add(entity.entity_id) + + if hasattr(entity, 'async_added_to_hass'): + yield from entity.async_added_to_hass() + + yield from entity.async_update_ha_state() + + @asyncio.coroutine + def async_reset(self): + """Remove all entities and reset data. + + This method must be run in the event loop. + """ + if not self.entities: + return + + tasks = [self._async_remove_entity(entity_id) + for entity_id in self.entities] + + yield from asyncio.wait(tasks, loop=self.hass.loop) + + if self._async_unsub_polling is not None: + self._async_unsub_polling() + self._async_unsub_polling = None + + @asyncio.coroutine + def async_remove_entity(self, entity_id): + """Remove entity id from platform.""" + yield from self._async_remove_entity(entity_id) + + # Clean up polling job if no longer needed + if (self._async_unsub_polling is not None and + not any(entity.should_poll for entity + in self.entities.values())): + self._async_unsub_polling() + self._async_unsub_polling = None + + @asyncio.coroutine + def _async_remove_entity(self, entity_id): + """Remove entity id from platform.""" + entity = self.entities.pop(entity_id) + + if hasattr(entity, 'async_will_remove_from_hass'): + yield from entity.async_will_remove_from_hass() + + self.hass.states.async_remove(entity_id) + + @asyncio.coroutine + def _update_entity_states(self, now): + """Update the states of all the polling entities. + + To protect from flooding the executor, we will update async entities + in parallel and other entities sequential. + + This method must be run in the event loop. + """ + if self._process_updates.locked(): + self.logger.warning( + "Updating %s %s took longer than the scheduled update " + "interval %s", self.platform_name, self.domain, + self.scan_interval) + return + + with (yield from self._process_updates): + tasks = [] + for entity in self.entities.values(): + if not entity.should_poll: + continue + tasks.append(entity.async_update_ha_state(True)) + + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) diff --git a/tests/common.py b/tests/common.py index ed4439c1c49..22af8ecb8a3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,7 +14,9 @@ from aiohttp import web from homeassistant import core as ha, loader from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config -from homeassistant.helpers import intent, dispatcher, entity, restore_state +from homeassistant.helpers import ( + intent, dispatcher, entity, restore_state, entity_registry, + entity_platform) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util import homeassistant.util.yaml as yaml @@ -22,7 +24,6 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) -from homeassistant.helpers import entity_component, entity_registry from homeassistant.components import mqtt, recorder from homeassistant.components.http.auth import auth_middleware from homeassistant.components.http.const import ( @@ -320,7 +321,7 @@ def mock_registry(hass): """Mock the Entity Registry.""" registry = entity_registry.EntityRegistry(hass) registry.entities = {} - hass.data[entity_component.DATA_REGISTRY] = registry + hass.data[entity_platform.DATA_REGISTRY] = registry return registry @@ -585,3 +586,40 @@ class MockDependency: func(*args, **kwargs) return run_mocked + + +class MockEntity(entity.Entity): + """Mock Entity class.""" + + def __init__(self, **values): + """Initialize an entity.""" + self._values = values + + if 'entity_id' in values: + self.entity_id = values['entity_id'] + + @property + def name(self): + """Return the name of the entity.""" + return self._handle('name') + + @property + def should_poll(self): + """Return the ste of the polling.""" + return self._handle('should_poll') + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._handle('unique_id') + + @property + def available(self): + """Return True if entity is available.""" + return self._handle('available') + + def _handle(self, attr): + """Helper for the attributes.""" + if attr in self._values: + return self._values[attr] + return getattr(super(), attr) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 349766d025e..ef92da3172b 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -4,67 +4,27 @@ import asyncio from collections import OrderedDict import logging import unittest -from unittest.mock import patch, Mock, MagicMock +from unittest.mock import patch, Mock from datetime import timedelta import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.exceptions import PlatformNotReady from homeassistant.components import group -from homeassistant.helpers.entity import Entity, generate_entity_id -from homeassistant.helpers.entity_component import ( - EntityComponent, DEFAULT_SCAN_INTERVAL, SLOW_SETUP_WARNING) -from homeassistant.helpers import entity_component +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import setup_component from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, - mock_coro, async_fire_time_changed, mock_registry) + get_test_home_assistant, MockPlatform, MockModule, mock_coro, + async_fire_time_changed, MockEntity) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" -class EntityTest(Entity): - """Test for the Entity component.""" - - def __init__(self, **values): - """Initialize an entity.""" - self._values = values - - if 'entity_id' in values: - self.entity_id = values['entity_id'] - - @property - def name(self): - """Return the name of the entity.""" - return self._handle('name') - - @property - def should_poll(self): - """Return the ste of the polling.""" - return self._handle('should_poll') - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._handle('unique_id') - - @property - def available(self): - """Return True if entity is available.""" - return self._handle('available') - - def _handle(self, attr): - """Helper for the attributes.""" - if attr in self._values: - return self._values[attr] - return getattr(super(), attr) - - class TestHelpersEntityComponent(unittest.TestCase): """Test homeassistant.helpers.entity_component module.""" @@ -85,7 +45,7 @@ class TestHelpersEntityComponent(unittest.TestCase): # No group after setup assert len(self.hass.states.entity_ids()) == 0 - component.add_entities([EntityTest()]) + component.add_entities([MockEntity()]) self.hass.block_till_done() # group exists @@ -98,7 +58,7 @@ class TestHelpersEntityComponent(unittest.TestCase): ('test_domain.unnamed_device',) # group extended - component.add_entities([EntityTest(name='goodbye')]) + component.add_entities([MockEntity(name='goodbye')]) self.hass.block_till_done() assert len(self.hass.states.entity_ids()) == 3 @@ -108,151 +68,6 @@ class TestHelpersEntityComponent(unittest.TestCase): assert group.attributes.get('entity_id') == \ ('test_domain.goodbye', 'test_domain.unnamed_device') - def test_polling_only_updates_entities_it_should_poll(self): - """Test the polling of only updated entities.""" - component = EntityComponent( - _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) - - no_poll_ent = EntityTest(should_poll=False) - no_poll_ent.async_update = Mock() - poll_ent = EntityTest(should_poll=True) - poll_ent.async_update = Mock() - - component.add_entities([no_poll_ent, poll_ent]) - - no_poll_ent.async_update.reset_mock() - poll_ent.async_update.reset_mock() - - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) - self.hass.block_till_done() - - assert not no_poll_ent.async_update.called - assert poll_ent.async_update.called - - def test_polling_updates_entities_with_exception(self): - """Test the updated entities that not break with an exception.""" - component = EntityComponent( - _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) - - update_ok = [] - update_err = [] - - def update_mock(): - """Mock normal update.""" - update_ok.append(None) - - def update_mock_err(): - """Mock error update.""" - update_err.append(None) - raise AssertionError("Fake error update") - - ent1 = EntityTest(should_poll=True) - ent1.update = update_mock_err - ent2 = EntityTest(should_poll=True) - ent2.update = update_mock - ent3 = EntityTest(should_poll=True) - ent3.update = update_mock - ent4 = EntityTest(should_poll=True) - ent4.update = update_mock - - component.add_entities([ent1, ent2, ent3, ent4]) - - update_ok.clear() - update_err.clear() - - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) - self.hass.block_till_done() - - assert len(update_ok) == 3 - assert len(update_err) == 1 - - def test_update_state_adds_entities(self): - """Test if updating poll entities cause an entity to be added works.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent1 = EntityTest() - ent2 = EntityTest(should_poll=True) - - component.add_entities([ent2]) - assert 1 == len(self.hass.states.entity_ids()) - ent2.update = lambda *_: component.add_entities([ent1]) - - fire_time_changed( - self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL - ) - self.hass.block_till_done() - - assert 2 == len(self.hass.states.entity_ids()) - - def test_update_state_adds_entities_with_update_before_add_true(self): - """Test if call update before add to state machine.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent = EntityTest() - ent.update = Mock(spec_set=True) - - component.add_entities([ent], True) - self.hass.block_till_done() - - assert 1 == len(self.hass.states.entity_ids()) - assert ent.update.called - - def test_update_state_adds_entities_with_update_before_add_false(self): - """Test if not call update before add to state machine.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent = EntityTest() - ent.update = Mock(spec_set=True) - - component.add_entities([ent], False) - self.hass.block_till_done() - - assert 1 == len(self.hass.states.entity_ids()) - assert not ent.update.called - - def test_extract_from_service_returns_all_if_no_entity_id(self): - """Test the extraction of everything from service.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - component.add_entities([ - EntityTest(name='test_1'), - EntityTest(name='test_2'), - ]) - - call = ha.ServiceCall('test', 'service') - - assert ['test_domain.test_1', 'test_domain.test_2'] == \ - sorted(ent.entity_id for ent in - component.extract_from_service(call)) - - def test_extract_from_service_filter_out_non_existing_entities(self): - """Test the extraction of non existing entities from service.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - component.add_entities([ - EntityTest(name='test_1'), - EntityTest(name='test_2'), - ]) - - call = ha.ServiceCall('test', 'service', { - 'entity_id': ['test_domain.test_2', 'test_domain.non_exist'] - }) - - assert ['test_domain.test_2'] == \ - [ent.entity_id for ent in component.extract_from_service(call)] - - def test_extract_from_service_no_group_expand(self): - """Test not expanding a group.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - test_group = group.Group.create_group( - self.hass, 'test_group', ['light.Ceiling', 'light.Kitchen']) - component.add_entities([test_group]) - - call = ha.ServiceCall('test', 'service', { - 'entity_id': ['group.test_group'] - }) - - extracted = component.extract_from_service(call, expand_group=False) - self.assertEqual([test_group], extracted) - def test_setup_loads_platforms(self): """Test the loading of the platforms.""" component_setup = Mock(return_value=True) @@ -320,13 +135,13 @@ class TestHelpersEntityComponent(unittest.TestCase): assert ('platform_test', {}, {'msg': 'discovery_info'}) == \ mock_setup.call_args[0] - @patch('homeassistant.helpers.entity_component.' + @patch('homeassistant.helpers.entity_platform.' 'async_track_time_interval') def test_set_scan_interval_via_config(self, mock_track): """Test the setting of the scan interval via configuration.""" def platform_setup(hass, config, add_devices, discovery_info=None): """Test the platform setup.""" - add_devices([EntityTest(should_poll=True)]) + add_devices([MockEntity(should_poll=True)]) loader.set_component('test_domain.platform', MockPlatform(platform_setup)) @@ -344,38 +159,13 @@ class TestHelpersEntityComponent(unittest.TestCase): assert mock_track.called assert timedelta(seconds=30) == mock_track.call_args[0][2] - @patch('homeassistant.helpers.entity_component.' - 'async_track_time_interval') - def test_set_scan_interval_via_platform(self, mock_track): - """Test the setting of the scan interval via platform.""" - def platform_setup(hass, config, add_devices, discovery_info=None): - """Test the platform setup.""" - add_devices([EntityTest(should_poll=True)]) - - platform = MockPlatform(platform_setup) - platform.SCAN_INTERVAL = timedelta(seconds=30) - - loader.set_component('test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - component.setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - - self.hass.block_till_done() - assert mock_track.called - assert timedelta(seconds=30) == mock_track.call_args[0][2] - def test_set_entity_namespace_via_config(self): """Test setting an entity namespace.""" def platform_setup(hass, config, add_devices, discovery_info=None): """Test the platform setup.""" add_devices([ - EntityTest(name='beer'), - EntityTest(name=None), + MockEntity(name='beer'), + MockEntity(name=None), ]) platform = MockPlatform(platform_setup) @@ -396,83 +186,16 @@ class TestHelpersEntityComponent(unittest.TestCase): assert sorted(self.hass.states.entity_ids()) == \ ['test_domain.yummy_beer', 'test_domain.yummy_unnamed_device'] - def test_adding_entities_with_generator_and_thread_callback(self): - """Test generator in add_entities that calls thread method. - - We should make sure we resolve the generator to a list before passing - it into an async context. - """ - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - def create_entity(number): - """Create entity helper.""" - entity = EntityTest() - entity.entity_id = generate_entity_id(component.entity_id_format, - 'Number', hass=self.hass) - return entity - - component.add_entities(create_entity(i) for i in range(2)) - - -@asyncio.coroutine -def test_platform_warn_slow_setup(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - loader.set_component('test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - - with patch.object(hass.loop, 'call_later', MagicMock()) \ - as mock_call: - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - assert mock_call.called - - timeout, logger_method = mock_call.mock_calls[0][1][:2] - - assert timeout == SLOW_SETUP_WARNING - assert logger_method == _LOGGER.warning - - assert mock_call().cancel.called - - -@asyncio.coroutine -def test_platform_error_slow_setup(hass, caplog): - """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" - with patch.object(entity_component, 'SLOW_SETUP_MAX_WAIT', 0): - called = [] - - @asyncio.coroutine - def setup_platform(*args): - called.append(1) - yield from asyncio.sleep(1, loop=hass.loop) - - platform = MockPlatform(async_setup_platform=setup_platform) - component = EntityComponent(_LOGGER, DOMAIN, hass) - loader.set_component('test_domain.test_platform', platform) - yield from component.async_setup({ - DOMAIN: { - 'platform': 'test_platform', - } - }) - assert len(called) == 1 - assert 'test_domain.test_platform' not in hass.config.components - assert 'test_platform is taking longer than 0 seconds' in caplog.text - @asyncio.coroutine def test_extract_from_service_available_device(hass): """Test the extraction of entity from service and device is available.""" component = EntityComponent(_LOGGER, DOMAIN, hass) yield from component.async_add_entities([ - EntityTest(name='test_1'), - EntityTest(name='test_2', available=False), - EntityTest(name='test_3'), - EntityTest(name='test_4', available=False), + MockEntity(name='test_1'), + MockEntity(name='test_2', available=False), + MockEntity(name='test_3'), + MockEntity(name='test_4', available=False), ]) call_1 = ha.ServiceCall('test', 'service') @@ -490,26 +213,6 @@ def test_extract_from_service_available_device(hass): component.async_extract_from_service(call_2)) -@asyncio.coroutine -def test_updated_state_used_for_entity_id(hass): - """Test that first update results used for entity ID generation.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - class EntityTestNameFetcher(EntityTest): - """Mock entity that fetches a friendly name.""" - - @asyncio.coroutine - def async_update(self): - """Mock update that assigns a name.""" - self._values['name'] = "Living Room" - - yield from component.async_add_entities([EntityTestNameFetcher()], True) - - entity_ids = hass.states.async_entity_ids() - assert 1 == len(entity_ids) - assert entity_ids[0] == "test_domain.living_room" - - @asyncio.coroutine def test_platform_not_ready(hass): """Test that we retry when platform not ready.""" @@ -555,188 +258,50 @@ def test_platform_not_ready(hass): @asyncio.coroutine -def test_parallel_updates_async_platform(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - @asyncio.coroutine - def mock_update(*args, **kwargs): - pass - - platform.async_setup_platform = mock_update - - loader.set_component('test_domain.platform', platform) - +def test_extract_from_service_returns_all_if_no_entity_id(hass): + """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - component._platforms = {} + yield from component.async_add_entities([ + MockEntity(name='test_1'), + MockEntity(name='test_2'), + ]) - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } + call = ha.ServiceCall('test', 'service') + + assert ['test_domain.test_1', 'test_domain.test_2'] == \ + sorted(ent.entity_id for ent in + component.async_extract_from_service(call)) + + +@asyncio.coroutine +def test_extract_from_service_filter_out_non_existing_entities(hass): + """Test the extraction of non existing entities from service.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + MockEntity(name='test_1'), + MockEntity(name='test_2'), + ]) + + call = ha.ServiceCall('test', 'service', { + 'entity_id': ['test_domain.test_2', 'test_domain.non_exist'] }) - handle = list(component._platforms.values())[-1] - - assert handle.parallel_updates is None + assert ['test_domain.test_2'] == \ + [ent.entity_id for ent + in component.async_extract_from_service(call)] @asyncio.coroutine -def test_parallel_updates_async_platform_with_constant(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - @asyncio.coroutine - def mock_update(*args, **kwargs): - pass - - platform.async_setup_platform = mock_update - platform.PARALLEL_UPDATES = 1 - - loader.set_component('test_domain.platform', platform) - +def test_extract_from_service_no_group_expand(hass): + """Test not expanding a group.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - component._platforms = {} + test_group = yield from group.Group.async_create_group( + hass, 'test_group', ['light.Ceiling', 'light.Kitchen']) + yield from component.async_add_entities([test_group]) - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } + call = ha.ServiceCall('test', 'service', { + 'entity_id': ['group.test_group'] }) - handle = list(component._platforms.values())[-1] - - assert handle.parallel_updates is not None - - -@asyncio.coroutine -def test_parallel_updates_sync_platform(hass): - """Warn we log when platform setup takes a long time.""" - platform = MockPlatform() - - loader.set_component('test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - component._platforms = {} - - yield from component.async_setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - - handle = list(component._platforms.values())[-1] - - assert handle.parallel_updates is not None - - -@asyncio.coroutine -def test_raise_error_on_update(hass): - """Test the add entity if they raise an error on update.""" - updates = [] - component = EntityComponent(_LOGGER, DOMAIN, hass) - entity1 = EntityTest(name='test_1') - entity2 = EntityTest(name='test_2') - - def _raise(): - """Helper to raise an exception.""" - raise AssertionError - - entity1.update = _raise - entity2.update = lambda: updates.append(1) - - yield from component.async_add_entities([entity1, entity2], True) - - assert len(updates) == 1 - assert 1 in updates - - -@asyncio.coroutine -def test_async_remove_with_platform(hass): - """Remove an entity from a platform.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - entity1 = EntityTest(name='test_1') - yield from component.async_add_entities([entity1]) - assert len(hass.states.async_entity_ids()) == 1 - yield from entity1.async_remove() - assert len(hass.states.async_entity_ids()) == 0 - - -@asyncio.coroutine -def test_not_adding_duplicate_entities_with_unique_id(hass): - """Test for not adding duplicate entities.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - yield from component.async_add_entities([ - EntityTest(name='test1', unique_id='not_very_unique')]) - - assert len(hass.states.async_entity_ids()) == 1 - - yield from component.async_add_entities([ - EntityTest(name='test2', unique_id='not_very_unique')]) - - assert len(hass.states.async_entity_ids()) == 1 - - -@asyncio.coroutine -def test_using_prescribed_entity_id(hass): - """Test for using predefined entity ID.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ - EntityTest(name='bla', entity_id='hello.world')]) - assert 'hello.world' in hass.states.async_entity_ids() - - -@asyncio.coroutine -def test_using_prescribed_entity_id_with_unique_id(hass): - """Test for ammending predefined entity ID because currently exists.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - - yield from component.async_add_entities([ - EntityTest(entity_id='test_domain.world')]) - yield from component.async_add_entities([ - EntityTest(entity_id='test_domain.world', unique_id='bla')]) - - assert 'test_domain.world_2' in hass.states.async_entity_ids() - - -@asyncio.coroutine -def test_using_prescribed_entity_id_which_is_registered(hass): - """Test not allowing predefined entity ID that already registered.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - registry = mock_registry(hass) - # Register test_domain.world - registry.async_get_or_create( - DOMAIN, 'test', '1234', suggested_object_id='world') - - # This entity_id will be rewritten - yield from component.async_add_entities([ - EntityTest(entity_id='test_domain.world')]) - - assert 'test_domain.world_2' in hass.states.async_entity_ids() - - -@asyncio.coroutine -def test_name_which_conflict_with_registered(hass): - """Test not generating conflicting entity ID based on name.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - registry = mock_registry(hass) - - # Register test_domain.world - registry.async_get_or_create( - DOMAIN, 'test', '1234', suggested_object_id='world') - - yield from component.async_add_entities([ - EntityTest(name='world')]) - - assert 'test_domain.world_2' in hass.states.async_entity_ids() - - -@asyncio.coroutine -def test_entity_with_name_and_entity_id_getting_registered(hass): - """Ensure that entity ID is used for registration.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ - EntityTest(unique_id='1234', name='bla', - entity_id='test_domain.world')]) - assert 'test_domain.world' in hass.states.async_entity_ids() + extracted = component.async_extract_from_service(call, expand_group=False) + assert extracted == [test_group] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py new file mode 100644 index 00000000000..4c27cc45a00 --- /dev/null +++ b/tests/helpers/test_entity_platform.py @@ -0,0 +1,435 @@ +"""Tests for the EntityPlatform helper.""" +import asyncio +import logging +import unittest +from unittest.mock import patch, Mock, MagicMock +from datetime import timedelta + +import homeassistant.loader as loader +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity_component import ( + EntityComponent, DEFAULT_SCAN_INTERVAL) +from homeassistant.helpers import entity_platform + +import homeassistant.util.dt as dt_util + +from tests.common import ( + get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry, + MockEntity) + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "test_domain" + + +class TestHelpersEntityPlatform(unittest.TestCase): + """Test homeassistant.helpers.entity_component module.""" + + def setUp(self): # pylint: disable=invalid-name + """Initialize a test Home Assistant instance.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Clean up the test Home Assistant instance.""" + self.hass.stop() + + def test_polling_only_updates_entities_it_should_poll(self): + """Test the polling of only updated entities.""" + component = EntityComponent( + _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) + + no_poll_ent = MockEntity(should_poll=False) + no_poll_ent.async_update = Mock() + poll_ent = MockEntity(should_poll=True) + poll_ent.async_update = Mock() + + component.add_entities([no_poll_ent, poll_ent]) + + no_poll_ent.async_update.reset_mock() + poll_ent.async_update.reset_mock() + + fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) + self.hass.block_till_done() + + assert not no_poll_ent.async_update.called + assert poll_ent.async_update.called + + def test_polling_updates_entities_with_exception(self): + """Test the updated entities that not break with an exception.""" + component = EntityComponent( + _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) + + update_ok = [] + update_err = [] + + def update_mock(): + """Mock normal update.""" + update_ok.append(None) + + def update_mock_err(): + """Mock error update.""" + update_err.append(None) + raise AssertionError("Fake error update") + + ent1 = MockEntity(should_poll=True) + ent1.update = update_mock_err + ent2 = MockEntity(should_poll=True) + ent2.update = update_mock + ent3 = MockEntity(should_poll=True) + ent3.update = update_mock + ent4 = MockEntity(should_poll=True) + ent4.update = update_mock + + component.add_entities([ent1, ent2, ent3, ent4]) + + update_ok.clear() + update_err.clear() + + fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) + self.hass.block_till_done() + + assert len(update_ok) == 3 + assert len(update_err) == 1 + + def test_update_state_adds_entities(self): + """Test if updating poll entities cause an entity to be added works.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent1 = MockEntity() + ent2 = MockEntity(should_poll=True) + + component.add_entities([ent2]) + assert 1 == len(self.hass.states.entity_ids()) + ent2.update = lambda *_: component.add_entities([ent1]) + + fire_time_changed( + self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL + ) + self.hass.block_till_done() + + assert 2 == len(self.hass.states.entity_ids()) + + def test_update_state_adds_entities_with_update_before_add_true(self): + """Test if call update before add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent = MockEntity() + ent.update = Mock(spec_set=True) + + component.add_entities([ent], True) + self.hass.block_till_done() + + assert 1 == len(self.hass.states.entity_ids()) + assert ent.update.called + + def test_update_state_adds_entities_with_update_before_add_false(self): + """Test if not call update before add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent = MockEntity() + ent.update = Mock(spec_set=True) + + component.add_entities([ent], False) + self.hass.block_till_done() + + assert 1 == len(self.hass.states.entity_ids()) + assert not ent.update.called + + @patch('homeassistant.helpers.entity_platform.' + 'async_track_time_interval') + def test_set_scan_interval_via_platform(self, mock_track): + """Test the setting of the scan interval via platform.""" + def platform_setup(hass, config, add_devices, discovery_info=None): + """Test the platform setup.""" + add_devices([MockEntity(should_poll=True)]) + + platform = MockPlatform(platform_setup) + platform.SCAN_INTERVAL = timedelta(seconds=30) + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + component.setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + self.hass.block_till_done() + assert mock_track.called + assert timedelta(seconds=30) == mock_track.call_args[0][2] + + def test_adding_entities_with_generator_and_thread_callback(self): + """Test generator in add_entities that calls thread method. + + We should make sure we resolve the generator to a list before passing + it into an async context. + """ + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + def create_entity(number): + """Create entity helper.""" + entity = MockEntity() + entity.entity_id = generate_entity_id(DOMAIN + '.{}', + 'Number', hass=self.hass) + return entity + + component.add_entities(create_entity(i) for i in range(2)) + + +@asyncio.coroutine +def test_platform_warn_slow_setup(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + with patch.object(hass.loop, 'call_later', MagicMock()) \ + as mock_call: + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + assert mock_call.called + + timeout, logger_method = mock_call.mock_calls[0][1][:2] + + assert timeout == entity_platform.SLOW_SETUP_WARNING + assert logger_method == _LOGGER.warning + + assert mock_call().cancel.called + + +@asyncio.coroutine +def test_platform_error_slow_setup(hass, caplog): + """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" + with patch.object(entity_platform, 'SLOW_SETUP_MAX_WAIT', 0): + called = [] + + @asyncio.coroutine + def setup_platform(*args): + called.append(1) + yield from asyncio.sleep(1, loop=hass.loop) + + platform = MockPlatform(async_setup_platform=setup_platform) + component = EntityComponent(_LOGGER, DOMAIN, hass) + loader.set_component('test_domain.test_platform', platform) + yield from component.async_setup({ + DOMAIN: { + 'platform': 'test_platform', + } + }) + assert len(called) == 1 + assert 'test_domain.test_platform' not in hass.config.components + assert 'test_platform is taking longer than 0 seconds' in caplog.text + + +@asyncio.coroutine +def test_updated_state_used_for_entity_id(hass): + """Test that first update results used for entity ID generation.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + class MockEntityNameFetcher(MockEntity): + """Mock entity that fetches a friendly name.""" + + @asyncio.coroutine + def async_update(self): + """Mock update that assigns a name.""" + self._values['name'] = "Living Room" + + yield from component.async_add_entities([MockEntityNameFetcher()], True) + + entity_ids = hass.states.async_entity_ids() + assert 1 == len(entity_ids) + assert entity_ids[0] == "test_domain.living_room" + + +@asyncio.coroutine +def test_parallel_updates_async_platform(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + @asyncio.coroutine + def mock_update(*args, **kwargs): + pass + + platform.async_setup_platform = mock_update + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is None + + +@asyncio.coroutine +def test_parallel_updates_async_platform_with_constant(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + @asyncio.coroutine + def mock_update(*args, **kwargs): + pass + + platform.async_setup_platform = mock_update + platform.PARALLEL_UPDATES = 1 + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is not None + + +@asyncio.coroutine +def test_parallel_updates_sync_platform(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is not None + + +@asyncio.coroutine +def test_raise_error_on_update(hass): + """Test the add entity if they raise an error on update.""" + updates = [] + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name='test_1') + entity2 = MockEntity(name='test_2') + + def _raise(): + """Helper to raise an exception.""" + raise AssertionError + + entity1.update = _raise + entity2.update = lambda: updates.append(1) + + yield from component.async_add_entities([entity1, entity2], True) + + assert len(updates) == 1 + assert 1 in updates + + +@asyncio.coroutine +def test_async_remove_with_platform(hass): + """Remove an entity from a platform.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name='test_1') + yield from component.async_add_entities([entity1]) + assert len(hass.states.async_entity_ids()) == 1 + yield from entity1.async_remove() + assert len(hass.states.async_entity_ids()) == 0 + + +@asyncio.coroutine +def test_not_adding_duplicate_entities_with_unique_id(hass): + """Test for not adding duplicate entities.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + MockEntity(name='test1', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + yield from component.async_add_entities([ + MockEntity(name='test2', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + +@asyncio.coroutine +def test_using_prescribed_entity_id(hass): + """Test for using predefined entity ID.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + MockEntity(name='bla', entity_id='hello.world')]) + assert 'hello.world' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_with_unique_id(hass): + """Test for ammending predefined entity ID because currently exists.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + MockEntity(entity_id='test_domain.world')]) + yield from component.async_add_entities([ + MockEntity(entity_id='test_domain.world', unique_id='bla')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_which_is_registered(hass): + """Test not allowing predefined entity ID that already registered.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + # This entity_id will be rewritten + yield from component.async_add_entities([ + MockEntity(entity_id='test_domain.world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_name_which_conflict_with_registered(hass): + """Test not generating conflicting entity ID based on name.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + yield from component.async_add_entities([ + MockEntity(name='world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_entity_with_name_and_entity_id_getting_registered(hass): + """Ensure that entity ID is used for registration.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + MockEntity(unique_id='1234', name='bla', + entity_id='test_domain.world')]) + assert 'test_domain.world' in hass.states.async_entity_ids() diff --git a/tests/test_config.py b/tests/test_config.py index 377c650e91f..541eaf4f79e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -255,18 +255,6 @@ class TestConfig(unittest.TestCase): return self.hass.states.get('test.test') - def test_entity_customization_false(self): - """Test entity customization through configuration.""" - config = {CONF_LATITUDE: 50, - CONF_LONGITUDE: 50, - CONF_NAME: 'Test', - CONF_CUSTOMIZE: { - 'test.test': {'hidden': False}}} - - state = self._compute_state(config) - - assert 'hidden' not in state.attributes - def test_entity_customization(self): """Test entity customization through configuration.""" config = {CONF_LATITUDE: 50, From 15368d4ca1136e400959f5cc918e7e1e8d9e994f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 8 Feb 2018 11:17:54 +0000 Subject: [PATCH 145/166] [SMALL PATCH] Sql sensor (#12242) * Initial Commit * Passed all checks * Make DB_URL required * addresses review comments from @fabaff * unused variable * return nothing --- homeassistant/components/sensor/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index a0648d4851a..99da8c3c680 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -134,7 +134,7 @@ class SQLSensor(Entity): if data is None: _LOGGER.error("%s returned no results", self._query) - return False + return if self._template is not None: self._state = self._template.async_render_with_possible_json_value( From 702b1be985d559f5b0bb40fc39c939ee8fde8eb8 Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Thu, 8 Feb 2018 12:20:51 +0100 Subject: [PATCH 146/166] Set tahoma cover update interval to default (#12232) revert 35f35050ff158b423dc437c66bdea5ebcbabc009#diff-c65eb52376398392f1395f8127364078 --- homeassistant/components/cover/tahoma.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index fd2b5847292..19bd9f01417 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.tahoma/ """ import logging -from datetime import timedelta from homeassistant.components.cover import CoverDevice from homeassistant.components.tahoma import ( @@ -15,8 +14,6 @@ DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Tahoma covers.""" From 6265d1b747c2953136a618b8d999baa4c0531727 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 8 Feb 2018 12:25:26 +0100 Subject: [PATCH 147/166] Avoid influxdb filling connection pool (#12182) * Add a processing queue to influxdb * Updates after reviews * Remove lint * Move retry loop to thread class * Move constant calculation out of loop * Deprecate retry_queue_limit --- homeassistant/components/influxdb.py | 153 +++++++++---------- tests/components/test_influxdb.py | 214 +++++++-------------------- 2 files changed, 128 insertions(+), 239 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 30a47828cc7..526b8057ce1 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -4,10 +4,11 @@ A component which allows you to send data to an Influx database. For more details about this component, please refer to the documentation at https://home-assistant.io/components/influxdb/ """ -from datetime import timedelta -from functools import partial, wraps import logging import re +import queue +import threading +import time import requests.exceptions import voluptuous as vol @@ -15,11 +16,11 @@ import voluptuous as vol from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) + EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, + STATE_UNKNOWN) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues -from homeassistant.util import utcnow REQUIREMENTS = ['influxdb==5.0.0'] @@ -41,13 +42,15 @@ DEFAULT_VERIFY_SSL = True DOMAIN = 'influxdb' TIMEOUT = 5 +RETRY_DELAY = 20 +QUEUE_BACKLOG_SECONDS = 10 COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({ vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + DOMAIN: vol.All(cv.deprecated(CONF_RETRY_QUEUE), vol.Schema({ vol.Optional(CONF_HOST): cv.string, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, @@ -79,7 +82,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}), - }), + })), }, extra=vol.ALLOW_EXTRA) RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$') @@ -128,7 +131,6 @@ def setup(hass, config): conf[CONF_COMPONENT_CONFIG_DOMAIN], conf[CONF_COMPONENT_CONFIG_GLOB]) max_tries = conf.get(CONF_RETRY_COUNT) - queue_limit = conf.get(CONF_RETRY_QUEUE) try: influx = InfluxDBClient(**kwargs) @@ -141,18 +143,18 @@ def setup(hass, config): "READ/WRITE", exc) return False - def influx_event_listener(event): - """Listen for new messages on the bus and sends them to Influx.""" + def influx_handle_event(event): + """Send an event to Influx.""" state = event.data.get('new_state') if state is None or state.state in ( STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ state.entity_id in blacklist_e or state.domain in blacklist_d: - return + return True try: if (whitelist_e and state.entity_id not in whitelist_e) or \ (whitelist_d and state.domain not in whitelist_d): - return + return True _include_state = _include_value = False @@ -222,91 +224,78 @@ def setup(hass, config): json_body[0]['tags'].update(tags) - _write_data(json_body) - - @RetryOnError(hass, retry_limit=max_tries, retry_delay=20, - queue_limit=queue_limit) - def _write_data(json_body): - """Write the data.""" try: influx.write_points(json_body) - except exceptions.InfluxDBClientError: - _LOGGER.exception("Error saving event %s to InfluxDB", json_body) + return True + except (exceptions.InfluxDBClientError, IOError): + return False - hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener) + instance = hass.data[DOMAIN] = InfluxThread( + hass, influx_handle_event, max_tries) + instance.start() + + def shutdown(event): + """Shut down the thread.""" + instance.queue.put(None) + instance.join() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) return True -class RetryOnError(object): - """A class for retrying a failed task a certain amount of tries. +class InfluxThread(threading.Thread): + """A threaded event handler class.""" - This method decorator makes a method retrying on errors. If there was an - uncaught exception, it schedules another try to execute the task after a - retry delay. It does this up to the maximum number of retries. + def __init__(self, hass, event_handler, max_tries): + """Initialize the listener.""" + threading.Thread.__init__(self, name='InfluxDB') + self.queue = queue.Queue() + self.event_handler = event_handler + self.max_tries = max_tries + hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) - It can be used for all probable "self-healing" problems like network - outages. The task will be rescheduled using HAs scheduling mechanism. + def _event_listener(self, event): + """Listen for new messages on the bus and queue them for Influx.""" + item = (time.monotonic(), event) + self.queue.put(item) - It takes a Hass instance, a maximum number of retries and a retry delay - in seconds as arguments. + def run(self): + """Process incoming events.""" + queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY - The queue limit defines the maximum number of calls that are allowed to - be queued at a time. If this number is reached, every new call discards - an old one. - """ + write_error = False + dropped = False - def __init__(self, hass, retry_limit=0, retry_delay=20, queue_limit=100): - """Initialize the decorator.""" - self.hass = hass - self.retry_limit = retry_limit - self.retry_delay = timedelta(seconds=retry_delay) - self.queue_limit = queue_limit + while True: + item = self.queue.get() - def __call__(self, method): - """Decorate the target method.""" - from homeassistant.helpers.event import track_point_in_utc_time + if item is None: + self.queue.task_done() + return - @wraps(method) - def wrapper(*args, **kwargs): - """Wrap method.""" - # pylint: disable=protected-access - if not hasattr(wrapper, "_retry_queue"): - wrapper._retry_queue = [] + timestamp, event = item + age = time.monotonic() - timestamp - def scheduled(retry=0, untrack=None, event=None): - """Call the target method. + if age < queue_seconds: + for retry in range(self.max_tries+1): + if self.event_handler(event): + if write_error: + _LOGGER.error("Resumed writing to InfluxDB") + write_error = False + dropped = False + break + elif retry < self.max_tries: + time.sleep(RETRY_DELAY) + elif not write_error: + _LOGGER.error("Error writing to InfluxDB") + write_error = True + elif not dropped: + _LOGGER.warning("Dropping old events to catch up") + dropped = True - It is called directly at the first time and then called - scheduled within the Hass mainloop. - """ - if untrack is not None: - wrapper._retry_queue.remove(untrack) + self.queue.task_done() - # pylint: disable=broad-except - try: - method(*args, **kwargs) - except Exception as ex: - if retry == self.retry_limit: - raise - if len(wrapper._retry_queue) >= self.queue_limit: - last = wrapper._retry_queue.pop(0) - if 'remove' in last: - func = last['remove'] - func() - if 'exc' in last: - _LOGGER.error( - "Retry queue overflow, drop oldest entry: %s", - str(last['exc'])) - - target = utcnow() + self.retry_delay - tracking = {'target': target} - remove = track_point_in_utc_time( - self.hass, partial(scheduled, retry + 1, tracking), - target) - tracking['remove'] = remove - tracking["exc"] = ex - wrapper._retry_queue.append(tracking) - - scheduled() - return wrapper + def block_till_done(self): + """Block till all events processed.""" + self.queue.join() diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index 0ec5f973ee4..4d12e436c02 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -1,15 +1,10 @@ """The tests for the InfluxDB component.""" -import unittest import datetime +import unittest from unittest import mock -from datetime import timedelta -from unittest.mock import MagicMock - import influxdb as influx_client -from homeassistant.util import dt as dt_util -from homeassistant import core as ha from homeassistant.setup import setup_component import homeassistant.components.influxdb as influxdb from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, \ @@ -169,6 +164,8 @@ class TestInfluxDB(unittest.TestCase): body[0]['fields']['value'] = out[1] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -203,6 +200,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -212,18 +210,6 @@ class TestInfluxDB(unittest.TestCase): ) mock_client.return_value.write_points.reset_mock() - def test_event_listener_fail_write(self, mock_client): - """Test the event listener for write failures.""" - self._setup() - - state = mock.MagicMock( - state=1, domain='fake', entity_id='fake.entity-id', - object_id='entity', attributes={}) - event = mock.MagicMock(data={'new_state': state}, time_fired=12345) - mock_client.return_value.write_points.side_effect = \ - influx_client.exceptions.InfluxDBClientError('foo') - self.handler_method(event) - def test_event_listener_states(self, mock_client): """Test the event listener against ignored states.""" self._setup() @@ -245,6 +231,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if state_state == 1: self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -278,6 +265,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if entity_id == 'ok': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -312,6 +300,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if domain == 'ok': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -356,6 +345,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if entity_id == 'included': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -401,6 +391,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if domain == 'fake': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -456,6 +447,7 @@ class TestInfluxDB(unittest.TestCase): body[0]['fields']['value'] = out[1] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -498,6 +490,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() if entity_id == 'ok': self.assertEqual( mock_client.return_value.write_points.call_count, 1 @@ -543,6 +536,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -588,6 +582,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -648,6 +643,7 @@ class TestInfluxDB(unittest.TestCase): }, }] self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() self.assertEqual( mock_client.return_value.write_points.call_count, 1 ) @@ -659,7 +655,16 @@ class TestInfluxDB(unittest.TestCase): def test_scheduled_write(self, mock_client): """Test the event listener to retry after write failures.""" - self._setup(max_retries=1) + config = { + 'influxdb': { + 'host': 'host', + 'username': 'user', + 'password': 'pass', + 'max_retries': 1 + } + } + assert setup_component(self.hass, influxdb.DOMAIN, config) + self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] state = mock.MagicMock( state=1, domain='fake', entity_id='entity.id', object_id='entity', @@ -668,152 +673,47 @@ class TestInfluxDB(unittest.TestCase): mock_client.return_value.write_points.side_effect = \ IOError('foo') - start = dt_util.utcnow() - - self.handler_method(event) + # Write fails + with mock.patch.object(influxdb.time, 'sleep') as mock_sleep: + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + assert mock_sleep.called json_data = mock_client.return_value.write_points.call_args[0][0] - self.assertEqual(mock_client.return_value.write_points.call_count, 1) - - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() self.assertEqual(mock_client.return_value.write_points.call_count, 2) mock_client.return_value.write_points.assert_called_with(json_data) - shifted_time = shifted_time + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_client.return_value.write_points.call_count, 2) + # Write works again + mock_client.return_value.write_points.side_effect = None + with mock.patch.object(influxdb.time, 'sleep') as mock_sleep: + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + assert not mock_sleep.called + self.assertEqual(mock_client.return_value.write_points.call_count, 3) + def test_queue_backlog_full(self, mock_client): + """Test the event listener to drop old events.""" + self._setup() -class TestRetryOnErrorDecorator(unittest.TestCase): - """Test the RetryOnError decorator.""" + state = mock.MagicMock( + state=1, domain='fake', entity_id='entity.id', object_id='entity', + attributes={}) + event = mock.MagicMock(data={'new_state': state}, time_fired=12345) - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + monotonic_time = 0 - def tearDown(self): - """Clear data.""" - self.hass.stop() + def fast_monotonic(): + """Monotonic time that ticks fast enough to cause a timeout.""" + nonlocal monotonic_time + monotonic_time += 60 + return monotonic_time - def test_no_retry(self): - """Test that it does not retry if configured.""" - mock_method = MagicMock() - wrapped = influxdb.RetryOnError(self.hass)(mock_method) - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) + with mock.patch('homeassistant.components.influxdb.time.monotonic', + new=fast_monotonic): + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() - mock_method.side_effect = Exception() - self.assertRaises(Exception, wrapped, 1, 2, test=3) - self.assertEqual(mock_method.call_count, 2) - mock_method.assert_called_with(1, 2, test=3) + self.assertEqual( + mock_client.return_value.write_points.call_count, 0 + ) - def test_single_retry(self): - """Test that retry stops after a single try if configured.""" - mock_method = MagicMock() - retryer = influxdb.RetryOnError(self.hass, retry_limit=1) - wrapped = retryer(mock_method) - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) - - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_method.call_count, 1) - - mock_method.side_effect = Exception() - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 2) - mock_method.assert_called_with(1, 2, test=3) - - for _ in range(3): - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_method.call_count, 3) - mock_method.assert_called_with(1, 2, test=3) - - def test_multi_retry(self): - """Test that multiple retries work.""" - mock_method = MagicMock() - retryer = influxdb.RetryOnError(self.hass, retry_limit=4) - wrapped = retryer(mock_method) - mock_method.side_effect = Exception() - - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) - - for cnt in range(3): - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - self.assertEqual(mock_method.call_count, cnt + 2) - mock_method.assert_called_with(1, 2, test=3) - - def test_max_queue(self): - """Test the maximum queue length.""" - # make a wrapped method - mock_method = MagicMock() - retryer = influxdb.RetryOnError( - self.hass, retry_limit=4, queue_limit=3) - wrapped = retryer(mock_method) - mock_method.side_effect = Exception() - - # call it once, call fails, queue fills to 1 - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 1) - mock_method.assert_called_with(1, 2, test=3) - self.assertEqual(len(wrapped._retry_queue), 1) - - # two more calls that failed. queue is 3 - wrapped(1, 2, test=3) - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 3) - self.assertEqual(len(wrapped._retry_queue), 3) - - # another call, queue gets limited to 3 - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 4) - self.assertEqual(len(wrapped._retry_queue), 3) - - # time passes - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - - # only the three queued calls where repeated - self.assertEqual(mock_method.call_count, 7) - self.assertEqual(len(wrapped._retry_queue), 3) - - # another call, queue stays limited - wrapped(1, 2, test=3) - self.assertEqual(mock_method.call_count, 8) - self.assertEqual(len(wrapped._retry_queue), 3) - - # disable the side effect - mock_method.side_effect = None - - # time passes, all calls should succeed - start = dt_util.utcnow() - shifted_time = start + (timedelta(seconds=20 + 1)) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: shifted_time}) - self.hass.block_till_done() - - # three queued calls succeeded, queue empty. - self.assertEqual(mock_method.call_count, 11) - self.assertEqual(len(wrapped._retry_queue), 0) + mock_client.return_value.write_points.reset_mock() From 25cbc8317f359c9924b561a8588a5934024dc7e8 Mon Sep 17 00:00:00 2001 From: mkfink Date: Thu, 8 Feb 2018 06:28:12 -0500 Subject: [PATCH 148/166] Force update support for mqtt binary sensor (#12092) --- .../components/binary_sensor/mqtt.py | 16 ++++- tests/components/binary_sensor/test_mqtt.py | 71 +++++++++++++++++-- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 650179f676b..e033355f655 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -14,8 +14,8 @@ import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, - CONF_DEVICE_CLASS) + CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, + CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) @@ -24,8 +24,10 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'MQTT Binary sensor' + DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_FORCE_UPDATE = False DEPENDENCIES = ['mqtt'] @@ -34,6 +36,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -53,6 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_DEVICE_CLASS), config.get(CONF_QOS), + config.get(CONF_FORCE_UPDATE), config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_AVAILABLE), @@ -65,7 +69,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, - qos, payload_on, payload_off, payload_available, + qos, force_update, payload_on, payload_off, payload_available, payload_not_available, value_template): """Initialize the MQTT binary sensor.""" super().__init__(availability_topic, qos, payload_available, @@ -77,6 +81,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._payload_on = payload_on self._payload_off = payload_off self._qos = qos + self._force_update = force_update self._template = value_template @asyncio.coroutine @@ -124,3 +129,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): def device_class(self): """Return the class of this sensor.""" return self._device_class + + @property + def force_update(self): + """Force update.""" + return self._force_update diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 396020561ac..9b5cf7aa736 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,13 +1,15 @@ """The tests for the MQTT binary sensor platform.""" import unittest +import homeassistant.core as ha from homeassistant.setup import setup_component import homeassistant.components.binary_sensor as binary_sensor -from homeassistant.const import (STATE_OFF, STATE_ON, - STATE_UNAVAILABLE) -from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE + +from tests.common import get_test_home_assistant, fire_mqtt_message +from tests.common import mock_component, mock_mqtt_component class TestSensorMQTT(unittest.TestCase): @@ -141,3 +143,64 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_force_update_disabled(self): + """Test force update option.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF' + } + }) + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + def test_force_update_enabled(self): + """Test force update option.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'force_update': True + } + }) + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(1, len(events)) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.block_till_done() + self.assertEqual(2, len(events)) From 905bb36e6a72367e063b047f12d1d2aa23facb13 Mon Sep 17 00:00:00 2001 From: Eu Date: Thu, 8 Feb 2018 12:29:33 +0100 Subject: [PATCH 149/166] Added password mode to input_text (obscure content of text box) (#11849) * Round values to one decimal Temperature detection range: -20 - 60 Deg.C ( + / - 0.3 Deg.C ) Humidity detection range: 0 - 100pct RH ( + / - 0.3pct ) Atmospheric pressure detection range: 30 - 110KPa ( + / - 120Pa ) * Add password mode option Hide the content of the input_text field * Revert "Round values to one decimal" This reverts commit a3124a6aaa2261eff558e2bd37a3eda57d1aac71. * Added test for mode option * Added newline (lint) --- homeassistant/components/input_text.py | 15 ++++++++--- tests/components/test_input_text.py | 35 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 583181fe453..6433a01fb6d 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME) + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -26,10 +26,14 @@ CONF_INITIAL = 'initial' CONF_MIN = 'min' CONF_MAX = 'max' +MODE_TEXT = 'text' +MODE_PASSWORD = 'password' + ATTR_VALUE = 'value' ATTR_MIN = 'min' ATTR_MAX = 'max' ATTR_PATTERN = 'pattern' +ATTR_MODE = 'mode' SERVICE_SET_VALUE = 'set_value' @@ -63,6 +67,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_ICON): cv.icon, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(ATTR_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): + vol.In([MODE_TEXT, MODE_PASSWORD]), }, _cv_input_text) }) }, required=True, extra=vol.ALLOW_EXTRA) @@ -92,10 +98,11 @@ def async_setup(hass, config): icon = cfg.get(CONF_ICON) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) pattern = cfg.get(ATTR_PATTERN) + mode = cfg.get(CONF_MODE) entities.append(InputText( object_id, name, initial, minimum, maximum, icon, unit, - pattern)) + pattern, mode)) if not entities: return False @@ -122,7 +129,7 @@ class InputText(Entity): """Represent a text box.""" def __init__(self, object_id, name, initial, minimum, maximum, icon, - unit, pattern): + unit, pattern, mode): """Initialize a text input.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name @@ -132,6 +139,7 @@ class InputText(Entity): self._icon = icon self._unit = unit self._pattern = pattern + self._mode = mode @property def should_poll(self): @@ -165,6 +173,7 @@ class InputText(Entity): ATTR_MIN: self._minimum, ATTR_MAX: self._maximum, ATTR_PATTERN: self._pattern, + ATTR_MODE: self._mode, } @asyncio.coroutine diff --git a/tests/components/test_input_text.py b/tests/components/test_input_text.py index be22e1122ea..c288375ec8f 100644 --- a/tests/components/test_input_text.py +++ b/tests/components/test_input_text.py @@ -64,6 +64,41 @@ class TestInputText(unittest.TestCase): state = self.hass.states.get(entity_id) self.assertEqual('testing', str(state.state)) + def test_mode(self): + """Test mode settings.""" + self.assertTrue( + setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_default_text': { + 'initial': 'test', + 'min': 3, + 'max': 10, + }, + 'test_explicit_text': { + 'initial': 'test', + 'min': 3, + 'max': 10, + 'mode': 'text', + }, + 'test_explicit_password': { + 'initial': 'test', + 'min': 3, + 'max': 10, + 'mode': 'password', + }, + }})) + + state = self.hass.states.get('input_text.test_default_text') + assert state + self.assertEqual('text', state.attributes['mode']) + + state = self.hass.states.get('input_text.test_explicit_text') + assert state + self.assertEqual('text', state.attributes['mode']) + + state = self.hass.states.get('input_text.test_explicit_password') + assert state + self.assertEqual('password', state.attributes['mode']) + @asyncio.coroutine def test_restore_state(hass): From 231b62d0437e48c6fc09d67fbcee44e3f2d794b9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 8 Feb 2018 16:09:42 +0100 Subject: [PATCH 150/166] Fix cover service description (#12243) Parameter for `set_cover_tilt_position` is `tilt_position` not `position`. --- homeassistant/components/cover/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 41be271fff0..1a3e020ed87 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -51,8 +51,8 @@ set_cover_tilt_position: entity_id: description: Name(s) of cover(s) to set cover tilt position. example: 'cover.living_room' - position: - description: Position of the cover (0 to 100). + tilt_position: + description: Tilt position of the cover (0 to 100). example: 30 stop_cover_tilt: From acb521330c7aa27dacc62851f96649cfdbb377bc Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 8 Feb 2018 18:44:19 +0100 Subject: [PATCH 151/166] Add explicit first-time config for new purge_keep_days default (#12246) --- homeassistant/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/config.py b/homeassistant/config.py index 3f4c4c174d7..5e82ef1baa0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -95,6 +95,10 @@ conversation: # Enables support for tracking state changes over time history: +# Tracked history is kept for 10 days +recorder: + purge_keep_days: 10 + # View all events in a logbook logbook: From b08294386b9f696a91065d64be102060f553b008 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Fri, 9 Feb 2018 00:14:49 +0100 Subject: [PATCH 152/166] added more debug logging for sensor.alpha_vantage (#12249) * added more debug logging for sensor.alpha_vantage * fixed typo in log statement, more fine grained logging * Capitalized first character in log statement * replaced quotes as proposed by @OttoWinter --- homeassistant/components/sensor/alpha_vantage.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 20899396052..6b224492ffb 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -89,6 +89,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] for symbol in symbols: try: + _LOGGER.debug("Configuring timeseries for symbols: %s", + symbol[CONF_SYMBOL]) timeseries.get_intraday(symbol[CONF_SYMBOL]) except ValueError: _LOGGER.error( @@ -100,6 +102,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from_cur = conversion.get(CONF_FROM) to_cur = conversion.get(CONF_TO) try: + _LOGGER.debug("Configuring forex %s - %s", + from_cur, to_cur) forex.get_currency_exchange_rate( from_currency=from_cur, to_currency=to_cur) except ValueError as error: @@ -110,6 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev.append(AlphaVantageForeignExchange(forex, conversion)) add_devices(dev, True) + _LOGGER.debug("Setup completed") class AlphaVantageSensor(Entity): @@ -158,8 +163,10 @@ class AlphaVantageSensor(Entity): def update(self): """Get the latest data and updates the states.""" + _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) self.values = next(iter(all_values.values())) + _LOGGER.debug("Received new values for symbol %s", self._symbol) class AlphaVantageForeignExchange(Entity): @@ -210,5 +217,11 @@ class AlphaVantageForeignExchange(Entity): def update(self): """Get the latest data and updates the states.""" + _LOGGER.debug("Requesting new data for forex %s - %s", + self._from_currency, + self._to_currency) self.values, _ = self._foreign_exchange.get_currency_exchange_rate( from_currency=self._from_currency, to_currency=self._to_currency) + _LOGGER.debug("Received new data for forex %s - %s", + self._from_currency, + self._to_currency) From 18d027a10daa87d8661f489b7bd24074fad9785c Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 8 Feb 2018 22:37:17 -0500 Subject: [PATCH 153/166] Add Service Schema to Broadlink Switch (#12253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add service schema to ensure single values are wrapped into a list Co-Authored-By: Paulus Schoutsen * 💐 --- homeassistant/components/switch/broadlink.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 8353b4bf8ad..e79b7c3f34c 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -144,7 +144,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register(DOMAIN, SERVICE_LEARN + '_' + ip_addr.replace('.', '_'), _learn_command) hass.services.register(DOMAIN, SERVICE_SEND + '_' + - ip_addr.replace('.', '_'), _send_packet) + ip_addr.replace('.', '_'), _send_packet, + vol.Schema({'packet': cv.ensure_list})) switches = [] for object_id, device_config in devices.items(): switches.append( From e4874fd7c75f8369a03f46a74b1bae85899ce8f6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 9 Feb 2018 05:57:05 +0100 Subject: [PATCH 154/166] Update aiohttp 2.3.10 / yarl 1.1.0 (#12244) * Update aiohttp 2.3.10 / yarl 1.1.0 * Update setup.py * Update package_constraints.txt * Update google.py * Update static.py --- homeassistant/components/http/static.py | 4 ++-- homeassistant/components/tts/google.py | 2 +- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- setup.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 74a5a8818a4..b34df1897f0 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -6,7 +6,7 @@ from aiohttp import hdrs from aiohttp.web import FileResponse, middleware from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_urldispatcher import StaticResource -from yarl import unquote +from yarl import URL _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) @@ -16,7 +16,7 @@ class CachingStaticResource(StaticResource): @asyncio.coroutine def _handle(self, request): - filename = unquote(request.match_info['filename']) + filename = URL(request.match_info['filename']).path try: # PyLint is wrong about resolve not being a member. # pylint: disable=no-member diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 85b223864e9..084a7229212 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -87,7 +87,7 @@ class GoogleProvider(Provider): url_param = { 'ie': 'UTF-8', 'tl': language, - 'q': yarl.quote(part), + 'q': yarl.URL(part).raw_path, 'tk': part_token, 'total': len(message_parts), 'idx': idx, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 995002df7e9..ee3a37bbd53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,8 +5,8 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.3.7 -yarl==0.18.0 +aiohttp==2.3.10 +yarl==1.1.0 async_timeout==2.0.0 chardet==3.0.4 astral==1.5 diff --git a/requirements_all.txt b/requirements_all.txt index 166f835c029..4c8f0f50498 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,8 +6,8 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.3.7 -yarl==0.18.0 +aiohttp==2.3.10 +yarl==1.1.0 async_timeout==2.0.0 chardet==3.0.4 astral==1.5 diff --git a/setup.py b/setup.py index f250dd739f7..5af84fc8e0e 100755 --- a/setup.py +++ b/setup.py @@ -55,8 +55,8 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.10.5', 'typing>=3,<4', - 'aiohttp==2.3.7', # If updated, check if yarl also needs an update! - 'yarl==0.18.0', + 'aiohttp==2.3.10', # If updated, check if yarl also needs an update! + 'yarl==1.1.0', 'async_timeout==2.0.0', 'chardet==3.0.4', 'astral==1.5', From 2ae0c5653ed32c8b7309686d9b897f7d7c6fc0fb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 9 Feb 2018 08:11:47 +0100 Subject: [PATCH 155/166] Fix source code using Windows newline (#12248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚜 Fix usage of carriage return * 🤝 Rebase and repeat * 🚜 Fix file permissions --- .../components/binary_sensor/mercedesme.py | 188 +-- homeassistant/components/camera/xeoma.py | 0 .../components/device_tracker/mercedesme.py | 142 +- homeassistant/components/mercedesme.py | 308 ++--- .../components/remote/xiaomi_miio.py | 0 homeassistant/components/sensor/mercedesme.py | 166 +-- tests/components/calendar/test_google.py | 846 ++++++------ tests/components/cloud/__init__.py | 2 +- .../components/device_tracker/test_xiaomi.py | 530 ++++---- tests/components/emulated_hue/test_init.py | 256 ++-- tests/components/light/test_mqtt_json.py | 1158 ++++++++--------- tests/components/light/test_mqtt_template.py | 1048 +++++++-------- tests/components/sensor/test_hddtemp.py | 432 +++--- tests/components/switch/test_wake_on_lan.py | 384 +++--- 14 files changed, 2730 insertions(+), 2730 deletions(-) mode change 100755 => 100644 homeassistant/components/binary_sensor/mercedesme.py mode change 100755 => 100644 homeassistant/components/camera/xeoma.py mode change 100755 => 100644 homeassistant/components/device_tracker/mercedesme.py mode change 100755 => 100644 homeassistant/components/mercedesme.py mode change 100755 => 100644 homeassistant/components/remote/xiaomi_miio.py mode change 100755 => 100644 homeassistant/components/sensor/mercedesme.py diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py old mode 100755 new mode 100644 index c817447f181..a6c8da56ce8 --- a/homeassistant/components/binary_sensor/mercedesme.py +++ b/homeassistant/components/binary_sensor/mercedesme.py @@ -1,94 +1,94 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.mercedesme/ -""" -import logging -import datetime - -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.mercedesme import ( - DATA_MME, MercedesMeEntity, BINARY_SENSORS) - -DEPENDENCIES = ['mercedesme'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" - data = hass.data[DATA_MME].data - - if not data.cars: - _LOGGER.error("No cars found. Check component log.") - return - - devices = [] - for car in data.cars: - for key, value in sorted(BINARY_SENSORS.items()): - devices.append(MercedesMEBinarySensor( - data, key, value[0], car["vin"], None)) - - add_devices(devices, True) - - -class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): - """Representation of a Sensor.""" - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._internal_name == "windowsClosed": - return { - "window_front_left": self._car["windowStatusFrontLeft"], - "window_front_right": self._car["windowStatusFrontRight"], - "window_rear_left": self._car["windowStatusRearLeft"], - "window_rear_right": self._car["windowStatusRearRight"], - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - elif self._internal_name == "tireWarningLight": - return { - "front_right_tire_pressure_kpa": - self._car["frontRightTirePressureKpa"], - "front_left_tire_pressure_kpa": - self._car["frontLeftTirePressureKpa"], - "rear_right_tire_pressure_kpa": - self._car["rearRightTirePressureKpa"], - "rear_left_tire_pressure_kpa": - self._car["rearLeftTirePressureKpa"], - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"], - } - return { - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - - def update(self): - """Fetch new state data for the sensor.""" - self._car = next( - car for car in self._data.cars if car["vin"] == self._vin) - - if self._internal_name == "windowsClosed": - self._state = bool(self._car[self._internal_name] == "CLOSED") - elif self._internal_name == "tireWarningLight": - self._state = bool(self._car[self._internal_name] != "INACTIVE") - else: - self._state = self._car[self._internal_name] is True - - _LOGGER.debug("Updated %s Value: %s IsOn: %s", - self._internal_name, self._state, self.is_on) +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.mercedesme/ +""" +import logging +import datetime + +from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, BINARY_SENSORS) + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + data = hass.data[DATA_MME].data + + if not data.cars: + _LOGGER.error("No cars found. Check component log.") + return + + devices = [] + for car in data.cars: + for key, value in sorted(BINARY_SENSORS.items()): + devices.append(MercedesMEBinarySensor( + data, key, value[0], car["vin"], None)) + + add_devices(devices, True) + + +class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): + """Representation of a Sensor.""" + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._internal_name == "windowsClosed": + return { + "window_front_left": self._car["windowStatusFrontLeft"], + "window_front_right": self._car["windowStatusFrontRight"], + "window_rear_left": self._car["windowStatusRearLeft"], + "window_rear_right": self._car["windowStatusRearRight"], + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + elif self._internal_name == "tireWarningLight": + return { + "front_right_tire_pressure_kpa": + self._car["frontRightTirePressureKpa"], + "front_left_tire_pressure_kpa": + self._car["frontLeftTirePressureKpa"], + "rear_right_tire_pressure_kpa": + self._car["rearRightTirePressureKpa"], + "rear_left_tire_pressure_kpa": + self._car["rearLeftTirePressureKpa"], + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"], + } + return { + "original_value": self._car[self._internal_name], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + def update(self): + """Fetch new state data for the sensor.""" + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + if self._internal_name == "windowsClosed": + self._state = bool(self._car[self._internal_name] == "CLOSED") + elif self._internal_name == "tireWarningLight": + self._state = bool(self._car[self._internal_name] != "INACTIVE") + else: + self._state = self._car[self._internal_name] is True + + _LOGGER.debug("Updated %s Value: %s IsOn: %s", + self._internal_name, self._state, self.is_on) diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py old mode 100755 new mode 100644 index ed516b738cc..0aa2be96290 --- a/homeassistant/components/device_tracker/mercedesme.py +++ b/homeassistant/components/device_tracker/mercedesme.py @@ -1,71 +1,71 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/device_tracker.mercedesme/ -""" -import logging -from datetime import timedelta - -from homeassistant.components.mercedesme import DATA_MME -from homeassistant.helpers.event import track_time_interval -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['mercedesme'] - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Mercedes ME tracker.""" - if discovery_info is None: - return False - - data = hass.data[DATA_MME].data - - if not data.cars: - return False - - MercedesMEDeviceTracker(hass, config, see, data) - - return True - - -class MercedesMEDeviceTracker(object): - """A class representing a Mercedes ME device tracker.""" - - def __init__(self, hass, config, see, data): - """Initialize the Mercedes ME device tracker.""" - self.see = see - self.data = data - self.update_info() - - track_time_interval( - hass, self.update_info, MIN_TIME_BETWEEN_SCANS) - - @Throttle(MIN_TIME_BETWEEN_SCANS) - def update_info(self, now=None): - """Update the device info.""" - for device in self.data.cars: - _LOGGER.debug("Updating %s", device["vin"]) - location = self.data.get_location(device["vin"]) - if location is None: - return False - dev_id = device["vin"] - name = device["license"] - - lat = location['positionLat']['value'] - lon = location['positionLong']['value'] - attrs = { - 'trackr_id': dev_id, - 'id': dev_id, - 'name': name - } - self.see( - dev_id=dev_id, host_name=name, - gps=(lat, lon), attributes=attrs - ) - - return True +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/device_tracker.mercedesme/ +""" +import logging +from datetime import timedelta + +from homeassistant.components.mercedesme import DATA_MME +from homeassistant.helpers.event import track_time_interval +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mercedesme'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Mercedes ME tracker.""" + if discovery_info is None: + return False + + data = hass.data[DATA_MME].data + + if not data.cars: + return False + + MercedesMEDeviceTracker(hass, config, see, data) + + return True + + +class MercedesMEDeviceTracker(object): + """A class representing a Mercedes ME device tracker.""" + + def __init__(self, hass, config, see, data): + """Initialize the Mercedes ME device tracker.""" + self.see = see + self.data = data + self.update_info() + + track_time_interval( + hass, self.update_info, MIN_TIME_BETWEEN_SCANS) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def update_info(self, now=None): + """Update the device info.""" + for device in self.data.cars: + _LOGGER.debug("Updating %s", device["vin"]) + location = self.data.get_location(device["vin"]) + if location is None: + return False + dev_id = device["vin"] + name = device["license"] + + lat = location['positionLat']['value'] + lon = location['positionLong']['value'] + attrs = { + 'trackr_id': dev_id, + 'id': dev_id, + 'name': name + } + self.see( + dev_id=dev_id, host_name=name, + gps=(lat, lon), attributes=attrs + ) + + return True diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py old mode 100755 new mode 100644 index 0ac58e9c62e..a228486e2c8 --- a/homeassistant/components/mercedesme.py +++ b/homeassistant/components/mercedesme.py @@ -1,154 +1,154 @@ -""" -Support for MercedesME System. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mercedesme/ -""" -import asyncio -import logging -from datetime import timedelta - -import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS) -from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval - -REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] - -_LOGGER = logging.getLogger(__name__) - -BINARY_SENSORS = { - 'doorsClosed': ['Doors closed'], - 'windowsClosed': ['Windows closed'], - 'locked': ['Doors locked'], - 'tireWarningLight': ['Tire Warning'] -} - -SENSORS = { - 'fuelLevelPercent': ['Fuel Level', '%'], - 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], - 'latestTrip': ['Latest Trip', None], - 'odometerKm': ['Odometer', LENGTH_KILOMETERS], - 'serviceIntervalDays': ['Next Service', 'days'] -} - -DATA_MME = 'mercedesme' -DOMAIN = 'mercedesme' - -NOTIFICATION_ID = 'mercedesme_integration_notification' -NOTIFICATION_TITLE = 'Mercedes me integration setup' - -SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=30): - vol.All(cv.positive_int, vol.Clamp(min=10)) - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up MercedesMe System.""" - from mercedesmejsonpy.controller import Controller - from mercedesmejsonpy import Exceptions - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - scan_interval = conf.get(CONF_SCAN_INTERVAL) - - try: - mercedesme_api = Controller(username, password, scan_interval) - if not mercedesme_api.is_valid_session: - raise Exceptions.MercedesMeException(500) - hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) - except Exceptions.MercedesMeException as ex: - if ex.code == 401: - hass.components.persistent_notification.create( - "Error:
    Please check username and password." - "You will need to restart Home Assistant after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - else: - hass.components.persistent_notification.create( - "Error:
    Can't communicate with Mercedes me API.
    " - "Error code: {} Reason: {}" - "You will need to restart Home Assistant after fixing." - "".format(ex.code, ex.message), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - _LOGGER.error("Unable to communicate with Mercedes me API: %s", - ex.message) - return False - - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - - def hub_refresh(event_time): - """Call Mercedes me API to refresh information.""" - _LOGGER.info("Updating Mercedes me component.") - hass.data[DATA_MME].data.update() - dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) - - track_time_interval( - hass, - hub_refresh, - timedelta(seconds=scan_interval)) - - return True - - -class MercedesMeHub(object): - """Representation of a base MercedesMe device.""" - - def __init__(self, data): - """Initialize the entity.""" - self.data = data - - -class MercedesMeEntity(Entity): - """Entity class for MercedesMe devices.""" - - def __init__(self, data, internal_name, sensor_name, vin, unit): - """Initialize the MercedesMe entity.""" - self._car = None - self._data = data - self._state = False - self._name = sensor_name - self._internal_name = internal_name - self._unit = unit - self._vin = vin - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @asyncio.coroutine - def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) - - def _update_callback(self): - """Callback update method.""" - # If the method is made a callback this should be changed - # to the async version. Check core.callback - self.schedule_update_ha_state(True) - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit +""" +Support for MercedesME System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS) +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSORS = { + 'doorsClosed': ['Doors closed'], + 'windowsClosed': ['Windows closed'], + 'locked': ['Doors locked'], + 'tireWarningLight': ['Tire Warning'] +} + +SENSORS = { + 'fuelLevelPercent': ['Fuel Level', '%'], + 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], + 'latestTrip': ['Latest Trip', None], + 'odometerKm': ['Odometer', LENGTH_KILOMETERS], + 'serviceIntervalDays': ['Next Service', 'days'] +} + +DATA_MME = 'mercedesme' +DOMAIN = 'mercedesme' + +NOTIFICATION_ID = 'mercedesme_integration_notification' +NOTIFICATION_TITLE = 'Mercedes me integration setup' + +SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=30): + vol.All(cv.positive_int, vol.Clamp(min=10)) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up MercedesMe System.""" + from mercedesmejsonpy.controller import Controller + from mercedesmejsonpy import Exceptions + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + mercedesme_api = Controller(username, password, scan_interval) + if not mercedesme_api.is_valid_session: + raise Exceptions.MercedesMeException(500) + hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) + except Exceptions.MercedesMeException as ex: + if ex.code == 401: + hass.components.persistent_notification.create( + "Error:
    Please check username and password." + "You will need to restart Home Assistant after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + else: + hass.components.persistent_notification.create( + "Error:
    Can't communicate with Mercedes me API.
    " + "Error code: {} Reason: {}" + "You will need to restart Home Assistant after fixing." + "".format(ex.code, ex.message), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + _LOGGER.error("Unable to communicate with Mercedes me API: %s", + ex.message) + return False + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + def hub_refresh(event_time): + """Call Mercedes me API to refresh information.""" + _LOGGER.info("Updating Mercedes me component.") + hass.data[DATA_MME].data.update() + dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) + + track_time_interval( + hass, + hub_refresh, + timedelta(seconds=scan_interval)) + + return True + + +class MercedesMeHub(object): + """Representation of a base MercedesMe device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class MercedesMeEntity(Entity): + """Entity class for MercedesMe devices.""" + + def __init__(self, data, internal_name, sensor_name, vin, unit): + """Initialize the MercedesMe entity.""" + self._car = None + self._data = data + self._state = False + self._name = sensor_name + self._internal_name = internal_name + self._unit = unit + self._vin = vin + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) + + def _update_callback(self): + """Callback update method.""" + # If the method is made a callback this should be changed + # to the async version. Check core.callback + self.schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py old mode 100755 new mode 100644 index 21a63dd562d..bc368745e40 --- a/homeassistant/components/sensor/mercedesme.py +++ b/homeassistant/components/sensor/mercedesme.py @@ -1,83 +1,83 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.mercedesme/ -""" -import logging -import datetime - -from homeassistant.components.mercedesme import ( - DATA_MME, MercedesMeEntity, SENSORS) - - -DEPENDENCIES = ['mercedesme'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" - if discovery_info is None: - return - - data = hass.data[DATA_MME].data - - if not data.cars: - return - - devices = [] - for car in data.cars: - for key, value in sorted(SENSORS.items()): - devices.append( - MercedesMESensor(data, key, value[0], car["vin"], value[1])) - - add_devices(devices, True) - - -class MercedesMESensor(MercedesMeEntity): - """Representation of a Sensor.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Get the latest data and updates the states.""" - _LOGGER.debug("Updating %s", self._internal_name) - - self._car = next( - car for car in self._data.cars if car["vin"] == self._vin) - - if self._internal_name == "latestTrip": - self._state = self._car["latestTrip"]["id"] - else: - self._state = self._car[self._internal_name] - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._internal_name == "latestTrip": - return { - "duration_seconds": - self._car["latestTrip"]["durationSeconds"], - "distance_traveled_km": - self._car["latestTrip"]["distanceTraveledKm"], - "started_at": datetime.datetime.fromtimestamp( - self._car["latestTrip"]["startedAt"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "average_speed_km_per_hr": - self._car["latestTrip"]["averageSpeedKmPerHr"], - "finished": self._car["latestTrip"]["finished"], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - - return { - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.mercedesme/ +""" +import logging +import datetime + +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, SENSORS) + + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + if discovery_info is None: + return + + data = hass.data[DATA_MME].data + + if not data.cars: + return + + devices = [] + for car in data.cars: + for key, value in sorted(SENSORS.items()): + devices.append( + MercedesMESensor(data, key, value[0], car["vin"], value[1])) + + add_devices(devices, True) + + +class MercedesMESensor(MercedesMeEntity): + """Representation of a Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating %s", self._internal_name) + + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + if self._internal_name == "latestTrip": + self._state = self._car["latestTrip"]["id"] + else: + self._state = self._car[self._internal_name] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._internal_name == "latestTrip": + return { + "duration_seconds": + self._car["latestTrip"]["durationSeconds"], + "distance_traveled_km": + self._car["latestTrip"]["distanceTraveledKm"], + "started_at": datetime.datetime.fromtimestamp( + self._car["latestTrip"]["startedAt"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "average_speed_km_per_hr": + self._car["latestTrip"]["averageSpeedKmPerHr"], + "finished": self._car["latestTrip"]["finished"], + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + return { + "last_update": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 1de825efd99..62c8ea8854f 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -1,423 +1,423 @@ -"""The tests for the google calendar component.""" -# pylint: disable=protected-access -import logging -import unittest -from unittest.mock import patch - -import pytest - -import homeassistant.components.calendar as calendar_base -import homeassistant.components.calendar.google as calendar -import homeassistant.util.dt as dt_util -from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON -from homeassistant.helpers.template import DATE_STR_FORMAT -from tests.common import get_test_home_assistant - -TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} - -_LOGGER = logging.getLogger(__name__) - - -class TestComponentsGoogleCalendar(unittest.TestCase): - """Test the Google calendar.""" - - hass = None # HomeAssistant - - # pylint: disable=invalid-name - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina')) - - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - dt_util.set_default_time_zone(dt_util.get_time_zone('UTC')) - - self.hass.stop() - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_all_day_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - week_from_today = dt_util.dt.date.today() \ - + dt_util.dt.timedelta(days=7) - event = { - 'summary': 'Test All Day Event', - 'start': { - 'date': week_from_today.isoformat() - }, - 'end': { - 'date': (week_from_today + dt_util.dt.timedelta(days=1)) - .isoformat() - }, - 'location': 'Test Cases', - 'description': 'We\'re just testing that all day events get setup ' - 'correctly', - 'kind': 'calendar#event', - 'created': '2016-06-23T16:37:57.000Z', - 'transparency': 'transparent', - 'updated': '2016-06-24T01:57:21.045Z', - 'reminders': {'useDefault': True}, - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'id': '_c8rinwq863h45qnucyoi43ny8', - 'etag': '"2933466882090000"', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - 'iCalUID': 'cydrevtfuybguinhomj@google.com', - 'status': 'confirmed' - } - - mock_next_event.return_value.event = event - - device_name = 'Test All Day' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, - '', {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event['summary'], - 'all_day': True, - 'offset_reached': False, - 'start_time': '{} 00:00:00'.format(event['start']['date']), - 'end_time': '{} 00:00:00'.format(event['end']['date']), - 'location': event['location'], - 'description': event['description'] - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_future_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - one_hour_from_now = dt_util.now() \ - + dt_util.dt.timedelta(minutes=30) - event = { - 'start': { - 'dateTime': one_hour_from_now.isoformat() - }, - 'end': { - 'dateTime': (one_hour_from_now - + dt_util.dt.timedelta(minutes=60)) - .isoformat() - }, - 'summary': 'Test Event in 30 minutes', - 'reminders': {'useDefault': True}, - 'id': 'aioehgni435lihje', - 'status': 'confirmed', - 'updated': '2016-11-05T15:52:07.329Z', - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True, - }, - 'created': '2016-11-05T15:52:07.000Z', - 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - }, - 'etag': '"2956722254658000"', - 'kind': 'calendar#event', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - } - mock_next_event.return_value.event = event - - device_name = 'Test Future Event' - device_id = 'test_future_event' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event['summary'], - 'all_day': False, - 'offset_reached': False, - 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT), - 'end_time': - (one_hour_from_now + dt_util.dt.timedelta(minutes=60)) - .strftime(DATE_STR_FORMAT), - 'location': '', - 'description': '' - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_in_progress_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() \ - - dt_util.dt.timedelta(minutes=30) - event = { - 'start': { - 'dateTime': middle_of_event.isoformat() - }, - 'end': { - 'dateTime': (middle_of_event + dt_util.dt - .timedelta(minutes=60)) - .isoformat() - }, - 'summary': 'Test Event in Progress', - 'reminders': {'useDefault': True}, - 'id': 'aioehgni435lihje', - 'status': 'confirmed', - 'updated': '2016-11-05T15:52:07.329Z', - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True, - }, - 'created': '2016-11-05T15:52:07.000Z', - 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - }, - 'etag': '"2956722254658000"', - 'kind': 'calendar#event', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - } - - mock_next_event.return_value.event = event - - device_name = 'Test Event in Progress' - device_id = 'test_event_in_progress' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_ON) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event['summary'], - 'all_day': False, - 'offset_reached': False, - 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), - 'end_time': - (middle_of_event + dt_util.dt.timedelta(minutes=60)) - .strftime(DATE_STR_FORMAT), - 'location': '', - 'description': '' - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_offset_in_progress_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() \ - + dt_util.dt.timedelta(minutes=14) - event_summary = 'Test Event in Progress' - event = { - 'start': { - 'dateTime': middle_of_event.isoformat() - }, - 'end': { - 'dateTime': (middle_of_event + dt_util.dt - .timedelta(minutes=60)) - .isoformat() - }, - 'summary': '{} !!-15'.format(event_summary), - 'reminders': {'useDefault': True}, - 'id': 'aioehgni435lihje', - 'status': 'confirmed', - 'updated': '2016-11-05T15:52:07.329Z', - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True, - }, - 'created': '2016-11-05T15:52:07.000Z', - 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - }, - 'etag': '"2956722254658000"', - 'kind': 'calendar#event', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - } - - mock_next_event.return_value.event = event - - device_name = 'Test Event in Progress' - device_id = 'test_event_in_progress' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertTrue(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event_summary, - 'all_day': False, - 'offset_reached': True, - 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), - 'end_time': - (middle_of_event + dt_util.dt.timedelta(minutes=60)) - .strftime(DATE_STR_FORMAT), - 'location': '', - 'description': '' - }) - - @pytest.mark.skip - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_all_day_offset_in_progress_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() \ - + dt_util.dt.timedelta(days=1) - - event_summary = 'Test All Day Event Offset In Progress' - event = { - 'summary': '{} !!-25:0'.format(event_summary), - 'start': { - 'date': tomorrow.isoformat() - }, - 'end': { - 'date': (tomorrow + dt_util.dt.timedelta(days=1)) - .isoformat() - }, - 'location': 'Test Cases', - 'description': 'We\'re just testing that all day events get setup ' - 'correctly', - 'kind': 'calendar#event', - 'created': '2016-06-23T16:37:57.000Z', - 'transparency': 'transparent', - 'updated': '2016-06-24T01:57:21.045Z', - 'reminders': {'useDefault': True}, - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'id': '_c8rinwq863h45qnucyoi43ny8', - 'etag': '"2933466882090000"', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - 'iCalUID': 'cydrevtfuybguinhomj@google.com', - 'status': 'confirmed' - } - - mock_next_event.return_value.event = event - - device_name = 'Test All Day Offset In Progress' - device_id = 'test_all_day_offset_in_progress' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertTrue(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event_summary, - 'all_day': True, - 'offset_reached': True, - 'start_time': '{} 06:00:00'.format(event['start']['date']), - 'end_time': '{} 06:00:00'.format(event['end']['date']), - 'location': event['location'], - 'description': event['description'] - }) - - @patch('homeassistant.components.calendar.google.GoogleCalendarData') - def test_all_day_offset_event(self, mock_next_event): - """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() \ - + dt_util.dt.timedelta(days=2) - - offset_hours = (1 + dt_util.now().hour) - event_summary = 'Test All Day Event Offset' - event = { - 'summary': '{} !!-{}:0'.format(event_summary, offset_hours), - 'start': { - 'date': tomorrow.isoformat() - }, - 'end': { - 'date': (tomorrow + dt_util.dt.timedelta(days=1)) - .isoformat() - }, - 'location': 'Test Cases', - 'description': 'We\'re just testing that all day events get setup ' - 'correctly', - 'kind': 'calendar#event', - 'created': '2016-06-23T16:37:57.000Z', - 'transparency': 'transparent', - 'updated': '2016-06-24T01:57:21.045Z', - 'reminders': {'useDefault': True}, - 'organizer': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'sequence': 0, - 'creator': { - 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', - 'displayName': 'Organizer Name', - 'self': True - }, - 'id': '_c8rinwq863h45qnucyoi43ny8', - 'etag': '"2933466882090000"', - 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', - 'iCalUID': 'cydrevtfuybguinhomj@google.com', - 'status': 'confirmed' - } - - mock_next_event.return_value.event = event - - device_name = 'Test All Day Offset' - device_id = 'test_all_day_offset' - - cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, - {'name': device_name}) - - self.assertEqual(cal.name, device_name) - - self.assertEqual(cal.state, STATE_OFF) - - self.assertFalse(cal.offset_reached()) - - self.assertEqual(cal.device_state_attributes, { - 'message': event_summary, - 'all_day': True, - 'offset_reached': False, - 'start_time': '{} 00:00:00'.format(event['start']['date']), - 'end_time': '{} 00:00:00'.format(event['end']['date']), - 'location': event['location'], - 'description': event['description'] - }) +"""The tests for the google calendar component.""" +# pylint: disable=protected-access +import logging +import unittest +from unittest.mock import patch + +import pytest + +import homeassistant.components.calendar as calendar_base +import homeassistant.components.calendar.google as calendar +import homeassistant.util.dt as dt_util +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.helpers.template import DATE_STR_FORMAT +from tests.common import get_test_home_assistant + +TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} + +_LOGGER = logging.getLogger(__name__) + + +class TestComponentsGoogleCalendar(unittest.TestCase): + """Test the Google calendar.""" + + hass = None # HomeAssistant + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina')) + + # pylint: disable=invalid-name + def tearDown(self): + """Stop everything that was started.""" + dt_util.set_default_time_zone(dt_util.get_time_zone('UTC')) + + self.hass.stop() + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_all_day_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + week_from_today = dt_util.dt.date.today() \ + + dt_util.dt.timedelta(days=7) + event = { + 'summary': 'Test All Day Event', + 'start': { + 'date': week_from_today.isoformat() + }, + 'end': { + 'date': (week_from_today + dt_util.dt.timedelta(days=1)) + .isoformat() + }, + 'location': 'Test Cases', + 'description': 'We\'re just testing that all day events get setup ' + 'correctly', + 'kind': 'calendar#event', + 'created': '2016-06-23T16:37:57.000Z', + 'transparency': 'transparent', + 'updated': '2016-06-24T01:57:21.045Z', + 'reminders': {'useDefault': True}, + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'id': '_c8rinwq863h45qnucyoi43ny8', + 'etag': '"2933466882090000"', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + 'iCalUID': 'cydrevtfuybguinhomj@google.com', + 'status': 'confirmed' + } + + mock_next_event.return_value.event = event + + device_name = 'Test All Day' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, + '', {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event['summary'], + 'all_day': True, + 'offset_reached': False, + 'start_time': '{} 00:00:00'.format(event['start']['date']), + 'end_time': '{} 00:00:00'.format(event['end']['date']), + 'location': event['location'], + 'description': event['description'] + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_future_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + one_hour_from_now = dt_util.now() \ + + dt_util.dt.timedelta(minutes=30) + event = { + 'start': { + 'dateTime': one_hour_from_now.isoformat() + }, + 'end': { + 'dateTime': (one_hour_from_now + + dt_util.dt.timedelta(minutes=60)) + .isoformat() + }, + 'summary': 'Test Event in 30 minutes', + 'reminders': {'useDefault': True}, + 'id': 'aioehgni435lihje', + 'status': 'confirmed', + 'updated': '2016-11-05T15:52:07.329Z', + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True, + }, + 'created': '2016-11-05T15:52:07.000Z', + 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + }, + 'etag': '"2956722254658000"', + 'kind': 'calendar#event', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + } + mock_next_event.return_value.event = event + + device_name = 'Test Future Event' + device_id = 'test_future_event' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event['summary'], + 'all_day': False, + 'offset_reached': False, + 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT), + 'end_time': + (one_hour_from_now + dt_util.dt.timedelta(minutes=60)) + .strftime(DATE_STR_FORMAT), + 'location': '', + 'description': '' + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_in_progress_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + middle_of_event = dt_util.now() \ + - dt_util.dt.timedelta(minutes=30) + event = { + 'start': { + 'dateTime': middle_of_event.isoformat() + }, + 'end': { + 'dateTime': (middle_of_event + dt_util.dt + .timedelta(minutes=60)) + .isoformat() + }, + 'summary': 'Test Event in Progress', + 'reminders': {'useDefault': True}, + 'id': 'aioehgni435lihje', + 'status': 'confirmed', + 'updated': '2016-11-05T15:52:07.329Z', + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True, + }, + 'created': '2016-11-05T15:52:07.000Z', + 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + }, + 'etag': '"2956722254658000"', + 'kind': 'calendar#event', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + } + + mock_next_event.return_value.event = event + + device_name = 'Test Event in Progress' + device_id = 'test_event_in_progress' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_ON) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event['summary'], + 'all_day': False, + 'offset_reached': False, + 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), + 'end_time': + (middle_of_event + dt_util.dt.timedelta(minutes=60)) + .strftime(DATE_STR_FORMAT), + 'location': '', + 'description': '' + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_offset_in_progress_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + middle_of_event = dt_util.now() \ + + dt_util.dt.timedelta(minutes=14) + event_summary = 'Test Event in Progress' + event = { + 'start': { + 'dateTime': middle_of_event.isoformat() + }, + 'end': { + 'dateTime': (middle_of_event + dt_util.dt + .timedelta(minutes=60)) + .isoformat() + }, + 'summary': '{} !!-15'.format(event_summary), + 'reminders': {'useDefault': True}, + 'id': 'aioehgni435lihje', + 'status': 'confirmed', + 'updated': '2016-11-05T15:52:07.329Z', + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True, + }, + 'created': '2016-11-05T15:52:07.000Z', + 'iCalUID': 'dsfohuygtfvgbhnuju@google.com', + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + }, + 'etag': '"2956722254658000"', + 'kind': 'calendar#event', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + } + + mock_next_event.return_value.event = event + + device_name = 'Test Event in Progress' + device_id = 'test_event_in_progress' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertTrue(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event_summary, + 'all_day': False, + 'offset_reached': True, + 'start_time': middle_of_event.strftime(DATE_STR_FORMAT), + 'end_time': + (middle_of_event + dt_util.dt.timedelta(minutes=60)) + .strftime(DATE_STR_FORMAT), + 'location': '', + 'description': '' + }) + + @pytest.mark.skip + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_all_day_offset_in_progress_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + tomorrow = dt_util.dt.date.today() \ + + dt_util.dt.timedelta(days=1) + + event_summary = 'Test All Day Event Offset In Progress' + event = { + 'summary': '{} !!-25:0'.format(event_summary), + 'start': { + 'date': tomorrow.isoformat() + }, + 'end': { + 'date': (tomorrow + dt_util.dt.timedelta(days=1)) + .isoformat() + }, + 'location': 'Test Cases', + 'description': 'We\'re just testing that all day events get setup ' + 'correctly', + 'kind': 'calendar#event', + 'created': '2016-06-23T16:37:57.000Z', + 'transparency': 'transparent', + 'updated': '2016-06-24T01:57:21.045Z', + 'reminders': {'useDefault': True}, + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'id': '_c8rinwq863h45qnucyoi43ny8', + 'etag': '"2933466882090000"', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + 'iCalUID': 'cydrevtfuybguinhomj@google.com', + 'status': 'confirmed' + } + + mock_next_event.return_value.event = event + + device_name = 'Test All Day Offset In Progress' + device_id = 'test_all_day_offset_in_progress' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertTrue(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event_summary, + 'all_day': True, + 'offset_reached': True, + 'start_time': '{} 06:00:00'.format(event['start']['date']), + 'end_time': '{} 06:00:00'.format(event['end']['date']), + 'location': event['location'], + 'description': event['description'] + }) + + @patch('homeassistant.components.calendar.google.GoogleCalendarData') + def test_all_day_offset_event(self, mock_next_event): + """Test that we can create an event trigger on device.""" + tomorrow = dt_util.dt.date.today() \ + + dt_util.dt.timedelta(days=2) + + offset_hours = (1 + dt_util.now().hour) + event_summary = 'Test All Day Event Offset' + event = { + 'summary': '{} !!-{}:0'.format(event_summary, offset_hours), + 'start': { + 'date': tomorrow.isoformat() + }, + 'end': { + 'date': (tomorrow + dt_util.dt.timedelta(days=1)) + .isoformat() + }, + 'location': 'Test Cases', + 'description': 'We\'re just testing that all day events get setup ' + 'correctly', + 'kind': 'calendar#event', + 'created': '2016-06-23T16:37:57.000Z', + 'transparency': 'transparent', + 'updated': '2016-06-24T01:57:21.045Z', + 'reminders': {'useDefault': True}, + 'organizer': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'sequence': 0, + 'creator': { + 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com', + 'displayName': 'Organizer Name', + 'self': True + }, + 'id': '_c8rinwq863h45qnucyoi43ny8', + 'etag': '"2933466882090000"', + 'htmlLink': 'https://www.google.com/calendar/event?eid=*******', + 'iCalUID': 'cydrevtfuybguinhomj@google.com', + 'status': 'confirmed' + } + + mock_next_event.return_value.event = event + + device_name = 'Test All Day Offset' + device_id = 'test_all_day_offset' + + cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id, + {'name': device_name}) + + self.assertEqual(cal.name, device_name) + + self.assertEqual(cal.state, STATE_OFF) + + self.assertFalse(cal.offset_reached()) + + self.assertEqual(cal.device_state_attributes, { + 'message': event_summary, + 'all_day': True, + 'offset_reached': False, + 'start_time': '{} 00:00:00'.format(event['start']['date']), + 'end_time': '{} 00:00:00'.format(event['end']['date']), + 'location': event['location'], + 'description': event['description'] + }) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 707e49f670f..7a4e9f2950e 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1 +1 @@ -"""Tests for the cloud component.""" +"""Tests for the cloud component.""" diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index 94a4566a17b..19f25b514db 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -1,265 +1,265 @@ -"""The tests for the Xiaomi router device tracker platform.""" -import logging -import unittest -from unittest import mock -from unittest.mock import patch - -import requests - -from homeassistant.components.device_tracker import DOMAIN, xiaomi as xiaomi -from homeassistant.components.device_tracker.xiaomi import get_scanner -from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, - CONF_PLATFORM) -from tests.common import get_test_home_assistant - -_LOGGER = logging.getLogger(__name__) - -INVALID_USERNAME = 'bob' -TOKEN_TIMEOUT_USERNAME = 'tok' -URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login' -URL_LIST_END = 'api/misystem/devicelist' - -FIRST_CALL = True - - -def mocked_requests(*args, **kwargs): - """Mock requests.get invocations.""" - class MockResponse: - """Class to represent a mocked response.""" - - def __init__(self, json_data, status_code): - """Initialize the mock response class.""" - self.json_data = json_data - self.status_code = status_code - - def json(self): - """Return the json of the response.""" - return self.json_data - - @property - def content(self): - """Return the content of the response.""" - return self.json() - - def raise_for_status(self): - """Raise an HTTPError if status is not 200.""" - if self.status_code != 200: - raise requests.HTTPError(self.status_code) - - data = kwargs.get('data') - global FIRST_CALL - - if data and data.get('username', None) == INVALID_USERNAME: - # deliver an invalid token - return MockResponse({ - "code": "401", - "msg": "Invalid token" - }, 200) - elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: - # deliver an expired token - return MockResponse({ - "url": "/cgi-bin/luci/;stok=ef5860/web/home", - "token": "timedOut", - "code": "0" - }, 200) - elif str(args[0]).startswith(URL_AUTHORIZE): - # deliver an authorized token - return MockResponse({ - "url": "/cgi-bin/luci/;stok=ef5860/web/home", - "token": "ef5860", - "code": "0" - }, 200) - elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \ - and FIRST_CALL is True: - FIRST_CALL = False - # deliver an error when called with expired token - return MockResponse({ - "code": "401", - "msg": "Invalid token" - }, 200) - elif str(args[0]).endswith(URL_LIST_END): - # deliver the device list - return MockResponse({ - "mac": "1C:98:EC:0E:D5:A4", - "list": [ - { - "mac": "23:83:BF:F6:38:A0", - "oname": "12255ff", - "isap": 0, - "parent": "", - "authority": { - "wan": 1, - "pridisk": 0, - "admin": 1, - "lan": 0 - }, - "push": 0, - "online": 1, - "name": "Device1", - "times": 0, - "ip": [ - { - "downspeed": "0", - "online": "496957", - "active": 1, - "upspeed": "0", - "ip": "192.168.0.25" - } - ], - "statistics": { - "downspeed": "0", - "online": "496957", - "upspeed": "0" - }, - "icon": "", - "type": 1 - }, - { - "mac": "1D:98:EC:5E:D5:A6", - "oname": "CdddFG58", - "isap": 0, - "parent": "", - "authority": { - "wan": 1, - "pridisk": 0, - "admin": 1, - "lan": 0 - }, - "push": 0, - "online": 1, - "name": "Device2", - "times": 0, - "ip": [ - { - "downspeed": "0", - "online": "347325", - "active": 1, - "upspeed": "0", - "ip": "192.168.0.3" - } - ], - "statistics": { - "downspeed": "0", - "online": "347325", - "upspeed": "0" - }, - "icon": "", - "type": 0 - }, - ], - "code": 0 - }, 200) - else: - _LOGGER.debug('UNKNOWN ROUTE') - - -class TestXiaomiDeviceScanner(unittest.TestCase): - """Xiaomi device scanner test class.""" - - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @mock.patch( - 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', - return_value=mock.MagicMock()) - def test_config(self, xiaomi_mock): - """Testing minimal configuration.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_PASSWORD: 'passwordTest' - }) - } - xiaomi.get_scanner(self.hass, config) - self.assertEqual(xiaomi_mock.call_count, 1) - self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) - call_arg = xiaomi_mock.call_args[0][0] - self.assertEqual(call_arg['username'], 'admin') - self.assertEqual(call_arg['password'], 'passwordTest') - self.assertEqual(call_arg['host'], '192.168.0.1') - self.assertEqual(call_arg['platform'], 'device_tracker') - - @mock.patch( - 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', - return_value=mock.MagicMock()) - def test_config_full(self, xiaomi_mock): - """Testing full configuration.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: 'alternativeAdminName', - CONF_PASSWORD: 'passwordTest' - }) - } - xiaomi.get_scanner(self.hass, config) - self.assertEqual(xiaomi_mock.call_count, 1) - self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) - call_arg = xiaomi_mock.call_args[0][0] - self.assertEqual(call_arg['username'], 'alternativeAdminName') - self.assertEqual(call_arg['password'], 'passwordTest') - self.assertEqual(call_arg['host'], '192.168.0.1') - self.assertEqual(call_arg['platform'], 'device_tracker') - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_invalid_credential(self, mock_get, mock_post): - """"Testing invalid credential handling.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: INVALID_USERNAME, - CONF_PASSWORD: 'passwordTest' - }) - } - self.assertIsNone(get_scanner(self.hass, config)) - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_valid_credential(self, mock_get, mock_post): - """"Testing valid refresh.""" - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: 'admin', - CONF_PASSWORD: 'passwordTest' - }) - } - scanner = get_scanner(self.hass, config) - self.assertIsNotNone(scanner) - self.assertEqual(2, len(scanner.scan_devices())) - self.assertEqual("Device1", - scanner.get_device_name("23:83:BF:F6:38:A0")) - self.assertEqual("Device2", - scanner.get_device_name("1D:98:EC:5E:D5:A6")) - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_token_timed_out(self, mock_get, mock_post): - """"Testing refresh with a timed out token. - - New token is requested and list is downloaded a second time. - """ - config = { - DOMAIN: xiaomi.PLATFORM_SCHEMA({ - CONF_PLATFORM: xiaomi.DOMAIN, - CONF_HOST: '192.168.0.1', - CONF_USERNAME: TOKEN_TIMEOUT_USERNAME, - CONF_PASSWORD: 'passwordTest' - }) - } - scanner = get_scanner(self.hass, config) - self.assertIsNotNone(scanner) - self.assertEqual(2, len(scanner.scan_devices())) - self.assertEqual("Device1", - scanner.get_device_name("23:83:BF:F6:38:A0")) - self.assertEqual("Device2", - scanner.get_device_name("1D:98:EC:5E:D5:A6")) +"""The tests for the Xiaomi router device tracker platform.""" +import logging +import unittest +from unittest import mock +from unittest.mock import patch + +import requests + +from homeassistant.components.device_tracker import DOMAIN, xiaomi as xiaomi +from homeassistant.components.device_tracker.xiaomi import get_scanner +from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PLATFORM) +from tests.common import get_test_home_assistant + +_LOGGER = logging.getLogger(__name__) + +INVALID_USERNAME = 'bob' +TOKEN_TIMEOUT_USERNAME = 'tok' +URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login' +URL_LIST_END = 'api/misystem/devicelist' + +FIRST_CALL = True + + +def mocked_requests(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + @property + def content(self): + """Return the content of the response.""" + return self.json() + + def raise_for_status(self): + """Raise an HTTPError if status is not 200.""" + if self.status_code != 200: + raise requests.HTTPError(self.status_code) + + data = kwargs.get('data') + global FIRST_CALL + + if data and data.get('username', None) == INVALID_USERNAME: + # deliver an invalid token + return MockResponse({ + "code": "401", + "msg": "Invalid token" + }, 200) + elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: + # deliver an expired token + return MockResponse({ + "url": "/cgi-bin/luci/;stok=ef5860/web/home", + "token": "timedOut", + "code": "0" + }, 200) + elif str(args[0]).startswith(URL_AUTHORIZE): + # deliver an authorized token + return MockResponse({ + "url": "/cgi-bin/luci/;stok=ef5860/web/home", + "token": "ef5860", + "code": "0" + }, 200) + elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \ + and FIRST_CALL is True: + FIRST_CALL = False + # deliver an error when called with expired token + return MockResponse({ + "code": "401", + "msg": "Invalid token" + }, 200) + elif str(args[0]).endswith(URL_LIST_END): + # deliver the device list + return MockResponse({ + "mac": "1C:98:EC:0E:D5:A4", + "list": [ + { + "mac": "23:83:BF:F6:38:A0", + "oname": "12255ff", + "isap": 0, + "parent": "", + "authority": { + "wan": 1, + "pridisk": 0, + "admin": 1, + "lan": 0 + }, + "push": 0, + "online": 1, + "name": "Device1", + "times": 0, + "ip": [ + { + "downspeed": "0", + "online": "496957", + "active": 1, + "upspeed": "0", + "ip": "192.168.0.25" + } + ], + "statistics": { + "downspeed": "0", + "online": "496957", + "upspeed": "0" + }, + "icon": "", + "type": 1 + }, + { + "mac": "1D:98:EC:5E:D5:A6", + "oname": "CdddFG58", + "isap": 0, + "parent": "", + "authority": { + "wan": 1, + "pridisk": 0, + "admin": 1, + "lan": 0 + }, + "push": 0, + "online": 1, + "name": "Device2", + "times": 0, + "ip": [ + { + "downspeed": "0", + "online": "347325", + "active": 1, + "upspeed": "0", + "ip": "192.168.0.3" + } + ], + "statistics": { + "downspeed": "0", + "online": "347325", + "upspeed": "0" + }, + "icon": "", + "type": 0 + }, + ], + "code": 0 + }, 200) + else: + _LOGGER.debug('UNKNOWN ROUTE') + + +class TestXiaomiDeviceScanner(unittest.TestCase): + """Xiaomi device scanner test class.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch( + 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', + return_value=mock.MagicMock()) + def test_config(self, xiaomi_mock): + """Testing minimal configuration.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_PASSWORD: 'passwordTest' + }) + } + xiaomi.get_scanner(self.hass, config) + self.assertEqual(xiaomi_mock.call_count, 1) + self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) + call_arg = xiaomi_mock.call_args[0][0] + self.assertEqual(call_arg['username'], 'admin') + self.assertEqual(call_arg['password'], 'passwordTest') + self.assertEqual(call_arg['host'], '192.168.0.1') + self.assertEqual(call_arg['platform'], 'device_tracker') + + @mock.patch( + 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', + return_value=mock.MagicMock()) + def test_config_full(self, xiaomi_mock): + """Testing full configuration.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: 'alternativeAdminName', + CONF_PASSWORD: 'passwordTest' + }) + } + xiaomi.get_scanner(self.hass, config) + self.assertEqual(xiaomi_mock.call_count, 1) + self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN])) + call_arg = xiaomi_mock.call_args[0][0] + self.assertEqual(call_arg['username'], 'alternativeAdminName') + self.assertEqual(call_arg['password'], 'passwordTest') + self.assertEqual(call_arg['host'], '192.168.0.1') + self.assertEqual(call_arg['platform'], 'device_tracker') + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_invalid_credential(self, mock_get, mock_post): + """"Testing invalid credential handling.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: INVALID_USERNAME, + CONF_PASSWORD: 'passwordTest' + }) + } + self.assertIsNone(get_scanner(self.hass, config)) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_valid_credential(self, mock_get, mock_post): + """"Testing valid refresh.""" + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: 'admin', + CONF_PASSWORD: 'passwordTest' + }) + } + scanner = get_scanner(self.hass, config) + self.assertIsNotNone(scanner) + self.assertEqual(2, len(scanner.scan_devices())) + self.assertEqual("Device1", + scanner.get_device_name("23:83:BF:F6:38:A0")) + self.assertEqual("Device2", + scanner.get_device_name("1D:98:EC:5E:D5:A6")) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_token_timed_out(self, mock_get, mock_post): + """"Testing refresh with a timed out token. + + New token is requested and list is downloaded a second time. + """ + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: TOKEN_TIMEOUT_USERNAME, + CONF_PASSWORD: 'passwordTest' + }) + } + scanner = get_scanner(self.hass, config) + self.assertIsNotNone(scanner) + self.assertEqual(2, len(scanner.scan_devices())) + self.assertEqual("Device1", + scanner.get_device_name("23:83:BF:F6:38:A0")) + self.assertEqual("Device2", + scanner.get_device_name("1D:98:EC:5E:D5:A6")) diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 25bcbc1dd55..06613f1336a 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,128 +1,128 @@ -"""Test the Emulated Hue component.""" -import json - -from unittest.mock import patch, Mock, mock_open - -from homeassistant.components.emulated_hue import Config, _LOGGER - - -def test_config_google_home_entity_id_to_number(): - """Test config adheres to the type.""" - conf = Config(Mock(), { - 'type': 'google_home' - }) - - mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) - handle = mop() - - with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } - - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 - - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' - - -def test_config_google_home_entity_id_to_number_altered(): - """Test config adheres to the type.""" - conf = Config(Mock(), { - 'type': 'google_home' - }) - - mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) - handle = mop() - - with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } - - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 - - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' - - -def test_config_google_home_entity_id_to_number_empty(): - """Test config adheres to the type.""" - conf = Config(Mock(), { - 'type': 'google_home' - }) - - mop = mock_open(read_data='') - handle = mop() - - with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } - - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 - - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' - - -def test_config_alexa_entity_id_to_number(): - """Test config adheres to the type.""" - conf = Config(None, { - 'type': 'alexa' - }) - - number = conf.entity_id_to_number('light.test') - assert number == 'light.test' - - number = conf.entity_id_to_number('light.test') - assert number == 'light.test' - - number = conf.entity_id_to_number('light.test2') - assert number == 'light.test2' - - entity_id = conf.number_to_entity_id('light.test') - assert entity_id == 'light.test' - - -def test_warning_config_google_home_listen_port(): - """Test we warn when non-default port is used for Google Home.""" - with patch.object(_LOGGER, 'warning') as mock_warn: - Config(None, { - 'type': 'google_home', - 'host_ip': '123.123.123.123', - 'listen_port': 8300 - }) - - assert mock_warn.called - assert mock_warn.mock_calls[0][1][0] == \ - "When targeting Google Home, listening port has to be port 80" +"""Test the Emulated Hue component.""" +import json + +from unittest.mock import patch, Mock, mock_open + +from homeassistant.components.emulated_hue import Config, _LOGGER + + +def test_config_google_home_entity_id_to_number(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) + handle = mop() + + with patch('homeassistant.util.json.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 + + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' + + +def test_config_google_home_entity_id_to_number_altered(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) + handle = mop() + + with patch('homeassistant.util.json.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 + + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' + + +def test_config_google_home_entity_id_to_number_empty(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data='') + handle = mop() + + with patch('homeassistant.util.json.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 + + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' + + +def test_config_alexa_entity_id_to_number(): + """Test config adheres to the type.""" + conf = Config(None, { + 'type': 'alexa' + }) + + number = conf.entity_id_to_number('light.test') + assert number == 'light.test' + + number = conf.entity_id_to_number('light.test') + assert number == 'light.test' + + number = conf.entity_id_to_number('light.test2') + assert number == 'light.test2' + + entity_id = conf.number_to_entity_id('light.test') + assert entity_id == 'light.test' + + +def test_warning_config_google_home_listen_port(): + """Test we warn when non-default port is used for Google Home.""" + with patch.object(_LOGGER, 'warning') as mock_warn: + Config(None, { + 'type': 'google_home', + 'host_ip': '123.123.123.123', + 'listen_port': 8300 + }) + + assert mock_warn.called + assert mock_warn.mock_calls[0][1][0] == \ + "When targeting Google Home, listening port has to be port 80" diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 5cb0d0cdc1b..a06f8e7d093 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -1,579 +1,579 @@ -"""The tests for the MQTT JSON light platform. - -Configuration with RGB, brightness, color temp, effect, white value and XY: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - white_value: true - xy: true - -Configuration with RGB, brightness, color temp, effect, white value: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - white_value: true - -Configuration with RGB, brightness, color temp and effect: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - -Configuration with RGB, brightness and color temp: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true - color_temp: true - -Configuration with RGB, brightness: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true - -Config without RGB: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - -Config without RGB and brightness: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - -Config with brightness and scale: - -light: - platform: mqtt_json - name: test - state_topic: "mqtt_json_light_1" - command_topic: "mqtt_json_light_1/set" - brightness: true - brightness_scale: 99 -""" - -import json -import unittest - -from homeassistant.setup import setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, - ATTR_SUPPORTED_FEATURES) -import homeassistant.components.light as light -from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) - - -class TestLightMQTTJSON(unittest.TestCase): - """Test the MQTT JSON light.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_fail_setup_if_no_command_topic(self): \ - # pylint: disable=invalid-name - """Test if setup fails with no command topic.""" - with assert_setup_component(0, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - } - }) - self.assertIsNone(self.hass.states.get('light.test')) - - def test_no_color_brightness_color_temp_white_val_if_no_topics(self): \ - # pylint: disable=invalid-name - """Test for no RGB, brightness, color temp, effect, white val or XY.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('xy_color')) - - fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('xy_color')) - - def test_controlling_state_via_topic(self): \ - # pylint: disable=invalid-name - """Test the controlling of the state via topic.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'xy': True, - 'qos': '0' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('xy_color')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # Turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255,' - '"x":0.123,"y":0.123},' - '"brightness":255,' - '"color_temp":155,' - '"effect":"colorloop",' - '"white_value":150}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) - self.assertEqual(255, state.attributes.get('brightness')) - self.assertEqual(155, state.attributes.get('color_temp')) - self.assertEqual('colorloop', state.attributes.get('effect')) - self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) - - # Turn the light off - fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"brightness":100}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(100, - light_state.attributes['brightness']) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":125,"g":125,"b":125}}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], - light_state.attributes.get('rgb_color')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"x":0.135,"y":0.135}}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual([0.135, 0.135], - light_state.attributes.get('xy_color')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color_temp":155}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual(155, light_state.attributes.get('color_temp')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"effect":"colorloop"}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual('colorloop', light_state.attributes.get('effect')) - - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"white_value":155}') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual(155, light_state.attributes.get('white_value')) - - def test_sending_mqtt_commands_and_optimistic(self): \ - # pylint: disable=invalid-name - """Test the sending of command in optimistic mode.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'qos': 2 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - - light.turn_on(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', '{"state": "ON"}', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - - light.turn_off(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', '{"state": "OFF"}', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - light.turn_on(self.hass, 'light.test', - brightness=50, color_temp=155, effect='colorloop', - white_value=170) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(50, message_json["brightness"]) - self.assertEqual(155, message_json["color_temp"]) - self.assertEqual('colorloop', message_json["effect"]) - self.assertEqual(170, message_json["white_value"]) - self.assertEqual("ON", message_json["state"]) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(50, state.attributes['brightness']) - self.assertEqual(155, state.attributes['color_temp']) - self.assertEqual('colorloop', state.attributes['effect']) - self.assertEqual(170, state.attributes['white_value']) - - def test_flash_short_and_long(self): \ - # pylint: disable=invalid-name - """Test for flash length being sent when included.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'flash_time_short': 5, - 'flash_time_long': 15, - 'qos': 0 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - - light.turn_on(self.hass, 'light.test', flash="short") - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(5, message_json["flash"]) - self.assertEqual("ON", message_json["state"]) - - light.turn_on(self.hass, 'light.test', flash="long") - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(15, message_json["flash"]) - self.assertEqual("ON", message_json["state"]) - - def test_transition(self): - """Test for transition time being sent when included.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'qos': 0 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - - light.turn_on(self.hass, 'light.test', transition=10) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(10, message_json["transition"]) - self.assertEqual("ON", message_json["state"]) - - # Transition back off - light.turn_off(self.hass, 'light.test', transition=10) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - # Get the sent message - message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) - self.assertEqual(10, message_json["transition"]) - self.assertEqual("OFF", message_json["state"]) - - def test_brightness_scale(self): - """Test for brightness scaling.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_bright_scale', - 'command_topic': 'test_light_bright_scale/set', - 'brightness': True, - 'brightness_scale': 99 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('brightness')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # Turn on the light - fire_mqtt_message(self.hass, 'test_light_bright_scale', - '{"state":"ON"}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - - # Turn on the light with brightness - fire_mqtt_message(self.hass, 'test_light_bright_scale', - '{"state":"ON",' - '"brightness": 99}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - - def test_invalid_color_brightness_and_white_values(self): \ - # pylint: disable=invalid-name - """Test that invalid color/brightness/white values are ignored.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'rgb': True, - 'white_value': True, - 'qos': '0' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertEqual(185, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # Turn on the light - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255},' - '"brightness": 255,' - '"white_value": 255}') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) - self.assertEqual(255, state.attributes.get('brightness')) - self.assertEqual(255, state.attributes.get('white_value')) - - # Bad color values - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"color":{"r":"bad","g":"val","b":"test"}}') - self.hass.block_till_done() - - # Color should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) - - # Bad brightness values - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"brightness": "badValue"}') - self.hass.block_till_done() - - # Brightness should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - - # Bad white value - fire_mqtt_message(self.hass, 'test_light_rgb', - '{"state":"ON",' - '"white_value": "badValue"}') - self.hass.block_till_done() - - # White value should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('white_value')) - - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'availability_topic': 'availability-topic' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) +"""The tests for the MQTT JSON light platform. + +Configuration with RGB, brightness, color temp, effect, white value and XY: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + white_value: true + xy: true + +Configuration with RGB, brightness, color temp, effect, white value: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + white_value: true + +Configuration with RGB, brightness, color temp and effect: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + +Configuration with RGB, brightness and color temp: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true + color_temp: true + +Configuration with RGB, brightness: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true + +Config without RGB: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + +Config without RGB and brightness: + +light: + platform: mqtt_json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + +Config with brightness and scale: + +light: + platform: mqtt_json + name: test + state_topic: "mqtt_json_light_1" + command_topic: "mqtt_json_light_1/set" + brightness: true + brightness_scale: 99 +""" + +import json +import unittest + +from homeassistant.setup import setup_component +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES) +import homeassistant.components.light as light +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, + assert_setup_component) + + +class TestLightMQTTJSON(unittest.TestCase): + """Test the MQTT JSON light.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_fail_setup_if_no_command_topic(self): \ + # pylint: disable=invalid-name + """Test if setup fails with no command topic.""" + with assert_setup_component(0, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + } + }) + self.assertIsNone(self.hass.states.get('light.test')) + + def test_no_color_brightness_color_temp_white_val_if_no_topics(self): \ + # pylint: disable=invalid-name + """Test for no RGB, brightness, color temp, effect, white val or XY.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('xy_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('xy_color')) + + def test_controlling_state_via_topic(self): \ + # pylint: disable=invalid-name + """Test the controlling of the state via topic.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'xy': True, + 'qos': '0' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('xy_color')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # Turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":255,"g":255,"b":255,' + '"x":0.123,"y":0.123},' + '"brightness":255,' + '"color_temp":155,' + '"effect":"colorloop",' + '"white_value":150}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual(255, state.attributes.get('brightness')) + self.assertEqual(155, state.attributes.get('color_temp')) + self.assertEqual('colorloop', state.attributes.get('effect')) + self.assertEqual(150, state.attributes.get('white_value')) + self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + + # Turn the light off + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"brightness":100}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(100, + light_state.attributes['brightness']) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":125,"g":125,"b":125}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([125, 125, 125], + light_state.attributes.get('rgb_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"x":0.135,"y":0.135}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([0.135, 0.135], + light_state.attributes.get('xy_color')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color_temp":155}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual(155, light_state.attributes.get('color_temp')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"effect":"colorloop"}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual('colorloop', light_state.attributes.get('effect')) + + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"white_value":155}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual(155, light_state.attributes.get('white_value')) + + def test_sending_mqtt_commands_and_optimistic(self): \ + # pylint: disable=invalid-name + """Test the sending of command in optimistic mode.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'qos': 2 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) + + light.turn_on(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', '{"state": "ON"}', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + light.turn_off(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', '{"state": "OFF"}', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + light.turn_on(self.hass, 'light.test', + brightness=50, color_temp=155, effect='colorloop', + white_value=170) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(50, message_json["brightness"]) + self.assertEqual(155, message_json["color_temp"]) + self.assertEqual('colorloop', message_json["effect"]) + self.assertEqual(170, message_json["white_value"]) + self.assertEqual("ON", message_json["state"]) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(50, state.attributes['brightness']) + self.assertEqual(155, state.attributes['color_temp']) + self.assertEqual('colorloop', state.attributes['effect']) + self.assertEqual(170, state.attributes['white_value']) + + def test_flash_short_and_long(self): \ + # pylint: disable=invalid-name + """Test for flash length being sent when included.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'flash_time_short': 5, + 'flash_time_long': 15, + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + + light.turn_on(self.hass, 'light.test', flash="short") + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(5, message_json["flash"]) + self.assertEqual("ON", message_json["state"]) + + light.turn_on(self.hass, 'light.test', flash="long") + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(15, message_json["flash"]) + self.assertEqual("ON", message_json["state"]) + + def test_transition(self): + """Test for transition time being sent when included.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + + light.turn_on(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(10, message_json["transition"]) + self.assertEqual("ON", message_json["state"]) + + # Transition back off + light.turn_off(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + # Get the sent message + message_json = json.loads(self.mock_publish.mock_calls[-2][1][1]) + self.assertEqual(10, message_json["transition"]) + self.assertEqual("OFF", message_json["state"]) + + def test_brightness_scale(self): + """Test for brightness scaling.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_bright_scale', + 'command_topic': 'test_light_bright_scale/set', + 'brightness': True, + 'brightness_scale': 99 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('brightness')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # Turn on the light + fire_mqtt_message(self.hass, 'test_light_bright_scale', + '{"state":"ON"}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + + # Turn on the light with brightness + fire_mqtt_message(self.hass, 'test_light_bright_scale', + '{"state":"ON",' + '"brightness": 99}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + + def test_invalid_color_brightness_and_white_values(self): \ + # pylint: disable=invalid-name + """Test that invalid color/brightness/white values are ignored.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'rgb': True, + 'white_value': True, + 'qos': '0' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(185, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # Turn on the light + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":255,"g":255,"b":255},' + '"brightness": 255,' + '"white_value": 255}') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual(255, state.attributes.get('brightness')) + self.assertEqual(255, state.attributes.get('white_value')) + + # Bad color values + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"r":"bad","g":"val","b":"test"}}') + self.hass.block_till_done() + + # Color should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + + # Bad brightness values + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"brightness": "badValue"}') + self.hass.block_till_done() + + # Brightness should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + + # Bad white value + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"white_value": "badValue"}') + self.hass.block_till_done() + + # White value should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('white_value')) + + def test_default_availability_payload(self): + """Test availability by default payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'availability_topic': 'availability-topic' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'offline') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index be1f119fc14..0df9d8136e1 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -1,524 +1,524 @@ -"""The tests for the MQTT Template light platform. - -Configuration example with all features: - -light: - platform: mqtt_template - name: mqtt_template_light_1 - state_topic: 'home/rgb1' - command_topic: 'home/rgb1/set' - command_on_template: > - on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} - command_off_template: 'off' - state_template: '{{ value.split(",")[0] }}' - brightness_template: '{{ value.split(",")[1] }}' - color_temp_template: '{{ value.split(",")[2] }}' - white_value_template: '{{ value.split(",")[3] }}' - red_template: '{{ value.split(",")[4].split("-")[0] }}' - green_template: '{{ value.split(",")[4].split("-")[1] }}' - blue_template: '{{ value.split(",")[4].split("-")[2] }}' - -If your light doesn't support brightness feature, omit `brightness_template`. - -If your light doesn't support color temp feature, omit `color_temp_template`. - -If your light doesn't support white value feature, omit `white_value_template`. - -If your light doesn't support RGB feature, omit `(red|green|blue)_template`. -""" -import unittest - -from homeassistant.setup import setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) -import homeassistant.components.light as light -from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) - - -class TestLightMQTTTemplate(unittest.TestCase): - """Test the MQTT Template light.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_fails(self): \ - # pylint: disable=invalid-name - """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - } - }) - self.assertIsNone(self.hass.states.get('light.test')) - - def test_state_change_via_topic(self): \ - # pylint: disable=invalid-name - """Test state change via topic.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'state_template': '{{ value.split(",")[0] }}' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - fire_mqtt_message(self.hass, 'test_light_rgb', 'on') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('white_value')) - - def test_state_brightness_color_effect_temp_white_change_via_topic(self): \ - # pylint: disable=invalid-name - """Test state, bri, color, effect, color temp, white val change.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'effect_list': ['rainbow', 'colorloop'], - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }},' - '{{ effect|d }}', - 'command_off_template': 'off', - 'state_template': '{{ value.split(",")[0] }}', - 'brightness_template': '{{ value.split(",")[1] }}', - 'color_temp_template': '{{ value.split(",")[2] }}', - 'white_value_template': '{{ value.split(",")[3] }}', - 'red_template': '{{ value.split(",")[4].' - 'split("-")[0] }}', - 'green_template': '{{ value.split(",")[4].' - 'split("-")[1] }}', - 'blue_template': '{{ value.split(",")[4].' - 'split("-")[2] }}', - 'effect_template': '{{ value.split(",")[5] }}' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,255,145,123,255-128-64,') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 128, 64], state.attributes.get('rgb_color')) - self.assertEqual(255, state.attributes.get('brightness')) - self.assertEqual(145, state.attributes.get('color_temp')) - self.assertEqual(123, state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get('effect')) - - # turn the light off - fire_mqtt_message(self.hass, 'test_light_rgb', 'off') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # lower the brightness - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(100, light_state.attributes['brightness']) - - # change the color temp - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,195') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(195, light_state.attributes['color_temp']) - - # change the color - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,,41-42-43') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) - - # change the white value - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.hass.block_till_done() - self.assertEqual(134, light_state.attributes['white_value']) - - # change the effect - fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,,,,41-42-43,rainbow') - self.hass.block_till_done() - - light_state = self.hass.states.get('light.test') - self.assertEqual('rainbow', light_state.attributes.get('effect')) - - def test_optimistic(self): \ - # pylint: disable=invalid-name - """Test optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'qos': 2 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - - # turn on the light - light.turn_on(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', 'on,,,,--', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - - # turn the light off - light.turn_off(self.hass, 'light.test') - self.hass.block_till_done() - - self.assertEqual(('test_light_rgb/set', 'off', 2, False), - self.mock_publish.mock_calls[-2][1]) - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # turn on the light with brightness, color - light.turn_on(self.hass, 'light.test', brightness=50, - rgb_color=[75, 75, 75]) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,50,,,75-75-75', payload) - - # turn on the light with color temp and white val - light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139) - self.hass.block_till_done() - - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,,200,139,--', payload) - - self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the state - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) - self.assertEqual(50, state.attributes['brightness']) - self.assertEqual(200, state.attributes['color_temp']) - self.assertEqual(139, state.attributes['white_value']) - - def test_flash(self): \ - # pylint: disable=invalid-name - """Test flash.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ flash }}', - 'command_off_template': 'off', - 'qos': 0 - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # short flash - light.turn_on(self.hass, 'light.test', flash='short') - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,short', payload) - - # long flash - light.turn_on(self.hass, 'light.test', flash='long') - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,long', payload) - - def test_transition(self): - """Test for transition time being sent when included.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ transition }}', - 'command_off_template': 'off,{{ transition|d }}' - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - - # transition on - light.turn_on(self.hass, 'light.test', transition=10) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('on,10', payload) - - # transition off - light.turn_off(self.hass, 'light.test', transition=4) - self.hass.block_till_done() - - self.assertEqual('test_light_rgb/set', - self.mock_publish.mock_calls[-2][1][0]) - self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) - self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) - - # check the payload - payload = self.mock_publish.mock_calls[-2][1][1] - self.assertEqual('off,4', payload) - - def test_invalid_values(self): \ - # pylint: disable=invalid-name - """Test that invalid values are ignored.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'effect_list': ['rainbow', 'colorloop'], - 'state_topic': 'test_light_rgb', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }},' - '{{ effect|d }}', - 'command_off_template': 'off', - 'state_template': '{{ value.split(",")[0] }}', - 'brightness_template': '{{ value.split(",")[1] }}', - 'color_temp_template': '{{ value.split(",")[2] }}', - 'white_value_template': '{{ value.split(",")[3] }}', - 'red_template': '{{ value.split(",")[4].' - 'split("-")[0] }}', - 'green_template': '{{ value.split(",")[4].' - 'split("-")[1] }}', - 'blue_template': '{{ value.split(",")[4].' - 'split("-")[2] }}', - 'effect_template': '{{ value.split(",")[5] }}', - } - }) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get('rgb_color')) - self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get('color_temp')) - self.assertIsNone(state.attributes.get('effect')) - self.assertIsNone(state.attributes.get('white_value')) - self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - - # turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,255,215,222,255-255-255,rainbow') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - self.assertEqual(255, state.attributes.get('brightness')) - self.assertEqual(215, state.attributes.get('color_temp')) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) - self.assertEqual(222, state.attributes.get('white_value')) - self.assertEqual('rainbow', state.attributes.get('effect')) - - # bad state value - fire_mqtt_message(self.hass, 'test_light_rgb', 'offf') - self.hass.block_till_done() - - # state should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(STATE_ON, state.state) - - # bad brightness values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255') - self.hass.block_till_done() - - # brightness should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(255, state.attributes.get('brightness')) - - # bad color temp values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,off,255-255-255') - self.hass.block_till_done() - - # color temp should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(215, state.attributes.get('color_temp')) - - # bad color values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c') - self.hass.block_till_done() - - # color should not have changed - state = self.hass.states.get('light.test') - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) - - # bad white value values - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255') - self.hass.block_till_done() - - # white value should not have changed - state = self.hass.states.get('light.test') - self.assertEqual(222, state.attributes.get('white_value')) - - # bad effect value - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white') - self.hass.block_till_done() - - # effect should not have changed - state = self.hass.states.get('light.test') - self.assertEqual('rainbow', state.attributes.get('effect')) - - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ transition }}', - 'command_off_template': 'off,{{ transition|d }}', - 'availability_topic': 'availability-topic' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - self.assertTrue(setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,{{ transition }}', - 'command_off_template': 'off,{{ transition|d }}', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - })) - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertNotEqual(STATE_UNAVAILABLE, state.state) - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('light.test') - self.assertEqual(STATE_UNAVAILABLE, state.state) +"""The tests for the MQTT Template light platform. + +Configuration example with all features: + +light: + platform: mqtt_template + name: mqtt_template_light_1 + state_topic: 'home/rgb1' + command_topic: 'home/rgb1/set' + command_on_template: > + on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} + command_off_template: 'off' + state_template: '{{ value.split(",")[0] }}' + brightness_template: '{{ value.split(",")[1] }}' + color_temp_template: '{{ value.split(",")[2] }}' + white_value_template: '{{ value.split(",")[3] }}' + red_template: '{{ value.split(",")[4].split("-")[0] }}' + green_template: '{{ value.split(",")[4].split("-")[1] }}' + blue_template: '{{ value.split(",")[4].split("-")[2] }}' + +If your light doesn't support brightness feature, omit `brightness_template`. + +If your light doesn't support color temp feature, omit `color_temp_template`. + +If your light doesn't support white value feature, omit `white_value_template`. + +If your light doesn't support RGB feature, omit `(red|green|blue)_template`. +""" +import unittest + +from homeassistant.setup import setup_component +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) +import homeassistant.components.light as light +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, + assert_setup_component) + + +class TestLightMQTTTemplate(unittest.TestCase): + """Test the MQTT Template light.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_fails(self): \ + # pylint: disable=invalid-name + """Test that setup fails with missing required configuration items.""" + with assert_setup_component(0, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + } + }) + self.assertIsNone(self.hass.states.get('light.test')) + + def test_state_change_via_topic(self): \ + # pylint: disable=invalid-name + """Test state change via topic.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'test_light_rgb', 'on') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('white_value')) + + def test_state_brightness_color_effect_temp_white_change_via_topic(self): \ + # pylint: disable=invalid-name + """Test state, bri, color, effect, color temp, white val change.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'effect_list': ['rainbow', 'colorloop'], + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }},' + '{{ effect|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}', + 'brightness_template': '{{ value.split(",")[1] }}', + 'color_temp_template': '{{ value.split(",")[2] }}', + 'white_value_template': '{{ value.split(",")[3] }}', + 'red_template': '{{ value.split(",")[4].' + 'split("-")[0] }}', + 'green_template': '{{ value.split(",")[4].' + 'split("-")[1] }}', + 'blue_template': '{{ value.split(",")[4].' + 'split("-")[2] }}', + 'effect_template': '{{ value.split(",")[5] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,255,145,123,255-128-64,') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual([255, 128, 64], state.attributes.get('rgb_color')) + self.assertEqual(255, state.attributes.get('brightness')) + self.assertEqual(145, state.attributes.get('color_temp')) + self.assertEqual(123, state.attributes.get('white_value')) + self.assertIsNone(state.attributes.get('effect')) + + # turn the light off + fire_mqtt_message(self.hass, 'test_light_rgb', 'off') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # lower the brightness + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(100, light_state.attributes['brightness']) + + # change the color temp + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,195') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(195, light_state.attributes['color_temp']) + + # change the color + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,,41-42-43') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + + # change the white value + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(134, light_state.attributes['white_value']) + + # change the effect + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,,,,41-42-43,rainbow') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual('rainbow', light_state.attributes.get('effect')) + + def test_optimistic(self): \ + # pylint: disable=invalid-name + """Test optimistic mode.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'qos': 2 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light + light.turn_on(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', 'on,,,,--', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + # turn the light off + light.turn_off(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', 'off', 2, False), + self.mock_publish.mock_calls[-2][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # turn on the light with brightness, color + light.turn_on(self.hass, 'light.test', brightness=50, + rgb_color=[75, 75, 75]) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,50,,,75-75-75', payload) + + # turn on the light with color temp and white val + light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139) + self.hass.block_till_done() + + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,,200,139,--', payload) + + self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the state + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual(50, state.attributes['brightness']) + self.assertEqual(200, state.attributes['color_temp']) + self.assertEqual(139, state.attributes['white_value']) + + def test_flash(self): \ + # pylint: disable=invalid-name + """Test flash.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ flash }}', + 'command_off_template': 'off', + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # short flash + light.turn_on(self.hass, 'light.test', flash='short') + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,short', payload) + + # long flash + light.turn_on(self.hass, 'light.test', flash='long') + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,long', payload) + + def test_transition(self): + """Test for transition time being sent when included.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # transition on + light.turn_on(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('on,10', payload) + + # transition off + light.turn_off(self.hass, 'light.test', transition=4) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-2][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-2][1][1] + self.assertEqual('off,4', payload) + + def test_invalid_values(self): \ + # pylint: disable=invalid-name + """Test that invalid values are ignored.""" + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'effect_list': ['rainbow', 'colorloop'], + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }},' + '{{ effect|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}', + 'brightness_template': '{{ value.split(",")[1] }}', + 'color_temp_template': '{{ value.split(",")[2] }}', + 'white_value_template': '{{ value.split(",")[3] }}', + 'red_template': '{{ value.split(",")[4].' + 'split("-")[0] }}', + 'green_template': '{{ value.split(",")[4].' + 'split("-")[1] }}', + 'blue_template': '{{ value.split(",")[4].' + 'split("-")[2] }}', + 'effect_template': '{{ value.split(",")[5] }}', + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('color_temp')) + self.assertIsNone(state.attributes.get('effect')) + self.assertIsNone(state.attributes.get('white_value')) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,255,215,222,255-255-255,rainbow') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + self.assertEqual(215, state.attributes.get('color_temp')) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual(222, state.attributes.get('white_value')) + self.assertEqual('rainbow', state.attributes.get('effect')) + + # bad state value + fire_mqtt_message(self.hass, 'test_light_rgb', 'offf') + self.hass.block_till_done() + + # state should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + # bad brightness values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255') + self.hass.block_till_done() + + # brightness should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(255, state.attributes.get('brightness')) + + # bad color temp values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,off,255-255-255') + self.hass.block_till_done() + + # color temp should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(215, state.attributes.get('color_temp')) + + # bad color values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c') + self.hass.block_till_done() + + # color should not have changed + state = self.hass.states.get('light.test') + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + + # bad white value values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255') + self.hass.block_till_done() + + # white value should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(222, state.attributes.get('white_value')) + + # bad effect value + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white') + self.hass.block_till_done() + + # effect should not have changed + state = self.hass.states.get('light.test') + self.assertEqual('rainbow', state.attributes.get('effect')) + + def test_default_availability_payload(self): + """Test availability by default payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'availability_topic': 'availability-topic' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'offline') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/sensor/test_hddtemp.py b/tests/components/sensor/test_hddtemp.py index 3be35f3281c..1b65af7fd7e 100644 --- a/tests/components/sensor/test_hddtemp.py +++ b/tests/components/sensor/test_hddtemp.py @@ -1,216 +1,216 @@ -"""The tests for the hddtemp platform.""" -import socket - -import unittest -from unittest.mock import patch - -from homeassistant.setup import setup_component - -from tests.common import get_test_home_assistant - -VALID_CONFIG_MINIMAL = { - 'sensor': { - 'platform': 'hddtemp', - } -} - -VALID_CONFIG_NAME = { - 'sensor': { - 'platform': 'hddtemp', - 'name': 'FooBar', - } -} - -VALID_CONFIG_ONE_DISK = { - 'sensor': { - 'platform': 'hddtemp', - 'disks': [ - '/dev/sdd1', - ], - } -} - -VALID_CONFIG_WRONG_DISK = { - 'sensor': { - 'platform': 'hddtemp', - 'disks': [ - '/dev/sdx1', - ], - } -} - -VALID_CONFIG_MULTIPLE_DISKS = { - 'sensor': { - 'platform': 'hddtemp', - 'host': 'foobar.local', - 'disks': [ - '/dev/sda1', - '/dev/sdb1', - '/dev/sdc1', - ], - } -} - -VALID_CONFIG_HOST = { - 'sensor': { - 'platform': 'hddtemp', - 'host': 'alice.local', - } -} - -VALID_CONFIG_HOST_UNREACHABLE = { - 'sensor': { - 'platform': 'hddtemp', - 'host': 'bob.local', - } -} - - -class TelnetMock(): - """Mock class for the telnetlib.Telnet object.""" - - def __init__(self, host, port, timeout=0): - """Initialize Telnet object.""" - self.host = host - self.port = port - self.timeout = timeout - self.sample_data = bytes('|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|' + - '|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|' + - '|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|' + - '|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|', - 'ascii') - - def read_all(self): - """Return sample values.""" - if self.host == 'alice.local': - raise ConnectionRefusedError - elif self.host == 'bob.local': - raise socket.gaierror - else: - return self.sample_data - return None - - -class TestHDDTempSensor(unittest.TestCase): - """Test the hddtemp sensor.""" - - def setUp(self): - """Set up things to run when tests begin.""" - self.hass = get_test_home_assistant() - self.config = VALID_CONFIG_ONE_DISK - self.reference = {'/dev/sda1': {'device': '/dev/sda1', - 'temperature': '29', - 'unit_of_measurement': '°C', - 'model': 'WDC WD30EZRX-12DC0B0', }, - '/dev/sdb1': {'device': '/dev/sdb1', - 'temperature': '32', - 'unit_of_measurement': '°C', - 'model': 'WDC WD15EADS-11P7B2', }, - '/dev/sdc1': {'device': '/dev/sdc1', - 'temperature': '29', - 'unit_of_measurement': '°C', - 'model': 'WDC WD20EARX-22MMMB0', }, - '/dev/sdd1': {'device': '/dev/sdd1', - 'temperature': '32', - 'unit_of_measurement': '°C', - 'model': 'WDC WD15EARS-00Z5B1', }, } - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_min_config(self): - """Test minimal hddtemp configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) - - entity = self.hass.states.all()[0].entity_id - state = self.hass.states.get(entity) - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.state, reference['temperature']) - self.assertEqual(state.attributes.get('device'), reference['device']) - self.assertEqual(state.attributes.get('model'), reference['model']) - self.assertEqual(state.attributes.get('unit_of_measurement'), - reference['unit_of_measurement']) - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_rename_config(self): - """Test hddtemp configuration with different name.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME) - - entity = self.hass.states.all()[0].entity_id - state = self.hass.states.get(entity) - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.attributes.get('friendly_name'), - 'FooBar ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_one_disk(self): - """Test hddtemp one disk configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_ONE_DISK) - - state = self.hass.states.get('sensor.hd_temperature_devsdd1') - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.state, reference['temperature']) - self.assertEqual(state.attributes.get('device'), reference['device']) - self.assertEqual(state.attributes.get('model'), reference['model']) - self.assertEqual(state.attributes.get('unit_of_measurement'), - reference['unit_of_measurement']) - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_wrong_disk(self): - """Test hddtemp wrong disk configuration.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_WRONG_DISK) - - self.assertEqual(len(self.hass.states.all()), 1) - state = self.hass.states.get('sensor.hd_temperature_devsdx1') - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + '/dev/sdx1') - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_multiple_disks(self): - """Test hddtemp multiple disk configuration.""" - assert setup_component(self.hass, - 'sensor', VALID_CONFIG_MULTIPLE_DISKS) - - for sensor in ['sensor.hd_temperature_devsda1', - 'sensor.hd_temperature_devsdb1', - 'sensor.hd_temperature_devsdc1']: - - state = self.hass.states.get(sensor) - - reference = self.reference[state.attributes.get('device')] - - self.assertEqual(state.state, - reference['temperature']) - self.assertEqual(state.attributes.get('device'), - reference['device']) - self.assertEqual(state.attributes.get('model'), - reference['model']) - self.assertEqual(state.attributes.get('unit_of_measurement'), - reference['unit_of_measurement']) - self.assertEqual(state.attributes.get('friendly_name'), - 'HD Temperature ' + reference['device']) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_host_refused(self): - """Test hddtemp if host unreachable.""" - assert setup_component(self.hass, 'sensor', VALID_CONFIG_HOST) - self.assertEqual(len(self.hass.states.all()), 0) - - @patch('telnetlib.Telnet', new=TelnetMock) - def test_hddtemp_host_unreachable(self): - """Test hddtemp if host unreachable.""" - assert setup_component(self.hass, 'sensor', - VALID_CONFIG_HOST_UNREACHABLE) - self.assertEqual(len(self.hass.states.all()), 0) +"""The tests for the hddtemp platform.""" +import socket + +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + +VALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'hddtemp', + } +} + +VALID_CONFIG_NAME = { + 'sensor': { + 'platform': 'hddtemp', + 'name': 'FooBar', + } +} + +VALID_CONFIG_ONE_DISK = { + 'sensor': { + 'platform': 'hddtemp', + 'disks': [ + '/dev/sdd1', + ], + } +} + +VALID_CONFIG_WRONG_DISK = { + 'sensor': { + 'platform': 'hddtemp', + 'disks': [ + '/dev/sdx1', + ], + } +} + +VALID_CONFIG_MULTIPLE_DISKS = { + 'sensor': { + 'platform': 'hddtemp', + 'host': 'foobar.local', + 'disks': [ + '/dev/sda1', + '/dev/sdb1', + '/dev/sdc1', + ], + } +} + +VALID_CONFIG_HOST = { + 'sensor': { + 'platform': 'hddtemp', + 'host': 'alice.local', + } +} + +VALID_CONFIG_HOST_UNREACHABLE = { + 'sensor': { + 'platform': 'hddtemp', + 'host': 'bob.local', + } +} + + +class TelnetMock(): + """Mock class for the telnetlib.Telnet object.""" + + def __init__(self, host, port, timeout=0): + """Initialize Telnet object.""" + self.host = host + self.port = port + self.timeout = timeout + self.sample_data = bytes('|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|' + + '|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|' + + '|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|' + + '|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|', + 'ascii') + + def read_all(self): + """Return sample values.""" + if self.host == 'alice.local': + raise ConnectionRefusedError + elif self.host == 'bob.local': + raise socket.gaierror + else: + return self.sample_data + return None + + +class TestHDDTempSensor(unittest.TestCase): + """Test the hddtemp sensor.""" + + def setUp(self): + """Set up things to run when tests begin.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG_ONE_DISK + self.reference = {'/dev/sda1': {'device': '/dev/sda1', + 'temperature': '29', + 'unit_of_measurement': '°C', + 'model': 'WDC WD30EZRX-12DC0B0', }, + '/dev/sdb1': {'device': '/dev/sdb1', + 'temperature': '32', + 'unit_of_measurement': '°C', + 'model': 'WDC WD15EADS-11P7B2', }, + '/dev/sdc1': {'device': '/dev/sdc1', + 'temperature': '29', + 'unit_of_measurement': '°C', + 'model': 'WDC WD20EARX-22MMMB0', }, + '/dev/sdd1': {'device': '/dev/sdd1', + 'temperature': '32', + 'unit_of_measurement': '°C', + 'model': 'WDC WD15EARS-00Z5B1', }, } + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_min_config(self): + """Test minimal hddtemp configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + + entity = self.hass.states.all()[0].entity_id + state = self.hass.states.get(entity) + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.state, reference['temperature']) + self.assertEqual(state.attributes.get('device'), reference['device']) + self.assertEqual(state.attributes.get('model'), reference['model']) + self.assertEqual(state.attributes.get('unit_of_measurement'), + reference['unit_of_measurement']) + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_rename_config(self): + """Test hddtemp configuration with different name.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME) + + entity = self.hass.states.all()[0].entity_id + state = self.hass.states.get(entity) + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.attributes.get('friendly_name'), + 'FooBar ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_one_disk(self): + """Test hddtemp one disk configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_ONE_DISK) + + state = self.hass.states.get('sensor.hd_temperature_devsdd1') + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.state, reference['temperature']) + self.assertEqual(state.attributes.get('device'), reference['device']) + self.assertEqual(state.attributes.get('model'), reference['model']) + self.assertEqual(state.attributes.get('unit_of_measurement'), + reference['unit_of_measurement']) + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_wrong_disk(self): + """Test hddtemp wrong disk configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_WRONG_DISK) + + self.assertEqual(len(self.hass.states.all()), 1) + state = self.hass.states.get('sensor.hd_temperature_devsdx1') + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + '/dev/sdx1') + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_multiple_disks(self): + """Test hddtemp multiple disk configuration.""" + assert setup_component(self.hass, + 'sensor', VALID_CONFIG_MULTIPLE_DISKS) + + for sensor in ['sensor.hd_temperature_devsda1', + 'sensor.hd_temperature_devsdb1', + 'sensor.hd_temperature_devsdc1']: + + state = self.hass.states.get(sensor) + + reference = self.reference[state.attributes.get('device')] + + self.assertEqual(state.state, + reference['temperature']) + self.assertEqual(state.attributes.get('device'), + reference['device']) + self.assertEqual(state.attributes.get('model'), + reference['model']) + self.assertEqual(state.attributes.get('unit_of_measurement'), + reference['unit_of_measurement']) + self.assertEqual(state.attributes.get('friendly_name'), + 'HD Temperature ' + reference['device']) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_host_refused(self): + """Test hddtemp if host unreachable.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_HOST) + self.assertEqual(len(self.hass.states.all()), 0) + + @patch('telnetlib.Telnet', new=TelnetMock) + def test_hddtemp_host_unreachable(self): + """Test hddtemp if host unreachable.""" + assert setup_component(self.hass, 'sensor', + VALID_CONFIG_HOST_UNREACHABLE) + self.assertEqual(len(self.hass.states.all()), 0) diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index d7945218e73..167c3bb35ac 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -1,192 +1,192 @@ -"""The tests for the wake on lan switch platform.""" -import unittest -from unittest.mock import patch - -from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.components.switch as switch - -from tests.common import get_test_home_assistant, mock_service - - -TEST_STATE = None - - -def send_magic_packet(*macs, **kwargs): - """Fake call for sending magic packets.""" - return - - -def call(cmd, stdout, stderr): - """Return fake subprocess return codes.""" - if cmd[5] == 'validhostname' and TEST_STATE: - return 0 - return 2 - - -def system(): - """Fake system call to test the windows platform.""" - return 'Windows' - - -class TestWOLSwitch(unittest.TestCase): - """Test the wol switch.""" - - 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() - - @patch('wakeonlan.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_valid_hostname(self): - """Test with valid hostname.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'validhostname', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - - switch.turn_off(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - - @patch('wakeonlan.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - @patch('platform.system', new=system) - def test_valid_hostname_windows(self): - """Test with valid hostname on windows.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'validhostname', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - - @patch('wakeonlan.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_minimal_config(self): - """Test with minimal config.""" - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - } - })) - - @patch('wakeonlan.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_broadcast_config(self): - """Test with broadcast address config.""" - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'broadcast_address': '255.255.255.255', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - @patch('wakeonlan.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - def test_off_script(self): - """Test with turn off script.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'validhostname', - 'turn_off': { - 'service': 'shell_command.turn_off_TARGET', - }, - } - })) - calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_ON, state.state) - assert len(calls) == 0 - - TEST_STATE = False - - switch.turn_off(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - assert len(calls) == 1 - - @patch('wakeonlan.send_magic_packet', new=send_magic_packet) - @patch('subprocess.call', new=call) - @patch('platform.system', new=system) - def test_invalid_hostname_windows(self): - """Test with invalid hostname on windows.""" - global TEST_STATE - TEST_STATE = False - self.assertTrue(setup_component(self.hass, switch.DOMAIN, { - 'switch': { - 'platform': 'wake_on_lan', - 'mac_address': '00-01-02-03-04-05', - 'host': 'invalidhostname', - } - })) - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) - - TEST_STATE = True - - switch.turn_on(self.hass, 'switch.wake_on_lan') - self.hass.block_till_done() - - state = self.hass.states.get('switch.wake_on_lan') - self.assertEqual(STATE_OFF, state.state) +"""The tests for the wake on lan switch platform.""" +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.components.switch as switch + +from tests.common import get_test_home_assistant, mock_service + + +TEST_STATE = None + + +def send_magic_packet(*macs, **kwargs): + """Fake call for sending magic packets.""" + return + + +def call(cmd, stdout, stderr): + """Return fake subprocess return codes.""" + if cmd[5] == 'validhostname' and TEST_STATE: + return 0 + return 2 + + +def system(): + """Fake system call to test the windows platform.""" + return 'Windows' + + +class TestWOLSwitch(unittest.TestCase): + """Test the wol switch.""" + + 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() + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_valid_hostname(self): + """Test with valid hostname.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'validhostname', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + @patch('platform.system', new=system) + def test_valid_hostname_windows(self): + """Test with valid hostname on windows.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'validhostname', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_minimal_config(self): + """Test with minimal config.""" + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + } + })) + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_broadcast_config(self): + """Test with broadcast address config.""" + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'broadcast_address': '255.255.255.255', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + def test_off_script(self): + """Test with turn off script.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'validhostname', + 'turn_off': { + 'service': 'shell_command.turn_off_TARGET', + }, + } + })) + calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_ON, state.state) + assert len(calls) == 0 + + TEST_STATE = False + + switch.turn_off(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + assert len(calls) == 1 + + @patch('wakeonlan.send_magic_packet', new=send_magic_packet) + @patch('subprocess.call', new=call) + @patch('platform.system', new=system) + def test_invalid_hostname_windows(self): + """Test with invalid hostname on windows.""" + global TEST_STATE + TEST_STATE = False + self.assertTrue(setup_component(self.hass, switch.DOMAIN, { + 'switch': { + 'platform': 'wake_on_lan', + 'mac_address': '00-01-02-03-04-05', + 'host': 'invalidhostname', + } + })) + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) + + TEST_STATE = True + + switch.turn_on(self.hass, 'switch.wake_on_lan') + self.hass.block_till_done() + + state = self.hass.states.get('switch.wake_on_lan') + self.assertEqual(STATE_OFF, state.state) From 2014e42e4e32f11af0908efb2bc1d1a67cac1f5b Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Fri, 9 Feb 2018 21:31:49 +0100 Subject: [PATCH 156/166] miflora - fix for exception handling bug (#12149) * updated to development branch of miflora * updated requirements_all.txt * upgraded to version 0.3 * updated requirements_all.txt --- homeassistant/components/sensor/miflora.py | 6 +++++- requirements_all.txt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 56f8c3cfe47..ec68588f241 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) -REQUIREMENTS = ['miflora==0.2.0'] +REQUIREMENTS = ['miflora==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -138,12 +138,16 @@ class MiFloraSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ + from miflora.backends import BluetoothBackendException try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) except IOError as ioerr: _LOGGER.info("Polling error %s", ioerr) return + except BluetoothBackendException as bterror: + _LOGGER.info("Polling error %s", bterror) + return if data is not None: _LOGGER.debug("%s = %s", self.name, data) diff --git a/requirements_all.txt b/requirements_all.txt index 4c8f0f50498..8766502b0eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -488,7 +488,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.2.0 +miflora==0.3.0 # homeassistant.components.upnp miniupnpc==2.0.2 From 0b947882aced1f741cab22b626f804503a8a4f5c Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 9 Feb 2018 21:59:10 +0100 Subject: [PATCH 157/166] Update pyhomematic to 0.1.39 (#12265) * Update pyhomematic --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 33df4bfbd17..9c08984a23e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.38'] +REQUIREMENTS = ['pyhomematic==0.1.39'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8766502b0eb..e6952d53bed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ pyhik==0.1.4 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.38 +pyhomematic==0.1.39 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 From 4c11a3461fc0ef0b39de73bb7f65c20739321f15 Mon Sep 17 00:00:00 2001 From: luca-angemi Date: Fri, 9 Feb 2018 23:06:31 +0100 Subject: [PATCH 158/166] Update owntracks.py (#12260) --- homeassistant/components/device_tracker/owntracks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 1742a0aed95..e99524c36db 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -143,6 +143,8 @@ def _parse_see_args(message, subscribe_topic): kwargs['attributes']['tid'] = message['tid'] if 'addr' in message: kwargs['attributes']['address'] = message['addr'] + if 'cog' in message: + kwargs['attributes']['course'] = message['cog'] if 't' in message: if message['t'] == 'c': kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS From 6174c1754be2287743cc8581051ffb41aa8cc038 Mon Sep 17 00:00:00 2001 From: Eleftherios Chamakiotis Date: Sat, 10 Feb 2018 00:09:34 +0200 Subject: [PATCH 159/166] Fix for iTunes media player not updating artwork (#12089) * Added timestamp at the end of the iTunes API URL from where HA retrieves the artwork, so that it's not cached, as the URL exposed by the API never changes * Rearranged imports according to pylint * Added content_id in media file URL instead of timestamp, according to Paulus' suggestion --- homeassistant/components/media_player/itunes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 3291c1ae13d..ca0979f1752 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -284,7 +284,7 @@ class ItunesDevice(MediaPlayerDevice): """Image url of current playing media.""" if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \ self.current_title is not None: - return self.client.artwork_url() + return self.client.artwork_url() + '?id=' + self.content_id return 'https://cloud.githubusercontent.com/assets/260/9829355' \ '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' From 129d720d8e2747a370f8b68e5f3dbf3210859cf1 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 9 Feb 2018 23:10:16 +0100 Subject: [PATCH 160/166] Fix duplicate entity_ids in System Monitor (#12124) * Fix duplicate entity_ids in System Monitor This fix makes it so `_percent` is appended to the `entity_id` of the following 3 resources, in order to make the `entity_id` unique: - disk_use_percent - memory_use_percent - swap_use_percent * match entity_id to resource name Match entity_id to resource name, to make resulting entity_id more predictable * match entity_id to more resource names match entity_id to more resource names * Add unique_id property * Revert "Add unique_id property" This reverts commit c213ac360e0fa90ab1b9cfbd2161413f038a4ff1. --- .../components/sensor/systemmonitor.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 57e03cf153f..ea8595e3991 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -24,27 +24,27 @@ CONF_ARG = 'arg' SENSOR_TYPES = { 'disk_free': ['Disk free', 'GiB', 'mdi:harddisk'], - 'disk_use': ['Disk used', 'GiB', 'mdi:harddisk'], - 'disk_use_percent': ['Disk used', '%', 'mdi:harddisk'], + 'disk_use': ['Disk use', 'GiB', 'mdi:harddisk'], + 'disk_use_percent': ['Disk use (percent)', '%', 'mdi:harddisk'], 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], 'last_boot': ['Last boot', '', 'mdi:clock'], - 'load_15m': ['Average load (15m)', '', 'mdi:memory'], - 'load_1m': ['Average load (1m)', '', 'mdi:memory'], - 'load_5m': ['Average load (5m)', '', 'mdi:memory'], - 'memory_free': ['RAM available', 'MiB', 'mdi:memory'], - 'memory_use': ['RAM used', 'MiB', 'mdi:memory'], - 'memory_use_percent': ['RAM used', '%', 'mdi:memory'], - 'network_in': ['Received', 'MiB', 'mdi:server-network'], - 'network_out': ['Sent', 'MiB', 'mdi:server-network'], - 'packets_in': ['Packets received', ' ', 'mdi:server-network'], - 'packets_out': ['Packets sent', ' ', 'mdi:server-network'], + 'load_15m': ['Load (15m)', '', 'mdi:memory'], + 'load_1m': ['Load (1m)', '', 'mdi:memory'], + 'load_5m': ['Load (5m)', '', 'mdi:memory'], + 'memory_free': ['Memory free', 'MiB', 'mdi:memory'], + 'memory_use': ['Memory use', 'MiB', 'mdi:memory'], + 'memory_use_percent': ['Memory use (percent)', '%', 'mdi:memory'], + 'network_in': ['Network in', 'MiB', 'mdi:server-network'], + 'network_out': ['Network out', 'MiB', 'mdi:server-network'], + 'packets_in': ['Packets in', ' ', 'mdi:server-network'], + 'packets_out': ['Packets out', ' ', 'mdi:server-network'], 'process': ['Process', ' ', 'mdi:memory'], - 'processor_use': ['CPU used', '%', 'mdi:memory'], + 'processor_use': ['Processor use', '%', 'mdi:memory'], 'since_last_boot': ['Since last boot', '', 'mdi:clock'], 'swap_free': ['Swap free', 'GiB', 'mdi:harddisk'], - 'swap_use': ['Swap used', 'GiB', 'mdi:harddisk'], - 'swap_use_percent': ['Swap used', '%', 'mdi:harddisk'], + 'swap_use': ['Swap use', 'GiB', 'mdi:harddisk'], + 'swap_use_percent': ['Swap use (percent)', '%', 'mdi:harddisk'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From b087ea101dba32f5e18a3769ad7879c8e9c31ab7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Feb 2018 14:34:20 -0800 Subject: [PATCH 161/166] Update frontend to 20180209.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 c3158731022..eedd33478a7 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==20180130.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180209.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 e6952d53bed..f50c010072e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -352,7 +352,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180130.0 +home-assistant-frontend==20180209.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 acddad7e942..1ae1b9f2e14 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==20180130.0 +home-assistant-frontend==20180209.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From b0780110c7ad54e271532172eda35acdfc04ae8d Mon Sep 17 00:00:00 2001 From: Anton Lundin Date: Fri, 9 Feb 2018 23:50:05 +0100 Subject: [PATCH 162/166] One bug fix and one improvement to the statistics sensor. (#12259) * Correct time on recorder loaded values in statistics sensor Previously, the current time was used when initial values was loaded form the recorder component. This changes that to use the stored time from recorder instead. Signed-off-by: Anton Lundin * Expose min / max age of values in the statistics sensor This is very useful when doing derived calculations, for example in a template sensor. Signed-off-by: Anton Lundin --- homeassistant/components/sensor/statistics.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 46c714d0dbf..b26fd5cc804 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -34,6 +34,8 @@ ATTR_VARIANCE = 'variance' ATTR_STANDARD_DEVIATION = 'standard_deviation' ATTR_SAMPLING_SIZE = 'sampling_size' ATTR_TOTAL = 'total' +ATTR_MAX_AGE = 'max_age' +ATTR_MIN_AGE = 'min_age' CONF_SAMPLING_SIZE = 'sampling_size' CONF_MAX_AGE = 'max_age' @@ -88,6 +90,7 @@ class StatisticsSensor(Entity): self.median = self.mean = self.variance = self.stdev = 0 self.min = self.max = self.total = self.count = 0 self.average_change = self.change = 0 + self.max_age = self.min_age = 0 if 'recorder' in self._hass.config.components: # only use the database if it's configured @@ -111,8 +114,7 @@ class StatisticsSensor(Entity): try: self.states.append(float(new_state.state)) if self._max_age is not None: - now = dt_util.utcnow() - self.ages.append(now) + self.ages.append(new_state.last_updated) self.count = self.count + 1 except ValueError: self.count = self.count + 1 @@ -141,7 +143,7 @@ class StatisticsSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" if not self.is_binary: - return { + state = { ATTR_MEAN: self.mean, ATTR_COUNT: self.count, ATTR_MAX_VALUE: self.max, @@ -154,6 +156,13 @@ class StatisticsSensor(Entity): ATTR_CHANGE: self.change, ATTR_AVERAGE_CHANGE: self.average_change, } + # Only return min/max age if we have a age span + if self._max_age: + state.update({ + ATTR_MAX_AGE: self.max_age, + ATTR_MIN_AGE: self.min_age, + }) + return state @property def icon(self): @@ -190,6 +199,7 @@ class StatisticsSensor(Entity): self.stdev = self.variance = STATE_UNKNOWN if self.states: + self.count = len(self.states) self.total = round(sum(self.states), 2) self.min = min(self.states) self.max = max(self.states) @@ -197,6 +207,9 @@ class StatisticsSensor(Entity): self.average_change = self.change if len(self.states) > 1: self.average_change /= len(self.states) - 1 + if self._max_age is not None: + self.max_age = max(self.ages) + self.min_age = min(self.ages) else: self.min = self.max = self.total = STATE_UNKNOWN self.average_change = self.change = STATE_UNKNOWN From 3333dcc6c2e921a052d17f9238085813cb3b13e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Feb 2018 14:55:20 -0800 Subject: [PATCH 163/166] Version bump to 0.63 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 915ee5ac216..1c923a35936 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 63 -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 a9412d27aab92a694127949f4a7b1385322d08a1 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 10 Feb 2018 00:22:50 +0100 Subject: [PATCH 164/166] allow wildcards in subscription (#12247) * allow wildcards in subscription * remove whitespaces * make function public * also implement for mqtt_json * avoid mqtt-outside topic matching * add wildcard tests * add not matching wildcard tests * fix not-matching tests --- .../components/device_tracker/mqtt.py | 17 ++--- .../components/device_tracker/mqtt_json.py | 42 +++++----- tests/components/device_tracker/test_mqtt.py | 76 +++++++++++++++++++ .../device_tracker/test_mqtt_json.py | 74 ++++++++++++++++++ 4 files changed, 175 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index aab5b43acea..2e2d9b10d98 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -31,17 +31,14 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): devices = config[CONF_DEVICES] qos = config[CONF_QOS] - dev_id_lookup = {} - - @callback - def async_tracker_message_received(topic, payload, qos): - """Handle received MQTT message.""" - hass.async_add_job( - async_see(dev_id=dev_id_lookup[topic], location_name=payload)) - for dev_id, topic in devices.items(): - dev_id_lookup[topic] = dev_id + @callback + def async_message_received(topic, payload, qos, dev_id=dev_id): + """Handle received MQTT message.""" + hass.async_add_job( + async_see(dev_id=dev_id, location_name=payload)) + yield from mqtt.async_subscribe( - hass, topic, async_tracker_message_received, qos) + hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 0ef4f1835b6..7bcad60236a 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -41,32 +41,26 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): devices = config[CONF_DEVICES] qos = config[CONF_QOS] - dev_id_lookup = {} - - @callback - def async_tracker_message_received(topic, payload, qos): - """Handle received MQTT message.""" - dev_id = dev_id_lookup[topic] - - try: - data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) - except vol.MultipleInvalid: - _LOGGER.error("Skipping update for following data " - "because of missing or malformatted data: %s", - payload) - return - except ValueError: - _LOGGER.error("Error parsing JSON payload: %s", payload) - return - - kwargs = _parse_see_args(dev_id, data) - hass.async_add_job( - async_see(**kwargs)) - for dev_id, topic in devices.items(): - dev_id_lookup[topic] = dev_id + @callback + def async_message_received(topic, payload, qos, dev_id=dev_id): + """Handle received MQTT message.""" + try: + data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) + except vol.MultipleInvalid: + _LOGGER.error("Skipping update for following data " + "because of missing or malformatted data: %s", + payload) + return + except ValueError: + _LOGGER.error("Error parsing JSON payload: %s", payload) + return + + kwargs = _parse_see_args(dev_id, data) + hass.async_add_job(async_see(**kwargs)) + yield from mqtt.async_subscribe( - hass, topic, async_tracker_message_received, qos) + hass, topic, async_message_received, qos) return True diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index 4905ab4d029..78750e91f83 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -70,3 +70,79 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): fire_mqtt_message(self.hass, topic, location) self.hass.block_till_done() self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_single_level_wildcard_topic(self): + """Test single level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/+/paulus' + topic = '/location/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_multi_level_wildcard_topic(self): + """Test multi level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/#' + topic = '/location/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_single_level_wildcard_topic_not_matching(self): + """Test not matching single level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/+/paulus' + topic = '/location/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) + + def test_multi_level_wildcard_topic_not_matching(self): + """Test not matching multi level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/#' + topic = '/somewhere/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) diff --git a/tests/components/device_tracker/test_mqtt_json.py b/tests/components/device_tracker/test_mqtt_json.py index 1755f424d29..43f4fc3bbf3 100644 --- a/tests/components/device_tracker/test_mqtt_json.py +++ b/tests/components/device_tracker/test_mqtt_json.py @@ -123,3 +123,77 @@ class TestComponentsDeviceTrackerJSONMQTT(unittest.TestCase): "Skipping update for following data because of missing " "or malformatted data: {\"longitude\": 2.0}", test_handle.output[0]) + + def test_single_level_wildcard_topic(self): + """Test single level wildcard topic.""" + dev_id = 'zanzito' + subscription = 'location/+/zanzito' + topic = 'location/room/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_multi_level_wildcard_topic(self): + """Test multi level wildcard topic.""" + dev_id = 'zanzito' + subscription = 'location/#' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_single_level_wildcard_topic_not_matching(self): + """Test not matching single level wildcard topic.""" + dev_id = 'zanzito' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = 'location/+/zanzito' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) + + def test_multi_level_wildcard_topic_not_matching(self): + """Test not matching multi level wildcard topic.""" + dev_id = 'zanzito' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = 'location/#' + topic = 'somewhere/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) From aad26599ae08cd21cff8c1c3fd0b9440980506e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Feb 2018 02:40:24 -0800 Subject: [PATCH 165/166] Retry keyset cloud (#12270) * Use less threads in helpers.event tests * Add helpers.event.async_call_later * Cloud: retry fetching keyset --- homeassistant/components/cloud/__init__.py | 56 ++++++++++------------ homeassistant/helpers/event.py | 9 ++++ tests/components/cloud/test_http_api.py | 4 +- tests/components/cloud/test_init.py | 2 +- tests/components/cloud/test_iot.py | 8 ++-- tests/helpers/test_event.py | 27 +++++++++-- 6 files changed, 66 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index a5bbf805d42..e17c9ee1b1e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -16,8 +16,7 @@ import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE) -from homeassistant.helpers import entityfilter -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import entityfilter, config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from homeassistant.components.alexa import smart_home as alexa_sh @@ -105,12 +104,7 @@ def async_setup(hass, config): ) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) - - success = yield from cloud.initialize() - - if not success: - return False - + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) yield from http_api.async_setup(hass) return True @@ -192,19 +186,6 @@ class Cloud: return self._gactions_config - @asyncio.coroutine - def initialize(self): - """Initialize and load cloud info.""" - jwt_success = yield from self._fetch_jwt_keyset() - - if not jwt_success: - return False - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._start_cloud) - - return True - def path(self, *parts): """Get config path inside cloud dir. @@ -234,19 +215,34 @@ class Cloud: 'refresh_token': self.refresh_token, }, indent=4)) - def _start_cloud(self, event): + @asyncio.coroutine + def async_start(self, _): """Start the cloud component.""" - # Ensure config dir exists - path = self.hass.config.path(CONFIG_DIR) - if not os.path.isdir(path): - os.mkdir(path) + success = yield from self._fetch_jwt_keyset() - user_info = self.user_info_path - if not os.path.isfile(user_info): + # Fetching keyset can fail if internet is not up yet. + if not success: + self.hass.helpers.async_call_later(5, self.async_start) return - with open(user_info, 'rt') as file: - info = json.loads(file.read()) + def load_config(): + """Load config.""" + # Ensure config dir exists + path = self.hass.config.path(CONFIG_DIR) + if not os.path.isdir(path): + os.mkdir(path) + + user_info = self.user_info_path + if not os.path.isfile(user_info): + return None + + with open(user_info, 'rt') as file: + return json.loads(file.read()) + + info = yield from self.hass.async_add_job(load_config) + + if info is None: + return # Validate tokens try: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index f11b2eacf3a..eab2d583f45 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,4 +1,5 @@ """Helpers for listening to events.""" +from datetime import timedelta import functools as ft from homeassistant.loader import bind_hass @@ -219,6 +220,14 @@ track_point_in_utc_time = threaded_listener_factory( async_track_point_in_utc_time) +@callback +@bind_hass +def async_call_later(hass, delay, action): + """Add a listener that is called in .""" + return async_track_point_in_utc_time( + hass, action, dt_util.utcnow() + timedelta(seconds=delay)) + + @callback @bind_hass def async_track_time_interval(hass, action, interval): diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 7623b25d401..69cd540e7d5 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -14,8 +14,8 @@ from tests.common import mock_coro @pytest.fixture def cloud_client(hass, test_client): """Fixture that can fetch from the cloud client.""" - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): hass.loop.run_until_complete(async_setup_component(hass, 'cloud', { 'cloud': { 'mode': 'development', diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 7d23d9faad4..70990519a0b 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -87,7 +87,7 @@ def test_initialize_loads_info(mock_os, hass): with patch('homeassistant.components.cloud.open', mopen, create=True), \ patch('homeassistant.components.cloud.Cloud._decode_claims'): - cl._start_cloud(None) + yield from cl.async_start(None) assert cl.id_token == 'test-id-token' assert cl.access_token == 'test-access-token' diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 529559f56af..53340ecede1 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -266,8 +266,8 @@ def test_handler_alexa(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): setup = yield from async_setup_component(hass, 'cloud', { 'cloud': { 'alexa': { @@ -309,8 +309,8 @@ def test_handler_google_actions(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): setup = yield from async_setup_component(hass, 'cloud', { 'cloud': { 'google_actions': { diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 7d601c7a78d..73f2b9ff5a4 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -7,10 +7,12 @@ from datetime import datetime, timedelta from astral import Astral import pytest +from homeassistant.core import callback from homeassistant.setup import setup_component import homeassistant.core as ha from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( + async_call_later, track_point_in_utc_time, track_point_in_time, track_utc_time_change, @@ -52,7 +54,7 @@ class TestEventHelpers(unittest.TestCase): runs = [] track_point_in_utc_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) self._send_time_changed(before_birthday) self.hass.block_till_done() @@ -68,14 +70,14 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(1, len(runs)) track_point_in_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) self._send_time_changed(after_birthday) self.hass.block_till_done() self.assertEqual(2, len(runs)) unsub = track_point_in_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) unsub() self._send_time_changed(after_birthday) @@ -642,3 +644,22 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) self.hass.block_till_done() self.assertEqual(0, len(specific_runs)) + + +@asyncio.coroutine +def test_async_call_later(hass): + """Test calling an action later.""" + def action(): pass + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.helpers.event' + '.async_track_point_in_utc_time') as mock, \ + patch('homeassistant.util.dt.utcnow', return_value=now): + remove = async_call_later(hass, 3, action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert hass is hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + assert remove is mock() From 18aa1037ddf7757e2792147c929649408add6d4b Mon Sep 17 00:00:00 2001 From: Slava Date: Sat, 10 Feb 2018 21:59:04 +0100 Subject: [PATCH 166/166] Update limitlessled requirement to v1.0.9 (#12275) * Update limitlessled requirement to v1.0.9 * trigger cla * take back empty line --- homeassistant/components/light/limitlessled.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index aad2abdd183..0c6b1143bbd 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['limitlessled==1.0.8'] +REQUIREMENTS = ['limitlessled==1.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f50c010072e..3ca5b9fc763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ liffylights==0.9.4 lightify==1.0.6.1 # homeassistant.components.light.limitlessled -limitlessled==1.0.8 +limitlessled==1.0.9 # homeassistant.components.linode linode-api==4.1.4b2