From 79406487251fd216229eabeb39fff7a139dea278 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 27 Aug 2016 21:07:55 -0700 Subject: [PATCH 01/22] 0.28.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a43e4e58a1a..ce46c62850b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = '0.27.0' +__version__ = '0.28.0.dev0' REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' From 17a57d3b47ea9058e2fb0d81e16b50dc28885ef0 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sun, 28 Aug 2016 17:58:50 +0200 Subject: [PATCH 02/22] Fixes wrong statevalue and problem with zwave setpoint (#3017) * Fixes wrong statevalue and problem with zwave setpoint * Fix demo test to match bugfix (#10) --- homeassistant/components/climate/__init__.py | 2 +- homeassistant/components/climate/zwave.py | 10 +--------- tests/components/climate/test_demo.py | 9 ++++++--- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6ed289b2008..e4215bcea85 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -351,7 +351,7 @@ class ClimateDevice(Entity): @property def state(self): """Return the current state.""" - return self.current_operation or STATE_UNKNOWN + return self.target_temperature or STATE_UNKNOWN @property def state_attributes(self): diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 24ef45eb952..466fdcedf57 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -78,7 +78,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._current_swing_mode = None self._swing_list = None self._unit = None - self._index = None self._zxt_120 = None self.update_properties() # register listener @@ -107,15 +106,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def update_properties(self): """Callback on data change for the registered node/value pair.""" # Set point - temps = [] for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): self._unit = value.units - temps.append(int(value.data)) - if value.index == self._index: - self._target_temperature = int(value.data) - self._target_temperature_high = max(temps) - self._target_temperature_low = min(temps) + self._target_temperature = int(value.data) # Operation Mode for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): @@ -209,8 +203,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Set new target temperature.""" for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): - if value.command_class != 67 and value.index != self._index: - continue if self._zxt_120: # ZXT-120 does not support get setpoint self._target_temperature = temperature diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 4dab359688c..4b3d4fcc64a 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -110,16 +110,19 @@ class TestDemoClimate(unittest.TestCase): def test_set_operation_bad_attr(self): """Test setting operation mode without required attribute.""" - self.assertEqual("Cool", self.hass.states.get(ENTITY_CLIMATE).state) + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("Cool", state.attributes.get('operation_mode')) climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE) self.hass.pool.block_till_done() - self.assertEqual("Cool", self.hass.states.get(ENTITY_CLIMATE).state) + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("Cool", state.attributes.get('operation_mode')) def test_set_operation(self): """Test setting of new operation mode.""" climate.set_operation_mode(self.hass, "Heat", ENTITY_CLIMATE) self.hass.pool.block_till_done() - self.assertEqual("Heat", self.hass.states.get(ENTITY_CLIMATE).state) + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("Heat", state.attributes.get('operation_mode')) def test_set_away_mode_bad_attr(self): """Test setting the away mode without required attribute.""" From 3313995c4c1c4bdedc4c3554876d74ade77800dd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 28 Aug 2016 20:00:44 +0200 Subject: [PATCH 03/22] fix voluptuous and cover autodiscovery (#3022) --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index c01a6fd2ce4..ed0d51b3278 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -129,7 +129,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.In(CONF_RESOLVENAMES_OPTIONS), vol.Optional(CONF_USERNAME, default="Admin"): cv.string, vol.Optional(CONF_PASSWORD, default=""): cv.string, - vol.Optional(CONF_DELAY, default=0.5): cv.string, + vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float), }), }, extra=vol.ALLOW_EXTRA) @@ -282,7 +282,7 @@ def _system_callback_handler(hass, config, src, *args): for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), ('light', DISCOVER_LIGHTS), - ('rollershutter', DISCOVER_COVER), + ('cover', DISCOVER_COVER), ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), ('climate', DISCOVER_CLIMATE)): From 2a5ca1c873203b86619bce8d59d72d897219d143 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sun, 28 Aug 2016 22:41:48 +0200 Subject: [PATCH 04/22] Map Modes to setpoint indexes (#3023) * Map Modes to setpoint indexes * Fixes devices with no thermostat mode * another try to fix devices without mode * another try to fix devices without mode 2 * another try to fix devices without mode 3 * fix setting setpoint for devices with no mode * fix setting setpoint for devices with no mode --- homeassistant/components/climate/zwave.py | 45 +++++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 466fdcedf57..c8425ab4e8c 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -35,11 +35,21 @@ DEVICE_MAPPINGS = { REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 } -ZXT_120_SET_TEMP = { +SET_TEMP_TO_INDEX = { 'Heat': 1, 'Cool': 2, + 'Auto': 3, + 'Aux Heat': 4, + 'Resume': 5, + 'Fan Only': 6, + 'Furnace': 7, 'Dry Air': 8, - 'Auto Changeover': 10 + 'Moist Air': 9, + 'Auto Changeover': 10, + 'Heat Econ': 11, + 'Cool Econ': 12, + 'Away': 13, + 'Unknown': 14 } @@ -109,7 +119,14 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): self._unit = value.units + if self.current_operation is not None: + if SET_TEMP_TO_INDEX.get(self._current_operation) \ + != value.index: + continue + if self._zxt_120: + continue self._target_temperature = int(value.data) + # Operation Mode for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): @@ -203,21 +220,25 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Set new target temperature.""" for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): - if self._zxt_120: - # ZXT-120 does not support get setpoint - self._target_temperature = temperature - if ZXT_120_SET_TEMP.get(self._current_operation) \ - != value.index: + if self.current_operation is not None: + if SET_TEMP_TO_INDEX.get(self._current_operation) \ + != value.index: continue - _LOGGER.debug("ZXT_120_SET_TEMP=%s and" + _LOGGER.debug("SET_TEMP_TO_INDEX=%s and" " self._current_operation=%s", - ZXT_120_SET_TEMP.get(self._current_operation), + SET_TEMP_TO_INDEX.get(self._current_operation), self._current_operation) - # ZXT-120 responds only to whole int - value.data = int(round(temperature, 0)) + if self._zxt_120: + # ZXT-120 does not support get setpoint + self._target_temperature = temperature + # ZXT-120 responds only to whole int + value.data = int(round(temperature, 0)) + else: + value.data = int(temperature) + break else: value.data = int(temperature) - break + break def set_fan_mode(self, fan): """Set new target fan mode.""" From 2d8bc754c81262463e41cf59f36d7ce102b88b06 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sun, 28 Aug 2016 22:51:56 +0200 Subject: [PATCH 05/22] Ecobee (#3025) * fix ecobee mode * Fixup --- homeassistant/components/climate/ecobee.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 76038085385..5c65a7d0b23 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -142,6 +142,11 @@ class Thermostat(ClimateDevice): else: return STATE_OFF + @property + def current_operation(self): + """Return current operation.""" + return self.operation_mode + @property def operation_mode(self): """Return current operation ie. heat, cool, idle.""" @@ -162,11 +167,6 @@ class Thermostat(ClimateDevice): """Return current mode ie. home, away, sleep.""" return self.thermostat['program']['currentClimateRef'] - @property - def current_operation(self): - """Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off.""" - return self.thermostat['settings']['hvacMode'] - @property def fan_min_on_time(self): """Return current fan minimum on time.""" @@ -180,7 +180,7 @@ class Thermostat(ClimateDevice): "humidity": self.current_humidity, "fan": self.fan, "mode": self.mode, - "operation_mode": self.current_operation, + "hvac_mode": self.thermostat['settings']['hvacMode'], "fan_min_on_time": self.fan_min_on_time } From 821b3d7facfb7a453eb0247f20399913af64a0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 29 Aug 2016 01:34:01 +0200 Subject: [PATCH 06/22] Bug fix for asuswrt device_tracker. Issue #3015 (#3016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐬 --- homeassistant/components/device_tracker/asuswrt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 37e8cb0b2e7..76a501f8b80 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = vol.All( vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PROTOCOL, default='ssh'): - vol.Schema(['ssh', 'telnet']), + vol.In(['ssh', 'telnet']), vol.Optional(CONF_MODE, default='router'): vol.Schema(['router', 'ap']), vol.Optional(CONF_SSH_KEY): cv.isfile, From b6ad0bfbea0fdbdc62de331124908ee49212edf9 Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Sun, 28 Aug 2016 19:09:34 -0600 Subject: [PATCH 07/22] Allow user to configure server id to perform speed test against (#3008) * Allow user to configure server id to perform speed test against * Don't overwrite list * Type-o * Convert to string * Append lists * str(None) => 'None' did not realize that. --- homeassistant/components/sensor/speedtest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 86fd48e4d03..9cf7bfdd208 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -29,6 +29,7 @@ CONF_SECOND = 'second' CONF_MINUTE = 'minute' CONF_HOUR = 'hour' CONF_DAY = 'day' +CONF_SERVER_ID = 'server_id' SENSOR_TYPES = { 'ping': ['Ping', 'ms'], 'download': ['Download', 'Mbit/s'], @@ -38,6 +39,7 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]), + vol.Optional(CONF_SERVER_ID): cv.positive_int, vol.Optional(CONF_SECOND, default=[0]): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), vol.Optional(CONF_MINUTE, default=[0]): @@ -131,6 +133,7 @@ class SpeedtestData(object): def __init__(self, hass, config): """Initialize the data object.""" self.data = None + self._server_id = config.get(CONF_SERVER_ID) track_time_change(hass, self.update, second=config.get(CONF_SECOND), minute=config.get(CONF_MINUTE), @@ -143,9 +146,12 @@ class SpeedtestData(object): _LOGGER.info('Executing speedtest') try: + args = [sys.executable, speedtest_cli.__file__, '--simple'] + if self._server_id: + args = args + ['--server', str(self._server_id)] + re_output = _SPEEDTEST_REGEX.split( - check_output([sys.executable, speedtest_cli.__file__, - '--simple']).decode("utf-8")) + check_output(args).decode("utf-8")) except CalledProcessError as process_error: _LOGGER.error('Error executing speedtest: %s', process_error) return From 1699885907d619e7e6e25c227019b022a40e0f96 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 29 Aug 2016 04:00:43 +0200 Subject: [PATCH 08/22] Fix media_player descriptions and select_source (#3030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐬 --- homeassistant/components/media_player/__init__.py | 1 - homeassistant/components/media_player/services.yaml | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5448671b14d..a3a6274a89e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -97,7 +97,6 @@ SERVICE_TO_METHOD = { SERVICE_MEDIA_STOP: 'media_stop', SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', - SERVICE_SELECT_SOURCE: 'select_source', SERVICE_CLEAR_PLAYLIST: 'clear_playlist' } diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 421010fc1a9..323fda37a02 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -40,25 +40,25 @@ volume_down: description: Name(s) of entities to turn volume down on example: 'media_player.living_room_sonos' -mute_volume: +volume_mute: description: Mute a media player's volume fields: entity_id: description: Name(s) of entities to mute example: 'media_player.living_room_sonos' - mute: + is_volume_muted: description: True/false for mute/unmute example: true -set_volume_level: +volume_set: description: Set a media player's volume level fields: entity_id: description: Name(s) of entities to set volume level on example: 'media_player.living_room_sonos' - volume: + volume_level: description: Volume level to set example: 60 @@ -117,7 +117,7 @@ media_seek: entity_id: description: Name(s) of entities to seek media on example: 'media_player.living_room_chromecast' - position: + seek_position: description: Position to seek to. The format is platform dependent. example: 100 From 39402aff2e6e6e830fa60d93aa32f579089f3d56 Mon Sep 17 00:00:00 2001 From: arsaboo Date: Sun, 28 Aug 2016 23:05:28 -0400 Subject: [PATCH 09/22] Remove units for humidity in Wundeground sensor (#3018) * Remove units for humidity Wunderground returns the information with the units. * Trim the % from the return value of humidity --- homeassistant/components/sensor/wunderground.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 0321c4f7dcb..623016518ac 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -101,7 +101,10 @@ class WUndergroundSensor(Entity): def state(self): """Return the state of the sensor.""" if self.rest.data and self._condition in self.rest.data: - return self.rest.data[self._condition] + if self._condition == 'relative_humidity': + return int(self.rest.data[self._condition][:-1]) + else: + return self.rest.data[self._condition] else: return STATE_UNKNOWN From 62bbda1f827cf6c889251518bfc36e77d18f73e8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 29 Aug 2016 08:23:20 +0200 Subject: [PATCH 10/22] Bug fix for asuswrt device_tracker. Issue #3015 --- homeassistant/components/device_tracker/asuswrt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 76a501f8b80..a125607a00f 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -36,7 +36,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), vol.Optional(CONF_MODE, default='router'): - vol.Schema(['router', 'ap']), + vol.In(['router', 'ap']), vol.Optional(CONF_SSH_KEY): cv.isfile, vol.Optional(CONF_PUB_KEY): cv.isfile })) From 1b718c62a34784bbdd6e15d18b409fe59239e6fd Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 29 Aug 2016 14:45:48 +0100 Subject: [PATCH 11/22] Fix bug in wemo discovery caused by voluptuous addition. (#3027) --- homeassistant/components/wemo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 29e6d53cd2c..c80ec8cbc12 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -81,7 +81,7 @@ def setup(hass, config): # Add static devices from the config file. devices.extend((address, None) - for address in config.get(DOMAIN, {}).get(CONF_STATIC)) + for address in config.get(DOMAIN, {}).get(CONF_STATIC, [])) for address, device in devices: port = pywemo.ouimeaux_device.probe_wemo(address) From 008e3000bbc9c1d541ad384892e86aaa2f13291a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 29 Aug 2016 22:16:10 +0200 Subject: [PATCH 12/22] Upgrade TwitterAPI to 2.4.2 (#3043) --- 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 5a37a9b9bab..9284c4fac93 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers import validate_config _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['TwitterAPI==2.4.1'] +REQUIREMENTS = ['TwitterAPI==2.4.2'] CONF_CONSUMER_KEY = "consumer_key" CONF_CONSUMER_SECRET = "consumer_secret" diff --git a/requirements_all.txt b/requirements_all.txt index f19f22813f2..77388e08f14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,7 @@ PyMata==2.12 SoCo==0.11.1 # homeassistant.components.notify.twitter -TwitterAPI==2.4.1 +TwitterAPI==2.4.2 # homeassistant.components.http Werkzeug==0.11.10 From c1794d111e1a899c7825a405a872da45f097c942 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 29 Aug 2016 22:16:18 +0200 Subject: [PATCH 13/22] Upgrade sendgrid to 3.2.10 (#3044) --- homeassistant/components/notify/sendgrid.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index 463f5fe0b42..894b35a85d4 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import ( ATTR_TITLE, DOMAIN, BaseNotificationService) from homeassistant.helpers import validate_config -REQUIREMENTS = ['sendgrid==3.1.10'] +REQUIREMENTS = ['sendgrid==3.2.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 77388e08f14..0bb6cd8f8df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ schiene==0.17 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==3.1.10 +sendgrid==3.2.10 # homeassistant.components.notify.slack slacker==0.9.24 From 4e044361c32813c1d6d15a53a6e18f4af3d0fd0b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 30 Aug 2016 00:56:40 +0200 Subject: [PATCH 14/22] Use voluptuous for smtp (#3048) Make note of the breaking change in release notes --- homeassistant/components/notify/smtp.py | 47 ++++++++++++++++--------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 27c74571d40..9ac73a49e3d 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -10,31 +10,46 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService) -from homeassistant.helpers import validate_config + ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_PORT) _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = 'images' # optional embedded image file attachments +CONF_STARTTLS = 'starttls' +CONF_SENDER = 'sender' +CONF_RECIPIENT = 'recipient' +CONF_DEBUG = 'debug' +CONF_SERVER = 'server' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RECIPIENT): cv.string, + vol.Optional(CONF_SERVER, default='localhost'): cv.string, + vol.Optional(CONF_PORT, default=25): cv.port, + vol.Optional(CONF_SENDER): cv.string, + vol.Optional(CONF_STARTTLS, default=False): cv.boolean, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, +}) + def get_service(hass, config): """Get the mail notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['recipient']}, - _LOGGER): - return None - mail_service = MailNotificationService( - config.get('server', 'localhost'), - int(config.get('port', '25')), - config.get('sender', None), - int(config.get('starttls', 0)), - config.get('username', None), - config.get('password', None), - config.get('recipient', None), - config.get('debug', 0)) + config.get(CONF_SERVER), + config.get(CONF_PORT), + config.get(CONF_SENDER), + config.get(CONF_STARTTLS), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + config.get(CONF_RECIPIENT), + config.get(CONF_DEBUG)) if mail_service.connection_is_valid(): return mail_service @@ -65,7 +80,7 @@ class MailNotificationService(BaseNotificationService): mail = smtplib.SMTP(self._server, self._port, timeout=5) mail.set_debuglevel(self.debug) mail.ehlo_or_helo_if_needed() - if self.starttls == 1: + if self.starttls: mail.starttls() mail.ehlo() if self.username and self.password: From 650ec1a337394dbe2f345031c6b15e7e1c2a57c3 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Mon, 29 Aug 2016 19:55:01 -0400 Subject: [PATCH 15/22] Added option to use effect:random for Flux Led light bulbs --- homeassistant/components/light/flux_led.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 13a52fcb1a1..b3aa7e59901 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -7,9 +7,11 @@ https://home-assistant.io/components/light.flux_led/ import logging import socket +import random import voluptuous as vol from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, + ATTR_EFFECT, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) import homeassistant.helpers.config_validation as cv @@ -125,10 +127,15 @@ class FluxLight(Light): rgb = kwargs.get(ATTR_RGB_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) + effect = kwargs.get(ATTR_EFFECT) if rgb: self._bulb.setRgb(*tuple(rgb)) elif brightness: self._bulb.setWarmWhite255(brightness) + elif effect == EFFECT_RANDOM: + self._bulb.setRgb(random.randrange(0, 255), + random.randrange(0, 255), + random.randrange(0, 255)) def turn_off(self, **kwargs): """Turn the specified or all lights off.""" From 55d305359e963da766ead6f1991d9bed4c00bb4f Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 30 Aug 2016 18:22:52 +0200 Subject: [PATCH 16/22] Device tracker component & platform validation. No more home_range. (#2908) * Device tracker component & platform validation. No more home_range. * Mock, bluetooth * Renamed _CONFIG_SCHEMA. Raise warning for #1606 * test duplicates * Fix assert * Coverage * Typing * T fixes --- .../components/device_tracker/__init__.py | 154 +++++++-------- .../device_tracker/bluetooth_le_tracker.py | 2 +- .../device_tracker/bluetooth_tracker.py | 2 +- homeassistant/helpers/typing.py | 25 +-- homeassistant/util/__init__.py | 6 +- tests/common.py | 18 +- tests/components/device_tracker/test_init.py | 182 ++++++++++++------ .../device_tracker/test_locative.py | 10 +- tests/components/device_tracker/test_mqtt.py | 25 +++ .../test_device_sun_light_trigger.py | 10 +- 10 files changed, 259 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 8c52f2147b3..b260eccd7d1 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -10,14 +10,19 @@ from datetime import timedelta import logging import os import threading +from typing import Any, Sequence, Callable -from homeassistant.bootstrap import prepare_setup_platform +import voluptuous as vol + +from homeassistant.bootstrap import ( + prepare_setup_platform, log_exception) from homeassistant.components import group, zone from homeassistant.components.discovery import SERVICE_NETGEAR from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv import homeassistant.util as util import homeassistant.util.dt as dt_util @@ -27,8 +32,7 @@ from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA -DOMAIN = "device_tracker" +DOMAIN = 'device_tracker' DEPENDENCIES = ['zone'] GROUP_NAME_ALL_DEVICES = 'all devices' @@ -38,21 +42,18 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' YAML_DEVICES = 'known_devices.yaml' -CONF_TRACK_NEW = "track_new_devices" -DEFAULT_CONF_TRACK_NEW = True +CONF_TRACK_NEW = 'track_new_devices' +DEFAULT_TRACK_NEW = True CONF_CONSIDER_HOME = 'consider_home' DEFAULT_CONSIDER_HOME = 180 # seconds -CONF_SCAN_INTERVAL = "interval_seconds" +CONF_SCAN_INTERVAL = 'interval_seconds' DEFAULT_SCAN_INTERVAL = 12 CONF_AWAY_HIDE = 'hide_if_away' DEFAULT_AWAY_HIDE = False -CONF_HOME_RANGE = 'home_range' -DEFAULT_HOME_RANGE = 100 - SERVICE_SEE = 'see' ATTR_MAC = 'mac' @@ -62,23 +63,33 @@ ATTR_LOCATION_NAME = 'location_name' ATTR_GPS = 'gps' ATTR_BATTERY = 'battery' +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds +}, extra=vol.ALLOW_EXTRA) + +_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [ + vol.Schema({ + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds + }, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA) + DISCOVERY_PLATFORMS = { SERVICE_NETGEAR: 'netgear', } _LOGGER = logging.getLogger(__name__) -# pylint: disable=too-many-arguments - -def is_on(hass, entity_id=None): +def is_on(hass: HomeAssistantType, entity_id: str=None): """Return the state if any or a specified device is home.""" entity = entity_id or ENTITY_ID_ALL_DEVICES return hass.states.is_state(entity, STATE_HOME) -def see(hass, mac=None, dev_id=None, host_name=None, location_name=None, - gps=None, gps_accuracy=None, battery=None): +def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, + host_name: str=None, location_name: str=None, + gps: GPSType=None, gps_accuracy=None, + battery=None): # pylint: disable=too-many-arguments """Call service to notify you see device.""" data = {key: value for key, value in ((ATTR_MAC, mac), @@ -91,27 +102,24 @@ def see(hass, mac=None, dev_id=None, host_name=None, location_name=None, hass.services.call(DOMAIN, SERVICE_SEE, data) -def setup(hass, config): +def setup(hass: HomeAssistantType, config: ConfigType): """Setup device tracker.""" yaml_path = hass.config.path(YAML_DEVICES) - conf = config.get(DOMAIN, {}) - - # Config can be an empty list. In that case, substitute a dict - if isinstance(conf, list): + try: + conf = _CONFIG_SCHEMA(config).get(DOMAIN, []) + except vol.Invalid as ex: + log_exception(ex, DOMAIN, config) + return False + else: conf = conf[0] if len(conf) > 0 else {} + consider_home = timedelta( + seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)) + track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - consider_home = timedelta( - seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int, - DEFAULT_CONSIDER_HOME)) - track_new = util.convert(conf.get(CONF_TRACK_NEW), bool, - DEFAULT_CONF_TRACK_NEW) - home_range = util.convert(conf.get(CONF_HOME_RANGE), int, - DEFAULT_HOME_RANGE) + devices = load_config(yaml_path, hass, consider_home) - devices = load_config(yaml_path, hass, consider_home, home_range) - tracker = DeviceTracker(hass, consider_home, track_new, home_range, - devices) + tracker = DeviceTracker(hass, consider_home, track_new, devices) def setup_platform(p_type, p_config, disc_info=None): """Setup a device tracker platform.""" @@ -170,30 +178,37 @@ def setup(hass, config): class DeviceTracker(object): """Representation of a device tracker.""" - def __init__(self, hass, consider_home, track_new, home_range, devices): + def __init__(self, hass: HomeAssistantType, consider_home: timedelta, + track_new: bool, devices: Sequence) -> None: """Initialize a device tracker.""" self.hass = hass self.devices = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} + for dev in devices: + if self.devices[dev.dev_id] is not dev: + _LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id) + if dev.mac and self.mac_to_dev[dev.mac] is not dev: + _LOGGER.warning('Duplicate device MAC addresses detected %s', + dev.mac) self.consider_home = consider_home self.track_new = track_new - self.home_range = home_range self.lock = threading.Lock() for device in devices: if device.track: device.update_ha_state() - self.group = None + self.group = None # type: group.Group - def see(self, mac=None, dev_id=None, host_name=None, location_name=None, - gps=None, gps_accuracy=None, battery=None): + def see(self, mac: str=None, dev_id: str=None, host_name: str=None, + location_name: str=None, gps: GPSType=None, gps_accuracy=None, + battery: str=None): """Notify the device tracker that you see a device.""" with self.lock: if mac is None and dev_id is None: raise HomeAssistantError('Neither mac or device id passed in') elif mac is not None: - mac = mac.upper() + mac = str(mac).upper() device = self.mac_to_dev.get(mac) if not device: dev_id = util.slugify(host_name or '') or util.slugify(mac) @@ -211,7 +226,7 @@ class DeviceTracker(object): # If no device can be found, create it dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) device = Device( - self.hass, self.consider_home, self.home_range, self.track_new, + self.hass, self.consider_home, self.track_new, dev_id, mac, (host_name or dev_id).replace('_', ' ')) self.devices[dev_id] = device if mac is not None: @@ -234,7 +249,7 @@ class DeviceTracker(object): self.group = group.Group( self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) - def update_stale(self, now): + def update_stale(self, now: dt_util.dt.datetime): """Update stale devices.""" with self.lock: for device in self.devices.values(): @@ -246,19 +261,21 @@ class DeviceTracker(object): class Device(Entity): """Represent a tracked device.""" - host_name = None - location_name = None - gps = None + host_name = None # type: str + location_name = None # type: str + gps = None # type: GPSType gps_accuracy = 0 - last_seen = None - battery = None + last_seen = None # type: dt_util.dt.datetime + battery = None # type: str # Track if the last update of this device was HOME. last_update_home = False _state = STATE_NOT_HOME - def __init__(self, hass, consider_home, home_range, track, dev_id, mac, - name=None, picture=None, gravatar=None, away_hide=False): + def __init__(self, hass: HomeAssistantType, consider_home: timedelta, + track: bool, dev_id: str, mac: str, name: str=None, + picture: str=None, gravatar: str=None, + away_hide: bool=False) -> None: """Initialize a device.""" self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -267,8 +284,6 @@ class Device(Entity): # detected anymore. self.consider_home = consider_home - # Distance in meters - self.home_range = home_range # Device ID self.dev_id = dev_id self.mac = mac @@ -287,13 +302,6 @@ class Device(Entity): self.away_hide = away_hide - @property - def gps_home(self): - """Return if device is within range of home.""" - distance = max( - 0, self.hass.config.distance(*self.gps) - self.gps_accuracy) - return self.gps is not None and distance <= self.home_range - @property def name(self): """Return the name of the entity.""" @@ -329,26 +337,24 @@ class Device(Entity): """If device should be hidden.""" return self.away_hide and self.state != STATE_HOME - def seen(self, host_name=None, location_name=None, gps=None, - gps_accuracy=0, battery=None): + def seen(self, host_name: str=None, location_name: str=None, + gps: GPSType=None, gps_accuracy=0, battery: str=None): """Mark the device as seen.""" self.last_seen = dt_util.utcnow() self.host_name = host_name self.location_name = location_name self.gps_accuracy = gps_accuracy or 0 self.battery = battery - if gps is None: - self.gps = None - else: + self.gps = None + if gps is not None: try: - self.gps = tuple(float(val) for val in gps) - except ValueError: + self.gps = float(gps[0]), float(gps[1]) + except (ValueError, TypeError, IndexError): _LOGGER.warning('Could not parse gps value for %s: %s', self.dev_id, gps) - self.gps = None self.update() - def stale(self, now=None): + def stale(self, now: dt_util.dt.datetime=None): """Return if device state is stale.""" return self.last_seen and \ (now or dt_util.utcnow()) - self.last_seen > self.consider_home @@ -377,32 +383,30 @@ class Device(Entity): self.last_update_home = True -def load_config(path, hass, consider_home, home_range): +def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): """Load devices from YAML configuration file.""" - if not os.path.isfile(path): - return [] try: return [ - Device(hass, consider_home, home_range, device.get('track', False), + Device(hass, consider_home, device.get('track', False), str(dev_id).lower(), str(device.get('mac')).upper(), device.get('name'), device.get('picture'), device.get('gravatar'), device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) for dev_id, device in load_yaml_config_file(path).items()] - except HomeAssistantError: + except (HomeAssistantError, FileNotFoundError): # When YAML file could not be loaded/did not contain a dict return [] -def setup_scanner_platform(hass, config, scanner, see_device): +def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, + scanner: Any, see_device: Callable): """Helper method to connect scanner-based platform to device tracker.""" - interval = util.convert(config.get(CONF_SCAN_INTERVAL), int, - DEFAULT_SCAN_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) # Initial scan of each mac we also tell about host name for config - seen = set() + seen = set() # type: Any - def device_tracker_scan(now): + def device_tracker_scan(now: dt_util.dt.datetime): """Called when interval matches.""" for mac in scanner.scan_devices(): if mac in seen: @@ -418,7 +422,7 @@ def setup_scanner_platform(hass, config, scanner, see_device): device_tracker_scan(None) -def update_config(path, dev_id, device): +def update_config(path: str, dev_id: str, device: Device): """Add device to YAML configuration file.""" with open(path, 'a') as out: out.write('\n') @@ -432,8 +436,8 @@ def update_config(path, dev_id, device): out.write(' {}: {}\n'.format(key, '' if value is None else value)) -def get_gravatar_for_email(email): +def get_gravatar_for_email(email: str): """Return an 80px Gravatar for the given email address.""" import hashlib - url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar" + url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar' return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest()) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index 7784a2326d8..6576f46bad7 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -63,7 +63,7 @@ def setup_scanner(hass, config, see): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in load_config(yaml_path, hass, 0, 0): + for device in load_config(yaml_path, hass, 0): # check if device is a valid bluetooth device if device.mac and device.mac[:3].upper() == BLE_PREFIX: if device.track: diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 70fefbca1b7..298eddc4bc4 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -45,7 +45,7 @@ def setup_scanner(hass, config, see): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in load_config(yaml_path, hass, 0, 0): + for device in load_config(yaml_path, hass, 0): # check if device is a valid bluetooth device if device.mac and device.mac[:3].upper() == BT_PREFIX: if device.track: diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 6eb53f14493..24774ac29da 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,24 +1,13 @@ """Typing Helpers for Home-Assistant.""" -from typing import Dict, Any +from typing import Dict, Any, Tuple -# NOTE: NewType added to typing in 3.5.2 in June, 2016; Since 3.5.2 includes -# security fixes everyone on 3.5 should upgrade "soon" -try: - from typing import NewType -except ImportError: - NewType = None +import homeassistant.core # pylint: disable=invalid-name -if NewType: - ConfigType = NewType('ConfigType', Dict[str, Any]) - # Custom type for recorder Queries - QueryType = NewType('QueryType', Any) +GPSType = Tuple[float, float] +ConfigType = Dict[str, Any] +HomeAssistantType = homeassistant.core.HomeAssistant -# Duplicates for 3.5.1 -# pylint: disable=invalid-name -else: - ConfigType = Dict[str, Any] # type: ignore - - # Custom type for recorder Queries - QueryType = Any # type: ignore +# Custom type for recorder Queries +QueryType = Any diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 032588f6cba..c5df3834e72 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -12,7 +12,7 @@ import string from functools import wraps from types import MappingProxyType -from typing import Any, Optional, TypeVar, Callable, Sequence +from typing import Any, Optional, TypeVar, Callable, Sequence, KeysView, Union from .dt import as_local, utcnow @@ -63,8 +63,8 @@ def convert(value: T, to_type: Callable[[T], U], return default -def ensure_unique_string(preferred_string: str, - current_strings: Sequence[str]) -> str: +def ensure_unique_string(preferred_string: str, current_strings: + Union[Sequence[str], KeysView[str]]) -> str: """Return a string that is not present in current_strings. If preferred string exists will append _2, _3, .. diff --git a/tests/common.py b/tests/common.py index 5d1f485d7fe..e51e4ba048a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -7,7 +7,7 @@ from io import StringIO import logging from homeassistant import core as ha, loader -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.helpers.entity import ToggleEntity from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util @@ -137,15 +137,15 @@ def mock_http_component(hass): hass.config.components.append('http') -@mock.patch('homeassistant.components.mqtt.MQTT') -def mock_mqtt_component(hass, mock_mqtt): +def mock_mqtt_component(hass): """Mock the MQTT component.""" - _setup_component(hass, mqtt.DOMAIN, { - mqtt.DOMAIN: { - mqtt.CONF_BROKER: 'mock-broker', - } - }) - return mock_mqtt + with mock.patch('homeassistant.components.mqtt.MQTT') as mock_mqtt: + setup_component(hass, mqtt.DOMAIN, { + mqtt.DOMAIN: { + mqtt.CONF_BROKER: 'mock-broker', + } + }) + return mock_mqtt class MockModule(object): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index c6721b8c8cc..7353cbae0d8 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,10 +1,10 @@ """The tests for the device tracker component.""" # pylint: disable=protected-access,too-many-public-methods +import logging import unittest from unittest.mock import patch from datetime import datetime, timedelta import os -import tempfile from homeassistant.loader import get_component import homeassistant.util.dt as dt_util @@ -12,13 +12,21 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM) import homeassistant.components.device_tracker as device_tracker +from homeassistant.exceptions import HomeAssistantError from tests.common import ( - get_test_home_assistant, fire_time_changed, fire_service_discovered) + get_test_home_assistant, fire_time_changed, fire_service_discovered, + patch_yaml_files) + +TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}} + +_LOGGER = logging.getLogger(__name__) class TestComponentsDeviceTracker(unittest.TestCase): """Test the Device tracker.""" + hass = None # HomeAssistant + yaml_devices = None # type: str def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" @@ -48,27 +56,28 @@ class TestComponentsDeviceTracker(unittest.TestCase): def test_reading_broken_yaml_config(self): # pylint: disable=no-self-use """Test when known devices contains invalid data.""" - with tempfile.NamedTemporaryFile() as fpt: - # file is empty - assert device_tracker.load_config(fpt.name, None, False, 0) == [] - - fpt.write('100'.encode('utf-8')) - fpt.flush() - - # file contains a non-dict format - assert device_tracker.load_config(fpt.name, None, False, 0) == [] + files = {'empty.yaml': '', + 'bad.yaml': '100', + 'ok.yaml': 'my_device:\n name: Device'} + with patch_yaml_files(files): + # File is empty + assert device_tracker.load_config('empty.yaml', None, False) == [] + # File contains a non-dict format + assert device_tracker.load_config('bad.yaml', None, False) == [] + # A file that works fine + assert len(device_tracker.load_config('ok.yaml', None, False)) == 1 def test_reading_yaml_config(self): """Test the rendering of the YAML configuration.""" dev_id = 'test' device = device_tracker.Device( - self.hass, timedelta(seconds=180), 0, True, dev_id, + self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture', away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) config = device_tracker.load_config(self.yaml_devices, self.hass, - device.consider_home, 0)[0] + device.consider_home)[0] self.assertEqual(device.dev_id, config.dev_id) self.assertEqual(device.track, config.track) self.assertEqual(device.mac, config.mac) @@ -76,12 +85,45 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.away_hide, config.away_hide) self.assertEqual(device.consider_home, config.consider_home) + @patch('homeassistant.components.device_tracker._LOGGER.warning') + def test_track_with_duplicate_mac_dev_id(self, mock_warning): \ + # pylint: disable=invalid-name + """Test adding duplicate MACs or device IDs to DeviceTracker.""" + + devices = [ + device_tracker.Device(self.hass, True, True, 'my_device', 'AB:01', + 'My device', None, None, False), + device_tracker.Device(self.hass, True, True, 'your_device', + 'AB:01', 'Your device', None, None, False)] + device_tracker.DeviceTracker(self.hass, False, True, devices) + _LOGGER.debug(mock_warning.call_args_list) + assert mock_warning.call_count == 1, \ + "The only warning call should be duplicates (check DEBUG)" + args, _ = mock_warning.call_args + assert 'Duplicate device MAC' in args[0], \ + 'Duplicate MAC warning expected' + + mock_warning.reset_mock() + devices = [ + device_tracker.Device(self.hass, True, True, 'my_device', + 'AB:01', 'My device', None, None, False), + device_tracker.Device(self.hass, True, True, 'my_device', + None, 'Your device', None, None, False)] + device_tracker.DeviceTracker(self.hass, False, True, devices) + + _LOGGER.debug(mock_warning.call_args_list) + assert mock_warning.call_count == 1, \ + "The only warning call should be duplicates (check DEBUG)" + args, _ = mock_warning.call_args + assert 'Duplicate device IDs' in args[0], \ + 'Duplicate device IDs warning expected' + def test_setup_without_yaml_file(self): """Test with no YAML file.""" - self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) - # pylint: disable=invalid-name - def test_adding_unknown_device_to_config(self): + def test_adding_unknown_device_to_config(self): \ + # pylint: disable=invalid-name """Test the adding of unknown devices to configuration file.""" scanner = get_component('device_tracker.test').SCANNER scanner.reset() @@ -90,7 +132,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertTrue(device_tracker.setup(self.hass, { device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) config = device_tracker.load_config(self.yaml_devices, self.hass, - timedelta(seconds=0), 0) + timedelta(seconds=0)) assert len(config) == 1 assert config[0].dev_id == 'dev1' assert config[0].track @@ -99,7 +141,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): """Test the Gravatar generation.""" dev_id = 'test' device = device_tracker.Device( - self.hass, timedelta(seconds=180), 0, True, dev_id, + self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', 'Test name', gravatar='test@example.com') gravatar_url = ("https://www.gravatar.com/avatar/" "55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar") @@ -109,7 +151,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): """Test that Gravatar overrides picture.""" dev_id = 'test' device = device_tracker.Device( - self.hass, timedelta(seconds=180), 0, True, dev_id, + self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture', gravatar='test@example.com') gravatar_url = ("https://www.gravatar.com/avatar/" @@ -122,8 +164,8 @@ class TestComponentsDeviceTracker(unittest.TestCase): with patch.dict(device_tracker.DISCOVERY_PLATFORMS, {'test': 'test'}): with patch.object(scanner, 'scan_devices') as mock_scan: - self.assertTrue(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + self.assertTrue(device_tracker.setup(self.hass, + TEST_PLATFORM)) fire_service_discovered(self.hass, 'test', {}) self.assertTrue(mock_scan.called) @@ -139,9 +181,9 @@ class TestComponentsDeviceTracker(unittest.TestCase): with patch('homeassistant.components.device_tracker.dt_util.utcnow', return_value=register_time): self.assertTrue(device_tracker.setup(self.hass, { - 'device_tracker': { - 'platform': 'test', - 'consider_home': 59, + device_tracker.DOMAIN: { + CONF_PLATFORM: 'test', + device_tracker.CONF_CONSIDER_HOME: 59, }})) self.assertEqual(STATE_HOME, @@ -165,11 +207,11 @@ class TestComponentsDeviceTracker(unittest.TestCase): picture = 'http://placehold.it/200x200' device = device_tracker.Device( - self.hass, timedelta(seconds=180), 0, True, dev_id, None, + self.hass, timedelta(seconds=180), True, dev_id, None, friendly_name, picture, away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) attrs = self.hass.states.get(entity_id).attributes @@ -181,15 +223,14 @@ class TestComponentsDeviceTracker(unittest.TestCase): dev_id = 'test_entity' entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) device = device_tracker.Device( - self.hass, timedelta(seconds=180), 0, True, dev_id, None, + self.hass, timedelta(seconds=180), True, dev_id, None, away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device) scanner = get_component('device_tracker.test').SCANNER scanner.reset() - self.assertTrue(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) self.assertTrue(self.hass.states.get(entity_id) .attributes.get(ATTR_HIDDEN)) @@ -199,15 +240,14 @@ class TestComponentsDeviceTracker(unittest.TestCase): dev_id = 'test_entity' entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) device = device_tracker.Device( - self.hass, timedelta(seconds=180), 0, True, dev_id, None, + self.hass, timedelta(seconds=180), True, dev_id, None, away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device) scanner = get_component('device_tracker.test').SCANNER scanner.reset() - self.assertTrue(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) state = self.hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES) self.assertIsNotNone(state) @@ -217,40 +257,31 @@ class TestComponentsDeviceTracker(unittest.TestCase): @patch('homeassistant.components.device_tracker.DeviceTracker.see') def test_see_service(self, mock_see): - """Test the see service.""" - self.assertTrue(device_tracker.setup(self.hass, {})) - mac = 'AB:CD:EF:GH' - dev_id = 'some_device' - host_name = 'example.com' - location_name = 'Work' - gps = [.3, .8] - - device_tracker.see(self.hass, mac, dev_id, host_name, location_name, - gps) - - self.hass.pool.block_till_done() - - mock_see.assert_called_once_with( - mac=mac, dev_id=dev_id, host_name=host_name, - location_name=location_name, gps=gps) - - @patch('homeassistant.components.device_tracker.DeviceTracker.see') - def test_see_service_unicode_dev_id(self, mock_see): """Test the see service with a unicode dev_id and NO MAC.""" - self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) params = { - 'dev_id': chr(233), # e' acute accent from icloud + 'dev_id': 'some_device', 'host_name': 'example.com', 'location_name': 'Work', 'gps': [.3, .8] } device_tracker.see(self.hass, **params) self.hass.pool.block_till_done() + assert mock_see.call_count == 1 mock_see.assert_called_once_with(**params) - def test_not_write_duplicate_yaml_keys(self): + mock_see.reset_mock() + params['dev_id'] += chr(233) # e' acute accent from icloud + + device_tracker.see(self.hass, **params) + self.hass.pool.block_till_done() + assert mock_see.call_count == 1 + mock_see.assert_called_once_with(**params) + + def test_not_write_duplicate_yaml_keys(self): \ + # pylint: disable=invalid-name """Test that the device tracker will not generate invalid YAML.""" - self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) device_tracker.see(self.hass, 'mac_1', host_name='hello') device_tracker.see(self.hass, 'mac_2', host_name='hello') @@ -258,15 +289,46 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.hass.pool.block_till_done() config = device_tracker.load_config(self.yaml_devices, self.hass, - timedelta(seconds=0), 0) + timedelta(seconds=0)) assert len(config) == 2 - def test_not_allow_invalid_dev_id(self): + def test_not_allow_invalid_dev_id(self): # pylint: disable=invalid-name """Test that the device tracker will not allow invalid dev ids.""" - self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM)) device_tracker.see(self.hass, dev_id='hello-world') config = device_tracker.load_config(self.yaml_devices, self.hass, - timedelta(seconds=0), 0) + timedelta(seconds=0)) assert len(config) == 0 + + @patch('homeassistant.components.device_tracker._LOGGER.warning') + def test_see_failures(self, mock_warning): + """Test that the device tracker see failures.""" + tracker = device_tracker.DeviceTracker( + self.hass, timedelta(seconds=60), 0, []) + + # MAC is not a string (but added) + tracker.see(mac=567, host_name="Number MAC") + + # No device id or MAC(not added) + with self.assertRaises(HomeAssistantError): + tracker.see() + assert mock_warning.call_count == 0 + + # Ignore gps on invalid GPS (both added & warnings) + tracker.see(mac='mac_1_bad_gps', gps=1) + tracker.see(mac='mac_2_bad_gps', gps=[1]) + tracker.see(mac='mac_3_bad_gps', gps='gps') + config = device_tracker.load_config(self.yaml_devices, self.hass, + timedelta(seconds=0)) + assert mock_warning.call_count == 3 + + assert len(config) == 4 + + @patch('homeassistant.components.device_tracker.log_exception') + def test_config_failure(self, mock_ex): + """Test that the device tracker see failures.""" + device_tracker.setup(self.hass, {device_tracker.DOMAIN: { + device_tracker.CONF_CONSIDER_HOME: -1}}) + assert mock_ex.call_count == 1 diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 427980be5f1..7c018eaa69a 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -8,17 +8,19 @@ import requests from homeassistant import bootstrap, const import homeassistant.components.device_tracker as device_tracker import homeassistant.components.http as http +from homeassistant.const import CONF_PLATFORM from tests.common import get_test_home_assistant, get_test_instance_port SERVER_PORT = get_test_instance_port() HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) -hass = None +hass = None # pylint: disable=invalid-name -def _url(data={}): +def _url(data=None): """Helper method to generate URLs.""" + data = data or {} data = "&".join(["{}={}".format(name, value) for name, value in data.items()]) return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) @@ -26,7 +28,7 @@ def _url(data={}): def setUpModule(): # pylint: disable=invalid-name """Initalize a Home Assistant server.""" - global hass + global hass # pylint: disable=invalid-name hass = get_test_home_assistant() bootstrap.setup_component(hass, http.DOMAIN, { @@ -38,7 +40,7 @@ def setUpModule(): # pylint: disable=invalid-name # Set up device tracker bootstrap.setup_component(hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { - 'platform': 'locative' + CONF_PLATFORM: 'locative' } }) diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index 139316a35bf..321ab16ac3f 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -1,5 +1,7 @@ """The tests for the MQTT device tracker platform.""" import unittest +from unittest.mock import patch +import logging import os from homeassistant.bootstrap import _setup_component @@ -9,6 +11,8 @@ from homeassistant.const import CONF_PLATFORM from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) +_LOGGER = logging.getLogger(__name__) + class TestComponentsDeviceTrackerMQTT(unittest.TestCase): """Test MQTT device tracker platform.""" @@ -25,6 +29,27 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): except FileNotFoundError: pass + def test_ensure_device_tracker_platform_validation(self): \ + # pylint: disable=invalid-name + """Test if platform validation was done.""" + def mock_setup_scanner(hass, config, see): + """Check that Qos was added by validation.""" + self.assertTrue('qos' in config) + + with patch('homeassistant.components.device_tracker.mqtt.' + 'setup_scanner', side_effect=mock_setup_scanner) as mock_sp: + + dev_id = 'paulus' + topic = '/location/paulus' + self.hass.config.components = ['mqtt', 'zone'] + assert _setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: topic} + } + }) + assert mock_sp.call_count == 1 + def test_new_message(self): """Test new message.""" dev_id = 'paulus' diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index ff134465174..88c0bae60ec 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -22,12 +22,12 @@ def setUpModule(): # pylint: disable=invalid-name """Write a device tracker known devices file to be used.""" device_tracker.update_config( KNOWN_DEV_YAML_PATH, 'device_1', device_tracker.Device( - None, None, None, True, 'device_1', 'DEV1', + None, None, True, 'device_1', 'DEV1', picture='http://example.com/dev1.jpg')) device_tracker.update_config( KNOWN_DEV_YAML_PATH, 'device_2', device_tracker.Device( - None, None, None, True, 'device_2', 'DEV2', + None, None, True, 'device_2', 'DEV2', picture='http://example.com/dev2.jpg')) @@ -83,7 +83,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.assertTrue(light.is_on(self.hass)) - def test_lights_turn_off_when_everyone_leaves(self): + def test_lights_turn_off_when_everyone_leaves(self): \ + # pylint: disable=invalid-name """Test lights turn off when everyone leaves the house.""" light.turn_on(self.hass) @@ -99,7 +100,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.assertFalse(light.is_on(self.hass)) - def test_lights_turn_on_when_coming_home_after_sun_set(self): + def test_lights_turn_on_when_coming_home_after_sun_set(self): \ + # pylint: disable=invalid-name """Test lights turn on when coming home after sun set.""" light.turn_off(self.hass) ensure_sun_set(self.hass) From cf9b49ac03dc1629e87d042db60c4073d8d75371 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 30 Aug 2016 20:58:37 +0200 Subject: [PATCH 17/22] update ha-ffmpeg version to 0.9 (#3059) --- homeassistant/components/binary_sensor/ffmpeg.py | 2 +- homeassistant/components/camera/ffmpeg.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index be4d595dcb9..e02a560ec54 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -16,7 +16,7 @@ from homeassistant.config import load_yaml_config_file from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME, ATTR_ENTITY_ID) -REQUIREMENTS = ["ha-ffmpeg==0.8"] +REQUIREMENTS = ["ha-ffmpeg==0.9"] SERVICE_RESTART = 'ffmpeg_restart' diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index f87b3074c1c..23d6874cd81 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -14,7 +14,7 @@ from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME -REQUIREMENTS = ['ha-ffmpeg==0.8'] +REQUIREMENTS = ['ha-ffmpeg==0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0bb6cd8f8df..934dec82bed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -104,7 +104,7 @@ gps3==0.33.2 # homeassistant.components.binary_sensor.ffmpeg # homeassistant.components.camera.ffmpeg -ha-ffmpeg==0.8 +ha-ffmpeg==0.9 # homeassistant.components.mqtt.server hbmqtt==0.7.1 From 7ceb22a08b53a1dfe4fb870592dbc5c432632b70 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Tue, 30 Aug 2016 21:04:53 +0200 Subject: [PATCH 18/22] Ecobee (#3055) * Added in list for opreation * Added attribute to reflect the old operation * fix humidity --- homeassistant/components/climate/ecobee.py | 40 ++++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 5c65a7d0b23..da4b29dfe92 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -80,6 +80,8 @@ class Thermostat(ClimateDevice): self.thermostat_index) self._name = self.thermostat['name'] self.hold_temp = hold_temp + self._operation_list = ['auto', 'auxHeatOnly', 'cool', + 'heat', 'off'] def update(self): """Get the latest state from the thermostat.""" @@ -124,11 +126,6 @@ class Thermostat(ClimateDevice): """Return the upper bound temperature we try to reach.""" return int(self.thermostat['runtime']['desiredCool'] / 10) - @property - def current_humidity(self): - """Return the current humidity.""" - return self.thermostat['runtime']['actualHumidity'] - @property def desired_fan_mode(self): """Return the desired fan mode of operation.""" @@ -147,20 +144,15 @@ class Thermostat(ClimateDevice): """Return current operation.""" return self.operation_mode + @property + def operation_list(self): + """Return the operation modes list.""" + return self._operation_list + @property def operation_mode(self): """Return current operation ie. heat, cool, idle.""" - status = self.thermostat['equipmentStatus'] - if status == '': - return STATE_IDLE - elif 'Cool' in status: - return STATE_COOL - elif 'auxHeat' in status: - return STATE_HEAT - elif 'heatPump' in status: - return STATE_HEAT - else: - return status + return self.thermostat['settings']['hvacMode'] @property def mode(self): @@ -176,11 +168,23 @@ class Thermostat(ClimateDevice): def device_state_attributes(self): """Return device specific state attributes.""" # Move these to Thermostat Device and make them global + status = self.thermostat['equipmentStatus'] + operation = None + if status == '': + operation = STATE_IDLE + elif 'Cool' in status: + operation = STATE_COOL + elif 'auxHeat' in status: + operation = STATE_HEAT + elif 'heatPump' in status: + operation = STATE_HEAT + else: + operation = status return { - "humidity": self.current_humidity, + "humidity": self.thermostat['runtime']['actualHumidity'], "fan": self.fan, "mode": self.mode, - "hvac_mode": self.thermostat['settings']['hvacMode'], + "operation": operation, "fan_min_on_time": self.fan_min_on_time } From eec96ea13738fed744f4144ee28e77be0bc49185 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 30 Aug 2016 21:34:33 +0200 Subject: [PATCH 19/22] Migrate to voluptuous (#2954) --- homeassistant/components/apcupsd.py | 36 +++++++++++-------- .../components/binary_sensor/apcupsd.py | 23 ++++++++---- homeassistant/components/sensor/apcupsd.py | 17 ++++++--- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index fd064075458..867208305b0 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -7,35 +7,43 @@ https://home-assistant.io/components/apcupsd/ import logging from datetime import timedelta +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -DOMAIN = "apcupsd" -REQUIREMENTS = ("apcaccess==0.0.4",) +REQUIREMENTS = ['apcaccess==0.0.4'] -CONF_HOST = "host" -CONF_PORT = "port" -CONF_TYPE = "type" +_LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = "localhost" +CONF_TYPE = 'type' + +DATA = None +DEFAULT_HOST = 'localhost' DEFAULT_PORT = 3551 +DOMAIN = 'apcupsd' -KEY_STATUS = "STATUS" - -VALUE_ONLINE = "ONLINE" +KEY_STATUS = 'STATUS' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -DATA = None +VALUE_ONLINE = 'ONLINE' -_LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Use config values to set up a function enabling status retrieval.""" global DATA - - host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST) - port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT) + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) DATA = APCUPSdData(host, port) diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/binary_sensor/apcupsd.py index 0c3fed960ea..05d0749b9ef 100644 --- a/homeassistant/components/binary_sensor/apcupsd.py +++ b/homeassistant/components/binary_sensor/apcupsd.py @@ -4,23 +4,32 @@ Support for tracking the online status of a UPS. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.apcupsd/ """ -from homeassistant.components import apcupsd -from homeassistant.components.binary_sensor import BinarySensorDevice +import voluptuous as vol +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.components import apcupsd + +DEFAULT_NAME = 'UPS Online Status' DEPENDENCIES = [apcupsd.DOMAIN] -DEFAULT_NAME = "UPS Online Status" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) def setup_platform(hass, config, add_entities, discovery_info=None): - """Instantiate an OnlineStatus binary sensor entity.""" + """Setup an Online Status binary sensor.""" add_entities((OnlineStatus(config, apcupsd.DATA),)) class OnlineStatus(BinarySensorDevice): - """Represent UPS online status.""" + """Representation of an UPS online status.""" def __init__(self, config, data): - """Initialize the APCUPSd device.""" + """Initialize the APCUPSd binary device.""" self._config = config self._data = data self._state = None @@ -29,7 +38,7 @@ class OnlineStatus(BinarySensorDevice): @property def name(self): """Return the name of the UPS online status sensor.""" - return self._config.get("name", DEFAULT_NAME) + return self._config.get(CONF_NAME) @property def is_on(self): diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/sensor/apcupsd.py index 4ae82cba602..8c2cf22655d 100644 --- a/homeassistant/components/sensor/apcupsd.py +++ b/homeassistant/components/sensor/apcupsd.py @@ -6,10 +6,16 @@ https://home-assistant.io/components/sensor.apcupsd/ """ import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.components import apcupsd -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES) from homeassistant.helpers.entity import Entity +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = [apcupsd.DOMAIN] SENSOR_PREFIX = 'UPS ' @@ -92,14 +98,17 @@ INFERRED_UNITS = { ' C': TEMP_CELSIUS, } -_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RESOURCES, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) def setup_platform(hass, config, add_entities, discovery_info=None): """Setup the APCUPSd sensors.""" entities = [] - for resource in config['resources']: + for resource in config[CONF_RESOURCES]: sensor_type = resource.lower() if sensor_type not in SENSOR_TYPES: @@ -109,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor_type.upper() not in apcupsd.DATA.status: _LOGGER.warning( 'Sensor type: "%s" does not appear in the APCUPSd status ' - 'output.', sensor_type) + 'output', sensor_type) entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type)) From 9a4447ca13a7b3b947b95468072319c38e1e83c1 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 30 Aug 2016 13:37:47 -0700 Subject: [PATCH 20/22] 0.28.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ce46c62850b..73aef3e2c22 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = '0.28.0.dev0' +__version__ = '0.28.1' REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' From d907902af8e02a763d78c84336af6250b1720db4 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 30 Aug 2016 13:44:35 -0700 Subject: [PATCH 21/22] 0.27.1 NOT 0.28.1, thanks for the catch @arsaboo --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73aef3e2c22..a720c989907 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = '0.28.1' +__version__ = '0.27.1' REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' From e9354bb1e83345bfe00aa78057716af10bae0f3e Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 30 Aug 2016 13:58:53 -0700 Subject: [PATCH 22/22] Make pep8 happy --- tests/components/device_tracker/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 7353cbae0d8..4aef11d3a36 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) class TestComponentsDeviceTracker(unittest.TestCase): """Test the Device tracker.""" + hass = None # HomeAssistant yaml_devices = None # type: str @@ -89,7 +90,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): def test_track_with_duplicate_mac_dev_id(self, mock_warning): \ # pylint: disable=invalid-name """Test adding duplicate MACs or device IDs to DeviceTracker.""" - devices = [ device_tracker.Device(self.hass, True, True, 'my_device', 'AB:01', 'My device', None, None, False),