diff --git a/.coveragerc b/.coveragerc index c74de0026b1..2d1bff462b9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,9 +11,15 @@ omit = homeassistant/components/alarmdecoder.py homeassistant/components/*/alarmdecoder.py + homeassistant/components/amcrest.py + homeassistant/components/*/amcrest.py + homeassistant/components/apcupsd.py homeassistant/components/*/apcupsd.py + homeassistant/components/apple_tv.py + homeassistant/components/*/apple_tv.py + homeassistant/components/arduino.py homeassistant/components/*/arduino.py @@ -89,6 +95,9 @@ omit = homeassistant/components/knx.py homeassistant/components/*/knx.py + homeassistant/components/lametric.py + homeassistant/components/*/lametric.py + homeassistant/components/lutron.py homeassistant/components/*/lutron.py @@ -164,6 +173,9 @@ omit = homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py + homeassistant/components/velux.py + homeassistant/components/*/velux.py + homeassistant/components/vera.py homeassistant/components/*/vera.py @@ -211,7 +223,6 @@ omit = homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/browser.py - homeassistant/components/camera/amcrest.py homeassistant/components/camera/bloomsky.py homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py @@ -301,8 +312,8 @@ omit = homeassistant/components/lock/nuki.py homeassistant/components/lock/lockitron.py homeassistant/components/lock/sesame.py + homeassistant/components/media_extractor.py homeassistant/components/media_player/anthemav.py - homeassistant/components/media_player/apple_tv.py homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py @@ -339,6 +350,7 @@ omit = homeassistant/components/media_player/sonos.py homeassistant/components/media_player/spotify.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/vizio.py homeassistant/components/media_player/vlc.py homeassistant/components/media_player/volumio.py homeassistant/components/media_player/yamaha.py @@ -375,11 +387,11 @@ omit = homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py homeassistant/components/nuimo_controller.py + homeassistant/components/prometheus.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py - homeassistant/components/sensor/amcrest.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py homeassistant/components/sensor/bbox.py @@ -390,7 +402,7 @@ omit = homeassistant/components/sensor/bom.py homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/buienradar.py - homeassistant/components/sensor/dublin_bus_transport.py + homeassistant/components/sensor/citybikes.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cert_expiry.py homeassistant/components/sensor/comed_hourly_pricing.py @@ -404,6 +416,7 @@ omit = homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dte_energy_bridge.py + homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/ebox.py homeassistant/components/sensor/eddystone_temperature.py homeassistant/components/sensor/eliqonline.py @@ -450,6 +463,7 @@ omit = homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/opensky.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/otp.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py @@ -511,6 +525,7 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py + homeassistant/components/switch/xiaomi_vacuum.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py homeassistant/components/tts/amazon_polly.py diff --git a/.travis.yml b/.travis.yml index 0bdde06bb35..fdc5650db22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ matrix: env: TOXENV=py35 - python: "3.6" env: TOXENV=py36 - - python: "3.6-dev" - env: TOXENV=py36 + # - python: "3.6-dev" + # env: TOXENV=py36 - python: "3.4.2" env: TOXENV=requirements # allow_failures: diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..3c975ca3862 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,41 @@ +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# https://github.com/blog/2392-introducing-code-owners + +setup.py @home-assistant/core +homeassistant/*.py @home-assistant/core +homeassistant/helpers/* @home-assistant/core +homeassistant/util/* @home-assistant/core +homeassistant/components/api.py @home-assistant/core +homeassistant/components/automation/* @home-assistant/core +homeassistant/components/configurator.py @home-assistant/core +homeassistant/components/group.py @home-assistant/core +homeassistant/components/history.py @home-assistant/core +homeassistant/components/http/* @home-assistant/core +homeassistant/components/input_*.py @home-assistant/core +homeassistant/components/introduction.py @home-assistant/core +homeassistant/components/logger.py @home-assistant/core +homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/panel_custom.py @home-assistant/core +homeassistant/components/panel_iframe.py @home-assistant/core +homeassistant/components/persistent_notification.py @home-assistant/core +homeassistant/components/scene/__init__.py @home-assistant/core +homeassistant/components/scene/hass.py @home-assistant/core +homeassistant/components/script.py @home-assistant/core +homeassistant/components/shell_command.py @home-assistant/core +homeassistant/components/sun.py @home-assistant/core +homeassistant/components/updater.py @home-assistant/core +homeassistant/components/weblink.py @home-assistant/core +homeassistant/components/websocket_api.py @home-assistant/core +homeassistant/components/zone.py @home-assistant/core + +Dockerfile @home-assistant/docker +virtualization/Docker/* @home-assistant/docker + +homeassistant/components/zwave/* @home-assistant/z-wave +homeassistant/components/*/zwave.py @home-assistant/z-wave + +# Indiviudal components +homeassistant/components/cover/template.py @PhracturedBlue +homeassistant/components/device_tracker/automatic.py @armills +homeassistant/components/media_player/kodi.py @armills diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 75aaeaa1fd1..2ce574ca15e 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -229,8 +229,8 @@ def cmdline() -> List[str]: os.environ['PYTHONPATH'] = os.path.dirname(modulepath) return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon'] - else: - return [arg for arg in sys.argv if arg != '--daemon'] + + return [arg for arg in sys.argv if arg != '--daemon'] def setup_and_run_hass(config_dir: str, diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index c372004e310..ecbb8036464 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -39,19 +39,19 @@ def is_on(hass, entity_id=None): else: entity_ids = hass.states.entity_ids() - for entity_id in entity_ids: - domain = ha.split_entity_id(entity_id)[0] + for ent_id in entity_ids: + domain = ha.split_entity_id(ent_id)[0] module = get_component(domain) try: - if module.is_on(hass, entity_id): + if module.is_on(hass, ent_id): return True except AttributeError: # module is None or method is_on does not exist _LOGGER.exception("Failed to call %s.is_on for %s", - module, entity_id) + module, ent_id) return False diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 75d3bc9922d..d5c15d82396 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -92,8 +92,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): return STATE_ALARM_ARMED_HOME elif self._alarm.state.lower() == 'armed away': return STATE_ALARM_ARMED_AWAY - else: - return STATE_UNKNOWN + return STATE_UNKNOWN @asyncio.coroutine def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index 6029816ba76..f6d388a6c5b 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -113,8 +113,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - else: - return '^\\d{4,6}$' + return '^\\d{4,6}$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index ba932a1c372..c87aea862d5 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -99,8 +99,7 @@ class ManualAlarm(alarm.AlarmControlPanel): self._trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED - else: - return self._pre_trigger_state + return self._pre_trigger_state return self._state diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 985d219865d..fadfbc41a6f 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -80,8 +80,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): """Return the name of the device.""" if self._name is not None: return self._name - else: - return 'Alarm {}'.format(self.simplisafe.location_id()) + return 'Alarm {}'.format(self.simplisafe.location_id()) @property def code_format(self): diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 13925d7bd02..9c0b5108fee 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -16,7 +16,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_NAME) -REQUIREMENTS = ['total_connect_client==0.7'] +REQUIREMENTS = ['total_connect_client==0.11'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index a8cad115883..8bc2539f772 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -39,10 +39,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): """Representation a Wink camera alarm.""" - def __init__(self, wink, hass): - """Initialize the Wink alarm.""" - super().__init__(wink, hass) - @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index b4de3c4a0f5..6356f429bed 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -271,14 +271,14 @@ class Alert(ToggleEntity): 'notify', target, {'message': self._done_message}) @asyncio.coroutine - def async_turn_on(self): + def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._name) self._ack = False yield from self.async_update_ha_state() @asyncio.coroutine - def async_turn_off(self): + def async_turn_off(self, **kwargs): """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._name) self._ack = True diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py new file mode 100644 index 00000000000..8a40c790c12 --- /dev/null +++ b/homeassistant/components/amcrest.py @@ -0,0 +1,149 @@ +""" +This component provides basic support for Amcrest IP cameras. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/amcrest/ +""" +import logging +from datetime import timedelta + +import aiohttp +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout + +import homeassistant.loader as loader +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, + CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['amcrest==1.2.0'] +DEPENDENCIES = ['ffmpeg'] + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTHENTICATION = 'authentication' +CONF_RESOLUTION = 'resolution' +CONF_STREAM_SOURCE = 'stream_source' +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' + +DEFAULT_NAME = 'Amcrest Camera' +DEFAULT_PORT = 80 +DEFAULT_RESOLUTION = 'high' +DEFAULT_STREAM_SOURCE = 'snapshot' +TIMEOUT = 10 + +DATA_AMCREST = 'amcrest' +DOMAIN = 'amcrest' + +NOTIFICATION_ID = 'amcrest_notification' +NOTIFICATION_TITLE = 'Amcrest Camera Setup' + +RESOLUTION_LIST = { + 'high': 0, + 'low': 1, +} + +SCAN_INTERVAL = timedelta(seconds=10) + +AUTHENTICATION_LIST = { + 'basic': 'basic' +} + +STREAM_SOURCE_LIST = { + 'mjpeg': 0, + 'snapshot': 1, + 'rtsp': 2, +} + +# Sensor types are defined like: Name, units, icon +SENSORS = { + 'motion_detector': ['Motion Detected', None, 'mdi:run'], + 'sdcard': ['SD Used', '%', 'mdi:sd'], + 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): + vol.All(vol.In(AUTHENTICATION_LIST)), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): + vol.All(vol.In(RESOLUTION_LIST)), + vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): + vol.All(vol.In(STREAM_SOURCE_LIST)), + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_SENSORS, default=None): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Amcrest IP Camera component.""" + from amcrest import AmcrestCamera + + amcrest_cams = config[DOMAIN] + + persistent_notification = loader.get_component('persistent_notification') + for device in amcrest_cams: + camera = AmcrestCamera(device.get(CONF_HOST), + device.get(CONF_PORT), + device.get(CONF_USERNAME), + device.get(CONF_PASSWORD)).camera + try: + camera.current_time + + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) + persistent_notification.create( + hass, 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) + name = device.get(CONF_NAME) + resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] + sensors = device.get(CONF_SENSORS) + stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] + + username = device.get(CONF_USERNAME) + password = device.get(CONF_PASSWORD) + + # currently aiohttp only works with basic authentication + # only valid for mjpeg streaming + if username is not None and password is not None: + if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION: + authentication = aiohttp.BasicAuth(username, password) + else: + authentication = None + + discovery.load_platform( + hass, 'camera', DOMAIN, { + 'device': camera, + CONF_AUTHENTICATION: authentication, + CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments, + CONF_NAME: name, + CONF_RESOLUTION: resolution, + CONF_STREAM_SOURCE: stream_source, + }, config) + + if sensors: + discovery.load_platform( + hass, 'sensor', DOMAIN, { + 'device': camera, + CONF_NAME: name, + CONF_SENSORS: sensors, + }, config) + + return True diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index b2423d44623..dd29e7d602f 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['apcaccess==0.0.10'] +REQUIREMENTS = ['apcaccess==0.0.13'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 8205029bd21..c22683970bf 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -198,8 +198,7 @@ class APIEntityStateView(HomeAssistantView): state = request.app['hass'].states.get(entity_id) if state: return self.json(state) - else: - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message('Entity not found', HTTP_NOT_FOUND) @asyncio.coroutine def post(self, request, entity_id): @@ -237,8 +236,7 @@ class APIEntityStateView(HomeAssistantView): """Remove entity.""" if request.app['hass'].states.async_remove(entity_id): return self.json_message('Entity removed') - else: - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message('Entity not found', HTTP_NOT_FOUND) class APIEventListenersView(HomeAssistantView): diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py new file mode 100644 index 00000000000..17cc46f3318 --- /dev/null +++ b/homeassistant/components/apple_tv.py @@ -0,0 +1,259 @@ +""" +Support for Apple TV. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/apple_tv/ +""" +import os +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID) +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import discovery +from homeassistant.components.discovery import SERVICE_APPLE_TV +from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyatv==0.3.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'apple_tv' + +SERVICE_SCAN = 'apple_tv_scan' +SERVICE_AUTHENTICATE = 'apple_tv_authenticate' + +ATTR_ATV = 'atv' +ATTR_POWER = 'power' + +CONF_LOGIN_ID = 'login_id' +CONF_START_OFF = 'start_off' +CONF_CREDENTIALS = 'credentials' + +DEFAULT_NAME = 'Apple TV' + +DATA_APPLE_TV = 'data_apple_tv' +DATA_ENTITIES = 'data_apple_tv_entities' + +KEY_CONFIG = 'apple_tv_configuring' + +NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification' +NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication' +NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' +NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_LOGIN_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CREDENTIALS, default=None): cv.string, + vol.Optional(CONF_START_OFF, default=False): cv.boolean + })]) +}, extra=vol.ALLOW_EXTRA) + +# Currently no attributes but it might change later +APPLE_TV_SCAN_SCHEMA = vol.Schema({}) + +APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, +}) + + +def request_configuration(hass, config, atv, credentials): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + + @asyncio.coroutine + def configuration_callback(callback_data): + """Handle the submitted configuration.""" + from pyatv import exceptions + pin = callback_data.get('pin') + notification = get_component('persistent_notification') + + try: + yield from atv.airplay.finish_authentication(pin) + notification.async_create( + hass, + 'Authentication succeeded!

Add the following ' + 'to credentials: in your apple_tv configuration:

' + '{0}'.format(credentials), + title=NOTIFICATION_AUTH_TITLE, + notification_id=NOTIFICATION_AUTH_ID) + except exceptions.DeviceAuthenticationError as ex: + notification.async_create( + hass, + 'Authentication failed! Did you enter correct PIN?

' + 'Details: {0}'.format(ex), + title=NOTIFICATION_AUTH_TITLE, + notification_id=NOTIFICATION_AUTH_ID) + + hass.async_add_job(configurator.request_done, instance) + + instance = configurator.request_config( + hass, 'Apple TV Authentication', configuration_callback, + description='Please enter PIN code shown on screen.', + submit_caption='Confirm', + fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}] + ) + + +@asyncio.coroutine +def scan_for_apple_tvs(hass): + """Scan for devices and present a notification of the ones found.""" + import pyatv + atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + + devices = [] + for atv in atvs: + login_id = atv.login_id + if login_id is None: + login_id = 'Home Sharing disabled' + devices.append('Name: {0}
Host: {1}
Login ID: {2}'.format( + atv.name, atv.address, login_id)) + + if not devices: + devices = ['No device(s) found'] + + notification = get_component('persistent_notification') + notification.async_create( + hass, + 'The following devices were found:

' + + '

'.join(devices), + title=NOTIFICATION_SCAN_TITLE, + notification_id=NOTIFICATION_SCAN_ID) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the Apple TV component.""" + if DATA_APPLE_TV not in hass.data: + hass.data[DATA_APPLE_TV] = {} + + @asyncio.coroutine + def async_service_handler(service): + """Handler for service calls.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + if entity_ids: + devices = [device for device in hass.data[DATA_ENTITIES] + if device.entity_id in entity_ids] + else: + devices = hass.data[DATA_ENTITIES] + + for device in devices: + atv = device.atv + if service.service == SERVICE_AUTHENTICATE: + credentials = yield from atv.airplay.generate_credentials() + yield from atv.airplay.load_credentials(credentials) + _LOGGER.debug('Generated new credentials: %s', credentials) + yield from atv.airplay.start_authentication() + hass.async_add_job(request_configuration, + hass, config, atv, credentials) + elif service.service == SERVICE_SCAN: + hass.async_add_job(scan_for_apple_tvs, hass) + + @asyncio.coroutine + def atv_discovered(service, info): + """Setup an Apple TV that was auto discovered.""" + yield from _setup_atv(hass, { + CONF_NAME: info['name'], + CONF_HOST: info['host'], + CONF_LOGIN_ID: info['properties']['hG'], + CONF_START_OFF: False + }) + + discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) + + tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])] + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_SCAN, async_service_handler, + descriptions.get(SERVICE_SCAN), + schema=APPLE_TV_SCAN_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, + descriptions.get(SERVICE_AUTHENTICATE), + schema=APPLE_TV_AUTHENTICATE_SCHEMA) + + return True + + +@asyncio.coroutine +def _setup_atv(hass, atv_config): + """Setup an Apple TV.""" + import pyatv + name = atv_config.get(CONF_NAME) + host = atv_config.get(CONF_HOST) + login_id = atv_config.get(CONF_LOGIN_ID) + start_off = atv_config.get(CONF_START_OFF) + credentials = atv_config.get(CONF_CREDENTIALS) + + if host in hass.data[DATA_APPLE_TV]: + return + + details = pyatv.AppleTVDevice(name, host, login_id) + session = async_get_clientsession(hass) + atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) + if credentials: + yield from atv.airplay.load_credentials(credentials) + + power = AppleTVPowerManager(hass, atv, start_off) + hass.data[DATA_APPLE_TV][host] = { + ATTR_ATV: atv, + ATTR_POWER: power + } + + hass.async_add_job(discovery.async_load_platform( + hass, 'media_player', DOMAIN, atv_config)) + + hass.async_add_job(discovery.async_load_platform( + hass, 'remote', DOMAIN, atv_config)) + + +class AppleTVPowerManager: + """Manager for global power management of an Apple TV. + + An instance is used per device to share the same power state between + several platforms. + """ + + def __init__(self, hass, atv, is_off): + """Initialize power manager.""" + self.hass = hass + self.atv = atv + self.listeners = [] + self._is_on = not is_off + + def init(self): + """Initialize power management.""" + if self._is_on: + self.atv.push_updater.start() + + @property + def turned_on(self): + """If device is on or off.""" + return self._is_on + + def set_power_on(self, value): + """Change if a device is on or off.""" + if value != self._is_on: + self._is_on = value + if not self._is_on: + self.atv.push_updater.stop() + else: + self.atv.push_updater.start() + + for listener in self.listeners: + self.hass.async_add_job(listener.async_update_ha_state()) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index dfed411745f..497b8453267 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -42,8 +42,6 @@ def async_trigger(hass, config, action): }, }) - # Do something to call action if event == SUN_EVENT_SUNRISE: return async_track_sunrise(hass, call_action, offset) - else: - return async_track_sunset(hass, call_action, offset) + return async_track_sunset(hass, call_action, offset) diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 972d9ca835c..5b58a6cbf6a 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -68,7 +68,7 @@ class ArestBinarySensor(BinarySensorDevice): if self._pin is not None: request = requests.get( '{}/mode/{}/i'.format(self._resource, self._pin), timeout=10) - if request.status_code is not 200: + if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @property diff --git a/homeassistant/components/binary_sensor/flic.py b/homeassistant/components/binary_sensor/flic.py index f78ee75ae25..51fffae5cc0 100644 --- a/homeassistant/components/binary_sensor/flic.py +++ b/homeassistant/components/binary_sensor/flic.py @@ -199,11 +199,10 @@ class FlicButton(BinarySensorDevice): "Queued %s dropped for %s. Time in queue was %s", click_type, self.address, time_string) return True - else: - _LOGGER.info( - "Queued %s allowed for %s. Time in queue was %s", - click_type, self.address, time_string) - return False + _LOGGER.info( + "Queued %s allowed for %s. Time in queue was %s", + click_type, self.address, time_string) + return False def _on_up_down(self, channel, click_type, was_queued, time_diff): """Update device state, if event was not queued.""" diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index 61e69e991b3..7f2127fcad5 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) -REQUIREMENTS = ['pyhik==0.1.2'] +REQUIREMENTS = ['pyhik==0.1.3'] _LOGGER = logging.getLogger(__name__) CONF_IGNORED = 'ignored' diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 29ad5847b32..a82431a5ab8 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devices = [] - for config in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMBinarySensor(hass, config) + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + new_device = HMBinarySensor(hass, conf) new_device.link_homematic() devices.append(new_device) diff --git a/homeassistant/components/binary_sensor/modbus.py b/homeassistant/components/binary_sensor/modbus.py index 3a9b57ba6de..00dc588a468 100644 --- a/homeassistant/components/binary_sensor/modbus.py +++ b/homeassistant/components/binary_sensor/modbus.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol import homeassistant.components.modbus as modbus -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_SLAVE from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -18,7 +18,6 @@ DEPENDENCIES = ['modbus'] CONF_COIL = 'coil' CONF_COILS = 'coils' -CONF_SLAVE = 'slave' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COILS): [{ @@ -50,6 +49,7 @@ class ModbusCoilSensor(BinarySensorDevice): self._coil = int(coil) self._value = None + @property def name(self): """Return the name of the sensor.""" return self._name @@ -62,4 +62,10 @@ class ModbusCoilSensor(BinarySensorDevice): def update(self): """Update the state of the sensor.""" result = modbus.HUB.read_coils(self._slave, self._coil, 1) - self._value = result.bits[0] + try: + self._value = result.bits[0] + except AttributeError: + _LOGGER.error( + 'No response from modbus slave %s coil %s', + self._slave, + self._coil) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 70887e9391d..ac479ef4277 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -156,8 +156,7 @@ class NetatmoBinarySensor(BinarySensorDevice): return WELCOME_SENSOR_TYPES.get(self._sensor_name) elif self._cameratype == 'NOC': return PRESENCE_SENSOR_TYPES.get(self._sensor_name) - else: - return TAG_SENSOR_TYPES.get(self._sensor_name) + return TAG_SENSOR_TYPES.get(self._sensor_name) @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 6e278ccfccf..f4e4e04717d 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -12,13 +12,12 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['octoprint'] - +DOMAIN = "octoprint" DEFAULT_NAME = 'OctoPrint' SENSOR_TYPES = { @@ -37,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available OctoPrint binary sensors.""" - octoprint = get_component('octoprint') + octoprint_api = hass.data[DOMAIN]["api"] name = config.get(CONF_NAME) monitored_conditions = config.get( CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys()) @@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for octo_type in monitored_conditions: new_sensor = OctoPrintBinarySensor( - octoprint.OCTOPRINT, octo_type, SENSOR_TYPES[octo_type][2], + octoprint_api, octo_type, SENSOR_TYPES[octo_type][2], name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], SENSOR_TYPES[octo_type][1], 'flags') devices.append(new_sensor) @@ -98,6 +97,3 @@ class OctoPrintBinarySensor(BinarySensorDevice): except requests.exceptions.ConnectionError: # Error calling the api, already logged in api.update() return - - if self._state is None: - _LOGGER.warning("Unable to locate value for %s", self.sensor_type) diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/binary_sensor/pilight.py index d0774ae12e6..c4c26d3a122 100644 --- a/homeassistant/components/binary_sensor/pilight.py +++ b/homeassistant/components/binary_sensor/pilight.py @@ -28,6 +28,7 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = 'variable' +CONF_RESET_DELAY_SEC = 'reset_delay_sec' DEFAULT_NAME = 'Pilight Binary Sensor' DEPENDENCIES = ['pilight'] @@ -38,7 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default='on'): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default='off'): cv.string, - vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean + vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean, + vol.Optional(CONF_RESET_DELAY_SEC, default=30): cv.positive_int }) @@ -54,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): payload=config.get(CONF_PAYLOAD), on_value=config.get(CONF_PAYLOAD_ON), off_value=config.get(CONF_PAYLOAD_OFF), + rst_dly_sec=config.get(CONF_RESET_DELAY_SEC), )]) else: add_devices([PilightBinarySensor( diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py index 4a23b2e42ca..cb5bc4333a2 100644 --- a/homeassistant/components/binary_sensor/ping.py +++ b/homeassistant/components/binary_sensor/ping.py @@ -126,14 +126,14 @@ class PingData(object): 'avg': rtt_avg, 'max': rtt_max, 'mdev': ''} - else: - match = PING_MATCHER.search(str(out).split('\n')[-1]) - rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() - return { - 'min': rtt_min, - 'avg': rtt_avg, - 'max': rtt_max, - 'mdev': rtt_mdev} + + match = PING_MATCHER.search(str(out).split('\n')[-1]) + rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() + return { + 'min': rtt_min, + 'avg': rtt_avg, + 'max': rtt_max, + 'mdev': rtt_mdev} except (subprocess.CalledProcessError, AttributeError): return False diff --git a/homeassistant/components/binary_sensor/volvooncall.py b/homeassistant/components/binary_sensor/volvooncall.py index 2c7c398d91a..471b3917054 100644 --- a/homeassistant/components/binary_sensor/volvooncall.py +++ b/homeassistant/components/binary_sensor/volvooncall.py @@ -30,8 +30,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice): return bool(val) elif self._attribute in ['doors', 'windows']: return any([val[key] for key in val if 'Open' in key]) - else: - return val != 'Normal' + return val != 'Normal' @property def device_class(self): diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index c16c62a5f81..b4910687da7 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -121,10 +121,6 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): class WinkSmokeDetector(WinkBinarySensorDevice): """Representation of a Wink Smoke detector.""" - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - @property def device_state_attributes(self): """Return the state attributes.""" @@ -136,10 +132,6 @@ class WinkSmokeDetector(WinkBinarySensorDevice): class WinkHub(WinkBinarySensorDevice): """Representation of a Wink Hub.""" - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - @property def device_state_attributes(self): """Return the state attributes.""" @@ -152,10 +144,6 @@ class WinkHub(WinkBinarySensorDevice): class WinkRemote(WinkBinarySensorDevice): """Representation of a Wink Lutron Connected bulb remote.""" - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - @property def device_state_attributes(self): """Return the state attributes.""" @@ -175,10 +163,6 @@ class WinkRemote(WinkBinarySensorDevice): class WinkButton(WinkBinarySensorDevice): """Representation of a Wink Relay button.""" - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - @property def device_state_attributes(self): """Return the state attributes.""" @@ -191,10 +175,6 @@ class WinkButton(WinkBinarySensorDevice): class WinkGang(WinkBinarySensorDevice): """Representation of a Wink Relay gang.""" - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - @property def is_on(self): """Return true if the gang is connected.""" diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 1d895fbf3a7..ad7c29badf9 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -34,10 +34,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): from bellows.zigbee.zcl.clusters.security import IasZone - clusters = discovery_info['clusters'] + in_clusters = discovery_info['in_clusters'] device_class = None - cluster = [c for c in clusters if isinstance(c, IasZone)][0] + cluster = in_clusters[IasZone.cluster_id] if discovery_info['new_join']: yield from cluster.bind() ieee = cluster.endpoint.device.application.ieee @@ -64,7 +64,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): super().__init__(**kwargs) self._device_class = device_class from bellows.zigbee.zcl.clusters.security import IasZone - self._ias_zone_cluster = self._clusters[IasZone.cluster_id] + self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] @property def is_on(self) -> bool: diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 2ccd591db2c..4e088c8a640 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -148,8 +148,7 @@ class CalendarEventDevice(Entity): if 'date' in date: return dt.start_of_local_day(dt.dt.datetime.combine( dt.parse_date(date['date']), dt.dt.time.min)) - else: - return dt.as_local(dt.parse_datetime(date['dateTime'])) + return dt.as_local(dt.parse_datetime(date['dateTime'])) start = _get_date(self.data.event['start']) end = _get_date(self.data.event['end']) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c84421f50ea..5b97e102d8d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -266,8 +266,7 @@ class Camera(Entity): return STATE_RECORDING elif self.is_streaming: return STATE_STREAMING - else: - return STATE_IDLE + return STATE_IDLE def enable_motion_detection(self): """Enable motion detection in the camera.""" diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 8f8b7e5f9f5..51b8ff13906 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/ import asyncio import logging -import aiohttp -import voluptuous as vol - -import homeassistant.loader as loader -from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.amcrest import ( + STREAM_SOURCE_LIST, TIMEOUT) +from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_aiohttp_proxy_web, async_aiohttp_proxy_stream) -REQUIREMENTS = ['amcrest==1.2.0'] -DEPENDENCIES = ['ffmpeg'] +DEPENDENCIES = ['amcrest', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) -CONF_RESOLUTION = 'resolution' -CONF_STREAM_SOURCE = 'stream_source' -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' -DEFAULT_NAME = 'Amcrest Camera' -DEFAULT_PORT = 80 -DEFAULT_RESOLUTION = 'high' -DEFAULT_STREAM_SOURCE = 'mjpeg' - -NOTIFICATION_ID = 'amcrest_notification' -NOTIFICATION_TITLE = 'Amcrest Camera Setup' - -RESOLUTION_LIST = { - 'high': 0, - 'low': 1, -} - -STREAM_SOURCE_LIST = { - 'mjpeg': 0, - 'snapshot': 1, - 'rtsp': 2, -} - -CONTENT_TYPE_HEADER = 'Content-Type' -TIMEOUT = 5 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): - vol.All(vol.In(RESOLUTION_LIST)), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): - vol.All(vol.In(STREAM_SOURCE_LIST)), - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up an Amcrest IP Camera.""" - from amcrest import AmcrestCamera - camera = AmcrestCamera( - config.get(CONF_HOST), config.get(CONF_PORT), - config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera + if discovery_info is None: + return - persistent_notification = loader.get_component('persistent_notification') - try: - camera.current_time - # pylint: disable=broad-except - except Exception as ex: - _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) - persistent_notification.create( - hass, 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False + device = discovery_info['device'] + authentication = discovery_info['authentication'] + ffmpeg_arguments = discovery_info['ffmpeg_arguments'] + name = discovery_info['name'] + resolution = discovery_info['resolution'] + stream_source = discovery_info['stream_source'] + + async_add_devices([ + AmcrestCam(hass, + name, + device, + authentication, + ffmpeg_arguments, + stream_source, + resolution)], True) - add_devices([AmcrestCam(hass, config, camera)]) return True class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" - def __init__(self, hass, device_info, camera): + def __init__(self, hass, name, camera, authentication, + ffmpeg_arguments, stream_source, resolution): """Initialize an Amcrest camera.""" super(AmcrestCam, self).__init__() + self._name = name self._camera = camera self._base_url = self._camera.get_base_url() - self._name = device_info.get(CONF_NAME) self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) - self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] - self._stream_source = STREAM_SOURCE_LIST[ - device_info.get(CONF_STREAM_SOURCE) - ] - self._token = self._auth = aiohttp.BasicAuth( - device_info.get(CONF_USERNAME), - password=device_info.get(CONF_PASSWORD) - ) + self._ffmpeg_arguments = ffmpeg_arguments + self._stream_source = stream_source + self._resolution = resolution + self._token = self._auth = authentication def camera_image(self): """Return a still image reponse from the camera.""" diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index 637b0dfc2e6..80833e34b20 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -49,7 +49,6 @@ class ArloCam(Camera): """Initialize an Arlo camera.""" super().__init__() self._camera = camera - self._base_stn = hass.data[DATA_ARLO].base_stations[0] self._name = self._camera.name self._motion_status = False self._ffmpeg = hass.data[DATA_FFMPEG] @@ -103,7 +102,16 @@ class ArloCam(Camera): def set_base_station_mode(self, mode): """Set the mode in the base station.""" - self._base_stn.mode = mode + # Get the list of base stations identified by library + base_stations = self.hass.data[DATA_ARLO].base_stations + + # Some Arlo cameras does not have basestation + # So check if there is base station detected first + # if yes, then choose the primary base station + # Set the mode on the chosen base station + if base_stations: + primary_base_station = base_stations[0] + primary_base_station.mode = mode def enable_motion_detection(self): """Enable the Motion detection in base station (Arm).""" diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index e5f22cced16..c1ec2db0a08 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -113,8 +113,7 @@ class NetatmoCamera(Camera): return "Presence" elif self._cameratype == "NACamera": return "Welcome" - else: - return None + return None @property def unique_id(self): diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index c0a8039ee64..3203a10b391 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to connect to NVR: %s", str(ex)) return False - identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid' + identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid' # Filter out airCam models, which are not supported in the latest # version of UnifiVideo and which are EOL by Ubiquiti cameras = [ diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index d00d30c3b19..5b6c025b3e3 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -398,16 +398,14 @@ class ClimateDevice(Entity): """Return the current state.""" if self.current_operation: return self.current_operation - else: - return STATE_UNKNOWN + return STATE_UNKNOWN @property def precision(self): """Return the precision of the system.""" if self.unit_of_measurement == TEMP_CELSIUS: return PRECISION_TENTHS - else: - return PRECISION_WHOLE + return PRECISION_WHOLE @property def state_attributes(self): @@ -709,6 +707,5 @@ class ClimateDevice(Entity): return round(temp * 2) / 2.0 elif self.precision == PRECISION_TENTHS: return round(temp, 1) - else: - # PRECISION_WHOLE as a fall back - return round(temp) + # PRECISION_WHOLE as a fall back + return round(temp) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 9e04013d53f..6780d3745f0 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -151,16 +151,14 @@ class Thermostat(ClimateDevice): """Return the lower bound temperature we try to reach.""" if self.current_operation == STATE_AUTO: return int(self.thermostat['runtime']['desiredHeat'] / 10) - else: - return None + return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" if self.current_operation == STATE_AUTO: return int(self.thermostat['runtime']['desiredCool'] / 10) - else: - return None + return None @property def target_temperature(self): @@ -171,8 +169,7 @@ class Thermostat(ClimateDevice): return int(self.thermostat['runtime']['desiredHeat'] / 10) elif self.current_operation == STATE_COOL: return int(self.thermostat['runtime']['desiredCool'] / 10) - else: - return None + return None @property def desired_fan_mode(self): @@ -184,8 +181,7 @@ class Thermostat(ClimateDevice): """Return the current fan state.""" if 'fan' in self.thermostat['equipmentStatus']: return STATE_ON - else: - return STATE_OFF + return STATE_OFF @property def current_hold_mode(self): @@ -199,15 +195,13 @@ class Thermostat(ClimateDevice): int(event['startDate'][0:4]) <= 1: # A temporary hold from away climate is a hold return 'away' - else: - # A permanent hold from away climate is away_mode - return None + # A permanent hold from away climate is away_mode + return None elif event['holdClimateRef'] != "": # Any other hold based on climate return event['holdClimateRef'] - else: - # Any hold not based on a climate is a temp hold - return TEMPERATURE_HOLD + # Any hold not based on a climate is a temp hold + return TEMPERATURE_HOLD elif event['type'].startswith('auto'): # All auto modes are treated as holds return event['type'][4:].lower() @@ -222,8 +216,7 @@ class Thermostat(ClimateDevice): if self.operation_mode == 'auxHeatOnly' or \ self.operation_mode == 'heatPump': return STATE_HEAT - else: - return self.operation_mode + return self.operation_mode @property def operation_list(self): @@ -384,8 +377,7 @@ class Thermostat(ClimateDevice): # add further conditions if other hold durations should be # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode - else: - return 'nextTransition' + return 'nextTransition' @property def climate_list(self): diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py index 5911486c761..c3ba2224b06 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -145,4 +145,4 @@ class Flexit(ClimateDevice): def set_fan_mode(self, fan): """Set new fan mode.""" - self.unit.set_fan_speed(fan) + self.unit.set_fan_speed(self._fan_list.index(fan)) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 11400466c4f..9442b7da194 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components import switch from homeassistant.components.climate import ( - STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) + STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, + STATE_AUTO) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) @@ -87,6 +88,7 @@ class GenericThermostat(ClimateDevice): self.min_cycle_duration = min_cycle_duration self._tolerance = tolerance self._keep_alive = keep_alive + self._enabled = True self._active = False self._cur_temp = None @@ -131,18 +133,39 @@ class GenericThermostat(ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" + if not self._enabled: + return STATE_OFF if self.ac_mode: cooling = self._active and self._is_device_active return STATE_COOL if cooling else STATE_IDLE - else: - heating = self._active and self._is_device_active - return STATE_HEAT if heating else STATE_IDLE + + heating = self._active and self._is_device_active + return STATE_HEAT if heating else STATE_IDLE @property def target_temperature(self): """Return the temperature we try to reach.""" return self._target_temp + @property + def operation_list(self): + """List of available operation modes.""" + return [STATE_AUTO, STATE_OFF] + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + if operation_mode == STATE_AUTO: + self._enabled = True + elif operation_mode == STATE_OFF: + self._enabled = False + if self._is_device_active: + switch.async_turn_off(self.hass, self.heater_entity_id) + else: + _LOGGER.error('Unrecognized operation mode: %s', operation_mode) + return + # Ensure we updae the current operation after changing the mode + self.schedule_update_ha_state() + @asyncio.coroutine def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -159,9 +182,9 @@ class GenericThermostat(ClimateDevice): # pylint: disable=no-member if self._min_temp: return self._min_temp - else: - # get default temp from super class - return ClimateDevice.min_temp.fget(self) + + # get default temp from super class + return ClimateDevice.min_temp.fget(self) @property def max_temp(self): @@ -169,9 +192,9 @@ class GenericThermostat(ClimateDevice): # pylint: disable=no-member if self._max_temp: return self._max_temp - else: - # Get default temp from super class - return ClimateDevice.max_temp.fget(self) + + # Get default temp from super class + return ClimateDevice.max_temp.fget(self) @asyncio.coroutine def _async_sensor_changed(self, entity_id, old_state, new_state): @@ -221,6 +244,9 @@ class GenericThermostat(ClimateDevice): if not self._active: return + if not self._enabled: + return + if self.min_cycle_duration: if self._is_device_active: current_state = STATE_ON diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index 1f9ca7c1abf..60cda24eef9 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -46,8 +46,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devices = [] - for config in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMThermostat(hass, config) + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + new_device = HMThermostat(hass, conf) new_device.link_homematic() devices.append(new_device) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 94c48258bf5..65e30e3cb26 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -60,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if region == 'us': return _setup_us(username, password, config, add_devices) - else: - return _setup_round(username, password, config, add_devices) + + return _setup_round(username, password, config, add_devices) def _setup_round(username, password, config, add_devices): @@ -251,8 +251,7 @@ class HoneywellUSThermostat(ClimateDevice): """Return the temperature we try to reach.""" if self._device.system_mode == 'cool': return self._device.setpoint_cool - else: - return self._device.setpoint_heat + return self._device.setpoint_heat @property def current_operation(self: ClimateDevice) -> str: diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 522a1bebb16..ac4f64f4ec8 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -109,16 +109,14 @@ class NestThermostat(ClimateDevice): return self._mode elif self._mode == STATE_HEAT_COOL: return STATE_AUTO - else: - return STATE_UNKNOWN + 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: return self._target_temperature - else: - return None + return None @property def target_temperature_low(self): @@ -129,8 +127,7 @@ class NestThermostat(ClimateDevice): return self._eco_temperature[0] if self._mode == STATE_HEAT_COOL: return self._target_temperature[0] - else: - return None + return None @property def target_temperature_high(self): @@ -141,8 +138,7 @@ class NestThermostat(ClimateDevice): return self._eco_temperature[1] if self._mode == STATE_HEAT_COOL: return self._target_temperature[1] - else: - return None + return None @property def is_away_mode_on(self): @@ -188,9 +184,8 @@ class NestThermostat(ClimateDevice): if self._has_fan: # Return whether the fan is on return STATE_ON if self._fan else STATE_AUTO - else: - # No Fan available so disable slider - return None + # No Fan available so disable slider + return None @property def fan_list(self): diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index cea1a41ec9f..3706a24bb52 100755 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -119,14 +119,14 @@ class NetatmoThermostat(ClimateDevice): self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._away = False - def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs): + def set_temperature(self, **kwargs): """Set new target temperature for 2 hours.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return mode = "manual" self._data.thermostatdata.setthermpoint( - mode, temperature, endTimeOffset) + mode, temperature, DEFAULT_TIME_OFFSET) self._target_temperature = temperature self._away = False diff --git a/homeassistant/components/climate/oem.py b/homeassistant/components/climate/oem.py index fd43ff799db..5909f26eb4f 100644 --- a/homeassistant/components/climate/oem.py +++ b/homeassistant/components/climate/oem.py @@ -92,8 +92,7 @@ class ThermostatDevice(ClimateDevice): """Return current operation i.e. heat, cool, idle.""" if self._state: return STATE_HEAT - else: - return STATE_IDLE + return STATE_IDLE @property def current_temperature(self): diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 8011f53f375..22e09ad0161 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -136,24 +136,53 @@ class RadioThermostat(ClimateDevice): return self._away def update(self): - """Update the data from the thermostat.""" - self._current_temperature = self.device.temp['raw'] + """Update and validate the data from the thermostat.""" + current_temp = self.device.temp['raw'] + if current_temp == -1: + _LOGGER.error("Couldn't get valid temperature reading") + return + self._current_temperature = current_temp self._name = self.device.name['raw'] - self._fmode = self.device.fmode['human'] - self._tmode = self.device.tmode['human'] - self._tstate = self.device.tstate['human'] + try: + self._fmode = self.device.fmode['human'] + except AttributeError: + _LOGGER.error("Couldn't get valid fan mode reading") + try: + self._tmode = self.device.tmode['human'] + except AttributeError: + _LOGGER.error("Couldn't get valid thermostat mode reading") + try: + self._tstate = self.device.tstate['human'] + except AttributeError: + _LOGGER.error("Couldn't get valid thermostat state reading") if self._tmode == 'Cool': - self._target_temperature = self.device.t_cool['raw'] + target_temp = self.device.t_cool['raw'] + if target_temp == -1: + _LOGGER.error("Couldn't get target reading") + return + self._target_temperature = target_temp self._current_operation = STATE_COOL elif self._tmode == 'Heat': - self._target_temperature = self.device.t_heat['raw'] + target_temp = self.device.t_heat['raw'] + if target_temp == -1: + _LOGGER.error("Couldn't get valid target reading") + return + self._target_temperature = target_temp self._current_operation = STATE_HEAT elif self._tmode == 'Auto': if self._tstate == 'Cool': - self._target_temperature = self.device.t_cool['raw'] + target_temp = self.device.t_cool['raw'] + if target_temp == -1: + _LOGGER.error("Couldn't get valid target reading") + return + self._target_temperature = target_temp elif self._tstate == 'Heat': - self._target_temperature = self.device.t_heat['raw'] + target_temp = self.device.t_heat['raw'] + if target_temp == -1: + _LOGGER.error("Couldn't get valid target reading") + return + self._target_temperature = target_temp self._current_operation = STATE_AUTO else: self._current_operation = STATE_IDLE diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 501a346d8c5..c55b4c9ce0d 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -117,9 +117,8 @@ class SensiboClimate(ClimateDevice): # We are working in same units as the a/c unit. Use whole degrees # like the API supports. return 1 - else: - # Unit conversion is going on. No point to stick to specific steps. - return None + # Unit conversion is going on. No point to stick to specific steps. + return None @property def current_operation(self): diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 832bca6f9b6..8a2e6621af3 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zones = tado.get_zones() except RuntimeError: _LOGGER.error("Unable to get zone info from mytado") - return False + return climate_devices = [] for zone in zones: @@ -61,9 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if climate_devices: add_devices(climate_devices, True) - return True - else: - return False def create_climate_device(tado, hass, zone, name, zone_id): @@ -150,8 +147,7 @@ class TadoClimate(ClimateDevice): """Return current readable operation mode.""" if self._cooling: return "Cooling" - else: - return OPERATION_LIST.get(self._current_operation) + return OPERATION_LIST.get(self._current_operation) @property def operation_list(self): @@ -163,16 +159,14 @@ class TadoClimate(ClimateDevice): """Return the fan setting.""" if self.ac_mode: return FAN_MODES_LIST.get(self._current_fan) - else: - return None + return None @property def fan_list(self): """List of available fan modes.""" if self.ac_mode: return list(FAN_MODES_LIST.values()) - else: - return None + return None @property def temperature_unit(self): @@ -218,18 +212,16 @@ class TadoClimate(ClimateDevice): """Return the minimum temperature.""" if self._min_temp: return self._min_temp - else: - # get default temp from super class - return super().min_temp + # get default temp from super class + return super().min_temp @property def max_temp(self): """Return the maximum temperature.""" if self._max_temp: return self._max_temp - else: - # Get default temp from super class - return super().max_temp + # Get default temp from super class + return super().max_temp def update(self): """Update the state of this climate device.""" diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index e3439bdfc74..f52340dc627 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -111,8 +111,8 @@ class WinkThermostat(WinkDevice, ClimateDevice): # This will address both possibilities if self.wink.current_humidity() < 1: return self.wink.current_humidity() * 100 - else: - return self.wink.current_humidity() + return self.wink.current_humidity() + return None @property def external_temperature(self): @@ -175,10 +175,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): return self.wink.current_max_set_point() elif self.current_operation == STATE_HEAT: return self.wink.current_min_set_point() - else: - return None - else: - return None + return None @property def target_temperature_low(self): @@ -206,8 +203,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): return True elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on(): return False - else: - return None + return None def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -270,9 +266,8 @@ class WinkThermostat(WinkDevice, ClimateDevice): return STATE_ON elif self.wink.current_fan_mode() == 'auto': return STATE_AUTO - else: - # No Fan available so disable slider - return None + # No Fan available so disable slider + return None @property def fan_list(self): @@ -451,8 +446,7 @@ class WinkAC(WinkDevice, ClimateDevice): return SPEED_MEDIUM elif speed <= 1.0 and speed > 0.8: return SPEED_HIGH - else: - return STATE_UNKNOWN + return STATE_UNKNOWN @property def fan_list(self): diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 949bd10e0c2..497916a3e4d 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -154,8 +154,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): return TEMP_CELSIUS elif self._unit == 'F': return TEMP_FAHRENHEIT - else: - return self._unit + return self._unit @property def current_temperature(self): diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index cd48238cae9..6d43b1d2166 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -112,10 +112,7 @@ class CommandCover(CoverDevice): def is_closed(self): """Return if the cover is closed.""" if self.current_cover_position is not None: - if self.current_cover_position > 0: - return False - else: - return True + return self.current_cover_position == 0 @property def current_cover_position(self): diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index 35574150596..ed060659746 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -79,8 +79,7 @@ class DemoCover(CoverDevice): """Flag supported features.""" if self._supported_features is not None: return self._supported_features - else: - return super().supported_features + return super().supported_features def close_cover(self, **kwargs): """Close the cover.""" diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index 457312c8a4a..02e970a0d0b 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -159,8 +159,7 @@ class GaradgetCover(CoverDevice): """Return if the cover is closed.""" if self._state == STATE_UNKNOWN: return None - else: - return self._state == STATE_CLOSED + return self._state == STATE_CLOSED @property def device_class(self): diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index 8fb003c6649..e8372b84ce4 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devices = [] - for config in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMCover(hass, config) + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + new_device = HMCover(hass, conf) new_device.link_homematic() devices.append(new_device) @@ -52,10 +52,7 @@ class HMCover(HMDevice, CoverDevice): def is_closed(self): """Return if the cover is closed.""" if self.current_cover_position is not None: - if self.current_cover_position > 0: - return False - else: - return True + return self.current_cover_position == 0 def open_cover(self, **kwargs): """Open the cover.""" diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index a53e659c448..eab64fd7abb 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -361,8 +361,7 @@ class MqttCover(CoverDevice): position_percentage = float(offset_position) / tilt_range * 100.0 if self._tilt_invert: return 100 - position_percentage - else: - return position_percentage + return position_percentage def find_in_range_from_percent(self, percentage): """ diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 43942dacd05..f48a2110eca 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -53,8 +53,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): set_req = self.gateway.const.SetReq if set_req.V_DIMMER in self._values: return self._values.get(set_req.V_DIMMER) == 0 - else: - return self._values.get(set_req.V_LIGHT) == STATE_OFF + return self._values.get(set_req.V_LIGHT) == STATE_OFF @property def current_cover_position(self): diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index d22e8f9104c..d98c71e25fb 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -117,8 +117,7 @@ class OpenGarageCover(CoverDevice): """Return if the cover is closed.""" if self._state == STATE_UNKNOWN: return None - else: - return self._state in [STATE_CLOSED, STATE_OPENING] + return self._state in [STATE_CLOSED, STATE_OPENING] def close_cover(self): """Close the cover.""" diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index fd746131288..769c2fc4ed6 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -40,14 +40,15 @@ STOP_ACTION = 'stop_cover' POSITION_ACTION = 'set_cover_position' TILT_ACTION = 'set_cover_tilt_position' CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position' +CONF_OPEN_OR_CLOSE = 'open_or_close' TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | SUPPORT_SET_TILT_POSITION) COVER_SCHEMA = vol.Schema({ - vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA, + vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA, + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA, + vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, vol.Exclusive(CONF_POSITION_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE): cv.template, vol.Exclusive(CONF_VALUE_TEMPLATE, @@ -77,9 +78,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) - open_action = device_config[OPEN_ACTION] - close_action = device_config[CLOSE_ACTION] - stop_action = device_config[STOP_ACTION] + open_action = device_config.get(OPEN_ACTION) + close_action = device_config.get(CLOSE_ACTION) + stop_action = device_config.get(STOP_ACTION) position_action = device_config.get(POSITION_ACTION) tilt_action = device_config.get(TILT_ACTION) @@ -88,6 +89,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE) continue + if position_action is None and open_action is None: + _LOGGER.error('Must specify at least one of %s' or '%s', + OPEN_ACTION, POSITION_ACTION) + continue template_entity_ids = set() if state_template is not None: temp_ids = state_template.extract_entities() @@ -147,9 +152,15 @@ class CoverTemplate(CoverDevice): self._position_template = position_template self._tilt_template = tilt_template self._icon_template = icon_template - self._open_script = Script(hass, open_action) - self._close_script = Script(hass, close_action) - self._stop_script = Script(hass, stop_action) + self._open_script = None + if open_action is not None: + self._open_script = Script(hass, open_action) + self._close_script = None + if close_action is not None: + self._close_script = Script(hass, close_action) + self._stop_script = None + if stop_action is not None: + self._stop_script = Script(hass, stop_action) self._position_script = None if position_action is not None: self._position_script = Script(hass, position_action) @@ -227,9 +238,12 @@ class CoverTemplate(CoverDevice): @property def supported_features(self): """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE - if self.current_cover_position is not None: + if self._stop_script is not None: + supported_features |= SUPPORT_STOP + + if self._position_script is not None: supported_features |= SUPPORT_SET_POSITION if self.current_cover_tilt_position is not None: @@ -245,23 +259,30 @@ class CoverTemplate(CoverDevice): @asyncio.coroutine def async_open_cover(self, **kwargs): """Move the cover up.""" - self.hass.async_add_job(self._open_script.async_run()) + if self._open_script: + self.hass.async_add_job(self._open_script.async_run()) + elif self._position_script: + self.hass.async_add_job(self._position_script.async_run( + {"position": 100})) @asyncio.coroutine def async_close_cover(self, **kwargs): """Move the cover down.""" - self.hass.async_add_job(self._close_script.async_run()) + if self._close_script: + self.hass.async_add_job(self._close_script.async_run()) + elif self._position_script: + self.hass.async_add_job(self._position_script.async_run( + {"position": 0})) @asyncio.coroutine def async_stop_cover(self, **kwargs): """Fire the stop action.""" - self.hass.async_add_job(self._stop_script.async_run()) + if self._stop_script: + self.hass.async_add_job(self._stop_script.async_run()) @asyncio.coroutine def async_set_cover_position(self, **kwargs): """Set cover position.""" - if ATTR_POSITION not in kwargs: - return self._position = kwargs[ATTR_POSITION] self.hass.async_add_job(self._position_script.async_run( {"position": self._position})) @@ -283,8 +304,6 @@ class CoverTemplate(CoverDevice): @asyncio.coroutine def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - if ATTR_TILT_POSITION not in kwargs: - return self._tilt_value = kwargs[ATTR_TILT_POSITION] self.hass.async_add_job(self._tilt_script.async_run( {"tilt": self._tilt_value})) diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py index bcccf17da8a..05be125ec6f 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -53,10 +53,7 @@ class VeraCover(VeraDevice, CoverDevice): def is_closed(self): """Return if the cover is closed.""" if self.current_cover_position is not None: - if self.current_cover_position > 0: - return False - else: - return True + return self.current_cover_position == 0 def open_cover(self, **kwargs): """Open the cover.""" diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index d5908c35ca2..ce96b4d75e0 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -29,20 +29,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkCoverDevice(WinkDevice, CoverDevice): """Representation of a Wink cover device.""" - def __init__(self, wink, hass): - """Initialize the cover.""" - super().__init__(wink, hass) - @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['cover'].append(self) - def close_cover(self): + def close_cover(self, **kwargs): """Close the shade.""" self.wink.set_state(0) - def open_cover(self): + def open_cover(self, **kwargs): """Open the shade.""" self.wink.set_state(1) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index b682bee3e20..6ec70d9a85a 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -73,8 +73,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): return None if self.current_cover_position > 0: return False - else: - return True + return True @property def current_cover_position(self): @@ -86,8 +85,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): return 0 elif self._current_position >= 95: return 100 - else: - return self._current_position + return self._current_position def open_cover(self, **kwargs): """Move the roller shutter up.""" diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index bdd28d1d168..b28d16cc4a1 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -60,20 +60,11 @@ _LEASES_REGEX = re.compile( r'(?P([^\s]+))') # Command to get both 5GHz and 2.4GHz clients -_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }' +_WL_CMD = 'for dev in `nvram get wl_ifnames`; do wl -i $dev assoclist; done' _WL_REGEX = re.compile( r'\w+\s' + r'(?P(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))') -_ARP_CMD = 'arp -n' -_ARP_REGEX = re.compile( - r'.+\s' + - r'\((?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' + - r'.+\s' + - r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' + - r'\s' + - r'.*') - _IP_NEIGH_CMD = 'ip neigh' _IP_NEIGH_REGEX = re.compile( r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3}|' @@ -84,15 +75,6 @@ _IP_NEIGH_REGEX = re.compile( r'\s?(router)?' r'(?P(\w+))') -_NVRAM_CMD = 'nvram get client_info_tmp' -_NVRAM_REGEX = re.compile( - r'.*>.*>' + - r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + - r'>' + - r'(?P(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' + - r'>' + - r'.*') - # pylint: disable=unused-argument def get_scanner(hass, config): @@ -102,7 +84,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram') +AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases') class AsusWrtDeviceScanner(DeviceScanner): @@ -173,7 +155,7 @@ class AsusWrtDeviceScanner(DeviceScanner): return False with self.lock: - _LOGGER.info('Checking ARP') + _LOGGER.info('Checking Devices') data = self.get_asuswrt_data() if not data: return False @@ -182,7 +164,7 @@ class AsusWrtDeviceScanner(DeviceScanner): client['status'] == 'REACHABLE' or client['status'] == 'DELAY' or client['status'] == 'STALE' or - client['status'] == 'IN_NVRAM'] + client['status'] == 'IN_ASSOCLIST'] self.last_results = active_clients return True @@ -204,41 +186,12 @@ class AsusWrtDeviceScanner(DeviceScanner): host = '' - # match mac addresses to IP addresses in ARP table - for arp in result.arp: - if match.group('mac').lower() in \ - arp.decode('utf-8').lower(): - arp_match = _ARP_REGEX.search( - arp.decode('utf-8').lower()) - if not arp_match: - _LOGGER.warning("Could not parse arp row: %s", arp) - continue - - devices[arp_match.group('ip')] = { - 'host': host, - 'status': '', - 'ip': arp_match.group('ip'), - 'mac': match.group('mac').upper(), - } - - # match mac addresses to IP addresses in NVRAM table - for nvr in result.nvram: - if match.group('mac').upper() in nvr.decode('utf-8'): - nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8')) - if not nvram_match: - _LOGGER.warning("Could not parse nvr row: %s", nvr) - continue - - # skip current check if already in ARP table - if nvram_match.group('ip') in devices.keys(): - continue - - devices[nvram_match.group('ip')] = { - 'host': host, - 'status': 'IN_NVRAM', - 'ip': nvram_match.group('ip'), - 'mac': match.group('mac').upper(), - } + devices[match.group('mac').upper()] = { + 'host': host, + 'status': 'IN_ASSOCLIST', + 'ip': '', + 'mac': match.group('mac').upper(), + } else: for lease in result.leases: @@ -256,20 +209,23 @@ class AsusWrtDeviceScanner(DeviceScanner): if host == '*': host = '' - devices[match.group('ip')] = { + devices[match.group('mac')] = { 'host': host, 'status': '', 'ip': match.group('ip'), 'mac': match.group('mac').upper(), } - for neighbor in result.neighbors: - match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) - if not match: - _LOGGER.warning("Could not parse neighbor row: %s", neighbor) - continue - if match.group('ip') in devices: - devices[match.group('ip')]['status'] = match.group('status') + for neighbor in result.neighbors: + match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) + if not match: + _LOGGER.warning("Could not parse neighbor row: %s", + neighbor) + continue + if match.group('mac') in devices: + devices[match.group('mac')]['status'] = ( + match.group('status')) + return devices @@ -317,27 +273,19 @@ class SshConnection(_Connection): try: if not self.connected: self.connect() - self._ssh.sendline(_IP_NEIGH_CMD) - self._ssh.prompt() - neighbors = self._ssh.before.split(b'\n')[1:-1] if self._ap: - self._ssh.sendline(_ARP_CMD) - self._ssh.prompt() - arp_result = self._ssh.before.split(b'\n')[1:-1] + neighbors = [''] self._ssh.sendline(_WL_CMD) self._ssh.prompt() leases_result = self._ssh.before.split(b'\n')[1:-1] - self._ssh.sendline(_NVRAM_CMD) - self._ssh.prompt() - nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:] else: - arp_result = [''] - nvram_result = [''] + self._ssh.sendline(_IP_NEIGH_CMD) + self._ssh.prompt() + neighbors = self._ssh.before.split(b'\n')[1:-1] self._ssh.sendline(_LEASES_CMD) self._ssh.prompt() leases_result = self._ssh.before.split(b'\n')[1:-1] - return AsusWrtResult(neighbors, leases_result, arp_result, - nvram_result) + return AsusWrtResult(neighbors, leases_result) except exceptions.EOF as err: _LOGGER.error("Connection refused. SSH enabled?") self.disconnect() @@ -407,23 +355,14 @@ class TelnetConnection(_Connection): neighbors = (self._telnet.read_until(self._prompt_string). split(b'\n')[1:-1]) if self._ap: - self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii')) - arp_result = (self._telnet.read_until(self._prompt_string). - split(b'\n')[1:-1]) self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii')) leases_result = (self._telnet.read_until(self._prompt_string). split(b'\n')[1:-1]) - self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii')) - nvram_result = (self._telnet.read_until(self._prompt_string). - split(b'\n')[1].split(b'<')[1:]) else: - arp_result = [''] - nvram_result = [''] self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii')) leases_result = (self._telnet.read_until(self._prompt_string). split(b'\n')[1:-1]) - return AsusWrtResult(neighbors, leases_result, arp_result, - nvram_result) + return AsusWrtResult(neighbors, leases_result) except EOFError: _LOGGER.error("Unexpected response from router") self.disconnect() diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py index 0d0cdd7b1d5..23a94d093e2 100644 --- a/homeassistant/components/device_tracker/bbox.py +++ b/homeassistant/components/device_tracker/bbox.py @@ -52,8 +52,7 @@ class BboxDeviceScanner(DeviceScanner): if filter_named: return filter_named[0] - else: - return None + return None @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py index 35ffa754348..99ed06de486 100644 --- a/homeassistant/components/device_tracker/cisco_ios.py +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -87,21 +87,20 @@ class CiscoDeviceScanner(DeviceScanner): lines_result = lines_result[2:] for line in lines_result: - if len(line.split()) is 6: - parts = line.split() - if len(parts) != 6: - continue + parts = line.split() + if len(parts) != 6: + continue - # ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA', - # 'GigabitEthernet0'] - age = parts[2] - hw_addr = parts[3] + # ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA', + # 'GigabitEthernet0'] + age = parts[2] + hw_addr = parts[3] - if age != "-": - mac = _parse_cisco_mac_address(hw_addr) - age = int(age) - if age < 1: - last_results.append(mac) + if age != "-": + mac = _parse_cisco_mac_address(hw_addr) + age = int(age) + if age < 1: + last_results.append(mac) self.last_results = last_results return True diff --git a/homeassistant/components/device_tracker/linksys_smart.py b/homeassistant/components/device_tracker/linksys_smart.py index 2d7fbfea33c..e71502ba5ee 100644 --- a/homeassistant/components/device_tracker/linksys_smart.py +++ b/homeassistant/components/device_tracker/linksys_smart.py @@ -83,11 +83,14 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): if not connections: _LOGGER.debug("Device %s is not connected", mac) continue - name = device["friendlyName"] - properties = device["properties"] - for prop in properties: + + name = None + for prop in device["properties"]: if prop["name"] == "userDeviceName": name = prop["value"] + if not name: + name = device.get("friendlyName", device["deviceID"]) + _LOGGER.debug("Device %s is connected", mac) self.last_results[mac] = name except (KeyError, IndexError): diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index ced24edde48..aee584aa953 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -94,21 +94,20 @@ class LocativeView(HomeAssistantView): partial(self.see, dev_id=device, location_name=location_name, gps=gps_location)) return 'Setting location to not home' - else: - # Ignore the message if it is telling us to exit a zone that we - # aren't currently in. This occurs when a zone is entered - # before the previous zone was exited. The enter message will - # be sent first, then the exit message will be sent second. - return 'Ignoring exit from {} (already in {})'.format( - location_name, current_state) + + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered + # before the previous zone was exited. The enter message will + # be sent first, then the exit message will be sent second. + return 'Ignoring exit from {} (already in {})'.format( + location_name, current_state) elif direction == 'test': # In the app, a test message can be sent. Just return something to # the user to let them know that it works. return 'Received test message.' - else: - _LOGGER.error('Received unidentified message from Locative: %s', - direction) - return ('Received unidentified message: {}'.format(direction), - HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error('Received unidentified message from Locative: %s', + direction) + return ('Received unidentified message: {}'.format(direction), + HTTP_UNPROCESSABLE_ENTITY) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 99dfc3829d7..8a845adf0b8 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -95,8 +95,7 @@ class NmapDeviceScanner(DeviceScanner): if filter_named: return filter_named[0] - else: - return None + return None @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 6aa44c17c9a..f88fda03cf7 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -131,8 +131,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): plaintext_payload = decrypt_payload(topic, data['data']) if plaintext_payload is None: return None - else: - return validate_payload(topic, plaintext_payload, data_type) + return validate_payload(topic, plaintext_payload, data_type) if not isinstance(data, dict) or data.get('_type') != data_type: _LOGGER.debug("Skipping %s update for following data " diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index 46096e62fbb..fca4998f7b5 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -90,8 +90,7 @@ class TadoDeviceScanner(DeviceScanner): if filter_named: return filter_named[0] - else: - return None + return None @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 51394ae64fe..0b330c933d8 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -73,8 +73,8 @@ class TomatoDeviceScanner(DeviceScanner): if not filter_named or not filter_named[0]: return None - else: - return filter_named[0] + + return filter_named[0] @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_tomato_info(self): diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index b34f16ebb5a..88b0abe8ce4 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -9,7 +9,7 @@ import hashlib import logging import re import threading -from datetime import timedelta +from datetime import timedelta, datetime import requests import voluptuous as vol @@ -33,8 +33,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_scanner(hass, config): """Validate the configuration and return a TP-Link scanner.""" - for cls in [Tplink4DeviceScanner, Tplink3DeviceScanner, - Tplink2DeviceScanner, TplinkDeviceScanner]: + for cls in [Tplink5DeviceScanner, Tplink4DeviceScanner, + Tplink3DeviceScanner, Tplink2DeviceScanner, + TplinkDeviceScanner]: scanner = cls(config[DOMAIN]) if scanner.success_init: return scanner @@ -234,10 +235,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): self.stok = '' self.sysauth = '' return False - else: - _LOGGER.error( - "An unknown error happened while fetching data") - return False + _LOGGER.error( + "An unknown error happened while fetching data") + return False except ValueError: _LOGGER.error("Router didn't respond with JSON. " "Check if credentials are correct") @@ -350,3 +350,83 @@ class Tplink4DeviceScanner(TplinkDeviceScanner): self.last_results = [mac.replace("-", ":") for mac in mac_results] return True + + +class Tplink5DeviceScanner(TplinkDeviceScanner): + """This class queries a TP-Link EAP-225 AP with newer TP-Link FW.""" + + def scan_devices(self): + """Scan for new devices and return a list with found MAC IDs.""" + self._update_info() + return self.last_results.keys() + + # pylint: disable=no-self-use + def get_device_name(self, device): + """Get firmware doesn't save the name of the wireless device.""" + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Ensure the information from the TP-Link AP is up to date. + + Return boolean if scanning successful. + """ + with self.lock: + _LOGGER.info("Loading wireless clients...") + + base_url = 'http://{}'.format(self.host) + + header = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" + " rv:53.0) Gecko/20100101 Firefox/53.0", + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "Accept-Language: en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "Content-Type": "application/x-www-form-urlencoded; " + "charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Referer": "http://" + self.host + "/", + "Connection": "keep-alive", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + } + + password_md5 = hashlib.md5(self.password).hexdigest().upper() + + # create a session to handle cookie easier + session = requests.session() + session.get(base_url, headers=header) + + login_data = {"username": self.username, "password": password_md5} + session.post(base_url, login_data, headers=header) + + # a timestamp is required to be sent as get parameter + timestamp = int(datetime.now().timestamp() * 1e3) + + client_list_url = '{}/data/monitor.client.client.json'.format( + base_url) + + get_params = { + 'operation': 'load', + '_': timestamp + } + + response = session.get(client_list_url, + headers=header, + params=get_params) + session.close() + try: + list_of_devices = response.json() + except ValueError: + _LOGGER.error("AP didn't respond with JSON. " + "Check if credentials are correct.") + return False + + if list_of_devices: + self.last_results = { + device['MAC'].replace('-', ':'): device['DeviceName'] + for device in list_of_devices['data'] + } + return True + + return False diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index ace7c4455a9..a6646c8d0a1 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -12,11 +12,10 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -25,12 +24,9 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = '192.168.0.1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string, }) -CMD_LOGIN = 15 -CMD_LOGOUT = 16 CMD_DEVICES = 123 @@ -38,7 +34,7 @@ CMD_DEVICES = 123 def async_get_scanner(hass, config): """Return the UPC device scanner.""" scanner = UPCDeviceScanner(hass, config[DOMAIN]) - success_init = yield from scanner.async_login() + success_init = yield from scanner.async_initialize_token() return scanner if success_init else None @@ -50,7 +46,6 @@ class UPCDeviceScanner(DeviceScanner): """Initialize the scanner.""" self.hass = hass self.host = config[CONF_HOST] - self.password = config[CONF_PASSWORD] self.data = {} self.token = None @@ -65,21 +60,12 @@ class UPCDeviceScanner(DeviceScanner): self.websession = async_get_clientsession(hass) - @asyncio.coroutine - def async_logout(event): - """Logout from upc connect box.""" - yield from self._async_ws_function(CMD_LOGOUT) - self.token = None - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_logout) - @asyncio.coroutine def async_scan_devices(self): """Scan for new devices and return a list with found device IDs.""" if self.token is None: - reconnect = yield from self.async_login() - if not reconnect: + token_initialized = yield from self.async_initialize_token() + if not token_initialized: _LOGGER.error("Not connected to %s", self.host) return [] @@ -95,55 +81,42 @@ class UPCDeviceScanner(DeviceScanner): @asyncio.coroutine def async_get_device_name(self, device): - """Ge the firmware doesn't save the name of the wireless device.""" + """The firmware doesn't save the name of the wireless device.""" return None @asyncio.coroutine - def async_login(self): - """Login into firmware and get first token.""" + def async_initialize_token(self): + """Get first token.""" try: # get first token with async_timeout.timeout(10, loop=self.hass.loop): response = yield from self.websession.get( - "http://{}/common_page/login.html".format(self.host) + "http://{}/common_page/login.html".format(self.host), + headers=self.headers ) yield from response.text() self.token = response.cookies['sessionToken'].value - # login - data = yield from self._async_ws_function(CMD_LOGIN, { - 'Username': 'NULL', - 'Password': self.password, - }) - - # Successful? - return data is not None + return True except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Can not load login page from %s", self.host) return False @asyncio.coroutine - def _async_ws_function(self, function, additional_form=None): + def _async_ws_function(self, function): """Execute a command on UPC firmware webservice.""" - form_data = { - 'token': self.token, - 'fun': function - } - - if additional_form: - form_data.update(additional_form) - - redirects = function != CMD_DEVICES try: with async_timeout.timeout(10, loop=self.hass.loop): + # The 'token' parameter has to be first, and 'fun' second + # or the UPC firmware will return an error response = yield from self.websession.post( "http://{}/xml/getter.xml".format(self.host), - data=form_data, + data="token={}&fun={}".format(self.token, function), headers=self.headers, - allow_redirects=redirects + allow_redirects=False ) # error? diff --git a/homeassistant/components/device_tracker/xiaomi.py b/homeassistant/components/device_tracker/xiaomi.py index e87cae3d50b..a7b0a1ad326 100644 --- a/homeassistant/components/device_tracker/xiaomi.py +++ b/homeassistant/components/device_tracker/xiaomi.py @@ -101,10 +101,10 @@ class XiaomiDeviceScanner(DeviceScanner): result = _retrieve_list(self.host, self.token) if result: return result - else: - _LOGGER.info("Refreshing token and retrying device list refresh") - self.token = _get_token(self.host, self.username, self.password) - return _retrieve_list(self.host, self.token) + + _LOGGER.info("Refreshing token and retrying device list refresh") + self.token = _get_token(self.host, self.username, self.password) + return _retrieve_list(self.host, self.token) def _store_result(self, result): """Extract and store the device list in self.last_results.""" diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index fc239bf70c5..3dfe4b9731c 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -32,6 +32,7 @@ SERVICE_HASS_IOS_APP = 'hass_ios' SERVICE_IKEA_TRADFRI = 'ikea_tradfri' SERVICE_HASSIO = 'hassio' SERVICE_AXIS = 'axis' +SERVICE_APPLE_TV = 'apple_tv' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), @@ -40,6 +41,7 @@ SERVICE_HANDLERS = { SERVICE_IKEA_TRADFRI: ('tradfri', None), SERVICE_HASSIO: ('hassio', None), SERVICE_AXIS: ('axis', None), + SERVICE_APPLE_TV: ('apple_tv', None), 'philips_hue': ('light', 'hue'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), @@ -52,7 +54,6 @@ SERVICE_HANDLERS = { 'denonavr': ('media_player', 'denonavr'), 'samsung_tv': ('media_player', 'samsungtv'), 'yeelight': ('light', 'yeelight'), - 'apple_tv': ('media_player', 'apple_tv'), 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), 'harmony': ('remote', 'harmony'), diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py index eb430582ba7..c5aaba6152b 100644 --- a/homeassistant/components/dyson.py +++ b/homeassistant/components/dyson.py @@ -1,4 +1,8 @@ -"""Parent component for Dyson Pure Cool Link devices.""" +"""Parent component for Dyson Pure Cool Link devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/dyson/ +""" import logging @@ -9,7 +13,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ CONF_DEVICES -REQUIREMENTS = ['libpurecoollink==0.1.5'] +REQUIREMENTS = ['libpurecoollink==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index b8f330fd1f4..de70c35739d 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -1,4 +1,8 @@ -"""Support for Dyson Pure Cool link fan.""" +"""Support for Dyson Pure Cool link fan. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.dyson/ +""" import logging import asyncio from os import path @@ -79,9 +83,11 @@ class DysonPureCoolLinkDevice(FanEntity): def on_message(self, message): """Called when new messages received from the fan.""" - _LOGGER.debug( - "Message received for fan device %s : %s", self.name, message) - self.schedule_update_ha_state() + from libpurecoollink.dyson import DysonState + if isinstance(message, DysonState): + _LOGGER.debug("Message received for fan device %s : %s", self.name, + message) + self.schedule_update_ha_state() @property def should_poll(self): @@ -157,8 +163,7 @@ class DysonPureCoolLinkDevice(FanEntity): from libpurecoollink.const import FanSpeed if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed - else: - return int(self._device.state.speed) + return int(self._device.state.speed) return None @property diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index 24039f94c00..a18c173ecca 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): insteonhub = hass.data['insteon_local'] conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF)) - if len(conf_fans): + if conf_fans: for device_id in conf_fans: setup_fan(device_id, conf_fans[device_id], insteonhub, hass, add_devices) diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 13f755bcdf3..c649009b230 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -33,10 +33,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkFanDevice(WinkDevice, FanEntity): """Representation of a Wink fan.""" - def __init__(self, wink, hass): - """Initialize the fan.""" - super().__init__(wink, hass) - @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8d55ad879fa..443ff6f3852 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -6,10 +6,13 @@ import logging import os from aiohttp import web +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.config import find_config_file, load_yaml_config_file +from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback -from homeassistant.const import HTTP_NOT_FOUND -from homeassistant.components import api, group +from homeassistant.components import api from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.auth import is_trusted_ip from homeassistant.components.http.const import KEY_DEVELOPMENT @@ -23,6 +26,8 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/') +ATTR_THEMES = 'themes' +DEFAULT_THEME_COLOR = '#03A9F4' MANIFEST_JSON = { 'background_color': '#FFFFFF', 'description': 'Open-source home automation platform running on Python 3.', @@ -33,7 +38,7 @@ MANIFEST_JSON = { 'name': 'Home Assistant', 'short_name': 'Assistant', 'start_url': '/', - 'theme_color': '#03A9F4' + 'theme_color': DEFAULT_THEME_COLOR } for size in (192, 384, 512, 1024): @@ -45,11 +50,30 @@ for size in (192, 384, 512, 1024): DATA_PANELS = 'frontend_panels' DATA_INDEX_VIEW = 'frontend_index_view' +DATA_THEMES = 'frontend_themes' +DATA_DEFAULT_THEME = 'frontend_default_theme' +DEFAULT_THEME = 'default' + +PRIMARY_COLOR = 'primary-color' # To keep track we don't register a component twice (gives a warning) _REGISTERED_COMPONENTS = set() _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(ATTR_THEMES): vol.Schema({ + cv.string: {cv.string: cv.string} + }), + }), +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SET_THEME = 'set_theme' +SERVICE_RELOAD_THEMES = 'reload_themes' +SERVICE_SET_THEME_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, +}) + def register_built_in_panel(hass, component_name, sidebar_title=None, sidebar_icon=None, url_path=None, config=None): @@ -138,6 +162,8 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, if index_view: hass.http.app.router.add_route( 'get', '/{}'.format(url_path), index_view.get) + hass.http.app.router.add_route( + 'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get) def add_manifest_json_key(key, val): @@ -172,20 +198,81 @@ def setup(hass, config): # Now register their urls. if DATA_PANELS in hass.data: for url_path in hass.data[DATA_PANELS]: - hass.http.app.router.add_route('get', '/{}'.format(url_path), - index_view.get) + hass.http.app.router.add_route( + 'get', '/{}'.format(url_path), index_view.get) + hass.http.app.router.add_route( + 'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get) else: hass.data[DATA_PANELS] = {} register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template'): + 'dev-template', 'kiosk'): register_built_in_panel(hass, panel) + themes = config.get(DOMAIN, {}).get(ATTR_THEMES) + setup_themes(hass, themes) + return True +def setup_themes(hass, themes): + """Set up themes data and services.""" + hass.http.register_view(ThemesView) + hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME + if themes is None: + hass.data[DATA_THEMES] = {} + return + + hass.data[DATA_THEMES] = themes + + @callback + def update_theme_and_fire_event(): + """Update theme_color in manifest.""" + name = hass.data[DATA_DEFAULT_THEME] + themes = hass.data[DATA_THEMES] + if name != DEFAULT_THEME and PRIMARY_COLOR in themes[name]: + MANIFEST_JSON['theme_color'] = themes[name][PRIMARY_COLOR] + else: + MANIFEST_JSON['theme_color'] = DEFAULT_THEME_COLOR + hass.bus.async_fire(EVENT_THEMES_UPDATED, { + 'themes': themes, + 'default_theme': name, + }) + + @callback + def set_theme(call): + """Set backend-prefered theme.""" + data = call.data + name = data[CONF_NAME] + if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]: + _LOGGER.info("Theme %s set as default", name) + hass.data[DATA_DEFAULT_THEME] = name + update_theme_and_fire_event() + else: + _LOGGER.warning("Theme %s is not defined.", name) + + @callback + def reload_themes(_): + """Reload themes.""" + path = find_config_file(hass.config.config_dir) + new_themes = load_yaml_config_file(path)[DOMAIN].get(ATTR_THEMES, {}) + hass.data[DATA_THEMES] = new_themes + if hass.data[DATA_DEFAULT_THEME] not in new_themes: + hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME + update_theme_and_fire_event() + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + hass.services.register(DOMAIN, SERVICE_SET_THEME, + set_theme, + descriptions[SERVICE_SET_THEME], + SERVICE_SET_THEME_SCHEMA) + hass.services.register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes, + descriptions[SERVICE_RELOAD_THEMES]) + + class BootstrapView(HomeAssistantView): """View to bootstrap frontend with all needed data.""" @@ -212,7 +299,7 @@ class IndexView(HomeAssistantView): url = '/' name = 'frontend:index' requires_auth = False - extra_urls = ['/states', '/states/{entity_id}'] + extra_urls = ['/states', '/states/{extra}'] def __init__(self): """Initialize the frontend view.""" @@ -225,17 +312,10 @@ class IndexView(HomeAssistantView): ) @asyncio.coroutine - def get(self, request, entity_id=None): + def get(self, request, extra=None): """Serve the index view.""" hass = request.app['hass'] - if entity_id is not None: - state = hass.states.get(entity_id) - - if (not state or state.domain != 'group' or - not state.attributes.get(group.ATTR_VIEW)): - return self.json_message('Entity not found', HTTP_NOT_FOUND) - if request.app[KEY_DEVELOPMENT]: core_url = '/static/home-assistant-polymer/build/core.js' compatibility_url = \ @@ -295,3 +375,21 @@ class ManifestJSONView(HomeAssistantView): """Return the manifest.json.""" msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') return web.Response(body=msg, content_type="application/manifest+json") + + +class ThemesView(HomeAssistantView): + """View to return defined themes.""" + + requires_auth = False + url = '/api/themes' + name = 'api:themes' + + @callback + def get(self, request): + """Return themes.""" + hass = request.app['hass'] + + return self.json({ + 'themes': hass.data[DATA_THEMES], + 'default_theme': hass.data[DATA_DEFAULT_THEME], + }) diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml new file mode 100644 index 00000000000..7d56cbb7693 --- /dev/null +++ b/homeassistant/components/frontend/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available frontend services + +set_theme: + description: Set a theme unless the client selected per-device theme. + fields: + name: + description: Name of a predefined theme or 'default'. + example: 'light' + +reload_themes: + description: Reload themes from yaml config. diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 8a90f4bd584..67c8bbac817 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,21 +3,21 @@ FINGERPRINTS = { "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", "core.js": "d4a7cb8c80c62b536764e0e81385f6aa", - "frontend.html": "bdcde4695ce32595a9e1d813b9d7c5f9", - "mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c", + "frontend.html": "a7d4cb8260e8094342b5bd8c36c4bf5b", + "mdi.html": "e91f61a039ed0a9936e7ee5360da3870", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680", + "panels/ha-panel-automation.html": "72a5c1856cece8d9246328e84185ab0b", "panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505", "panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61", "panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc", - "panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852", + "panels/ha-panel-dev-service.html": "ac2c50e486927dc4443e93d79f08c06e", "panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869", - "panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139", - "panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229", + "panels/ha-panel-dev-template.html": "82cd543177c417e5c6612e07df851e6b", + "panels/ha-panel-hassio.html": "262d31efd9add719e0325da5cf79a096", "panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", + "panels/ha-panel-kiosk.html": "2ac2df41bd447600692a0054892fc094", "panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d", - "panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050", - "panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11", - "websocket_test.html": "575de64b431fe11c3785bf96d7813450" + "panels/ha-panel-map.html": "d3dae1400ec4e4cd7681d2aa79131d55", + "panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11" } diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index da6b5e0163a..41e9975e8b3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2,4 +2,4 @@ this._useContent&&u.Logical.saveChildNodes(this)},_setupRoot:function(){this._useContent&&(this._createLocalRoot(),this.dataHost||l(u.Logical.getChildNodes(this)))},_createLocalRoot:function(){this.shadyRoot=this.root,this.shadyRoot._distributionClean=!1,this.shadyRoot._hasDistributed=!1,this.shadyRoot._isShadyRoot=!0,this.shadyRoot._dirtyRoots=[];var e=this.shadyRoot._insertionPoints=!this._notes||this._notes._hasContent?this.shadyRoot.querySelectorAll("content"):[];u.Logical.saveChildNodes(this.shadyRoot);for(var t,o=0;o0?~setTimeout(e,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(e<0)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;t \ No newline at end of file +return performance.now()};else var t=function(){return Date.now()};var e=function(t,e,i){this.target=t,this.currentTime=e,this.timelineTime=i,this.type="cancel",this.bubbles=!1,this.cancelable=!1,this.currentTarget=t,this.defaultPrevented=!1,this.eventPhase=Event.AT_TARGET,this.timeStamp=Date.now()},i=window.Element.prototype.animate;window.Element.prototype.animate=function(n,r){var o=i.call(this,n,r);o._cancelHandlers=[],o.oncancel=null;var a=o.cancel;o.cancel=function(){a.call(this);var i=new e(this,null,t()),n=this._cancelHandlers.concat(this.oncancel?[this.oncancel]:[]);setTimeout(function(){n.forEach(function(t){t.call(i.target,i)})},0)};var s=o.addEventListener;o.addEventListener=function(t,e){"function"==typeof e&&"cancel"==t?this._cancelHandlers.push(e):s.call(this,t,e)};var u=o.removeEventListener;return o.removeEventListener=function(t,e){if("cancel"==t){var i=this._cancelHandlers.indexOf(e);i>=0&&this._cancelHandlers.splice(i,1)}else u.call(this,t,e)},o}}}(),function(t){var e=document.documentElement,i=null,n=!1;try{var r=getComputedStyle(e).getPropertyValue("opacity"),o="0"==r?"1":"0";i=e.animate({opacity:[o,o]},{duration:1}),i.currentTime=0,n=getComputedStyle(e).getPropertyValue("opacity")==o}catch(t){}finally{i&&i.cancel()}if(!n){var a=window.Element.prototype.animate;window.Element.prototype.animate=function(e,i){return window.Symbol&&Symbol.iterator&&Array.prototype.from&&e[Symbol.iterator]&&(e=Array.from(e)),Array.isArray(e)||null===e||(e=t.convertToArrayForm(e)),a.call(this,e,i)}}}(c),function(t,e,i){function n(t){var i=e.timeline;i.currentTime=t,i._discardAnimations(),0==i._animations.length?o=!1:requestAnimationFrame(n)}var r=window.requestAnimationFrame;window.requestAnimationFrame=function(t){return r(function(i){e.timeline._updateAnimationsPromises(),t(i),e.timeline._updateAnimationsPromises()})},e.AnimationTimeline=function(){this._animations=[],this.currentTime=void 0},e.AnimationTimeline.prototype={getAnimations:function(){return this._discardAnimations(),this._animations.slice()},_updateAnimationsPromises:function(){e.animationsWithPromises=e.animationsWithPromises.filter(function(t){return t._updatePromises()})},_discardAnimations:function(){this._updateAnimationsPromises(),this._animations=this._animations.filter(function(t){return"finished"!=t.playState&&"idle"!=t.playState})},_play:function(t){var i=new e.Animation(t,this);return this._animations.push(i),e.restartWebAnimationsNextTick(),i._updatePromises(),i._animation.play(),i._updatePromises(),i},play:function(t){return t&&t.remove(),this._play(t)}};var o=!1;e.restartWebAnimationsNextTick=function(){o||(o=!0,requestAnimationFrame(n))};var a=new e.AnimationTimeline;e.timeline=a;try{Object.defineProperty(window.document,"timeline",{configurable:!0,get:function(){return a}})}catch(t){}try{window.document.timeline=a}catch(t){}}(0,e),function(t,e,i){e.animationsWithPromises=[],e.Animation=function(e,i){if(this.id="",e&&e._id&&(this.id=e._id),this.effect=e,e&&(e._animation=this),!i)throw new Error("Animation with null timeline is not supported");this._timeline=i,this._sequenceNumber=t.sequenceNumber++,this._holdTime=0,this._paused=!1,this._isGroup=!1,this._animation=null,this._childAnimations=[],this._callback=null,this._oldPlayState="idle",this._rebuildUnderlyingAnimation(),this._animation.cancel(),this._updatePromises()},e.Animation.prototype={_updatePromises:function(){var t=this._oldPlayState,e=this.playState;return this._readyPromise&&e!==t&&("idle"==e?(this._rejectReadyPromise(),this._readyPromise=void 0):"pending"==t?this._resolveReadyPromise():"pending"==e&&(this._readyPromise=void 0)),this._finishedPromise&&e!==t&&("idle"==e?(this._rejectFinishedPromise(),this._finishedPromise=void 0):"finished"==e?this._resolveFinishedPromise():"finished"==t&&(this._finishedPromise=void 0)),this._oldPlayState=this.playState,this._readyPromise||this._finishedPromise},_rebuildUnderlyingAnimation:function(){this._updatePromises();var t,i,n,r,o=!!this._animation;o&&(t=this.playbackRate,i=this._paused,n=this.startTime,r=this.currentTime,this._animation.cancel(),this._animation._wrapper=null,this._animation=null),(!this.effect||this.effect instanceof window.KeyframeEffect)&&(this._animation=e.newUnderlyingAnimationForKeyframeEffect(this.effect),e.bindAnimationForKeyframeEffect(this)),(this.effect instanceof window.SequenceEffect||this.effect instanceof window.GroupEffect)&&(this._animation=e.newUnderlyingAnimationForGroup(this.effect),e.bindAnimationForGroup(this)),this.effect&&this.effect._onsample&&e.bindAnimationForCustomEffect(this),o&&(1!=t&&(this.playbackRate=t),null!==n?this.startTime=n:null!==r?this.currentTime=r:null!==this._holdTime&&(this.currentTime=this._holdTime),i&&this.pause()),this._updatePromises()},_updateChildren:function(){if(this.effect&&"idle"!=this.playState){var t=this.effect._timing.delay;this._childAnimations.forEach(function(i){this._arrangeChildren(i,t),this.effect instanceof window.SequenceEffect&&(t+=e.groupChildDuration(i.effect))}.bind(this))}},_setExternalAnimation:function(t){if(this.effect&&this._isGroup)for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index acaa25d4521..c04830c01a9 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 6e30534e2dc..7c079dc01f0 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 6e30534e2dc906d29ce497bf0a2eacb18e36f9c5 +Subproject commit 7c079dc01f03d7599762df1ac8c6eab8e4b90004 diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index e5029bc4289..efb18bac53f 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/mdi.html.gz b/homeassistant/components/frontend/www_static/mdi.html.gz index 99be72e33ea..d983d4a89f8 100644 Binary files a/homeassistant/components/frontend/www_static/mdi.html.gz and b/homeassistant/components/frontend/www_static/mdi.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html index 51724060959..4da09363373 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz index 804850c2558..7d3cf0d8152 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index a8477f931b4..ed1ff9fe85d 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index 1cb7353dd33..c76c9bc6de1 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html index c6005c3b639..bb27f9a2d7d 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index 32190ba8cec..9c3063c0d6c 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html index 906a7294b1f..f339403019b 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz index 4e2422b0c61..6a1c2905cf0 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-kiosk.html b/homeassistant/components/frontend/www_static/panels/ha-panel-kiosk.html new file mode 100644 index 00000000000..54698b63987 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-kiosk.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-kiosk.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-kiosk.html.gz new file mode 100644 index 00000000000..c2d53ec9d4a Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-kiosk.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html index 345b2140db8..500f812a738 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html @@ -1,5 +1,5 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz index 9e633a1d0cc..a5e2b5e4fff 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index c27b7abc28c..2a123e7a0ab 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","8f8a8203cafc8087fd3f0a40e5734751"],["/frontend/panels/dev-event-4886c821235492b1b92739b580d21c61.html","0f16df49a7d965ddc1fd55f7bd3ffd3f"],["/frontend/panels/dev-info-24e888ec7a8acd0c395b34396e9001bc.html","7bb116813e8dbab7bcfabdf4de3ec83f"],["/frontend/panels/dev-service-92c6be30b1af95791d5a6429df505852.html","e2025f3182edb738c22248ede9506554"],["/frontend/panels/dev-state-8f1a27c04db6329d31cfcc7d0d6a0869.html","002ea95ab67f5c06f9112008a81e571b"],["/frontend/panels/dev-template-d33a55b937b50cdfe8b6fae81f70a139.html","6b8fb2bd7aa5015264ddaae479141f6a"],["/frontend/panels/map-0ba605729197c4724ecc7310b08f7050.html","aa9df3b4bf299bfe0815527f52d5a50b"],["/static/compatibility-8e4c44b5f4288cc48ec1ba94a9bec812.js","4704a985ad259e324c3d8a0a40f6d937"],["/static/core-d4a7cb8c80c62b536764e0e81385f6aa.js","37e34ec6aa0fa155c7d50e2883be1ead"],["/static/frontend-bdcde4695ce32595a9e1d813b9d7c5f9.html","c01b4f5da77016285fe075202dbc4cb6"],["/static/mdi-c92bd28c434865d6cabb34cd3c0a3e4c.html","7e24a0584d139fef75d7678ef3c3b008"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.pathname.match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a)){var n=new Request(a,{credentials:"same-origin"});return fetch(n).then(function(t){if(!t.ok)throw new Error("Request for "+a+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(a,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e.delete(a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);t||(a=addDirectoryIndex(a,"index.html"),t=urlsToCacheKeys.has(a));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(a=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url)&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var a,n;for(a=0;a - - - - WebSocket debug - - - -
- -
-Examples:
-{
-  "id": 2, "type": "subscribe_events", "event_type": "state_changed"
-}
-
-{
-  "id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
-}
-
-{
-  "id": 4, "type": "unsubscribe_events", "subscription": 2
-}
-
-{
-  "id": 5, "type": "get_states"
-}
-
-{
-  "id": 6, "type": "get_config"
-}
-
-{
-  "id": 7, "type": "get_services"
-}
-
-{
-  "id": 8, "type": "get_panels"
-}
-      
-
-
- - - -
- -

-
-    
-    
-  
-
diff --git a/homeassistant/components/frontend/www_static/websocket_test.html.gz b/homeassistant/components/frontend/www_static/websocket_test.html.gz
deleted file mode 100644
index 8c526d1dfd2..00000000000
Binary files a/homeassistant/components/frontend/www_static/websocket_test.html.gz and /dev/null differ
diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py
index c628d04679f..d07e506e897 100644
--- a/homeassistant/components/group.py
+++ b/homeassistant/components/group.py
@@ -184,7 +184,6 @@ def expand_entity_ids(hass, entity_ids):
     Async friendly.
     """
     found_ids = []
-
     for entity_id in entity_ids:
         if not isinstance(entity_id, str):
             continue
@@ -196,9 +195,13 @@ def expand_entity_ids(hass, entity_ids):
             domain, _ = ha.split_entity_id(entity_id)
 
             if domain == DOMAIN:
+                child_entities = get_entity_ids(hass, entity_id)
+                if entity_id in child_entities:
+                    child_entities = list(child_entities)
+                    child_entities.remove(entity_id)
                 found_ids.extend(
                     ent_id for ent_id
-                    in expand_entity_ids(hass, get_entity_ids(hass, entity_id))
+                    in expand_entity_ids(hass, child_entities)
                     if ent_id not in found_ids)
 
             else:
@@ -223,7 +226,6 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
         return []
 
     entity_ids = group.attributes[ATTR_ENTITY_ID]
-
     if not domain_filter:
         return entity_ids
 
diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py
index e33a387eada..4ce60fed014 100644
--- a/homeassistant/components/hassio.py
+++ b/homeassistant/components/hassio.py
@@ -26,17 +26,7 @@ DOMAIN = 'hassio'
 DEPENDENCIES = ['http']
 
 TIMEOUT = 10
-
-ADDON_REST_COMMANDS = {
-    'install': ['POST'],
-    'uninstall': ['POST'],
-    'start': ['POST'],
-    'stop': ['POST'],
-    'update': ['POST'],
-    'options': ['POST'],
-    'info': ['GET'],
-    'logs': ['GET'],
-}
+NO_TIMEOUT = set(['homeassistant/update', 'host/update', 'supervisor/update'])
 
 
 @asyncio.coroutine
@@ -107,6 +97,8 @@ class HassIO(object):
 
         This method is a coroutine.
         """
+        read_timeout = 0 if path in NO_TIMEOUT else 300
+
         try:
             data = None
             headers = None
@@ -120,7 +112,7 @@ class HassIO(object):
             method = getattr(self.websession, request.method.lower())
             client = yield from method(
                 "http://{}/{}".format(self._ip, path), data=data,
-                headers=headers
+                headers=headers, timeout=read_timeout
             )
 
             return client
diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py
index 8faf8f30b1d..9800a15c16b 100644
--- a/homeassistant/components/history.py
+++ b/homeassistant/components/history.py
@@ -161,8 +161,8 @@ def states_to_json(hass, states, start_time, entity_id, filters=None):
         result[state.entity_id].append(state)
 
     # Append all changes to it
-    for entity_id, group in groupby(states, lambda state: state.entity_id):
-        result[entity_id].extend(group)
+    for ent_id, group in groupby(states, lambda state: state.entity_id):
+        result[ent_id].extend(group)
     return result
 
 
diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py
index aca3fd9a949..a8fc645ee4c 100644
--- a/homeassistant/components/homematic.py
+++ b/homeassistant/components/homematic.py
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.event import track_time_interval
 from homeassistant.config import load_yaml_config_file
 
-REQUIREMENTS = ['pyhomematic==0.1.28']
+REQUIREMENTS = ['pyhomematic==0.1.29']
 
 DOMAIN = 'homematic'
 
@@ -68,7 +68,7 @@ HM_DEVICE_TYPES = {
         'FillingLevel', 'ValveDrive', 'EcoLogic'],
     DISCOVER_CLIMATE: [
         'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
-        'MAXWallThermostat', 'IPThermostat'],
+        'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall'],
     DISCOVER_BINARY_SENSORS: [
         'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
         'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py
index 3308edf53b5..7c0c0e26649 100644
--- a/homeassistant/components/image_processing/dlib_face_detect.py
+++ b/homeassistant/components/image_processing/dlib_face_detect.py
@@ -15,7 +15,7 @@ from homeassistant.components.image_processing import (
 from homeassistant.components.image_processing.microsoft_face_identify import (
     ImageProcessingFaceEntity)
 
-REQUIREMENTS = ['face_recognition==0.1.14']
+REQUIREMENTS = ['face_recognition==0.2.0']
 
 _LOGGER = logging.getLogger(__name__)
 
diff --git a/homeassistant/components/image_processing/dlib_face_identify.py b/homeassistant/components/image_processing/dlib_face_identify.py
index 8baa7ac7ec3..8df6de91da4 100644
--- a/homeassistant/components/image_processing/dlib_face_identify.py
+++ b/homeassistant/components/image_processing/dlib_face_identify.py
@@ -16,7 +16,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import (
     ImageProcessingFaceEntity)
 import homeassistant.helpers.config_validation as cv
 
-REQUIREMENTS = ['face_recognition==0.1.14']
+REQUIREMENTS = ['face_recognition==0.2.0']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -57,9 +57,9 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
                 split_entity_id(camera_entity)[1])
 
         self._faces = {}
-        for name, face_file in faces.items():
+        for face_name, face_file in faces.items():
             image = face_recognition.load_image_file(face_file)
-            self._faces[name] = face_recognition.face_encodings(image)[0]
+            self._faces[face_name] = face_recognition.face_encodings(image)[0]
 
     @property
     def camera_entity(self):
diff --git a/homeassistant/components/insteon_local.py b/homeassistant/components/insteon_local.py
index 90e146d0e4f..711dafb6b73 100644
--- a/homeassistant/components/insteon_local.py
+++ b/homeassistant/components/insteon_local.py
@@ -67,10 +67,9 @@ def setup(hass, config):
     except requests.exceptions.RequestException:
         if insteonhub.http_code == 401:
             _LOGGER.error("Bad user/pass for insteon_local hub")
-            return False
         else:
             _LOGGER.error("Error on insteon_local hub check", exc_info=True)
-            return False
+        return False
 
     hass.data['insteon_local'] = insteonhub
 
diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py
index 9155304def7..ec533a7850b 100644
--- a/homeassistant/components/knx.py
+++ b/homeassistant/components/knx.py
@@ -13,7 +13,7 @@ from homeassistant.const import (
     EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT)
 from homeassistant.helpers.entity import Entity
 
-REQUIREMENTS = ['knxip==0.3.3']
+REQUIREMENTS = ['knxip==0.4']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -22,13 +22,17 @@ DEFAULT_PORT = 3671
 DOMAIN = 'knx'
 
 EVENT_KNX_FRAME_RECEIVED = 'knx_frame_received'
+EVENT_KNX_FRAME_SEND = 'knx_frame_send'
 
 KNXTUNNEL = None
+CONF_LISTEN = "listen"
 
 CONFIG_SCHEMA = vol.Schema({
     DOMAIN: vol.Schema({
         vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
         vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+        vol.Optional(CONF_LISTEN, default=[]):
+            vol.All(cv.ensure_list, [cv.string]),
     }),
 }, extra=vol.ALLOW_EXTRA)
 
@@ -38,12 +42,12 @@ def setup(hass, config):
     global KNXTUNNEL
 
     from knxip.ip import KNXIPTunnel
-    from knxip.core import KNXException
+    from knxip.core import KNXException, parse_group_address
 
     host = config[DOMAIN].get(CONF_HOST)
     port = config[DOMAIN].get(CONF_PORT)
 
-    if host is '0.0.0.0':
+    if host == '0.0.0.0':
         _LOGGER.debug("Will try to auto-detect KNX/IP gateway")
 
     KNXTUNNEL = KNXIPTunnel(host, port)
@@ -61,7 +65,69 @@ def setup(hass, config):
 
     _LOGGER.info("KNX IP tunnel to %s:%i established", host, port)
 
+    def received_knx_event(address, data):
+        """Process received KNX message."""
+        if len(data) == 1:
+            data = data[0]
+        hass.bus.fire('knx_event', {
+            'address': address,
+            'data': data
+        })
+
+    for listen in config[DOMAIN].get(CONF_LISTEN):
+        _LOGGER.debug("Registering listener for %s", listen)
+        try:
+            KNXTUNNEL.register_listener(parse_group_address(listen),
+                                        received_knx_event)
+        except KNXException as knxexception:
+            _LOGGER.error("Can't register KNX listener for address %s (%s)",
+                          listen, knxexception)
+
     hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel)
+
+    # Listen to KNX events and send them to the bus
+    def handle_knx_send(event):
+        """Bridge knx_frame_send events to the KNX bus."""
+        try:
+            addr = event.data["address"]
+        except KeyError:
+            _LOGGER.error("KNX group address is missing")
+            return
+
+        try:
+            data = event.data["data"]
+        except KeyError:
+            _LOGGER.error("KNX data block missing")
+            return
+
+        knxaddr = None
+        try:
+            addr = int(addr)
+        except ValueError:
+            pass
+
+        if knxaddr is None:
+            try:
+                knxaddr = parse_group_address(addr)
+            except KNXException:
+                _LOGGER.error("KNX address format incorrect")
+                return
+
+        knxdata = None
+        if isinstance(data, list):
+            knxdata = data
+        else:
+            try:
+                knxdata = [int(data) & 0xff]
+            except ValueError:
+                _LOGGER.error("KNX data format incorrect")
+                return
+
+        KNXTUNNEL.group_write(knxaddr, knxdata)
+
+    # Listen for when knx_frame_send event is fired
+    hass.bus.listen(EVENT_KNX_FRAME_SEND, handle_knx_send)
+
     return True
 
 
diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric.py
new file mode 100644
index 00000000000..b11d874127f
--- /dev/null
+++ b/homeassistant/components/lametric.py
@@ -0,0 +1,83 @@
+"""
+Support for LaMetric time.
+
+This is the base platform to support LaMetric components:
+Notify, Light, Mediaplayer
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/lametric/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['lmnotify==0.0.4']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_CLIENT_ID = 'client_id'
+CONF_CLIENT_SECRET = 'client_secret'
+
+DOMAIN = 'lametric'
+LAMETRIC_DEVICES = 'LAMETRIC_DEVICES'
+
+CONFIG_SCHEMA = vol.Schema({
+    DOMAIN: vol.Schema({
+        vol.Required(CONF_CLIENT_ID): cv.string,
+        vol.Required(CONF_CLIENT_SECRET): cv.string,
+    }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+# pylint: disable=broad-except
+def setup(hass, config):
+    """Set up the LaMetricManager."""
+    _LOGGER.debug("Setting up LaMetric platform")
+    conf = config[DOMAIN]
+    hlmn = HassLaMetricManager(client_id=conf[CONF_CLIENT_ID],
+                               client_secret=conf[CONF_CLIENT_SECRET])
+    devices = hlmn.manager().get_devices()
+
+    found = False
+    hass.data[DOMAIN] = hlmn
+    for dev in devices:
+        _LOGGER.debug("Discovered LaMetric device: %s", dev)
+        found = True
+
+    return found
+
+
+class HassLaMetricManager():
+    """
+    A class that encapsulated requests to the LaMetric manager.
+
+    As the original class does not have a re-connect feature that is needed
+    for applications running for a long time as the OAuth tokens expire. This
+    class implements this reconnect() feature.
+    """
+
+    def __init__(self, client_id, client_secret):
+        """Initialize HassLaMetricManager and connect to LaMetric."""
+        from lmnotify import LaMetricManager
+
+        _LOGGER.debug("Connecting to LaMetric")
+        self.lmn = LaMetricManager(client_id, client_secret)
+        self._client_id = client_id
+        self._client_secret = client_secret
+
+    def reconnect(self):
+        """
+        Reconnect to LaMetric.
+
+        This is usually necessary when the OAuth token is expired.
+        """
+        from lmnotify import LaMetricManager
+        _LOGGER.debug("Reconnecting to LaMetric")
+        self.lmn = LaMetricManager(self._client_id,
+                                   self._client_secret)
+
+    def manager(self):
+        """Return the global LaMetricManager instance."""
+        return self.lmn
diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py
index 9b717c64c86..f214d47fa1b 100644
--- a/homeassistant/components/light/avion.py
+++ b/homeassistant/components/light/avion.py
@@ -5,16 +5,19 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/light.avion/
 """
 import logging
+import time
 
 import voluptuous as vol
 
-from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
+from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME, \
+    CONF_USERNAME, CONF_PASSWORD
+
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light,
     PLATFORM_SCHEMA)
 import homeassistant.helpers.config_validation as cv
 
-REQUIREMENTS = ['avion==0.6']
+REQUIREMENTS = ['avion==0.7']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -27,20 +30,37 @@ DEVICE_SCHEMA = vol.Schema({
 
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
     vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
+    vol.Optional(CONF_USERNAME): cv.string,
+    vol.Optional(CONF_PASSWORD): cv.string,
 })
 
 
 def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up an Avion switch."""
+    # pylint: disable=import-error
+    import avion
+
     lights = []
+    if CONF_USERNAME in config and CONF_PASSWORD in config:
+        data = avion.avion_info(config[CONF_USERNAME], config[CONF_PASSWORD])
+        for location in data['locations']:
+            for avion_device in location['location']['devices']:
+                device = {}
+                mac = avion_device['device']['mac_address']
+                device['name'] = avion_device['device']['name']
+                device['key'] = location['location']['passphrase']
+                device['address'] = '%s%s:%s%s:%s%s:%s%s:%s%s:%s%s' % \
+                                    (mac[8], mac[9], mac[10], mac[11], mac[4],
+                                     mac[5], mac[6], mac[7], mac[0], mac[1],
+                                     mac[2], mac[3])
+                lights.append(AvionLight(device))
+
     for address, device_config in config[CONF_DEVICES].items():
         device = {}
         device['name'] = device_config[CONF_NAME]
         device['key'] = device_config[CONF_API_KEY]
         device['address'] = address
-        light = AvionLight(device)
-        if light.is_valid:
-            lights.append(light)
+        lights.append(AvionLight(device))
 
     add_devices(lights)
 
@@ -59,8 +79,6 @@ class AvionLight(Light):
         self._brightness = 255
         self._state = False
         self._switch = avion.avion(self._address, self._key)
-        self._switch.connect()
-        self.is_valid = True
 
     @property
     def unique_id(self):
@@ -99,7 +117,20 @@ class AvionLight(Light):
 
     def set_state(self, brightness):
         """Set the state of this lamp to the provided brightness."""
-        self._switch.set_brightness(brightness)
+        # pylint: disable=import-error
+        import avion
+
+        # Bluetooth LE is unreliable, and the connection may drop at any
+        # time. Make an effort to re-establish the link.
+        initial = time.monotonic()
+        while True:
+            if time.monotonic() - initial >= 10:
+                return False
+            try:
+                self._switch.set_brightness(brightness)
+                break
+            except avion.avionException:
+                self._switch.connect()
         return True
 
     def turn_on(self, **kwargs):
diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py
index c653f4b1ed4..bc45870f5f2 100644
--- a/homeassistant/components/light/decora.py
+++ b/homeassistant/components/light/decora.py
@@ -5,6 +5,8 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/light.decora/
 """
 import logging
+from functools import wraps
+import time
 
 import voluptuous as vol
 
@@ -30,6 +32,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
 })
 
 
+def retry(method):
+    """Retry bluetooth commands."""
+    @wraps(method)
+    def wrapper_retry(device, *args, **kwds):
+        """Try send command and retry on error."""
+        # pylint: disable=import-error
+        import decora
+
+        initial = time.monotonic()
+        while True:
+            if time.monotonic() - initial >= 10:
+                return None
+            try:
+                return method(device, *args, **kwds)
+            except (decora.decoraException, AttributeError):
+                # pylint: disable=protected-access
+                device._switch.connect()
+    return wrapper_retry
+
+
 def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up an Decora switch."""
     lights = []
@@ -39,8 +61,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
         device['key'] = device_config[CONF_API_KEY]
         device['address'] = address
         light = DecoraLight(device)
-        if light.is_valid:
-            lights.append(light)
+        lights.append(light)
 
     add_devices(lights)
 
@@ -57,10 +78,8 @@ class DecoraLight(Light):
         self._address = device['address']
         self._key = device["key"]
         self._switch = decora.decora(self._address, self._key)
-        self._switch.connect()
-        self._state = self._switch.get_on()
-        self._brightness = self._switch.get_brightness() * 2.55
-        self.is_valid = True
+        self._brightness = 0
+        self._state = False
 
     @property
     def unique_id(self):
@@ -97,27 +116,29 @@ class DecoraLight(Light):
         """We can read the actual state."""
         return False
 
+    @retry
     def set_state(self, brightness):
         """Set the state of this lamp to the provided brightness."""
-        self._switch.set_brightness(int(brightness / 2.55))
+        self._switch.set_brightness(brightness / 2.55)
         self._brightness = brightness
-        return True
 
+    @retry
     def turn_on(self, **kwargs):
         """Turn the specified or all lights on."""
         brightness = kwargs.get(ATTR_BRIGHTNESS)
-
         self._switch.on()
+        self._state = True
+
         if brightness is not None:
             self.set_state(brightness)
 
-        self._state = True
-
+    @retry
     def turn_off(self, **kwargs):
         """Turn the specified or all lights off."""
         self._switch.off()
         self._state = False
 
+    @retry
     def update(self):
         """Synchronise internal state with the actual light state."""
         self._brightness = self._switch.get_brightness() * 2.55
diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py
index 11bba4260d5..60865dd223e 100644
--- a/homeassistant/components/light/homematic.py
+++ b/homeassistant/components/light/homematic.py
@@ -23,8 +23,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
         return
 
     devices = []
-    for config in discovery_info[ATTR_DISCOVER_DEVICES]:
-        new_device = HMLight(hass, config)
+    for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
+        new_device = HMLight(hass, conf)
         new_device.link_homematic()
         devices.append(new_device)
 
@@ -38,10 +38,9 @@ class HMLight(HMDevice, Light):
     def brightness(self):
         """Return the brightness of this light between 0..255."""
         # Is dimmer?
-        if self._state is "LEVEL":
+        if self._state == "LEVEL":
             return int(self._hm_get_state() * 255)
-        else:
-            return None
+        return None
 
     @property
     def is_on(self):
@@ -58,7 +57,7 @@ class HMLight(HMDevice, Light):
 
     def turn_on(self, **kwargs):
         """Turn the light on."""
-        if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL":
+        if ATTR_BRIGHTNESS in kwargs and self._state == "LEVEL":
             percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255
             self._hmdevice.set_level(percent_bright, self._channel)
         else:
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 0f00519ab91..3344de02e75 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -27,7 +27,7 @@ from homeassistant.loader import get_component
 from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE
 import homeassistant.helpers.config_validation as cv
 
-REQUIREMENTS = ['phue==0.9']
+REQUIREMENTS = ['phue==1.0']
 
 # Track previously setup bridges
 _CONFIGURED_BRIDGES = {}
@@ -330,36 +330,31 @@ class HueLight(Light):
         """Return the brightness of this light between 0..255."""
         if self.is_group:
             return self.info['action'].get('bri')
-        else:
-            return self.info['state'].get('bri')
+        return self.info['state'].get('bri')
 
     @property
     def xy_color(self):
         """Return the XY color value."""
         if self.is_group:
             return self.info['action'].get('xy')
-        else:
-            return self.info['state'].get('xy')
+        return self.info['state'].get('xy')
 
     @property
     def color_temp(self):
         """Return the CT color value."""
         if self.is_group:
             return self.info['action'].get('ct')
-        else:
-            return self.info['state'].get('ct')
+        return self.info['state'].get('ct')
 
     @property
     def is_on(self):
         """Return true if device is on."""
         if self.is_group:
             return self.info['state']['any_on']
-        else:
-            if self.allow_unreachable:
-                return self.info['state']['on']
-            else:
-                return self.info['state']['reachable'] and \
-                    self.info['state']['on']
+        elif self.allow_unreachable:
+            return self.info['state']['on']
+        return self.info['state']['reachable'] and \
+            self.info['state']['on']
 
     @property
     def supported_features(self):
diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py
index 99fc557af20..ec91ba582fb 100644
--- a/homeassistant/components/light/hyperion.py
+++ b/homeassistant/components/light/hyperion.py
@@ -62,7 +62,7 @@ class Hyperion(Light):
 
     @property
     def name(self):
-        """Return the hostname of the server."""
+        """Return the name of the light."""
         return self._name
 
     @property
@@ -114,7 +114,8 @@ class Hyperion(Light):
         """Get the hostname of the remote."""
         response = self.json_request({'command': 'serverinfo'})
         if response:
-            self._name = response['info']['hostname']
+            if self._name == self._host:
+                self._name = response['info']['hostname']
             return True
         return False
 
diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py
index 6d5cd12c23e..d89d45e99a7 100644
--- a/homeassistant/components/light/knx.py
+++ b/homeassistant/components/light/knx.py
@@ -4,15 +4,22 @@ Support KNX Lighting actuators.
 For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/Light.knx/
 """
+import logging
 import voluptuous as vol
 
-from homeassistant.components.knx import (KNXConfig, KNXGroupAddress)
-from homeassistant.components.light import (Light, PLATFORM_SCHEMA)
+from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
+from homeassistant.components.light import (Light, PLATFORM_SCHEMA,
+                                            SUPPORT_BRIGHTNESS,
+                                            ATTR_BRIGHTNESS)
 from homeassistant.const import CONF_NAME
 import homeassistant.helpers.config_validation as cv
 
 CONF_ADDRESS = 'address'
 CONF_STATE_ADDRESS = 'state_address'
+CONF_BRIGHTNESS_ADDRESS = 'brightness_address'
+CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address'
+
+_LOGGER = logging.getLogger(__name__)
 
 DEFAULT_NAME = 'KNX Light'
 DEPENDENCIES = ['knx']
@@ -21,6 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
     vol.Required(CONF_ADDRESS): cv.string,
     vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
     vol.Optional(CONF_STATE_ADDRESS): cv.string,
+    vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
+    vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
 })
 
 
@@ -29,16 +38,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
     add_devices([KNXLight(hass, KNXConfig(config))])
 
 
-class KNXLight(KNXGroupAddress, Light):
+class KNXLight(KNXMultiAddressDevice, Light):
     """Representation of a KNX Light device."""
 
+    def __init__(self, hass, config):
+        """Initialize the cover."""
+        KNXMultiAddressDevice.__init__(
+            self, hass, config,
+            [],  # required
+            optional=['state', 'brightness', 'brightness_state']
+        )
+        self._hass = hass
+        self._supported_features = 0
+
+        if CONF_BRIGHTNESS_ADDRESS in config.config:
+            _LOGGER.debug("%s is dimmable", self.name)
+            self._supported_features = self._supported_features | \
+                SUPPORT_BRIGHTNESS
+            self._brightness = None
+
     def turn_on(self, **kwargs):
         """Turn the switch on.
 
         This sends a value 1 to the group address of the device
         """
-        self.group_write(1)
-        self._state = [1]
+        _LOGGER.debug("%s: turn on", self.name)
+        self.set_value('base', [1])
+        self._state = 1
+
+        if ATTR_BRIGHTNESS in kwargs:
+            self._brightness = kwargs[ATTR_BRIGHTNESS]
+            _LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
+                          self.name, self._brightness)
+            assert self._brightness <= 255
+            self.set_value("brightness", [self._brightness])
+
         if not self.should_poll:
             self.schedule_update_ha_state()
 
@@ -47,7 +81,36 @@ class KNXLight(KNXGroupAddress, Light):
 
         This sends a value 1 to the group address of the device
         """
-        self.group_write(0)
-        self._state = [0]
+        _LOGGER.debug("%s: turn off", self.name)
+        self.set_value('base', [0])
+        self._state = 0
         if not self.should_poll:
             self.schedule_update_ha_state()
+
+    @property
+    def is_on(self):
+        """Return True if the value is not 0 is on, else False."""
+        return self._state != 0
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        return self._supported_features
+
+    def update(self):
+        """Update device state."""
+        super().update()
+        if self.has_attribute('brightness_state'):
+            value = self.value('brightness_state')
+            if value is not None:
+                self._brightness = int.from_bytes(value, byteorder='little')
+                _LOGGER.debug("%s: brightness = %d",
+                              self.name, self._brightness)
+
+        if self.has_attribute('state'):
+            self._state = self.value("state")[0]
+            _LOGGER.debug("%s: state = %d", self.name, self._state)
+
+    def should_poll(self):
+        """No polling needed for a KNX light."""
+        return False
diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py
index f1784618d94..a32aa0c4a6b 100644
--- a/homeassistant/components/light/lifx.py
+++ b/homeassistant/components/light/lifx.py
@@ -33,23 +33,32 @@ import homeassistant.util.color as color_util
 
 _LOGGER = logging.getLogger(__name__)
 
-REQUIREMENTS = ['aiolifx==0.5.0', 'aiolifx_effects==0.1.0']
+REQUIREMENTS = ['aiolifx==0.5.2', 'aiolifx_effects==0.1.1']
 
 UDP_BROADCAST_PORT = 56700
 
+DISCOVERY_INTERVAL = 60
+MESSAGE_TIMEOUT = 1.0
+MESSAGE_RETRIES = 8
+UNAVAILABLE_GRACE = 90
+
 CONF_SERVER = 'server'
+CONF_BROADCAST = 'broadcast'
 
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
     vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
+    vol.Optional(CONF_BROADCAST, default='255.255.255.255'): cv.string,
 })
 
 SERVICE_LIFX_SET_STATE = 'lifx_set_state'
 
 ATTR_INFRARED = 'infrared'
+ATTR_ZONES = 'zones'
 ATTR_POWER = 'power'
 
 LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
     ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
+    ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]),
     ATTR_POWER: cv.boolean,
 })
 
@@ -105,11 +114,21 @@ LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
 })
 
 
+def aiolifx():
+    """Return the aiolifx module."""
+    import aiolifx as aiolifx_module
+    return aiolifx_module
+
+
+def aiolifx_effects():
+    """Return the aiolifx_effects module."""
+    import aiolifx_effects as aiolifx_effects_module
+    return aiolifx_effects_module
+
+
 @asyncio.coroutine
 def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     """Set up the LIFX platform."""
-    import aiolifx
-
     if sys.platform == 'win32':
         _LOGGER.warning("The lifx platform is known to not work on Windows. "
                         "Consider using the lifx_legacy platform instead")
@@ -117,7 +136,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     server_addr = config.get(CONF_SERVER)
 
     lifx_manager = LIFXManager(hass, async_add_devices)
-    lifx_discovery = aiolifx.LifxDiscovery(hass.loop, lifx_manager)
+    lifx_discovery = aiolifx().LifxDiscovery(
+        hass.loop,
+        lifx_manager,
+        discovery_interval=DISCOVERY_INTERVAL,
+        broadcast_ip=config.get(CONF_BROADCAST))
 
     coro = hass.loop.create_datagram_endpoint(
         lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
@@ -134,6 +157,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     return True
 
 
+def lifxwhite(device):
+    """Return whether this is a white-only bulb."""
+    return not aiolifx().products.features_map[device.product]["color"]
+
+
+def lifxmultizone(device):
+    """Return whether this is a multizone bulb/strip."""
+    return aiolifx().products.features_map[device.product]["multizone"]
+
+
 def find_hsbk(**kwargs):
     """Find the desired color from a number of possible inputs."""
     hue, saturation, brightness, kelvin = [None]*4
@@ -176,11 +209,10 @@ class LIFXManager(object):
 
     def __init__(self, hass, async_add_devices):
         """Initialize the light."""
-        import aiolifx_effects
         self.entities = {}
         self.hass = hass
         self.async_add_devices = async_add_devices
-        self.effects_conductor = aiolifx_effects.Conductor(loop=hass.loop)
+        self.effects_conductor = aiolifx_effects().Conductor(loop=hass.loop)
 
         descriptions = load_yaml_config_file(
             path.join(path.dirname(__file__), 'services.yaml'))
@@ -234,11 +266,10 @@ class LIFXManager(object):
     @asyncio.coroutine
     def start_effect(self, entities, service, **kwargs):
         """Start a light effect on entities."""
-        import aiolifx_effects
         devices = list(map(lambda l: l.device, entities))
 
         if service == SERVICE_EFFECT_PULSE:
-            effect = aiolifx_effects.EffectPulse(
+            effect = aiolifx_effects().EffectPulse(
                 power_on=kwargs.get(ATTR_POWER_ON),
                 period=kwargs.get(ATTR_PERIOD),
                 cycles=kwargs.get(ATTR_CYCLES),
@@ -253,7 +284,7 @@ class LIFXManager(object):
             if ATTR_BRIGHTNESS in kwargs:
                 brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
 
-            effect = aiolifx_effects.EffectColorloop(
+            effect = aiolifx_effects().EffectColorloop(
                 power_on=kwargs.get(ATTR_POWER_ON),
                 period=kwargs.get(ATTR_PERIOD),
                 change=kwargs.get(ATTR_CHANGE),
@@ -278,29 +309,39 @@ class LIFXManager(object):
 
     @callback
     def register(self, device):
-        """Handle for newly detected bulb."""
+        """Handler for newly detected bulb."""
+        self.hass.async_add_job(self.async_register(device))
+
+    @asyncio.coroutine
+    def async_register(self, device):
+        """Handler for newly detected bulb."""
         if device.mac_addr in self.entities:
             entity = self.entities[device.mac_addr]
-            entity.device = device
             entity.registered = True
             _LOGGER.debug("%s register AGAIN", entity.who)
-            self.hass.async_add_job(entity.async_update_ha_state())
+            yield from entity.async_update()
+            yield from entity.async_update_ha_state()
         else:
             _LOGGER.debug("%s register NEW", device.ip_addr)
-            device.get_version(self.got_version)
+            device.timeout = MESSAGE_TIMEOUT
+            device.retry_count = MESSAGE_RETRIES
+            device.unregister_timeout = UNAVAILABLE_GRACE
 
-    @callback
-    def got_version(self, device, msg):
-        """Request current color setting once we have the product version."""
-        device.get_color(self.ready)
+            ack = AwaitAioLIFX().wait
+            yield from ack(device.get_version)
+            yield from ack(device.get_color)
 
-    @callback
-    def ready(self, device, msg):
-        """Handle the device once all data is retrieved."""
-        entity = LIFXLight(device, self.effects_conductor)
-        _LOGGER.debug("%s register READY", entity.who)
-        self.entities[device.mac_addr] = entity
-        self.async_add_devices([entity])
+            if lifxwhite(device):
+                entity = LIFXWhite(device, self.effects_conductor)
+            elif lifxmultizone(device):
+                yield from ack(partial(device.get_color_zones, start_index=0))
+                entity = LIFXStrip(device, self.effects_conductor)
+            else:
+                entity = LIFXColor(device, self.effects_conductor)
+
+            _LOGGER.debug("%s register READY", entity.who)
+            self.entities[device.mac_addr] = entity
+            self.async_add_devices([entity])
 
     @callback
     def unregister(self, device):
@@ -315,9 +356,8 @@ class LIFXManager(object):
 class AwaitAioLIFX:
     """Wait for an aiolifx callback and return the message."""
 
-    def __init__(self, light):
+    def __init__(self):
         """Initialize the wrapper."""
-        self.light = light
         self.device = None
         self.message = None
         self.event = asyncio.Event()
@@ -359,15 +399,8 @@ class LIFXLight(Light):
         self.device = device
         self.effects_conductor = effects_conductor
         self.registered = True
-        self.product = device.product
         self.postponed_update = None
 
-    @property
-    def lifxwhite(self):
-        """Return whether this is a white-only bulb."""
-        # https://lan.developer.lifx.com/docs/lifx-products
-        return self.product in [10, 11, 18]
-
     @property
     def available(self):
         """Return the availability of the device."""
@@ -383,14 +416,6 @@ class LIFXLight(Light):
         """Return a string identifying the device."""
         return "%s (%s)" % (self.device.ip_addr, self.name)
 
-    @property
-    def rgb_color(self):
-        """Return the RGB value."""
-        hue, sat, bri, _ = self.device.color
-
-        return color_util.color_hsv_to_RGB(
-            hue, convert_16_to_8(sat), convert_16_to_8(bri))
-
     @property
     def brightness(self):
         """Return the brightness of this light between 0..255."""
@@ -407,26 +432,6 @@ class LIFXLight(Light):
         _LOGGER.debug("color_temp: %d", temperature)
         return temperature
 
-    @property
-    def min_mireds(self):
-        """Return the coldest color_temp that this light supports."""
-        # The 3 LIFX "White" products supported a limited temperature range
-        if self.lifxwhite:
-            kelvin = 6500
-        else:
-            kelvin = 9000
-        return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin))
-
-    @property
-    def max_mireds(self):
-        """Return the warmest color_temp that this light supports."""
-        # The 3 LIFX "White" products supported a limited temperature range
-        if self.lifxwhite:
-            kelvin = 2700
-        else:
-            kelvin = 2500
-        return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin))
-
     @property
     def is_on(self):
         """Return true if device is on."""
@@ -440,32 +445,6 @@ class LIFXLight(Light):
             return 'lifx_effect_' + effect.name
         return None
 
-    @property
-    def supported_features(self):
-        """Flag supported features."""
-        features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
-                    SUPPORT_TRANSITION | SUPPORT_EFFECT)
-
-        if not self.lifxwhite:
-            features |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
-
-        return features
-
-    @property
-    def effect_list(self):
-        """Return the list of supported effects for this light."""
-        if self.lifxwhite:
-            return [
-                SERVICE_EFFECT_PULSE,
-                SERVICE_EFFECT_STOP,
-            ]
-
-        return [
-            SERVICE_EFFECT_COLORLOOP,
-            SERVICE_EFFECT_PULSE,
-            SERVICE_EFFECT_STOP,
-        ]
-
     @asyncio.coroutine
     def update_after_transition(self, now):
         """Request new status after completion of the last transition."""
@@ -516,30 +495,36 @@ class LIFXLight(Light):
         power_on = kwargs.get(ATTR_POWER, False)
         power_off = not kwargs.get(ATTR_POWER, True)
 
-        hsbk = merge_hsbk(self.device.color, find_hsbk(**kwargs))
+        hsbk = find_hsbk(**kwargs)
 
         # Send messages, waiting for ACK each time
-        ack = AwaitAioLIFX(self).wait
+        ack = AwaitAioLIFX().wait
         bulb = self.device
 
         if not self.is_on:
             if power_off:
                 yield from ack(partial(bulb.set_power, False))
             if hsbk:
-                yield from ack(partial(bulb.set_color, hsbk))
+                yield from self.send_color(ack, hsbk, kwargs, duration=0)
             if power_on:
                 yield from ack(partial(bulb.set_power, True, duration=fade))
         else:
             if power_on:
                 yield from ack(partial(bulb.set_power, True))
             if hsbk:
-                yield from ack(partial(bulb.set_color, hsbk, duration=fade))
+                yield from self.send_color(ack, hsbk, kwargs, duration=fade)
             if power_off:
                 yield from ack(partial(bulb.set_power, False, duration=fade))
 
         # Schedule an update when the transition is complete
         self.update_later(fade)
 
+    @asyncio.coroutine
+    def send_color(self, ack, hsbk, kwargs, duration):
+        """Send a color change to the device."""
+        hsbk = merge_hsbk(self.device.color, hsbk)
+        yield from ack(partial(self.device.set_color, hsbk, duration=duration))
+
     @asyncio.coroutine
     def default_effect(self, **kwargs):
         """Start an effect with default parameters."""
@@ -555,5 +540,117 @@ class LIFXLight(Light):
         _LOGGER.debug("%s async_update", self.who)
         if self.available:
             # Avoid state ping-pong by holding off updates as the state settles
-            yield from asyncio.sleep(0.25)
-            yield from AwaitAioLIFX(self).wait(self.device.get_color)
+            yield from asyncio.sleep(0.3)
+            yield from AwaitAioLIFX().wait(self.device.get_color)
+
+
+class LIFXWhite(LIFXLight):
+    """Representation of a white-only LIFX light."""
+
+    @property
+    def min_mireds(self):
+        """Return the coldest color_temp that this light supports."""
+        return math.floor(color_util.color_temperature_kelvin_to_mired(6500))
+
+    @property
+    def max_mireds(self):
+        """Return the warmest color_temp that this light supports."""
+        return math.ceil(color_util.color_temperature_kelvin_to_mired(2700))
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
+                SUPPORT_EFFECT)
+
+    @property
+    def effect_list(self):
+        """Return the list of supported effects for this light."""
+        return [
+            SERVICE_EFFECT_PULSE,
+            SERVICE_EFFECT_STOP,
+        ]
+
+
+class LIFXColor(LIFXLight):
+    """Representation of a color LIFX light."""
+
+    @property
+    def min_mireds(self):
+        """Return the coldest color_temp that this light supports."""
+        return math.floor(color_util.color_temperature_kelvin_to_mired(9000))
+
+    @property
+    def max_mireds(self):
+        """Return the warmest color_temp that this light supports."""
+        return math.ceil(color_util.color_temperature_kelvin_to_mired(2500))
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
+                SUPPORT_EFFECT | SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR)
+
+    @property
+    def effect_list(self):
+        """Return the list of supported effects for this light."""
+        return [
+            SERVICE_EFFECT_COLORLOOP,
+            SERVICE_EFFECT_PULSE,
+            SERVICE_EFFECT_STOP,
+        ]
+
+    @property
+    def rgb_color(self):
+        """Return the RGB value."""
+        hue, sat, bri, _ = self.device.color
+
+        return color_util.color_hsv_to_RGB(
+            hue, convert_16_to_8(sat), convert_16_to_8(bri))
+
+
+class LIFXStrip(LIFXColor):
+    """Representation of a LIFX light strip with multiple zones."""
+
+    @asyncio.coroutine
+    def send_color(self, ack, hsbk, kwargs, duration):
+        """Send a color change to the device."""
+        bulb = self.device
+        num_zones = len(bulb.color_zones)
+
+        # Zone brightness is not reported when powered off
+        if not self.is_on and hsbk[2] is None:
+            yield from ack(partial(bulb.set_power, True))
+            yield from self.async_update()
+            yield from ack(partial(bulb.set_power, False))
+
+        zones = kwargs.get(ATTR_ZONES, None)
+        if zones is None:
+            zones = list(range(0, num_zones))
+        else:
+            zones = list(filter(lambda x: x < num_zones, set(zones)))
+
+        # Send new color to each zone
+        for index, zone in enumerate(zones):
+            zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk)
+            apply = 1 if (index == len(zones)-1) else 0
+            set_zone = partial(bulb.set_color_zones,
+                               start_index=zone,
+                               end_index=zone,
+                               color=zone_hsbk,
+                               duration=duration,
+                               apply=apply)
+            yield from ack(set_zone)
+
+    @asyncio.coroutine
+    def async_update(self):
+        """Update strip status."""
+        if self.available:
+            yield from super().async_update()
+
+            ack = AwaitAioLIFX().wait
+            bulb = self.device
+
+            # Each get_color_zones returns the next 8 zones
+            for zone in range(0, len(bulb.color_zones), 8):
+                yield from ack(partial(bulb.get_color_zones, start_index=zone))
diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py
index 2eb7c106bf2..ecb120e3079 100644
--- a/homeassistant/components/light/mystrom.py
+++ b/homeassistant/components/light/mystrom.py
@@ -8,10 +8,11 @@ import logging
 
 import voluptuous as vol
 
-from homeassistant.components.light import (
-    Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS)
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
 import homeassistant.helpers.config_validation as cv
+from homeassistant.components.light import (
+    Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
+    SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH)
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
 
 REQUIREMENTS = ['python-mystrom==0.3.8']
 
@@ -19,7 +20,15 @@ _LOGGER = logging.getLogger(__name__)
 
 DEFAULT_NAME = 'myStrom bulb'
 
-SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS)
+SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH)
+
+EFFECT_RAINBOW = 'rainbow'
+EFFECT_SUNRISE = 'sunrise'
+
+MYSTROM_EFFECT_LIST = [
+    EFFECT_RAINBOW,
+    EFFECT_SUNRISE,
+]
 
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
     vol.Required(CONF_HOST): cv.string,
@@ -58,7 +67,6 @@ class MyStromLight(Light):
         self._state = None
         self._available = False
         self._brightness = 0
-        self._rgb_color = [0, 0, 0]
 
     @property
     def name(self):
@@ -80,6 +88,11 @@ class MyStromLight(Light):
         """Return True if entity is available."""
         return self._available
 
+    @property
+    def effect_list(self):
+        """Return the list of supported effects."""
+        return MYSTROM_EFFECT_LIST
+
     @property
     def is_on(self):
         """Return true if light is on."""
@@ -90,12 +103,17 @@ class MyStromLight(Light):
         from pymystrom.exceptions import MyStromConnectionError
 
         brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
+        effect = kwargs.get(ATTR_EFFECT)
 
         try:
             if not self.is_on:
                 self._bulb.set_on()
             if brightness is not None:
                 self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255))
+            if effect == EFFECT_SUNRISE:
+                self._bulb.set_sunrise(30)
+            if effect == EFFECT_RAINBOW:
+                self._bulb.set_rainbow(30)
         except MyStromConnectionError:
             _LOGGER.warning("myStrom bulb not online")
 
diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py
index fb054407dff..0b56f1de0ac 100644
--- a/homeassistant/components/light/rflink.py
+++ b/homeassistant/components/light/rflink.py
@@ -10,13 +10,16 @@ import logging
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
 from homeassistant.components.rflink import (
-    CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES,
-    CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES,
+    CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS,
+    CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES,
+    CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES, CONF_NOGROUP_ALIASES,
     CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
     DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA,
-    DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
+    DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv,
+    remove_deprecated, vol)
 from homeassistant.const import (
     CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
+from homeassistant.helpers.deprecation import get_deprecated
 
 DEPENDENCIES = ['rflink']
 
@@ -39,15 +42,22 @@ PLATFORM_SCHEMA = vol.Schema({
             vol.Optional(CONF_TYPE):
                 vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE,
                         TYPE_HYBRID, TYPE_TOGGLE),
-            vol.Optional(CONF_ALIASSES, default=[]):
+            vol.Optional(CONF_ALIASES, default=[]):
                 vol.All(cv.ensure_list, [cv.string]),
-            vol.Optional(CONF_GROUP_ALIASSES, default=[]):
+            vol.Optional(CONF_GROUP_ALIASES, default=[]):
                 vol.All(cv.ensure_list, [cv.string]),
-            vol.Optional(CONF_NOGROUP_ALIASSES, default=[]):
+            vol.Optional(CONF_NOGROUP_ALIASES, default=[]):
                 vol.All(cv.ensure_list, [cv.string]),
             vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
             vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int),
             vol.Optional(CONF_GROUP, default=True): cv.boolean,
+            # deprecated config options
+            vol.Optional(CONF_ALIASSES):
+                vol.All(cv.ensure_list, [cv.string]),
+            vol.Optional(CONF_GROUP_ALIASSES):
+                vol.All(cv.ensure_list, [cv.string]),
+            vol.Optional(CONF_NOGROUP_ALIASSES):
+                vol.All(cv.ensure_list, [cv.string]),
         },
     }),
 })
@@ -103,6 +113,7 @@ def devices_from_config(domain_config, hass=None):
         entity_class = entity_class_for_type(entity_type)
 
         device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config)
+        remove_deprecated(device_config)
 
         is_hybrid = entity_class is HybridRflinkLight
 
@@ -117,25 +128,27 @@ def devices_from_config(domain_config, hass=None):
         device = entity_class(device_id, hass, **device_config)
         devices.append(device)
 
-        # Register entity (and aliasses) to listen to incoming rflink events
+        # Register entity (and aliases) to listen to incoming rflink events
 
-        # Device id and normal aliasses respond to normal and group command
+        # Device id and normal aliases respond to normal and group command
         hass.data[DATA_ENTITY_LOOKUP][
             EVENT_KEY_COMMAND][device_id].append(device)
         if config[CONF_GROUP]:
             hass.data[DATA_ENTITY_GROUP_LOOKUP][
                 EVENT_KEY_COMMAND][device_id].append(device)
-        for _id in config[CONF_ALIASSES]:
+        for _id in get_deprecated(config, CONF_ALIASES, CONF_ALIASSES):
             hass.data[DATA_ENTITY_LOOKUP][
                 EVENT_KEY_COMMAND][_id].append(device)
             hass.data[DATA_ENTITY_GROUP_LOOKUP][
                 EVENT_KEY_COMMAND][_id].append(device)
-        # group_aliasses only respond to group commands
-        for _id in config[CONF_GROUP_ALIASSES]:
+        # group_aliases only respond to group commands
+        for _id in get_deprecated(
+                config, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES):
             hass.data[DATA_ENTITY_GROUP_LOOKUP][
                 EVENT_KEY_COMMAND][_id].append(device)
-        # nogroup_aliasses only respond to normal commands
-        for _id in config[CONF_NOGROUP_ALIASSES]:
+        # nogroup_aliases only respond to normal commands
+        for _id in get_deprecated(
+                config, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES):
             hass.data[DATA_ENTITY_LOOKUP][
                 EVENT_KEY_COMMAND][_id].append(device)
 
diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml
index ef99f18fb42..782d4496442 100644
--- a/homeassistant/components/light/services.yaml
+++ b/homeassistant/components/light/services.yaml
@@ -117,6 +117,10 @@ lifx_set_state:
       description: Automatic infrared level (0..255) when light brightness is low
       example: 255
 
+    zones:
+      description: List of zone numbers to affect (8 per LIFX Z, starts at 0)
+      example: '[0,5]'
+
     transition:
       description: Duration in seconds it takes to get to the final state
       example: 10
diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py
index d2a6dadd9da..98af61ffb7d 100644
--- a/homeassistant/components/light/tellstick.py
+++ b/homeassistant/components/light/tellstick.py
@@ -60,8 +60,7 @@ class TellstickLight(TellstickDevice, Light):
         if tellcore_data is not None:
             brightness = int(tellcore_data)
             return brightness
-        else:
-            return None
+        return None
 
     def _update_model(self, new_state, data):
         """Update the device entity state to match the arguments."""
diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py
index 3d19c3fcea2..b3be93d82e2 100644
--- a/homeassistant/components/light/vera.py
+++ b/homeassistant/components/light/vera.py
@@ -50,8 +50,7 @@ class VeraLight(VeraDevice, Light):
         """Flag supported features."""
         if self._color:
             return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
-        else:
-            return SUPPORT_BRIGHTNESS
+        return SUPPORT_BRIGHTNESS
 
     def turn_on(self, **kwargs):
         """Turn the light on."""
diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py
index b936e9addd6..93b23a9d7ba 100644
--- a/homeassistant/components/light/wink.py
+++ b/homeassistant/components/light/wink.py
@@ -33,10 +33,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
 class WinkLight(WinkDevice, Light):
     """Representation of a Wink light."""
 
-    def __init__(self, wink, hass):
-        """Initialize the Wink device."""
-        super().__init__(wink, hass)
-
     @asyncio.coroutine
     def async_added_to_hass(self):
         """Callback when entity is added to hass."""
@@ -52,8 +48,7 @@ class WinkLight(WinkDevice, Light):
         """Return the brightness of the light."""
         if self.wink.brightness() is not None:
             return int(self.wink.brightness() * 255)
-        else:
-            return None
+        return None
 
     @property
     def rgb_color(self):
diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py
index 0c230877625..619723d3168 100644
--- a/homeassistant/components/light/zha.py
+++ b/homeassistant/components/light/zha.py
@@ -29,7 +29,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     except (AttributeError, KeyError):
         pass
 
-    async_add_devices([Light(**discovery_info)])
+    async_add_devices([Light(**discovery_info)], update_before_add=True)
 
 
 class Light(zha.Entity, light.Light):
@@ -46,10 +46,10 @@ class Light(zha.Entity, light.Light):
         self._brightness = None
 
         import bellows.zigbee.zcl.clusters as zcl_clusters
-        if zcl_clusters.general.LevelControl.cluster_id in self._clusters:
+        if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters:
             self._supported_features |= light.SUPPORT_BRIGHTNESS
             self._brightness = 0
-        if zcl_clusters.lighting.Color.cluster_id in self._clusters:
+        if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
             # Not sure all color lights necessarily support this directly
             # Should we emulate it?
             self._supported_features |= light.SUPPORT_COLOR_TEMP
@@ -99,16 +99,19 @@ class Light(zha.Entity, light.Light):
                 duration
             )
             self._state = 1
+            self.hass.async_add_job(self.async_update_ha_state())
             return
 
         yield from self._endpoint.on_off.on()
         self._state = 1
+        self.hass.async_add_job(self.async_update_ha_state())
 
     @asyncio.coroutine
     def async_turn_off(self, **kwargs):
         """Turn the entity off."""
         yield from self._endpoint.on_off.off()
         self._state = 0
+        self.hass.async_add_job(self.async_update_ha_state())
 
     @property
     def brightness(self):
@@ -129,3 +132,52 @@ class Light(zha.Entity, light.Light):
     def supported_features(self):
         """Flag supported features."""
         return self._supported_features
+
+    @asyncio.coroutine
+    def async_update(self):
+        """Retrieve latest state."""
+        _LOGGER.debug("%s async_update", self.entity_id)
+
+        @asyncio.coroutine
+        def safe_read(cluster, attributes):
+            """Swallow all exceptions from network read.
+
+            If we throw during initialization, setup fails. Rather have an
+            entity that exists, but is in a maybe wrong state, than no entity.
+            """
+            try:
+                result, _ = yield from cluster.read_attributes(
+                    attributes,
+                    allow_cache=False,
+                )
+                return result
+            except Exception:  # pylint: disable=broad-except
+                return {}
+
+        result = yield from safe_read(self._endpoint.on_off, ['on_off'])
+        self._state = result.get('on_off', self._state)
+
+        if self._supported_features & light.SUPPORT_BRIGHTNESS:
+            result = yield from safe_read(self._endpoint.level,
+                                          ['current_level'])
+            self._brightness = result.get('current_level', self._brightness)
+
+        if self._supported_features & light.SUPPORT_COLOR_TEMP:
+            result = yield from safe_read(self._endpoint.light_color,
+                                          ['color_temperature'])
+            self._color_temp = result.get('color_temperature',
+                                          self._color_temp)
+
+        if self._supported_features & light.SUPPORT_XY_COLOR:
+            result = yield from safe_read(self._endpoint.light_color,
+                                          ['current_x', 'current_y'])
+            if 'current_x' in result and 'current_y' in result:
+                self._xy_color = (result['current_x'], result['current_y'])
+
+    @property
+    def should_poll(self) -> bool:
+        """Return True if entity has to be polled for state.
+
+        False if entity pushes its state to HA.
+        """
+        return False
diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py
index 752928f71a4..64c6530dd2b 100644
--- a/homeassistant/components/light/zwave.py
+++ b/homeassistant/components/light/zwave.py
@@ -55,16 +55,14 @@ def get_device(node, values, node_config, **kwargs):
 
     if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
         return ZwaveColorLight(values, refresh, delay)
-    else:
-        return ZwaveDimmer(values, refresh, delay)
+    return ZwaveDimmer(values, refresh, delay)
 
 
 def brightness_state(value):
     """Return the brightness and state."""
     if value.data > 0:
         return round((value.data / 99) * 255, 0), STATE_ON
-    else:
-        return 0, STATE_OFF
+    return 0, STATE_OFF
 
 
 def ct_to_rgb(temp):
diff --git a/homeassistant/components/lock/lockitron.py b/homeassistant/components/lock/lockitron.py
index eb301eeb013..ea79848f60c 100644
--- a/homeassistant/components/lock/lockitron.py
+++ b/homeassistant/components/lock/lockitron.py
@@ -86,7 +86,7 @@ class Lockitron(LockDevice):
             self.device_id, self.access_token, requested_state), timeout=5)
         if response.status_code == 200:
             return response.json()['state']
-        else:
-            _LOGGER.error("Error setting lock state: %s\n%s",
-                          requested_state, response.text)
-            return self._state
+
+        _LOGGER.error("Error setting lock state: %s\n%s",
+                      requested_state, response.text)
+        return self._state
diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py
index 6fbf9edf954..020fc00ab9a 100644
--- a/homeassistant/components/lock/wink.py
+++ b/homeassistant/components/lock/wink.py
@@ -119,10 +119,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
 class WinkLockDevice(WinkDevice, LockDevice):
     """Representation of a Wink lock."""
 
-    def __init__(self, wink, hass):
-        """Initialize the lock."""
-        super().__init__(wink, hass)
-
     @asyncio.coroutine
     def async_added_to_hass(self):
         """Callback when entity is added to hass."""
diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py
index e9199290e30..009d4cf1069 100644
--- a/homeassistant/components/lock/zwave.py
+++ b/homeassistant/components/lock/zwave.py
@@ -251,7 +251,7 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
 
         if not alarm_type:
             return
-        if alarm_type is 21:
+        if alarm_type == 21:
             self._lock_status = '{}{}'.format(
                 LOCK_ALARM_TYPE.get(str(alarm_type)),
                 MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level)))
@@ -260,7 +260,7 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
             self._lock_status = '{}{}'.format(
                 LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level))
             return
-        if alarm_type is 161:
+        if alarm_type == 161:
             self._lock_status = '{}{}'.format(
                 LOCK_ALARM_TYPE.get(str(alarm_type)),
                 TAMPER_ALARM_LEVEL.get(str(alarm_level)))
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index 29c69409774..4facf1334c6 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -367,14 +367,12 @@ def _entry_message_from_state(domain, state):
     if domain == 'device_tracker':
         if state.state == STATE_NOT_HOME:
             return 'is away'
-        else:
-            return 'is at {}'.format(state.state)
+        return 'is at {}'.format(state.state)
 
     elif domain == 'sun':
         if state.state == sun.STATE_ABOVE_HORIZON:
             return 'has risen'
-        else:
-            return 'has set'
+        return 'has set'
 
     elif state.state == STATE_ON:
         # Future: combine groups and its entity entries ?
diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py
index d9b943762dc..819844325d1 100644
--- a/homeassistant/components/lutron.py
+++ b/homeassistant/components/lutron.py
@@ -41,7 +41,7 @@ def setup(hass, base_config):
 
     config = base_config.get(DOMAIN)
     hass.data[LUTRON_CONTROLLER] = Lutron(
-        config[CONF_HOST], config[CONF_USERNAME], config[CONF_USERNAME])
+        config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD])
 
     hass.data[LUTRON_CONTROLLER].load_xml_db()
     hass.data[LUTRON_CONTROLLER].connect()
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
new file mode 100644
index 00000000000..c7d019973a3
--- /dev/null
+++ b/homeassistant/components/media_extractor.py
@@ -0,0 +1,101 @@
+"""
+Decorator service for the media_player.play_media service.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/media_extractor/
+"""
+import logging
+import os
+
+from homeassistant.components.media_player import (
+    ATTR_MEDIA_CONTENT_ID, DOMAIN as MEDIA_PLAYER_DOMAIN,
+    MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, SERVICE_PLAY_MEDIA)
+from homeassistant.config import load_yaml_config_file
+
+REQUIREMENTS = ['youtube_dl==2017.7.9']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'media_extractor'
+DEPENDENCIES = ['media_player']
+
+
+def setup(hass, config):
+    """Set up the media extractor service."""
+    descriptions = load_yaml_config_file(
+        os.path.join(os.path.dirname(__file__),
+                     'media_player', 'services.yaml'))
+
+    def play_media(call):
+        """Get stream URL and send it to the media_player.play_media."""
+        media_url = call.data.get(ATTR_MEDIA_CONTENT_ID)
+
+        try:
+            stream_url = get_media_stream_url(media_url)
+        except YDException:
+            _LOGGER.error("Could not retrieve data for the URL: %s",
+                          media_url)
+            return
+        else:
+            data = {k: v for k, v in call.data.items()
+                    if k != ATTR_MEDIA_CONTENT_ID}
+            data[ATTR_MEDIA_CONTENT_ID] = stream_url
+
+            hass.async_add_job(
+                hass.services.async_call(
+                    MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data)
+            )
+
+    hass.services.register(DOMAIN,
+                           SERVICE_PLAY_MEDIA,
+                           play_media,
+                           description=descriptions[SERVICE_PLAY_MEDIA],
+                           schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA)
+
+    return True
+
+
+class YDException(Exception):
+    """General service exception."""
+
+    pass
+
+
+def get_media_stream_url(media_url):
+    """Extract stream URL from the media URL."""
+    from youtube_dl import YoutubeDL
+    from youtube_dl.utils import DownloadError, ExtractorError
+
+    ydl = YoutubeDL({'quiet': True, 'logger': _LOGGER})
+
+    try:
+        all_media_streams = ydl.extract_info(media_url, process=False)
+    except DownloadError:
+        # This exception will be logged by youtube-dl itself
+        raise YDException()
+
+    if 'entries' in all_media_streams:
+        _LOGGER.warning("Playlists are not supported, "
+                        "looking for the first video")
+        try:
+            selected_stream = next(all_media_streams['entries'])
+        except StopIteration:
+            _LOGGER.error("Playlist is empty")
+            raise YDException()
+    else:
+        selected_stream = all_media_streams
+
+    try:
+        media_info = ydl.process_ie_result(selected_stream, download=False)
+    except (ExtractorError, DownloadError):
+        # This exception will be logged by youtube-dl itself
+        raise YDException()
+
+    format_selector = ydl.build_format_selector('best')
+
+    try:
+        best_quality_stream = next(format_selector(media_info))
+    except (KeyError, StopIteration):
+        best_quality_stream = media_info
+
+    return best_quality_stream['url']
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index e17935814cc..35981d89d6d 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -801,8 +801,7 @@ class MediaPlayerDevice(Entity):
 
         if self.state in [STATE_OFF, STATE_IDLE]:
             return self.async_turn_on()
-        else:
-            return self.async_turn_off()
+        return self.async_turn_off()
 
     @asyncio.coroutine
     def async_volume_up(self):
@@ -845,8 +844,7 @@ class MediaPlayerDevice(Entity):
 
         if self.state == STATE_PLAYING:
             return self.async_media_pause()
-        else:
-            return self.async_media_play()
+        return self.async_media_play()
 
     @property
     def entity_picture(self):
diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py
index 64b8e826cfb..293c6e51d52 100644
--- a/homeassistant/components/media_player/anthemav.py
+++ b/homeassistant/components/media_player/anthemav.py
@@ -102,8 +102,7 @@ class AnthemAVR(MediaPlayerDevice):
             return STATE_ON
         elif pwrstate is False:
             return STATE_OFF
-        else:
-            return STATE_UNKNOWN
+        return STATE_UNKNOWN
 
     @property
     def is_volume_muted(self):
diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py
index 97114a6bc84..3ecb1c0922e 100644
--- a/homeassistant/components/media_player/apple_tv.py
+++ b/homeassistant/components/media_player/apple_tv.py
@@ -6,70 +6,41 @@ https://home-assistant.io/components/media_player.apple_tv/
 """
 import asyncio
 import logging
-import hashlib
-
-import voluptuous as vol
 
 from homeassistant.core import callback
+from homeassistant.components.apple_tv import (
+    ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES)
 from homeassistant.components.media_player import (
     SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
     SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_ON,
-    SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
+    SUPPORT_TURN_OFF, MediaPlayerDevice, MEDIA_TYPE_MUSIC,
     MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW)
 from homeassistant.const import (
     STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST,
     STATE_OFF, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-import homeassistant.helpers.config_validation as cv
 import homeassistant.util.dt as dt_util
 
 
-REQUIREMENTS = ['pyatv==0.2.1']
+DEPENDENCIES = ['apple_tv']
 
 _LOGGER = logging.getLogger(__name__)
 
-CONF_LOGIN_ID = 'login_id'
-CONF_START_OFF = 'start_off'
-
-DEFAULT_NAME = 'Apple TV'
-
-DATA_APPLE_TV = 'apple_tv'
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
-    vol.Required(CONF_HOST): cv.string,
-    vol.Required(CONF_LOGIN_ID): cv.string,
-    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
-    vol.Optional(CONF_START_OFF, default=False): cv.boolean
-})
-
 
 @asyncio.coroutine
 def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     """Set up the Apple TV platform."""
-    import pyatv
+    if not discovery_info:
+        return
 
-    if discovery_info is not None:
-        name = discovery_info['name']
-        host = discovery_info['host']
-        login_id = discovery_info['properties']['hG']
-        start_off = False
-    else:
-        name = config.get(CONF_NAME)
-        host = config.get(CONF_HOST)
-        login_id = config.get(CONF_LOGIN_ID)
-        start_off = config.get(CONF_START_OFF)
+    # Manage entity cache for service handler
+    if DATA_ENTITIES not in hass.data:
+        hass.data[DATA_ENTITIES] = []
 
-    if DATA_APPLE_TV not in hass.data:
-        hass.data[DATA_APPLE_TV] = []
-
-    if host in hass.data[DATA_APPLE_TV]:
-        return False
-    hass.data[DATA_APPLE_TV].append(host)
-
-    details = pyatv.AppleTVDevice(name, host, login_id)
-    session = async_get_clientsession(hass)
-    atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
-    entity = AppleTvDevice(atv, name, start_off)
+    name = discovery_info[CONF_NAME]
+    host = discovery_info[CONF_HOST]
+    atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
+    power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
+    entity = AppleTvDevice(atv, name, power)
 
     @callback
     def on_hass_stop(event):
@@ -78,44 +49,39 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
 
     hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
 
+    if entity not in hass.data[DATA_ENTITIES]:
+        hass.data[DATA_ENTITIES].append(entity)
+
     async_add_devices([entity])
 
 
 class AppleTvDevice(MediaPlayerDevice):
     """Representation of an Apple TV device."""
 
-    def __init__(self, atv, name, is_off):
+    def __init__(self, atv, name, power):
         """Initialize the Apple TV device."""
-        self._atv = atv
+        self.atv = atv
         self._name = name
-        self._is_off = is_off
         self._playing = None
-        self._artwork_hash = None
-        self._atv.push_updater.listener = self
+        self._power = power
+        self._power.listeners.append(self)
+        self.atv.push_updater.listener = self
 
     @asyncio.coroutine
     def async_added_to_hass(self):
         """Handle when an entity is about to be added to Home Assistant."""
-        if not self._is_off:
-            self._atv.push_updater.start()
-
-    @callback
-    def _set_power_off(self, is_off):
-        """Set the power to off."""
-        self._playing = None
-        self._artwork_hash = None
-        self._is_off = is_off
-        if is_off:
-            self._atv.push_updater.stop()
-        else:
-            self._atv.push_updater.start()
-        self.hass.async_add_job(self.async_update_ha_state())
+        self._power.init()
 
     @property
     def name(self):
         """Return the name of the device."""
         return self._name
 
+    @property
+    def unique_id(self):
+        """Return an unique ID."""
+        return self.atv.metadata.device_id
+
     @property
     def should_poll(self):
         """No polling needed."""
@@ -124,47 +90,30 @@ class AppleTvDevice(MediaPlayerDevice):
     @property
     def state(self):
         """Return the state of the device."""
-        if self._is_off:
+        if not self._power.turned_on:
             return STATE_OFF
 
         if self._playing is not None:
             from pyatv import const
             state = self._playing.play_state
-            if state == const.PLAY_STATE_NO_MEDIA:
-                return STATE_IDLE
-            elif state == const.PLAY_STATE_PLAYING or \
+            if state == const.PLAY_STATE_NO_MEDIA or \
                     state == const.PLAY_STATE_LOADING:
+                return STATE_IDLE
+            elif state == const.PLAY_STATE_PLAYING:
                 return STATE_PLAYING
             elif state == const.PLAY_STATE_PAUSED or \
                     state == const.PLAY_STATE_FAST_FORWARD or \
                     state == const.PLAY_STATE_FAST_BACKWARD:
                 # Catch fast forward/backward here so "play" is default action
                 return STATE_PAUSED
-            else:
-                return STATE_STANDBY  # Bad or unknown state?
+            return STATE_STANDBY  # Bad or unknown state?
 
     @callback
     def playstatus_update(self, updater, playing):
         """Print what is currently playing when it changes."""
         self._playing = playing
-
-        if self.state == STATE_IDLE:
-            self._artwork_hash = None
-        elif self._has_playing_media_changed(playing):
-            base = str(playing.title) + str(playing.artist) + \
-                str(playing.album) + str(playing.total_time)
-            self._artwork_hash = hashlib.md5(
-                base.encode('utf-8')).hexdigest()
-
         self.hass.async_add_job(self.async_update_ha_state())
 
-    def _has_playing_media_changed(self, new_playing):
-        if self._playing is None:
-            return True
-        old_playing = self._playing
-        return new_playing.media_type != old_playing.media_type or \
-            new_playing.title != old_playing.title
-
     @callback
     def playstatus_error(self, updater, exception):
         """Inform about an error and restart push updates."""
@@ -177,7 +126,6 @@ class AppleTvDevice(MediaPlayerDevice):
         # implemented here later.
         updater.start(initial_delay=10)
         self._playing = None
-        self._artwork_hash = None
         self.hass.async_add_job(self.async_update_ha_state())
 
     @property
@@ -215,18 +163,18 @@ class AppleTvDevice(MediaPlayerDevice):
     @asyncio.coroutine
     def async_play_media(self, media_type, media_id, **kwargs):
         """Send the play_media command to the media player."""
-        yield from self._atv.remote_control.play_url(media_id, 0)
+        yield from self.atv.airplay.play_url(media_id)
 
     @property
     def media_image_hash(self):
         """Hash value for media image."""
-        if self.state != STATE_IDLE:
-            return self._artwork_hash
+        if self._playing is not None and self.state != STATE_IDLE:
+            return self._playing.hash
 
     @asyncio.coroutine
     def async_get_media_image(self):
         """Fetch media image of current playing image."""
-        return (yield from self._atv.metadata.artwork()), 'image/png'
+        return (yield from self.atv.metadata.artwork()), 'image/png'
 
     @property
     def media_title(self):
@@ -235,9 +183,9 @@ class AppleTvDevice(MediaPlayerDevice):
             if self.state == STATE_IDLE:
                 return 'Nothing playing'
             title = self._playing.title
-            return title if title else "No title"
+            return title if title else 'No title'
 
-        return 'Not connected to Apple TV'
+        return 'Establishing a connection to {0}...'.format(self._name)
 
     @property
     def supported_features(self):
@@ -254,12 +202,13 @@ class AppleTvDevice(MediaPlayerDevice):
     @asyncio.coroutine
     def async_turn_on(self):
         """Turn the media player on."""
-        self._set_power_off(False)
+        self._power.set_power_on(True)
 
     @asyncio.coroutine
     def async_turn_off(self):
         """Turn the media player off."""
-        self._set_power_off(True)
+        self._playing = None
+        self._power.set_power_on(False)
 
     def async_media_play_pause(self):
         """Pause media on media player.
@@ -269,9 +218,9 @@ class AppleTvDevice(MediaPlayerDevice):
         if self._playing is not None:
             state = self.state
             if state == STATE_PAUSED:
-                return self._atv.remote_control.play()
+                return self.atv.remote_control.play()
             elif state == STATE_PLAYING:
-                return self._atv.remote_control.pause()
+                return self.atv.remote_control.pause()
 
     def async_media_play(self):
         """Play media.
@@ -279,7 +228,15 @@ class AppleTvDevice(MediaPlayerDevice):
         This method must be run in the event loop and returns a coroutine.
         """
         if self._playing is not None:
-            return self._atv.remote_control.play()
+            return self.atv.remote_control.play()
+
+    def async_media_stop(self):
+        """Stop the media player.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        if self._playing is not None:
+            return self.atv.remote_control.stop()
 
     def async_media_pause(self):
         """Pause the media player.
@@ -287,7 +244,7 @@ class AppleTvDevice(MediaPlayerDevice):
         This method must be run in the event loop and returns a coroutine.
         """
         if self._playing is not None:
-            return self._atv.remote_control.pause()
+            return self.atv.remote_control.pause()
 
     def async_media_next_track(self):
         """Send next track command.
@@ -295,7 +252,7 @@ class AppleTvDevice(MediaPlayerDevice):
         This method must be run in the event loop and returns a coroutine.
         """
         if self._playing is not None:
-            return self._atv.remote_control.next()
+            return self.atv.remote_control.next()
 
     def async_media_previous_track(self):
         """Send previous track command.
@@ -303,7 +260,7 @@ class AppleTvDevice(MediaPlayerDevice):
         This method must be run in the event loop and returns a coroutine.
         """
         if self._playing is not None:
-            return self._atv.remote_control.previous()
+            return self.atv.remote_control.previous()
 
     def async_media_seek(self, position):
         """Send seek command.
@@ -311,4 +268,4 @@ class AppleTvDevice(MediaPlayerDevice):
         This method must be run in the event loop and returns a coroutine.
         """
         if self._playing is not None:
-            return self._atv.remote_control.set_position(position)
+            return self.atv.remote_control.set_position(position)
diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py
index 1c8419ba144..93071b9840f 100644
--- a/homeassistant/components/media_player/braviatv.py
+++ b/homeassistant/components/media_player/braviatv.py
@@ -59,8 +59,7 @@ def _get_mac_address(ip_address):
                       pid_component)
     if match is not None:
         return match.groups()[0]
-    else:
-        return None
+    return None
 
 
 def _config_from_file(filename, config=None):
@@ -307,8 +306,7 @@ class BraviaTVDevice(MediaPlayerDevice):
         """Volume level of the media player (0..1)."""
         if self._volume is not None:
             return self._volume / 100
-        else:
-            return None
+        return None
 
     @property
     def is_volume_muted(self):
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index e4ecd1bd37d..51acf68d819 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -131,8 +131,7 @@ class CastDevice(MediaPlayerDevice):
             return STATE_IDLE
         elif self.cast.is_idle:
             return STATE_OFF
-        else:
-            return STATE_UNKNOWN
+        return STATE_UNKNOWN
 
     @property
     def volume_level(self):
@@ -235,18 +234,13 @@ class CastDevice(MediaPlayerDevice):
     @property
     def media_position(self):
         """Position of current playing media in seconds."""
-        if self.media_status is None or self.media_status_received is None or \
+        if self.media_status is None or \
                 not (self.media_status.player_is_playing or
+                     self.media_status.player_is_paused or
                      self.media_status.player_is_idle):
             return None
 
-        position = self.media_status.current_time
-
-        if self.media_status.player_is_playing:
-            position += (dt_util.utcnow() -
-                         self.media_status_received).total_seconds()
-
-        return position
+        return self.media_status.current_time
 
     @property
     def media_position_updated_at(self):
diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py
index aefe5bced18..949965200aa 100644
--- a/homeassistant/components/media_player/cmus.py
+++ b/homeassistant/components/media_player/cmus.py
@@ -94,8 +94,7 @@ class CmusDevice(MediaPlayerDevice):
             return STATE_PLAYING
         elif self.status.get('status') == 'paused':
             return STATE_PAUSED
-        else:
-            return STATE_OFF
+        return STATE_OFF
 
     @property
     def media_content_id(self):
diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py
index 08e08dd1650..68fb629e5ea 100755
--- a/homeassistant/components/media_player/denon.py
+++ b/homeassistant/components/media_player/denon.py
@@ -55,9 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
 
     if denon.update():
         add_devices([denon])
-        return True
-    else:
-        return False
 
 
 class DenonDevice(MediaPlayerDevice):
@@ -197,8 +194,7 @@ class DenonDevice(MediaPlayerDevice):
         """Flag media player features that are supported."""
         if self._mediasource in MEDIA_MODES.values():
             return SUPPORT_DENON | SUPPORT_MEDIA_MODES
-        else:
-            return SUPPORT_DENON
+        return SUPPORT_DENON
 
     @property
     def source(self):
diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py
index 5f3b88ccf52..06f95a7d3a7 100644
--- a/homeassistant/components/media_player/denonavr.py
+++ b/homeassistant/components/media_player/denonavr.py
@@ -20,7 +20,7 @@ from homeassistant.const import (
     CONF_NAME, STATE_ON, CONF_ZONE)
 import homeassistant.helpers.config_validation as cv
 
-REQUIREMENTS = ['denonavr==0.5.1']
+REQUIREMENTS = ['denonavr==0.5.2']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -197,8 +197,7 @@ class DenonDevice(MediaPlayerDevice):
         """Flag media player features that are supported."""
         if self._current_source in self._receiver.netaudio_func_list:
             return SUPPORT_DENON | SUPPORT_MEDIA_MODES
-        else:
-            return SUPPORT_DENON
+        return SUPPORT_DENON
 
     @property
     def media_content_id(self):
@@ -210,8 +209,7 @@ class DenonDevice(MediaPlayerDevice):
         """Content type of current playing media."""
         if self._state == STATE_PLAYING or self._state == STATE_PAUSED:
             return MEDIA_TYPE_MUSIC
-        else:
-            return MEDIA_TYPE_CHANNEL
+        return MEDIA_TYPE_CHANNEL
 
     @property
     def media_duration(self):
@@ -223,8 +221,7 @@ class DenonDevice(MediaPlayerDevice):
         """Image url of current playing media."""
         if self._current_source in self._receiver.playing_func_list:
             return self._media_image_url
-        else:
-            return None
+        return None
 
     @property
     def media_title(self):
@@ -233,24 +230,21 @@ class DenonDevice(MediaPlayerDevice):
             return self._current_source
         elif self._title is not None:
             return self._title
-        else:
-            return self._frequency
+        return self._frequency
 
     @property
     def media_artist(self):
         """Artist of current playing media, music track only."""
         if self._artist is not None:
             return self._artist
-        else:
-            return self._band
+        return self._band
 
     @property
     def media_album_name(self):
         """Album name of current playing media, music track only."""
         if self._album is not None:
             return self._album
-        else:
-            return self._station
+        return self._station
 
     @property
     def media_album_artist(self):
@@ -297,17 +291,11 @@ class DenonDevice(MediaPlayerDevice):
         """Turn on media player."""
         if self._receiver.power_on():
             self._state = STATE_ON
-            return True
-        else:
-            return False
 
     def turn_off(self):
         """Turn off media player."""
         if self._receiver.power_off():
             self._state = STATE_OFF
-            return True
-        else:
-            return False
 
     def volume_up(self):
         """Volume up the media player."""
@@ -327,11 +315,8 @@ class DenonDevice(MediaPlayerDevice):
         try:
             if self._receiver.set_volume(volume_denon):
                 self._volume = volume_denon
-                return True
-            else:
-                return False
         except ValueError:
-            return False
+            pass
 
     def mute_volume(self, mute):
         """Send mute command."""
diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py
index c1b690bc370..b46e256bab3 100644
--- a/homeassistant/components/media_player/directv.py
+++ b/homeassistant/components/media_player/directv.py
@@ -95,43 +95,37 @@ class DirecTvDevice(MediaPlayerDevice):
         if self._is_standby:
             return STATE_OFF
         # Haven't determined a way to see if the content is paused
-        else:
-            return STATE_PLAYING
+        return STATE_PLAYING
 
     @property
     def media_content_id(self):
         """Return the content ID of current playing media."""
         if self._is_standby:
             return None
-        else:
-            return self._current['programId']
+        return self._current['programId']
 
     @property
     def media_duration(self):
         """Return the duration of current playing media in seconds."""
         if self._is_standby:
             return None
-        else:
-            return self._current['duration']
+        return self._current['duration']
 
     @property
     def media_title(self):
         """Return the title of current playing media."""
         if self._is_standby:
             return None
-        else:
-            return self._current['title']
+        return self._current['title']
 
     @property
     def media_series_title(self):
         """Return the title of current episode of TV show."""
         if self._is_standby:
             return None
-        else:
-            if 'episodeTitle' in self._current:
-                return self._current['episodeTitle']
-            else:
-                return None
+        elif 'episodeTitle' in self._current:
+            return self._current['episodeTitle']
+        return None
 
     @property
     def supported_features(self):
@@ -143,18 +137,16 @@ class DirecTvDevice(MediaPlayerDevice):
         """Return the content type of current playing media."""
         if 'episodeTitle' in self._current:
             return MEDIA_TYPE_TVSHOW
-        else:
-            return MEDIA_TYPE_VIDEO
+        return MEDIA_TYPE_VIDEO
 
     @property
     def media_channel(self):
         """Return the channel current playing media."""
         if self._is_standby:
             return None
-        else:
-            chan = "{} ({})".format(
-                self._current['callsign'], self._current['major'])
-            return chan
+
+        return "{} ({})".format(
+            self._current['callsign'], self._current['major'])
 
     def turn_on(self):
         """Turn on the receiver."""
diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py
index 9f9e9e19dfe..8df6bc4fd1b 100644
--- a/homeassistant/components/media_player/emby.py
+++ b/homeassistant/components/media_player/emby.py
@@ -21,7 +21,7 @@ from homeassistant.core import callback
 import homeassistant.helpers.config_validation as cv
 import homeassistant.util.dt as dt_util
 
-REQUIREMENTS = ['pyemby==1.3']
+REQUIREMENTS = ['pyemby==1.4']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -306,8 +306,7 @@ class EmbyDevice(MediaPlayerDevice):
         """Flag media player features that are supported."""
         if self.supports_remote_control:
             return SUPPORT_EMBY
-        else:
-            return None
+        return None
 
     def async_media_play(self):
         """Play media.
diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py
index 514bf24a21a..575ea414fa3 100644
--- a/homeassistant/components/media_player/itunes.py
+++ b/homeassistant/components/media_player/itunes.py
@@ -60,8 +60,8 @@ class Itunes(object):
 
         if self.port:
             return '{}{}:{}'.format(uri_scheme, self.host, self.port)
-        else:
-            return '{}{}'.format(uri_scheme, self.host)
+
+        return '{}{}'.format(uri_scheme, self.host)
 
     def _request(self, method, path, params=None):
         """Make the actual request and return the parsed response."""
@@ -225,8 +225,8 @@ class ItunesDevice(MediaPlayerDevice):
 
         if self.player_state == 'paused':
             return STATE_PAUSED
-        else:
-            return STATE_PLAYING
+
+        return STATE_PLAYING
 
     def update(self):
         """Retrieve latest state."""
@@ -281,9 +281,9 @@ class ItunesDevice(MediaPlayerDevice):
         if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \
            self.current_title is not None:
             return self.client.artwork_url()
-        else:
-            return 'https://cloud.githubusercontent.com/assets/260/9829355' \
-                '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png'
+
+        return 'https://cloud.githubusercontent.com/assets/260/9829355' \
+            '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png'
 
     @property
     def media_title(self):
@@ -400,16 +400,16 @@ class AirPlayDevice(MediaPlayerDevice):
         """Return the icon to use in the frontend, if any."""
         if self.selected is True:
             return 'mdi:volume-high'
-        else:
-            return 'mdi:volume-off'
+
+        return 'mdi:volume-off'
 
     @property
     def state(self):
         """Return the state of the device."""
         if self.selected is True:
             return STATE_ON
-        else:
-            return STATE_OFF
+
+        return STATE_OFF
 
     def update(self):
         """Retrieve latest state."""
diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py
index 18860acb9a6..f484c04a058 100644
--- a/homeassistant/components/media_player/kodi.py
+++ b/homeassistant/components/media_player/kodi.py
@@ -334,8 +334,8 @@ class KodiDevice(MediaPlayerDevice):
 
         if self._properties['speed'] == 0 and not self._properties['live']:
             return STATE_PAUSED
-        else:
-            return STATE_PLAYING
+
+        return STATE_PLAYING
 
     @asyncio.coroutine
     def async_ws_connect(self):
@@ -407,8 +407,8 @@ class KodiDevice(MediaPlayerDevice):
         """Active server for json-rpc requests."""
         if self._enable_websocket and self._ws_server.connected:
             return self._ws_server
-        else:
-            return self._http_server
+
+        return self._http_server
 
     @property
     def name(self):
@@ -503,8 +503,8 @@ class KodiDevice(MediaPlayerDevice):
         artists = self._item.get('artist', [])
         if artists:
             return artists[0]
-        else:
-            return None
+
+        return None
 
     @property
     def media_album_artist(self):
@@ -512,8 +512,8 @@ class KodiDevice(MediaPlayerDevice):
         artists = self._item.get('albumartist', [])
         if artists:
             return artists[0]
-        else:
-            return None
+
+        return None
 
     @property
     def supported_features(self):
@@ -678,9 +678,9 @@ class KodiDevice(MediaPlayerDevice):
         elif media_type == "PLAYLIST":
             return self.server.Player.Open(
                 {"item": {"playlistid": int(media_id)}})
-        else:
-            return self.server.Player.Open(
-                {"item": {"file": str(media_id)}})
+
+        return self.server.Player.Open(
+            {"item": {"file": str(media_id)}})
 
     @asyncio.coroutine
     def async_set_shuffle(self, shuffle):
@@ -794,9 +794,9 @@ class KodiDevice(MediaPlayerDevice):
         """Get albums list."""
         if artist_id is None:
             return (yield from self.server.AudioLibrary.GetAlbums())
-        else:
-            return (yield from self.server.AudioLibrary.GetAlbums(
-                {"filter": {"artistid": int(artist_id)}}))
+
+        return (yield from self.server.AudioLibrary.GetAlbums(
+            {"filter": {"artistid": int(artist_id)}}))
 
     @asyncio.coroutine
     def async_find_artist(self, artist_name):
@@ -815,9 +815,9 @@ class KodiDevice(MediaPlayerDevice):
         """Get songs list."""
         if artist_id is None:
             return (yield from self.server.AudioLibrary.GetSongs())
-        else:
-            return (yield from self.server.AudioLibrary.GetSongs(
-                {"filter": {"artistid": int(artist_id)}}))
+
+        return (yield from self.server.AudioLibrary.GetSongs(
+            {"filter": {"artistid": int(artist_id)}}))
 
     @asyncio.coroutine
     def async_find_song(self, song_name, artist_name=''):
diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py
index eef5a890e8e..43678d90829 100644
--- a/homeassistant/components/media_player/liveboxplaytv.py
+++ b/homeassistant/components/media_player/liveboxplaytv.py
@@ -159,9 +159,8 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
             return STATE_PLAYING
         elif state == 'PAUSE':
             return STATE_PAUSED
-        else:
-            return STATE_ON if self._client.is_on else STATE_OFF
-        return STATE_UNKNOWN
+
+        return STATE_ON if self._client.is_on else STATE_OFF
 
     def turn_off(self):
         """Turn off media player."""
diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py
index 964c2b5d009..3f7ab5a6e5e 100644
--- a/homeassistant/components/media_player/mpchc.py
+++ b/homeassistant/components/media_player/mpchc.py
@@ -97,8 +97,8 @@ class MpcHcDevice(MediaPlayerDevice):
             return STATE_PLAYING
         elif state == 'paused':
             return STATE_PAUSED
-        else:
-            return STATE_IDLE
+
+        return STATE_IDLE
 
     @property
     def media_title(self):
diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py
index f4dad2d001b..d8dafc2be10 100644
--- a/homeassistant/components/media_player/mpd.py
+++ b/homeassistant/components/media_player/mpd.py
@@ -133,8 +133,8 @@ class MpdDevice(MediaPlayerDevice):
             return STATE_PLAYING
         elif self.status['state'] == 'pause':
             return STATE_PAUSED
-        else:
-            return STATE_OFF
+
+        return STATE_OFF
 
     @property
     def media_content_id(self):
@@ -164,8 +164,8 @@ class MpdDevice(MediaPlayerDevice):
             return title
         elif title is None:
             return name
-        else:
-            return '{}: {}'.format(name, title)
+
+        return '{}: {}'.format(name, title)
 
     @property
     def media_artist(self):
diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py
index 4e36fdab5a2..d66811eed66 100644
--- a/homeassistant/components/media_player/pandora.py
+++ b/homeassistant/components/media_player/pandora.py
@@ -358,9 +358,9 @@ def _pianobar_exists():
     pianobar_exe = shutil.which('pianobar')
     if pianobar_exe:
         return True
-    else:
-        _LOGGER.warning(
-            "The Pandora component depends on the Pianobar client, which "
-            "cannot be found. Please install using instructions at "
-            "https://home-assistant.io/components/media_player.pandora/")
-        return False
+
+    _LOGGER.warning(
+        "The Pandora component depends on the Pianobar client, which "
+        "cannot be found. Please install using instructions at "
+        "https://home-assistant.io/components/media_player.pandora/")
+    return False
diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py
index b79f3eeeb89..da572896ee0 100644
--- a/homeassistant/components/media_player/philips_js.py
+++ b/homeassistant/components/media_player/philips_js.py
@@ -90,8 +90,7 @@ class PhilipsTV(MediaPlayerDevice):
         """Flag media player features that are supported."""
         if self._watching_tv:
             return SUPPORT_PHILIPS_JS_TV
-        else:
-            return SUPPORT_PHILIPS_JS
+        return SUPPORT_PHILIPS_JS
 
     @property
     def state(self):
@@ -162,13 +161,9 @@ class PhilipsTV(MediaPlayerDevice):
     @property
     def media_title(self):
         """Title of current playing media."""
-        if self._watching_tv:
-            if self._channel_name:
-                return '{} - {}'.format(self._source, self._channel_name)
-            else:
-                return self._source
-        else:
-            return self._source
+        if self._watching_tv and self._channel_name:
+            return '{} - {}'.format(self._source, self._channel_name)
+        return self._source
 
     @Throttle(MIN_TIME_BETWEEN_UPDATES)
     def update(self):
diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py
index eb346502d95..ba08003b9bf 100644
--- a/homeassistant/components/media_player/pioneer.py
+++ b/homeassistant/components/media_player/pioneer.py
@@ -47,9 +47,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
 
     if pioneer.update():
         add_devices([pioneer])
-        return True
-    else:
-        return False
 
 
 class PioneerDevice(MediaPlayerDevice):
diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py
index d54b8f2ca77..f4c69ba1fe6 100644
--- a/homeassistant/components/media_player/plex.py
+++ b/homeassistant/components/media_player/plex.py
@@ -82,8 +82,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
 
     if file_config:
         # Setup a configured PlexServer
-        host, token = file_config.popitem()
-        token = token['token']
+        host, host_config = file_config.popitem()
+        token = host_config['token']
+        try:
+            has_ssl = host_config['ssl']
+        except KeyError:
+            has_ssl = False
+        try:
+            verify_ssl = host_config['verify']
+        except KeyError:
+            verify_ssl = True
+
     # Via discovery
     elif discovery_info is not None:
         # Parse discovery data
@@ -95,19 +104,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
         if host in _CONFIGURING:
             return
         token = None
+        has_ssl = False
+        verify_ssl = True
     else:
         return
 
-    setup_plexserver(host, token, hass, config, add_devices_callback)
+    setup_plexserver(
+        host, token, has_ssl, verify_ssl,
+        hass, config, add_devices_callback
+    )
 
 
-def setup_plexserver(host, token, hass, config, add_devices_callback):
+def setup_plexserver(
+        host, token, has_ssl, verify_ssl, hass, config, add_devices_callback):
     """Set up a plexserver based on host parameter."""
     import plexapi.server
     import plexapi.exceptions
 
+    cert_session = None
+    http_prefix = 'https' if has_ssl else 'http'
+    if has_ssl and (verify_ssl is False):
+        _LOGGER.info("Ignoring SSL verification")
+        cert_session = requests.Session()
+        cert_session.verify = False
     try:
-        plexserver = plexapi.server.PlexServer('http://%s' % host, token)
+        plexserver = plexapi.server.PlexServer(
+            '%s://%s' % (http_prefix, host),
+            token, cert_session
+        )
         _LOGGER.info("Discovery configuration done (no token needed)")
     except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
             plexapi.exceptions.NotFound) as error:
@@ -126,11 +150,13 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
     # Save config
     if not config_from_file(
             hass.config.path(PLEX_CONFIG_FILE), {host: {
-                'token': token
+                'token': token,
+                'ssl': has_ssl,
+                'verify': verify_ssl,
             }}):
         _LOGGER.error("Failed to save configuration file")
 
-    _LOGGER.info('Connected to: http://%s', host)
+    _LOGGER.info('Connected to: %s://%s', http_prefix, host)
 
     plex_clients = {}
     plex_sessions = {}
@@ -144,9 +170,9 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
         except plexapi.exceptions.BadRequest:
             _LOGGER.exception("Error listing plex devices")
             return
-        except OSError:
-            _LOGGER.error("Could not connect to plex server at http://%s",
-                          host)
+        except requests.exceptions.RequestException as ex:
+            _LOGGER.error("Could not connect to plex server at http://%s (%s)",
+                          host, ex)
             return
 
         new_plex_clients = []
@@ -193,6 +219,10 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
         except plexapi.exceptions.BadRequest:
             _LOGGER.exception("Error listing plex sessions")
             return
+        except requests.exceptions.RequestException as ex:
+            _LOGGER.error("Could not connect to plex server at http://%s (%s)",
+                          host, ex)
+            return
 
         plex_sessions.clear()
         for session in sessions:
@@ -217,7 +247,11 @@ def request_configuration(host, hass, config, add_devices_callback):
     def plex_configuration_callback(data):
         """Handle configuration changes."""
         setup_plexserver(
-            host, data.get('token'), hass, config, add_devices_callback)
+            host, data.get('token'),
+            cv.boolean(data.get('has_ssl')),
+            cv.boolean(data.get('do_not_verify')),
+            hass, config, add_devices_callback
+        )
 
     _CONFIGURING[host] = configurator.request_config(
         hass,
@@ -230,6 +264,14 @@ def request_configuration(host, hass, config, add_devices_callback):
             'id': 'token',
             'name': 'X-Plex-Token',
             'type': ''
+        }, {
+            'id': 'has_ssl',
+            'name': 'Use SSL',
+            'type': ''
+        }, {
+            'id': 'do_not_verify_ssl',
+            'name': 'Do not verify SSL',
+            'type': ''
         }])
 
 
@@ -531,16 +573,16 @@ class PlexClient(MediaPlayerDevice):
         # type so that lower layers don't think it's a URL and choke on it
         if value is self.na_type:
             return None
-        else:
-            return value
+
+        return value
 
     @property
     def _active_media_plexapi_type(self):
         """Get the active media type required by PlexAPI commands."""
         if self.media_content_type is MEDIA_TYPE_MUSIC:
             return 'music'
-        else:
-            return 'video'
+
+        return 'video'
 
     @property
     def media_content_id(self):
@@ -560,8 +602,8 @@ class PlexClient(MediaPlayerDevice):
             return MEDIA_TYPE_VIDEO
         elif self._session_type == 'track':
             return MEDIA_TYPE_MUSIC
-        else:
-            return None
+
+        return None
 
     @property
     def media_artist(self):
@@ -657,8 +699,8 @@ class PlexClient(MediaPlayerDevice):
                     SUPPORT_NEXT_TRACK | SUPPORT_STOP |
                     SUPPORT_VOLUME_SET | SUPPORT_PLAY |
                     SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
-        else:
-            return None
+
+        return None
 
     def _local_client_control_fix(self):
         """Detect if local client and adjust url to allow control."""
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index b26f0f48a77..33decf35f89 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -220,9 +220,9 @@ def _parse_timespan(timespan):
     """Parse a time-span into number of seconds."""
     if timespan in ('', 'NOT_IMPLEMENTED', None):
         return None
-    else:
-        return sum(60 ** x[0] * int(x[1]) for x in enumerate(
-            reversed(timespan.split(':'))))
+
+    return sum(60 ** x[0] * int(x[1]) for x in enumerate(
+        reversed(timespan.split(':'))))
 
 
 class _ProcessSonosEventQueue():
@@ -765,8 +765,8 @@ class SonosDevice(MediaPlayerDevice):
         """Content ID of current playing media."""
         if self._coordinator:
             return self._coordinator.media_content_id
-        else:
-            return self._media_content_id
+
+        return self._media_content_id
 
     @property
     def media_content_type(self):
@@ -778,16 +778,16 @@ class SonosDevice(MediaPlayerDevice):
         """Duration of current playing media in seconds."""
         if self._coordinator:
             return self._coordinator.media_duration
-        else:
-            return self._media_duration
+
+        return self._media_duration
 
     @property
     def media_position(self):
         """Position of current playing media in seconds."""
         if self._coordinator:
             return self._coordinator.media_position
-        else:
-            return self._media_position
+
+        return self._media_position
 
     @property
     def media_position_updated_at(self):
@@ -797,40 +797,40 @@ class SonosDevice(MediaPlayerDevice):
         """
         if self._coordinator:
             return self._coordinator.media_position_updated_at
-        else:
-            return self._media_position_updated_at
+
+        return self._media_position_updated_at
 
     @property
     def media_image_url(self):
         """Image url of current playing media."""
         if self._coordinator:
             return self._coordinator.media_image_url
-        else:
-            return self._media_image_url
+
+        return self._media_image_url
 
     @property
     def media_artist(self):
         """Artist of current playing media, music track only."""
         if self._coordinator:
             return self._coordinator.media_artist
-        else:
-            return self._media_artist
+
+        return self._media_artist
 
     @property
     def media_album_name(self):
         """Album name of current playing media, music track only."""
         if self._coordinator:
             return self._coordinator.media_album_name
-        else:
-            return self._media_album_name
+
+        return self._media_album_name
 
     @property
     def media_title(self):
         """Title of current playing media."""
         if self._coordinator:
             return self._coordinator.media_title
-        else:
-            return self._media_title
+
+        return self._media_title
 
     @property
     def supported_features(self):
@@ -919,8 +919,8 @@ class SonosDevice(MediaPlayerDevice):
         """Name of the current input source."""
         if self._coordinator:
             return self._coordinator.source
-        else:
-            return self._source_name
+
+        return self._source_name
 
     @soco_error
     def turn_off(self):
diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py
index 3890e52bd73..c04d3b4d77f 100644
--- a/homeassistant/components/media_player/soundtouch.py
+++ b/homeassistant/components/media_player/soundtouch.py
@@ -7,6 +7,7 @@ https://home-assistant.io/components/media_player.soundtouch/
 import logging
 
 from os import path
+import re
 import voluptuous as vol
 
 import homeassistant.helpers.config_validation as cv
@@ -20,7 +21,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
                                  STATE_PAUSED, STATE_PLAYING,
                                  STATE_UNAVAILABLE)
 
-REQUIREMENTS = ['libsoundtouch==0.6.2']
+REQUIREMENTS = ['libsoundtouch==0.7.2']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -200,8 +201,8 @@ class SoundTouchDevice(MediaPlayerDevice):
         """Return the state of the device."""
         if self._status.source == 'STANDBY':
             return STATE_OFF
-        else:
-            return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE)
+
+        return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE)
 
     @property
     def is_volume_muted(self):
@@ -280,8 +281,8 @@ class SoundTouchDevice(MediaPlayerDevice):
             return self._status.station_name
         elif self._status.artist is not None:
             return self._status.artist + " - " + self._status.track
-        else:
-            return None
+
+        return None
 
     @property
     def media_duration(self):
@@ -305,15 +306,22 @@ class SoundTouchDevice(MediaPlayerDevice):
 
     def play_media(self, media_type, media_id, **kwargs):
         """Play a piece of media."""
-        _LOGGER.info("Starting media with media_id:" + str(media_id))
-        presets = self._device.presets()
-        preset = next([preset for preset in presets if
-                       preset.preset_id == str(media_id)].__iter__(), None)
-        if preset is not None:
-            _LOGGER.info("Playing preset: " + preset.name)
-            self._device.select_preset(preset)
+        _LOGGER.debug("Starting media with media_id: " + str(media_id))
+        if re.match(r'http://', str(media_id)):
+            # URL
+            _LOGGER.debug("Playing URL %s", str(media_id))
+            self._device.play_url(str(media_id))
         else:
-            _LOGGER.warning("Unable to find preset with id " + str(media_id))
+            # Preset
+            presets = self._device.presets()
+            preset = next([preset for preset in presets if
+                           preset.preset_id == str(media_id)].__iter__(), None)
+            if preset is not None:
+                _LOGGER.debug("Playing preset: " + preset.name)
+                self._device.select_preset(preset)
+            else:
+                _LOGGER.warning(
+                    "Unable to find preset with id " + str(media_id))
 
     def create_zone(self, slaves):
         """
diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py
index b3405707877..bc0728c7ff2 100644
--- a/homeassistant/components/media_player/spotify.py
+++ b/homeassistant/components/media_player/spotify.py
@@ -171,7 +171,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
                                  for device in devices}
                 device_diff = {name: id for name, id in self._devices.items()
                                if old_devices.get(name, None) is None}
-                if len(device_diff) > 0:
+                if device_diff:
                     _LOGGER.info("New Devices: %s", str(device_diff))
         # Current playback state
         current = self._player.current_playback()
@@ -312,5 +312,9 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
         """Return the media player features that are supported."""
         if self._user is not None and self._user['product'] == 'premium':
             return SUPPORT_SPOTIFY
-        else:
-            return None
+        return None
+
+    @property
+    def media_content_type(self):
+        """Return the media type."""
+        return MEDIA_TYPE_MUSIC
diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py
index 68c2a06cf23..daf874a31dd 100644
--- a/homeassistant/components/media_player/universal.py
+++ b/homeassistant/components/media_player/universal.py
@@ -215,8 +215,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
             master_state = self._entity_lkp(
                 self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1])
             return master_state if master_state else STATE_OFF
-        else:
-            return None
+
+        return None
 
     @property
     def name(self):
diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py
new file mode 100644
index 00000000000..4ae8f037a4f
--- /dev/null
+++ b/homeassistant/components/media_player/vizio.py
@@ -0,0 +1,189 @@
+"""
+Vizio SmartCast TV support.
+
+Usually only 2016+ models come with SmartCast capabilities.
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/media_player.vizio/
+"""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+import homeassistant.util as util
+from homeassistant.components.media_player import (
+    PLATFORM_SCHEMA,
+    SUPPORT_TURN_ON,
+    SUPPORT_TURN_OFF,
+    SUPPORT_SELECT_SOURCE,
+    SUPPORT_PREVIOUS_TRACK,
+    SUPPORT_NEXT_TRACK,
+    SUPPORT_VOLUME_MUTE,
+    SUPPORT_VOLUME_STEP,
+    MediaPlayerDevice
+)
+from homeassistant.const import (
+    STATE_UNKNOWN,
+    STATE_OFF,
+    STATE_ON,
+    CONF_NAME,
+    CONF_HOST,
+    CONF_ACCESS_TOKEN
+)
+from homeassistant.helpers import config_validation as cv
+
+REQUIREMENTS = ['pyvizio==0.0.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_SUPPRESS_WARNING = 'suppress_warning'
+CONF_VOLUME_STEP = 'volume_step'
+
+ICON = 'mdi:television'
+DEFAULT_NAME = 'Vizio SmartCast'
+DEFAULT_VOLUME_STEP = 1
+DEVICE_NAME = 'Python Vizio'
+DEVICE_ID = 'pyvizio'
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
+SUPPORTED_COMMANDS = SUPPORT_TURN_ON | SUPPORT_TURN_OFF \
+                     | SUPPORT_SELECT_SOURCE \
+                     | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \
+                     | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Required(CONF_HOST): cv.string,
+    vol.Required(CONF_ACCESS_TOKEN): cv.string,
+    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+    vol.Optional(CONF_SUPPRESS_WARNING, default=False): cv.boolean,
+    vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP):
+        vol.All(vol.Coerce(int), vol.Range(min=1, max=10)),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the VizioTV media player platform."""
+    host = config.get(CONF_HOST)
+    token = config.get(CONF_ACCESS_TOKEN)
+    name = config.get(CONF_NAME)
+    volume_step = config.get(CONF_VOLUME_STEP)
+
+    device = VizioDevice(host, token, name, volume_step)
+    if device.validate_setup() is False:
+        _LOGGER.error('Failed to setup Vizio TV platform, '
+                      'please check if host and API key are correct.')
+        return False
+
+    if config.get(CONF_SUPPRESS_WARNING):
+        import requests
+        from requests.packages.urllib3.exceptions import InsecureRequestWarning
+        _LOGGER.warning('InsecureRequestWarning is disabled '
+                        'because of Vizio platform configuration.')
+        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+    add_devices([device], True)
+
+
+class VizioDevice(MediaPlayerDevice):
+    """Media Player implementation which performs REST requests to TV."""
+
+    def __init__(self, host, token, name, volume_step):
+        """Initialize Vizio device."""
+        import pyvizio
+        self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token)
+        self._name = name
+        self._state = STATE_UNKNOWN
+        self._volume_level = None
+        self._volume_step = volume_step
+        self._current_input = None
+        self._available_inputs = None
+
+    @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
+    def update(self):
+        """Retrieve latest state of the TV."""
+        is_on = self._device.get_power_state()
+        if is_on is None:
+            self._state = STATE_UNKNOWN
+            return
+        elif is_on is False:
+            self._state = STATE_OFF
+        else:
+            self._state = STATE_ON
+
+        self._volume_level = self._device.get_current_volume()
+        input_ = self._device.get_current_input()
+        if input_ is not None:
+            self._current_input = input_.meta_name
+        inputs = self._device.get_inputs()
+        if inputs is not None:
+            self._available_inputs = []
+            for input_ in inputs:
+                self._available_inputs.append(input_.name)
+
+    @property
+    def state(self):
+        """Return the state of the TV."""
+        return self._state
+
+    @property
+    def name(self):
+        """Return the name of the TV."""
+        return self._name
+
+    @property
+    def volume_level(self):
+        """Return the volume level of the TV."""
+        return self._volume_level
+
+    @property
+    def source(self):
+        """Return current input of the TV."""
+        return self._current_input
+
+    @property
+    def source_list(self):
+        """Return list of available inputs of the TV."""
+        return self._available_inputs
+
+    @property
+    def supported_features(self):
+        """Flag TV features that are supported."""
+        return SUPPORTED_COMMANDS
+
+    def turn_on(self):
+        """Turn the TV player on."""
+        self._device.pow_on()
+
+    def turn_off(self):
+        """Turn the TV player off."""
+        self._device.pow_off()
+
+    def mute_volume(self, mute):
+        """Mute the volume."""
+        if mute:
+            self._device.mute_on()
+        else:
+            self._device.mute_off()
+
+    def media_previous_track(self):
+        """Send previous channel command."""
+        self._device.ch_down()
+
+    def media_next_track(self):
+        """Send next channel command."""
+        self._device.ch_up()
+
+    def select_source(self, source):
+        """Select input source."""
+        self._device.input_switch(source)
+
+    def volume_up(self):
+        """Increasing volume of the TV."""
+        self._device.vol_up(num=self._volume_step)
+
+    def volume_down(self):
+        """Decreasing volume of the TV."""
+        self._device.vol_down(num=self._volume_step)
+
+    def validate_setup(self):
+        """Validating if host is available and key is correct."""
+        return self._device.get_current_volume() is not None
diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py
index ade49b8116e..eda0bc2b326 100755
--- a/homeassistant/components/media_player/volumio.py
+++ b/homeassistant/components/media_player/volumio.py
@@ -113,8 +113,8 @@ class Volumio(MediaPlayerDevice):
             return STATE_PAUSED
         elif status == 'play':
             return STATE_PLAYING
-        else:
-            return STATE_IDLE
+
+        return STATE_IDLE
 
     @property
     def media_title(self):
@@ -207,6 +207,6 @@ class Volumio(MediaPlayerDevice):
             self._lastvol = self._state['volume']
             return self.send_volumio_msg(
                 'commands', params={'cmd': 'volume', 'volume': mutecmd})
-        else:
-            return self.send_volumio_msg(
-                'commands', params={'cmd': 'volume', 'volume': self._lastvol})
+
+        return self.send_volumio_msg(
+            'commands', params={'cmd': 'volume', 'volume': self._lastvol})
diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py
index de19ab238b5..4d10c722800 100644
--- a/homeassistant/components/media_player/yamaha.py
+++ b/homeassistant/components/media_player/yamaha.py
@@ -289,5 +289,5 @@ class YamahaDevice(MediaPlayerDevice):
             # just the one we have.
             if song and station:
                 return '{}: {}'.format(station, song)
-            else:
-                return song or station
+
+            return song or station
diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py
index 0315682bae0..9075eab2cdd 100644
--- a/homeassistant/components/modbus.py
+++ b/homeassistant/components/modbus.py
@@ -6,17 +6,19 @@ https://home-assistant.io/components/modbus/
 """
 import logging
 import threading
+import os
 
 import voluptuous as vol
 
 import homeassistant.helpers.config_validation as cv
+from homeassistant.config import load_yaml_config_file
 from homeassistant.const import (
     EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
-    CONF_HOST, CONF_METHOD, CONF_PORT)
+    CONF_HOST, CONF_METHOD, CONF_PORT, ATTR_STATE)
 
 DOMAIN = 'modbus'
 
-REQUIREMENTS = ['pymodbus==1.3.0rc1']
+REQUIREMENTS = ['pymodbus==1.3.1']
 
 # Type of network
 CONF_BAUDRATE = 'baudrate'
@@ -50,6 +52,7 @@ CONFIG_SCHEMA = vol.Schema({
 _LOGGER = logging.getLogger(__name__)
 
 SERVICE_WRITE_REGISTER = 'write_register'
+SERVICE_WRITE_COIL = 'write_coil'
 
 ATTR_ADDRESS = 'address'
 ATTR_UNIT = 'unit'
@@ -61,6 +64,11 @@ SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({
     vol.Required(ATTR_VALUE): vol.All(cv.ensure_list, [cv.positive_int])
 })
 
+SERVICE_WRITE_COIL_SCHEMA = vol.Schema({
+    vol.Required(ATTR_UNIT): cv.positive_int,
+    vol.Required(ATTR_ADDRESS): cv.positive_int,
+    vol.Required(ATTR_STATE): cv.boolean
+})
 
 HUB = None
 
@@ -105,9 +113,18 @@ def setup(hass, config):
         HUB.connect()
         hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
 
+        descriptions = load_yaml_config_file(os.path.join(
+            os.path.dirname(__file__), 'services.yaml')).get(DOMAIN)
+
         # Register services for modbus
-        hass.services.register(DOMAIN, SERVICE_WRITE_REGISTER, write_register,
-                               schema=SERVICE_WRITE_REGISTER_SCHEMA)
+        hass.services.register(
+            DOMAIN, SERVICE_WRITE_REGISTER, write_register,
+            descriptions.get(SERVICE_WRITE_REGISTER),
+            schema=SERVICE_WRITE_REGISTER_SCHEMA)
+        hass.services.register(
+            DOMAIN, SERVICE_WRITE_COIL, write_coil,
+            descriptions.get(SERVICE_WRITE_COIL),
+            schema=SERVICE_WRITE_COIL_SCHEMA)
 
     def write_register(service):
         """Write modbus registers."""
@@ -125,6 +142,13 @@ def setup(hass, config):
                 address,
                 int(float(value)))
 
+    def write_coil(service):
+        """Write modbus coil."""
+        unit = service.data.get(ATTR_UNIT)
+        address = service.data.get(ATTR_ADDRESS)
+        state = service.data.get(ATTR_STATE)
+        HUB.write_coil(unit, address, state)
+
     hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
 
     return True
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 64c963a6b8e..f5a66412962 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -315,15 +315,14 @@ def async_setup(hass, config):
         client_cert = conf.get(CONF_CLIENT_CERT)
         tls_insecure = conf.get(CONF_TLS_INSECURE)
         protocol = conf[CONF_PROTOCOL]
-
-        # hbmqtt requires a client id to be set.
-        if client_id is None:
-            client_id = 'home-assistant'
     elif broker_config:
         # If no broker passed in, auto config to internal server
         broker, port, username, password, certificate, protocol = broker_config
         # Embedded broker doesn't have some ssl variables
         client_key, client_cert, tls_insecure = None, None, None
+        # hbmqtt requires a client id to be set.
+        if client_id is None:
+            client_id = 'home-assistant'
     else:
         err = "Unable to start MQTT broker."
         if conf.get(CONF_EMBEDDED) is not None:
@@ -456,8 +455,8 @@ class MQTT(object):
                 certificate, certfile=client_cert,
                 keyfile=client_key, tls_version=tls_version)
 
-        if tls_insecure is not None:
-            self._mqttc.tls_insecure_set(tls_insecure)
+            if tls_insecure is not None:
+                self._mqttc.tls_insecure_set(tls_insecure)
 
         self._mqttc.on_subscribe = self._mqtt_on_subscribe
         self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py
index 189aa0d02bb..691ff158012 100644
--- a/homeassistant/components/notify/discord.py
+++ b/homeassistant/components/notify/discord.py
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
 
 _LOGGER = logging.getLogger(__name__)
 
-REQUIREMENTS = ['discord.py==0.16.0']
+REQUIREMENTS = ['discord.py==0.16.8']
 
 CONF_TOKEN = 'token'
 
diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py
new file mode 100644
index 00000000000..a3af1eb1914
--- /dev/null
+++ b/homeassistant/components/notify/lametric.py
@@ -0,0 +1,91 @@
+"""
+Notifier for LaMetric time.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/notify.lametric/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.notify import (
+    ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
+from homeassistant.const import CONF_ICON
+import homeassistant.helpers.config_validation as cv
+
+from homeassistant.components.lametric import DOMAIN
+
+REQUIREMENTS = ['lmnotify==0.0.4']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_DISPLAY_TIME = "display_time"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Optional(CONF_ICON, default="i555"): cv.string,
+    vol.Optional(CONF_DISPLAY_TIME, default=10): cv.positive_int,
+})
+
+
+# pylint: disable=unused-variable
+def get_service(hass, config, discovery_info=None):
+    """Get the Slack notification service."""
+    hlmn = hass.data.get(DOMAIN)
+    return LaMetricNotificationService(hlmn,
+                                       config[CONF_ICON],
+                                       config[CONF_DISPLAY_TIME] * 1000)
+
+
+class LaMetricNotificationService(BaseNotificationService):
+    """Implement the notification service for LaMetric."""
+
+    def __init__(self, hasslametricmanager, icon, display_time):
+        """Initialize the service."""
+        self.hasslametricmanager = hasslametricmanager
+        self._icon = icon
+        self._display_time = display_time
+
+    # pylint: disable=broad-except
+    def send_message(self, message="", **kwargs):
+        """Send a message to some LaMetric deviced."""
+        from lmnotify import SimpleFrame, Sound, Model
+
+        targets = kwargs.get(ATTR_TARGET)
+        data = kwargs.get(ATTR_DATA)
+        _LOGGER.debug("Targets/Data: %s/%s", targets, data)
+        icon = self._icon
+        sound = None
+
+        # User-defined icon?
+        if data is not None:
+            if "icon" in data:
+                icon = data["icon"]
+            if "sound" in data:
+                try:
+                    sound = Sound(category="notifications",
+                                  sound_id=data["sound"])
+                    _LOGGER.debug("Adding notification sound %s",
+                                  data["sound"])
+                except AssertionError:
+                    _LOGGER.error("Sound ID %s unknown, ignoring",
+                                  data["sound"])
+
+        text_frame = SimpleFrame(icon, message)
+        _LOGGER.debug("Icon/Message/Duration: %s, %s, %d",
+                      icon, message, self._display_time)
+
+        frames = [text_frame]
+
+        if sound is not None:
+            frames.append(sound)
+
+        _LOGGER.debug(frames)
+
+        model = Model(frames=frames)
+        lmn = self.hasslametricmanager.manager()
+        devices = lmn.get_devices()
+        for dev in devices:
+            if (targets is None) or (dev["name"] in targets):
+                lmn.set_device(dev)
+                lmn.send_notification(model, lifetime=self._display_time)
+                _LOGGER.debug("Sent notification to LaMetric %s", dev["name"])
diff --git a/homeassistant/components/notify/mailgun.py b/homeassistant/components/notify/mailgun.py
index 59c6a50ffc9..1aa403f0ba8 100644
--- a/homeassistant/components/notify/mailgun.py
+++ b/homeassistant/components/notify/mailgun.py
@@ -42,8 +42,8 @@ def get_service(hass, config, discovery_info=None):
         config.get(CONF_RECIPIENT))
     if mailgun_service.connection_is_valid():
         return mailgun_service
-    else:
-        return None
+
+    return None
 
 
 class MailgunNotificationService(BaseNotificationService):
diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py
index a3d42f2be7d..6258f6272c0 100644
--- a/homeassistant/components/notify/sendgrid.py
+++ b/homeassistant/components/notify/sendgrid.py
@@ -74,5 +74,5 @@ class SendgridNotificationService(BaseNotificationService):
         }
 
         response = self._sg.client.mail.send.post(request_body=data)
-        if response.status_code is not 202:
+        if response.status_code != 202:
             _LOGGER.error("Unable to send notification")
diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py
index fa7332326da..a6257970566 100644
--- a/homeassistant/components/notify/slack.py
+++ b/homeassistant/components/notify/slack.py
@@ -5,11 +5,15 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/notify.slack/
 """
 import logging
+import requests
+from requests.auth import HTTPDigestAuth
+from requests.auth import HTTPBasicAuth
 
 import voluptuous as vol
 
 from homeassistant.components.notify import (
-    ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
+    ATTR_TARGET, ATTR_TITLE, ATTR_DATA,
+    PLATFORM_SCHEMA, BaseNotificationService)
 from homeassistant.const import (
     CONF_API_KEY, CONF_USERNAME, CONF_ICON)
 import homeassistant.helpers.config_validation as cv
@@ -19,6 +23,19 @@ REQUIREMENTS = ['slacker==0.9.50']
 _LOGGER = logging.getLogger(__name__)
 
 CONF_CHANNEL = 'default_channel'
+CONF_TIMEOUT = 15
+
+# Top level attributes in 'data'
+ATTR_ATTACHMENTS = 'attachments'
+ATTR_FILE = 'file'
+# Attributes contained in file
+ATTR_FILE_URL = 'url'
+ATTR_FILE_PATH = 'path'
+ATTR_FILE_USERNAME = 'username'
+ATTR_FILE_PASSWORD = 'password'
+ATTR_FILE_AUTH = 'auth'
+# Any other value or absense of 'auth' lead to basic authentication being used
+ATTR_FILE_AUTH_DIGEST = 'digest'
 
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
     vol.Required(CONF_API_KEY): cv.string,
@@ -38,7 +55,8 @@ def get_service(hass, config, discovery_info=None):
             config[CONF_CHANNEL],
             config[CONF_API_KEY],
             config.get(CONF_USERNAME, None),
-            config.get(CONF_ICON, None))
+            config.get(CONF_ICON, None),
+            hass.config.is_allowed_path)
 
     except slacker.Error:
         _LOGGER.exception("Authentication failed")
@@ -48,7 +66,9 @@ def get_service(hass, config, discovery_info=None):
 class SlackNotificationService(BaseNotificationService):
     """Implement the notification service for Slack."""
 
-    def __init__(self, default_channel, api_token, username, icon):
+    def __init__(self, default_channel,
+                 api_token, username,
+                 icon, is_allowed_path):
         """Initialize the service."""
         from slacker import Slacker
         self._default_channel = default_channel
@@ -60,6 +80,7 @@ class SlackNotificationService(BaseNotificationService):
         else:
             self._as_user = True
 
+        self.is_allowed_path = is_allowed_path
         self.slack = Slacker(self._api_token)
         self.slack.auth.test()
 
@@ -72,14 +93,79 @@ class SlackNotificationService(BaseNotificationService):
         else:
             targets = kwargs.get(ATTR_TARGET)
 
-        data = kwargs.get('data')
-        attachments = data.get('attachments') if data else None
+        data = kwargs.get(ATTR_DATA)
+        attachments = data.get(ATTR_ATTACHMENTS) if data else None
+        file = data.get(ATTR_FILE) if data else None
+        title = kwargs.get(ATTR_TITLE)
 
         for target in targets:
             try:
-                self.slack.chat.post_message(
-                    target, message, as_user=self._as_user,
-                    username=self._username, icon_emoji=self._icon,
-                    attachments=attachments, link_names=True)
+                if file is not None:
+                    # Load from file or url
+                    file_as_bytes = self.load_file(
+                        url=file.get(ATTR_FILE_URL),
+                        local_path=file.get(ATTR_FILE_PATH),
+                        username=file.get(ATTR_FILE_USERNAME),
+                        password=file.get(ATTR_FILE_PASSWORD),
+                        auth=file.get(ATTR_FILE_AUTH))
+                    # Choose filename
+                    if file.get(ATTR_FILE_URL):
+                        filename = file.get(ATTR_FILE_URL)
+                    else:
+                        filename = file.get(ATTR_FILE_PATH)
+                    # Prepare structure for slack API
+                    data = {
+                        'content': None,
+                        'filetype': None,
+                        'filename': filename,
+                        # if optional title is none use the filename
+                        'title': title if title else filename,
+                        'initial_comment': message,
+                        'channels': target
+                    }
+                    # Post to slack
+                    self.slack.files.post('files.upload',
+                                          data=data,
+                                          files={'file': file_as_bytes})
+                else:
+                    self.slack.chat.post_message(
+                        target, message, as_user=self._as_user,
+                        username=self._username, icon_emoji=self._icon,
+                        attachments=attachments, link_names=True)
             except slacker.Error as err:
                 _LOGGER.error("Could not send notification. Error: %s", err)
+
+    def load_file(self, url=None, local_path=None,
+                  username=None, password=None, auth=None):
+        """Load image/document/etc from a local path or url."""
+        try:
+            if url is not None:
+                # check whether authentication parameters are provided
+                if username is not None and password is not None:
+                    # Use digest or basic authentication
+                    if ATTR_FILE_AUTH_DIGEST == auth:
+                        auth_ = HTTPDigestAuth(username, password)
+                    else:
+                        auth_ = HTTPBasicAuth(username, password)
+                    # load file from url with authentication
+                    req = requests.get(url, auth=auth_, timeout=CONF_TIMEOUT)
+                else:
+                    # load file from url without authentication
+                    req = requests.get(url, timeout=CONF_TIMEOUT)
+                return req.content
+
+            elif local_path is not None:
+                # Check whether path is whitelisted in configuration.yaml
+                if self.is_allowed_path(local_path):
+                    # load file from local path on server
+                    return open(local_path, "rb")
+                _LOGGER.warning("'%s' is not secure to load data from!",
+                                local_path)
+            else:
+                # neither url nor path provided
+                _LOGGER.warning("Neither url nor local path found in params!")
+
+        except OSError as error:
+            _LOGGER.error("Can't load from url or local path: %s", error)
+
+        return None
diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py
index aaf1a729f2d..fc38647a065 100644
--- a/homeassistant/components/notify/smtp.py
+++ b/homeassistant/components/notify/smtp.py
@@ -74,8 +74,8 @@ def get_service(hass, config, discovery_info=None):
 
     if mail_service.connection_is_valid():
         return mail_service
-    else:
-        return None
+
+    return None
 
 
 class MailNotificationService(BaseNotificationService):
diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py
index 4bbe8a5d9e1..6d74f86132a 100644
--- a/homeassistant/components/notify/twitter.py
+++ b/homeassistant/components/notify/twitter.py
@@ -4,13 +4,16 @@ Twitter platform for notify component.
 For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/notify.twitter/
 """
+import json
 import logging
+import mimetypes
+import os
 
 import voluptuous as vol
 
 import homeassistant.helpers.config_validation as cv
 from homeassistant.components.notify import (
-    PLATFORM_SCHEMA, BaseNotificationService)
+    ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
 
 REQUIREMENTS = ['TwitterAPI==2.4.5']
@@ -21,6 +24,8 @@ CONF_CONSUMER_KEY = 'consumer_key'
 CONF_CONSUMER_SECRET = 'consumer_secret'
 CONF_ACCESS_TOKEN_SECRET = 'access_token_secret'
 
+ATTR_MEDIA = 'media'
+
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
     vol.Required(CONF_ACCESS_TOKEN): cv.string,
     vol.Required(CONF_ACCESS_TOKEN_SECRET): cv.string,
@@ -33,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
 def get_service(hass, config, discovery_info=None):
     """Get the Twitter notification service."""
     return TwitterNotificationService(
+        hass,
         config[CONF_CONSUMER_KEY], config[CONF_CONSUMER_SECRET],
         config[CONF_ACCESS_TOKEN], config[CONF_ACCESS_TOKEN_SECRET],
         config.get(CONF_USERNAME)
@@ -42,26 +48,113 @@ def get_service(hass, config, discovery_info=None):
 class TwitterNotificationService(BaseNotificationService):
     """Implementation of a notification service for the Twitter service."""
 
-    def __init__(self, consumer_key, consumer_secret, access_token_key,
+    def __init__(self, hass, consumer_key, consumer_secret, access_token_key,
                  access_token_secret, username):
         """Initialize the service."""
         from TwitterAPI import TwitterAPI
         self.user = username
+        self.hass = hass
         self.api = TwitterAPI(consumer_key, consumer_secret, access_token_key,
                               access_token_secret)
 
     def send_message(self, message="", **kwargs):
-        """Tweet a message."""
+        """Tweet a message, optionally with media."""
+        data = kwargs.get(ATTR_DATA)
+        media = data.get(ATTR_MEDIA)
+        if not self.hass.config.is_allowed_path(media):
+            _LOGGER.warning("'%s' is not in a whitelisted area.", media)
+            return
+
+        media_id = self.upload_media(media)
+
         if self.user:
-            resp = self.api.request(
-                'direct_messages/new', {'text': message, 'user': self.user})
+            resp = self.api.request('direct_messages/new',
+                                    {'text': message, 'user': self.user,
+                                     'media_ids': media_id})
         else:
-            resp = self.api.request('statuses/update', {'status': message})
+            resp = self.api.request('statuses/update',
+                                    {'status': message, 'media_ids': media_id})
 
         if resp.status_code != 200:
-            import json
-            obj = json.loads(resp.text)
-            error_message = obj['errors'][0]['message']
-            error_code = obj['errors'][0]['code']
-            _LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
-                          error_message, error_code)
+            self.log_error_resp(resp)
+
+    def upload_media(self, media_path=None):
+        """Upload media."""
+        if not media_path:
+            return None
+
+        (media_type, _) = mimetypes.guess_type(media_path)
+        total_bytes = os.path.getsize(media_path)
+
+        file = open(media_path, 'rb')
+        resp = self.upload_media_init(media_type, total_bytes)
+
+        if 199 > resp.status_code < 300:
+            self.log_error_resp(resp)
+            return None
+
+        media_id = resp.json()['media_id']
+        media_id = self.upload_media_chunked(file, total_bytes,
+                                             media_id)
+
+        resp = self.upload_media_finalize(media_id)
+        if 199 > resp.status_code < 300:
+            self.log_error_resp(resp)
+
+        return media_id
+
+    def upload_media_init(self, media_type, total_bytes):
+        """Upload media, INIT phase."""
+        resp = self.api.request('media/upload',
+                                {'command': 'INIT', 'media_type': media_type,
+                                 'total_bytes': total_bytes})
+        return resp
+
+    def upload_media_chunked(self, file, total_bytes, media_id):
+        """Upload media, chunked append."""
+        segment_id = 0
+        bytes_sent = 0
+        while bytes_sent < total_bytes:
+            chunk = file.read(4 * 1024 * 1024)
+            resp = self.upload_media_append(chunk, media_id, segment_id)
+            if resp.status_code not in range(200, 299):
+                self.log_error_resp_append(resp)
+                return None
+            segment_id = segment_id + 1
+            bytes_sent = file.tell()
+            self.log_bytes_sent(bytes_sent, total_bytes)
+        return media_id
+
+    def upload_media_append(self, chunk, media_id, segment_id):
+        """Upload media, append phase."""
+        return self.api.request('media/upload',
+                                {'command': 'APPEND', 'media_id': media_id,
+                                 'segment_index': segment_id},
+                                {'media': chunk})
+
+    def upload_media_finalize(self, media_id):
+        """Upload media, finalize phase."""
+        return self.api.request('media/upload',
+                                {'command': 'FINALIZE', 'media_id': media_id})
+
+    @staticmethod
+    def log_bytes_sent(bytes_sent, total_bytes):
+        """Log upload progress."""
+        _LOGGER.debug("%s of %s bytes uploaded", str(bytes_sent),
+                      str(total_bytes))
+
+    @staticmethod
+    def log_error_resp(resp):
+        """Log error response."""
+        obj = json.loads(resp.text)
+        error_message = obj['error']
+        _LOGGER.error("Error %s : %s", resp.status_code, error_message)
+
+    @staticmethod
+    def log_error_resp_append(resp):
+        """Log error response, during upload append phase."""
+        obj = json.loads(resp.text)
+        error_message = obj['errors'][0]['message']
+        error_code = obj['errors'][0]['code']
+        _LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
+                      error_message, error_code)
diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py
index b06b15c7973..204490ce36c 100644
--- a/homeassistant/components/octoprint.py
+++ b/homeassistant/components/octoprint.py
@@ -17,8 +17,6 @@ _LOGGER = logging.getLogger(__name__)
 
 DOMAIN = 'octoprint'
 
-OCTOPRINT = None
-
 CONFIG_SCHEMA = vol.Schema({
     DOMAIN: vol.Schema({
         vol.Required(CONF_API_KEY): cv.string,
@@ -32,14 +30,15 @@ def setup(hass, config):
     base_url = 'http://{}/api/'.format(config[DOMAIN][CONF_HOST])
     api_key = config[DOMAIN][CONF_API_KEY]
 
-    global OCTOPRINT
+    hass.data[DOMAIN] = {"api": None}
+
     try:
-        OCTOPRINT = OctoPrintAPI(base_url, api_key)
-        OCTOPRINT.get('printer')
-        OCTOPRINT.get('job')
+        octoprint_api = OctoPrintAPI(base_url, api_key)
+        hass.data[DOMAIN]["api"] = octoprint_api
+        octoprint_api.get('printer')
+        octoprint_api.get('job')
     except requests.exceptions.RequestException as conn_err:
         _LOGGER.error("Error setting up OctoPrint API: %r", conn_err)
-        return False
 
     return True
 
@@ -54,6 +53,11 @@ class OctoPrintAPI(object):
                         'X-Api-Key': key}
         self.printer_last_reading = [{}, None]
         self.job_last_reading = [{}, None]
+        self.job_available = False
+        self.printer_available = False
+        self.available = False
+        self.printer_error_logged = False
+        self.job_error_logged = False
 
     def get_tools(self):
         """Get the dynamic list of tools that temperature is monitored on."""
@@ -62,6 +66,7 @@ class OctoPrintAPI(object):
 
     def get(self, endpoint):
         """Send a get request, and return the response as a dict."""
+        # Only query the API at most every 30 seconds
         now = time.time()
         if endpoint == "job":
             last_time = self.job_last_reading[1]
@@ -73,39 +78,67 @@ class OctoPrintAPI(object):
             if last_time is not None:
                 if now - last_time < 30.0:
                     return self.printer_last_reading[0]
+
         url = self.api_url + endpoint
         try:
             response = requests.get(
-                url, headers=self.headers, timeout=30)
+                url, headers=self.headers, timeout=9)
             response.raise_for_status()
             if endpoint == "job":
                 self.job_last_reading[0] = response.json()
                 self.job_last_reading[1] = time.time()
+                self.job_available = True
             elif endpoint == "printer":
                 self.printer_last_reading[0] = response.json()
                 self.printer_last_reading[1] = time.time()
+                self.printer_available = True
+            self.available = self.printer_available and self.job_available
+            if self.available:
+                self.job_error_logged = False
+                self.printer_error_logged = False
             return response.json()
         except (requests.exceptions.ConnectionError,
-                requests.exceptions.HTTPError) as conn_exc:
-            _LOGGER.error("Failed to update OctoPrint status.  Error: %s",
-                          conn_exc)
+                requests.exceptions.HTTPError,
+                requests.exceptions.ReadTimeout) as conn_exc:
+            log_string = "Failed to update OctoPrint status. " + \
+                               "  Error: %s" % (conn_exc)
+            # Only log the first failure
+            if endpoint == "job":
+                log_string = "Endpoint: job " + log_string
+                if not self.job_error_logged:
+                    _LOGGER.error(log_string)
+                    self.job_error_logged = True
+                    self.job_available = False
+            elif endpoint == "printer":
+                log_string = "Endpoint: printer " + log_string
+                if not self.printer_error_logged:
+                    _LOGGER.error(log_string)
+                    self.printer_error_logged = True
+                    self.printer_available = False
+            self.available = False
+            return None
 
     def update(self, sensor_type, end_point, group, tool=None):
         """Return the value for sensor_type from the provided endpoint."""
         response = self.get(end_point)
         if response is not None:
             return get_value_from_json(response, sensor_type, group, tool)
+        return response
 
 
 # pylint: disable=unused-variable
 def get_value_from_json(json_dict, sensor_type, group, tool):
     """Return the value for sensor_type from the JSON."""
-    if group in json_dict:
-        if sensor_type in json_dict[group]:
-            if sensor_type == "target" and json_dict[sensor_type] is None:
-                return 0
-            else:
-                return json_dict[group][sensor_type]
-        elif tool is not None:
-            if sensor_type in json_dict[group][tool]:
-                return json_dict[group][tool][sensor_type]
+    if group not in json_dict:
+        return None
+
+    if sensor_type in json_dict[group]:
+        if sensor_type == "target" and json_dict[sensor_type] is None:
+            return 0
+        return json_dict[group][sensor_type]
+
+    elif tool is not None:
+        if sensor_type in json_dict[group][tool]:
+            return json_dict[group][tool][sensor_type]
+
+    return None
diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py
index cd43fbf715c..9b9e11e0fbb 100644
--- a/homeassistant/components/plant.py
+++ b/homeassistant/components/plant.py
@@ -204,13 +204,13 @@ class Plant(Entity):
                         result.append('{} high'.format(sensor_name))
                         self._icon = params['icon']
 
-        if len(result) == 0:
+        if result:
+            self._state = STATE_PROBLEM
+            self._problems = ','.join(result)
+        else:
             self._state = STATE_OK
             self._icon = 'mdi:thumb-up'
             self._problems = PROBLEM_NONE
-        else:
-            self._state = STATE_PROBLEM
-            self._problems = ','.join(result)
         _LOGGER.debug("New data processed")
         self.hass.async_add_job(self.async_update_ha_state())
 
diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py
new file mode 100644
index 00000000000..4ed6028ac56
--- /dev/null
+++ b/homeassistant/components/prometheus.py
@@ -0,0 +1,234 @@
+"""
+Support for Prometheus metrics export.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/prometheus/
+"""
+import asyncio
+import logging
+
+import voluptuous as vol
+from aiohttp import web
+
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components import recorder
+from homeassistant.const import (CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE,
+                                 CONF_INCLUDE, EVENT_STATE_CHANGED,
+                                 TEMP_CELSIUS, TEMP_FAHRENHEIT)
+from homeassistant import core as hacore
+from homeassistant.helpers import state as state_helper
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['prometheus_client==0.0.19']
+
+DOMAIN = 'prometheus'
+DEPENDENCIES = ['http']
+
+CONFIG_SCHEMA = vol.Schema({
+    DOMAIN: recorder.FILTER_SCHEMA,
+}, extra=vol.ALLOW_EXTRA)
+
+API_ENDPOINT = '/api/prometheus'
+
+
+def setup(hass, config):
+    """Activate Prometheus component."""
+    import prometheus_client
+
+    hass.http.register_view(PrometheusView(prometheus_client))
+
+    conf = config.get(DOMAIN, {})
+    exclude = conf.get(CONF_EXCLUDE, {})
+    include = conf.get(CONF_INCLUDE, {})
+    metrics = Metrics(prometheus_client, exclude, include)
+
+    hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event)
+
+    return True
+
+
+class Metrics:
+    """Model all of the metrics which should be exposed to Prometheus."""
+
+    def __init__(self, prometheus_client, exclude, include):
+        """Initialize Prometheus Metrics."""
+        self.prometheus_client = prometheus_client
+        self.exclude = exclude.get(CONF_ENTITIES, []) + \
+            exclude.get(CONF_DOMAINS, [])
+        self.include_domains = include.get(CONF_DOMAINS, [])
+        self.include_entities = include.get(CONF_ENTITIES, [])
+        self._metrics = {}
+
+    def handle_event(self, event):
+        """Listen for new messages on the bus, and add them to Prometheus."""
+        state = event.data.get('new_state')
+        if state is None:
+            return
+
+        entity_id = state.entity_id
+        _LOGGER.debug("Handling state update for %s", entity_id)
+        domain, _ = hacore.split_entity_id(entity_id)
+
+        if entity_id in self.exclude:
+            return
+        if domain in self.exclude and entity_id not in self.include_entities:
+            return
+        if self.include_domains and domain not in self.include_domains:
+            return
+        if not self.exclude and (self.include_entities and
+                                 entity_id not in self.include_entities):
+            return
+
+        handler = '_handle_' + domain
+
+        if hasattr(self, handler):
+            getattr(self, handler)(state)
+
+    def _metric(self, metric, factory, documentation, labels=None):
+        if labels is None:
+            labels = ['entity', 'friendly_name']
+
+        try:
+            return self._metrics[metric]
+        except KeyError:
+            self._metrics[metric] = factory(metric, documentation, labels)
+            return self._metrics[metric]
+
+    @staticmethod
+    def _labels(state):
+        return {
+            'entity': state.entity_id,
+            'friendly_name': state.attributes.get('friendly_name'),
+        }
+
+    def _battery(self, state):
+        if 'battery_level' in state.attributes:
+            metric = self._metric(
+                'battery_level_percent',
+                self.prometheus_client.Gauge,
+                'Battery level as a percentage of its capacity',
+            )
+            try:
+                value = float(state.attributes['battery_level'])
+                metric.labels(**self._labels(state)).set(value)
+            except ValueError:
+                pass
+
+    def _handle_binary_sensor(self, state):
+        metric = self._metric(
+            'binary_sensor_state',
+            self.prometheus_client.Gauge,
+            'State of the binary sensor (0/1)',
+        )
+        value = state_helper.state_as_number(state)
+        metric.labels(**self._labels(state)).set(value)
+
+    def _handle_device_tracker(self, state):
+        metric = self._metric(
+            'device_tracker_state',
+            self.prometheus_client.Gauge,
+            'State of the device tracker (0/1)',
+        )
+        value = state_helper.state_as_number(state)
+        metric.labels(**self._labels(state)).set(value)
+
+    def _handle_light(self, state):
+        metric = self._metric(
+            'light_state',
+            self.prometheus_client.Gauge,
+            'Load level of a light (0..1)',
+        )
+
+        try:
+            if 'brightness' in state.attributes:
+                value = state.attributes['brightness'] / 255.0
+            else:
+                value = state_helper.state_as_number(state)
+            value = value * 100
+            metric.labels(**self._labels(state)).set(value)
+        except ValueError:
+            pass
+
+    def _handle_lock(self, state):
+        metric = self._metric(
+            'lock_state',
+            self.prometheus_client.Gauge,
+            'State of the lock (0/1)',
+        )
+        value = state_helper.state_as_number(state)
+        metric.labels(**self._labels(state)).set(value)
+
+    def _handle_sensor(self, state):
+        _sensor_types = {
+            TEMP_CELSIUS: (
+                'temperature_c', self.prometheus_client.Gauge,
+                'Temperature in degrees Celsius',
+            ),
+            TEMP_FAHRENHEIT: (
+                'temperature_c', self.prometheus_client.Gauge,
+                'Temperature in degrees Celsius',
+            ),
+            '%': (
+                'relative_humidity', self.prometheus_client.Gauge,
+                'Relative humidity (0..100)',
+            ),
+            'lux': (
+                'light_lux', self.prometheus_client.Gauge,
+                'Light level in lux',
+            ),
+            'kWh': (
+                'electricity_used_kwh', self.prometheus_client.Gauge,
+                'Electricity used by this device in KWh',
+            ),
+            'V': (
+                'voltage', self.prometheus_client.Gauge,
+                'Currently reported voltage in Volts',
+            ),
+            'W': (
+                'electricity_usage_w', self.prometheus_client.Gauge,
+                'Currently reported electricity draw in Watts',
+            ),
+        }
+
+        unit = state.attributes.get('unit_of_measurement')
+        metric = _sensor_types.get(unit)
+
+        if metric is not None:
+            metric = self._metric(*metric)
+            try:
+                value = state_helper.state_as_number(state)
+                metric.labels(**self._labels(state)).set(value)
+            except ValueError:
+                pass
+
+        self._battery(state)
+
+    def _handle_switch(self, state):
+        metric = self._metric(
+            'switch_state',
+            self.prometheus_client.Gauge,
+            'State of the switch (0/1)',
+        )
+        value = state_helper.state_as_number(state)
+        metric.labels(**self._labels(state)).set(value)
+
+
+class PrometheusView(HomeAssistantView):
+    """Handle Prometheus requests."""
+
+    url = API_ENDPOINT
+    name = 'api:prometheus'
+
+    def __init__(self, prometheus_client):
+        """Initialize Prometheus view."""
+        self.prometheus_client = prometheus_client
+
+    @asyncio.coroutine
+    def get(self, request):
+        """Handle request for Prometheus metrics."""
+        _LOGGER.debug('Received Prometheus metrics request')
+
+        return web.Response(
+            body=self.prometheus_client.generate_latest(),
+            content_type="text/plain")
diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py
index cfb7098bbdf..c159bec0f75 100644
--- a/homeassistant/components/python_script.py
+++ b/homeassistant/components/python_script.py
@@ -61,6 +61,7 @@ def execute(hass, filename, source, data=None):
     """Execute Python source."""
     from RestrictedPython import compile_restricted_exec
     from RestrictedPython.Guards import safe_builtins, full_write_guard
+    from RestrictedPython.Utilities import utility_builtins
 
     compiled = compile_restricted_exec(source, filename=filename)
 
@@ -87,8 +88,10 @@ def execute(hass, filename, source, data=None):
 
         return getattr(obj, name, default)
 
+    builtins = safe_builtins.copy()
+    builtins.update(utility_builtins)
     restricted_globals = {
-        '__builtins__': safe_builtins,
+        '__builtins__': builtins,
         '_print_': StubPrinter,
         '_getattr_': protected_getattr,
         '_write_': full_write_guard,
diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py
index dd8ceefb169..d5d6f657bc6 100644
--- a/homeassistant/components/qwikswitch.py
+++ b/homeassistant/components/qwikswitch.py
@@ -183,10 +183,10 @@ def setup(hass, config):
         qsreply = qsusb.devices()
         if qsreply is False:
             return
-        for item in qsreply:
-            if item[QS_ID] in QSUSB:
-                QSUSB[item[QS_ID]].update_value(
-                    round(min(item[PQS_VALUE], 100) * 2.55))
+        for itm in qsreply:
+            if itm[QS_ID] in QSUSB:
+                QSUSB[itm[QS_ID]].update_value(
+                    round(min(itm[PQS_VALUE], 100) * 2.55))
 
     def _start(event):
         """Start listening."""
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 49af353aab8..63dbf9fc1b1 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -158,6 +158,7 @@ class Recorder(threading.Thread):
         """Start processing events to save."""
         from .models import States, Events
         from homeassistant.components import persistent_notification
+        from sqlalchemy import exc
 
         tries = 1
         connected = False
@@ -273,14 +274,31 @@ class Recorder(threading.Thread):
                     self.queue.task_done()
                     continue
 
-            with session_scope(session=self.get_session()) as session:
-                dbevent = Events.from_event(event)
-                session.add(dbevent)
+            tries = 1
+            updated = False
+            while not updated and tries <= 10:
+                if tries != 1:
+                    time.sleep(CONNECT_RETRY_WAIT)
+                try:
+                    with session_scope(session=self.get_session()) as session:
+                        dbevent = Events.from_event(event)
+                        session.add(dbevent)
 
-                if event.event_type == EVENT_STATE_CHANGED:
-                    dbstate = States.from_event(event)
-                    dbstate.event_id = dbevent.event_id
-                    session.add(dbstate)
+                        if event.event_type == EVENT_STATE_CHANGED:
+                            dbstate = States.from_event(event)
+                            dbstate.event_id = dbevent.event_id
+                            session.add(dbstate)
+                    updated = True
+
+                except exc.OperationalError as err:
+                    _LOGGER.error("Error in database connectivity: %s. "
+                                  "(retrying in %s seconds)", err,
+                                  CONNECT_RETRY_WAIT)
+                    tries += 1
+
+            if not updated:
+                _LOGGER.error("Error in database update. Could not save "
+                              "after %d tries. Giving up", tries)
 
             self.queue.task_done()
 
diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py
index 65c7dc6abb6..a87eabc44e6 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -173,5 +173,5 @@ def _process_timestamp(ts):
         return None
     elif ts.tzinfo is None:
         return dt_util.UTC.localize(ts)
-    else:
-        return dt_util.as_utc(ts)
+
+    return dt_util.as_utc(ts)
diff --git a/homeassistant/components/remote/apple_tv.py b/homeassistant/components/remote/apple_tv.py
new file mode 100644
index 00000000000..a7ea113c2db
--- /dev/null
+++ b/homeassistant/components/remote/apple_tv.py
@@ -0,0 +1,87 @@
+"""
+Remote control support for Apple TV.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/remote.apple_tv/
+"""
+import asyncio
+
+from homeassistant.components.apple_tv import (
+    ATTR_ATV, ATTR_POWER, DATA_APPLE_TV)
+from homeassistant.components.remote import ATTR_COMMAND
+from homeassistant.components import remote
+from homeassistant.const import (CONF_NAME, CONF_HOST)
+
+
+DEPENDENCIES = ['apple_tv']
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+    """Set up the Apple TV remote platform."""
+    if not discovery_info:
+        return
+
+    name = discovery_info[CONF_NAME]
+    host = discovery_info[CONF_HOST]
+    atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
+    power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
+    async_add_devices([AppleTVRemote(atv, power, name)])
+
+
+class AppleTVRemote(remote.RemoteDevice):
+    """Device that sends commands to an Apple TV."""
+
+    def __init__(self, atv, power, name):
+        """Initialize device."""
+        self._atv = atv
+        self._name = name
+        self._power = power
+        self._power.listeners.append(self)
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self._name
+
+    @property
+    def is_on(self):
+        """Return true if device is on."""
+        return self._power.turned_on
+
+    @property
+    def should_poll(self):
+        """No polling needed for Apple TV."""
+        return False
+
+    @asyncio.coroutine
+    def async_turn_on(self, **kwargs):
+        """Turn the device on.
+
+        This method is a coroutine.
+        """
+        self._power.set_power_on(True)
+
+    @asyncio.coroutine
+    def async_turn_off(self):
+        """Turn the device off.
+
+        This method is a coroutine.
+        """
+        self._power.set_power_on(False)
+
+    def async_send_command(self, **kwargs):
+        """Send a command to one device.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        # Send commands in specified order but schedule only one coroutine
+        @asyncio.coroutine
+        def _send_commads():
+            for command in kwargs[ATTR_COMMAND]:
+                if not hasattr(self._atv.remote_control, command):
+                    continue
+
+                yield from getattr(self._atv.remote_control, command)()
+
+        return _send_commads()
diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index 74772943691..2cdce927cd8 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -10,15 +10,15 @@ import functools as ft
 import logging
 
 import async_timeout
-import voluptuous as vol
-
 from homeassistant.const import (
     ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP,
     STATE_UNKNOWN)
 from homeassistant.core import CoreState, callback
 from homeassistant.exceptions import HomeAssistantError
 import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.deprecation import get_deprecated
 from homeassistant.helpers.entity import Entity
+import voluptuous as vol
 
 REQUIREMENTS = ['rflink==0.0.34']
 
@@ -27,9 +27,12 @@ _LOGGER = logging.getLogger(__name__)
 ATTR_EVENT = 'event'
 ATTR_STATE = 'state'
 
+CONF_ALIASES = 'aliases'
 CONF_ALIASSES = 'aliasses'
+CONF_GROUP_ALIASES = 'group_aliases'
 CONF_GROUP_ALIASSES = 'group_aliasses'
 CONF_GROUP = 'group'
+CONF_NOGROUP_ALIASES = 'nogroup_aliases'
 CONF_NOGROUP_ALIASSES = 'nogroup_aliasses'
 CONF_DEVICE_DEFAULTS = 'device_defaults'
 CONF_DEVICES = 'devices'
@@ -85,8 +88,7 @@ def identify_event_type(event):
         return EVENT_KEY_COMMAND
     elif EVENT_KEY_SENSOR in event:
         return EVENT_KEY_SENSOR
-    else:
-        return 'unknown'
+    return 'unknown'
 
 
 @asyncio.coroutine
@@ -220,8 +222,8 @@ class RflinkDevice(Entity):
     platform = None
     _state = STATE_UNKNOWN
 
-    def __init__(self, device_id, hass, name=None, aliasses=None, group=True,
-                 group_aliasses=None, nogroup_aliasses=None, fire_event=False,
+    def __init__(self, device_id, hass, name=None, aliases=None, group=True,
+                 group_aliases=None, nogroup_aliases=None, fire_event=False,
                  signal_repetitions=DEFAULT_SIGNAL_REPETITIONS):
         """Initialize the device."""
         self.hass = hass
@@ -399,3 +401,24 @@ class SwitchableRflinkDevice(RflinkCommand):
     def async_turn_off(self, **kwargs):
         """Turn the device off."""
         return self._async_handle_command("turn_off")
+
+
+DEPRECATED_CONFIG_OPTIONS = [
+    CONF_ALIASSES,
+    CONF_GROUP_ALIASSES,
+    CONF_NOGROUP_ALIASSES]
+REPLACEMENT_CONFIG_OPTIONS = [
+    CONF_ALIASES,
+    CONF_GROUP_ALIASES,
+    CONF_NOGROUP_ALIASES]
+
+
+def remove_deprecated(config):
+    """Remove deprecated config options from device config."""
+    for index, deprecated_option in enumerate(DEPRECATED_CONFIG_OPTIONS):
+        if deprecated_option in config:
+            replacement_option = REPLACEMENT_CONFIG_OPTIONS[index]
+            # generate deprecation warning
+            get_deprecated(config, replacement_option, deprecated_option)
+            # remove old config value replacing new one
+            config[replacement_option] = config.pop(deprecated_option)
diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py
index fd34eddf916..b6e4d3415f4 100644
--- a/homeassistant/components/rfxtrx.py
+++ b/homeassistant/components/rfxtrx.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
 )
 from homeassistant.helpers.entity import Entity
 
-REQUIREMENTS = ['pyRFXtrx==0.18.0']
+REQUIREMENTS = ['pyRFXtrx==0.19.0']
 
 DOMAIN = 'rfxtrx'
 
diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py
index b96a56ca2bd..e6f5be71a80 100644
--- a/homeassistant/components/scene/lifx_cloud.py
+++ b/homeassistant/components/scene/lifx_cloud.py
@@ -62,9 +62,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     elif status == 401:
         _LOGGER.error("Unauthorized (bad token?) on %s", url)
         return False
-    else:
-        _LOGGER.error("HTTP error %d on %s", scenes_resp.status, url)
-        return False
+
+    _LOGGER.error("HTTP error %d on %s", scenes_resp.status, url)
+    return False
 
 
 class LifxCloudScene(Scene):
diff --git a/homeassistant/components/scene/velux.py b/homeassistant/components/scene/velux.py
new file mode 100644
index 00000000000..9da7a662117
--- /dev/null
+++ b/homeassistant/components/scene/velux.py
@@ -0,0 +1,53 @@
+"""
+Support for VELUX scenes.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/scene.velux/
+"""
+from homeassistant.components.scene import Scene
+from homeassistant.components.velux import _LOGGER, DATA_VELUX
+
+
+DEPENDENCIES = ['velux']
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the scenes for velux platform."""
+    if DATA_VELUX not in hass.data \
+            or not hass.data[DATA_VELUX].initialized:
+        return False
+
+    entities = []
+    for scene in hass.data[DATA_VELUX].pyvlx.scenes:
+        entities.append(VeluxScene(hass, scene))
+    add_devices(entities)
+    return True
+
+
+class VeluxScene(Scene):
+    """Representation of a velux scene."""
+
+    def __init__(self, hass, scene):
+        """Init velux scene."""
+        _LOGGER.info("Adding VELUX scene: %s", scene)
+        self.hass = hass
+        self.scene = scene
+
+    @property
+    def name(self):
+        """Return the name of the scene."""
+        return self.scene.name
+
+    @property
+    def should_poll(self):
+        """Return that polling is not necessary."""
+        return False
+
+    @property
+    def is_on(self):
+        """There is no way of detecting if a scene is active (yet)."""
+        return False
+
+    def activate(self, **kwargs):
+        """Activate the scene."""
+        self.hass.async_add_job(self.scene.run())
diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py
index 23f7fc4dfbe..e7bf309c33a 100644
--- a/homeassistant/components/sensor/amcrest.py
+++ b/homeassistant/components/sensor/amcrest.py
@@ -4,92 +4,50 @@ This component provides HA sensor support for Amcrest IP cameras.
 For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/sensor.amcrest/
 """
+import asyncio
 from datetime import timedelta
 import logging
 
-import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
-    CONF_HOST, CONF_NAME, CONF_MONITORED_CONDITIONS,
-    CONF_USERNAME, CONF_PASSWORD, CONF_PORT, STATE_UNKNOWN)
+from homeassistant.components.amcrest import SENSORS
 from homeassistant.helpers.entity import Entity
-import homeassistant.loader as loader
+from homeassistant.const import STATE_UNKNOWN
 
-from requests.exceptions import HTTPError, ConnectTimeout
-
-REQUIREMENTS = ['amcrest==1.2.0']
+DEPENDENCIES = ['amcrest']
 
 _LOGGER = logging.getLogger(__name__)
 
-NOTIFICATION_ID = 'amcrest_notification'
-NOTIFICATION_TITLE = 'Amcrest Sensor Setup'
-
-DEFAULT_NAME = 'Amcrest'
-DEFAULT_PORT = 80
 SCAN_INTERVAL = timedelta(seconds=10)
 
-# Sensor types are defined like: Name, units, icon
-SENSOR_TYPES = {
-    'motion_detector': ['Motion Detected', None, 'run'],
-    'sdcard': ['SD Used', '%', 'sd'],
-    'ptz_preset': ['PTZ Preset', None, 'camera-iris'],
-}
 
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
-    vol.Required(CONF_HOST): cv.string,
-    vol.Required(CONF_USERNAME): cv.string,
-    vol.Required(CONF_PASSWORD): cv.string,
-    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
-    vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
-    vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
-        vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
-})
-
-
-def setup_platform(hass, config, add_devices, discovery_info=None):
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     """Set up a sensor for an Amcrest IP Camera."""
-    from amcrest import AmcrestCamera
+    if discovery_info is None:
+        return
 
-    camera = AmcrestCamera(
-        config.get(CONF_HOST), config.get(CONF_PORT),
-        config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
+    device = discovery_info['device']
+    name = discovery_info['name']
+    sensors = discovery_info['sensors']
 
-    persistent_notification = loader.get_component('persistent_notification')
-    try:
-        camera.current_time
-    except (ConnectTimeout, HTTPError) as ex:
-        _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
-        persistent_notification.create(
-            hass, 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - sensors.append(AmcrestSensor(config, camera, sensor_type)) - - add_devices(sensors, True) + amcrest_sensors = [] + for sensor_type in sensors: + amcrest_sensors.append(AmcrestSensor(name, device, sensor_type)) + async_add_devices(amcrest_sensors, True) return True class AmcrestSensor(Entity): """A sensor implementation for Amcrest IP camera.""" - def __init__(self, device_info, camera, sensor_type): + def __init__(self, name, camera, sensor_type): """Initialize a sensor for Amcrest camera.""" - super(AmcrestSensor, self).__init__() self._attrs = {} self._camera = camera self._sensor_type = sensor_type - self._name = '{0}_{1}'.format(device_info.get(CONF_NAME), - SENSOR_TYPES.get(self._sensor_type)[0]) - self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) + self._name = '{0}_{1}'.format(name, + SENSORS.get(self._sensor_type)[0]) + self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2]) self._state = STATE_UNKNOWN @property @@ -115,7 +73,7 @@ class AmcrestSensor(Entity): @property def unit_of_measurement(self): """Return the units of measurement.""" - return SENSOR_TYPES.get(self._sensor_type)[1] + return SENSORS.get(self._sensor_type)[1] def update(self): """Get the latest data and updates the state.""" diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/sensor/apcupsd.py index ec4db5e2934..0ddf6f160ae 100644 --- a/homeassistant/components/sensor/apcupsd.py +++ b/homeassistant/components/sensor/apcupsd.py @@ -96,6 +96,7 @@ INFERRED_UNITS = { ' Watts': 'W', ' Hz': 'Hz', ' C': TEMP_CELSIUS, + ' Percent Load Capacity': '%', } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index b7e224a7447..19860ba84fd 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -123,7 +123,7 @@ class ArestSensor(Entity): if self._pin is not None: request = requests.get( '{}/mode/{}/i'.format(self._resource, self._pin), timeout=10) - if request.status_code is not 200: + if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @property diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index a63451771d6..4aa8e20cb75 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -38,6 +38,13 @@ def discover_sensors(topic, payload): else: unit = TEMP_CELSIUS return ArwnSensor(name, 'temp', unit) + if domain == "moisture": + name = parts[2] + " Moisture" + return ArwnSensor(name, 'moisture', unit, "mdi:water-percent") + if domain == "rain": + if len(parts) >= 2 and parts[2] == "today": + return ArwnSensor("Rain Since Midnight", 'since_midnight', + "in", "mdi:water") if domain == 'barometer': return ArwnSensor('Barometer', 'pressure', unit, "mdi:thermometer-lines") @@ -146,7 +153,4 @@ class ArwnSensor(Entity): @property def icon(self): """Return the icon of device based on its type.""" - if self._icon: - return self._icon - else: - return super().icon + return self._icon diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 4033211e461..545bef12d83 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -139,17 +139,17 @@ class BOMCurrentSensor(Entity): """Return the name of the sensor.""" if self.stationname is None: return 'BOM {}'.format(SENSOR_TYPES[self._condition][0]) - else: - return 'BOM {} {}'.format( - self.stationname, SENSOR_TYPES[self._condition][0]) + + return 'BOM {} {}'.format( + self.stationname, SENSOR_TYPES[self._condition][0]) @property 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] - else: - return STATE_UNKNOWN + + return STATE_UNKNOWN @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 7d5018e054c..97b34b0c881 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['broadlink==0.3'] +REQUIREMENTS = ['broadlink==0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 2d1e716fd1b..753782786d7 100755 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -23,11 +23,18 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time) from homeassistant.util import dt as dt_util -REQUIREMENTS = ['buienradar==0.6'] +REQUIREMENTS = ['buienradar==0.7'] _LOGGER = logging.getLogger(__name__) +TIMEFRAME_LABEL = 'Timeframe' +# Schedule next call after (minutes): +SCHEDULE_OK = 10 +# When an error occurred, new call after (minutes): +SCHEDULE_NOK = 2 + # Supported sensor types: +# Key: ['label', unit, icon] SENSOR_TYPES = { 'stationname': ['Stationname', None, None], 'symbol': ['Symbol', None, None], @@ -47,7 +54,7 @@ SENSOR_TYPES = { 'precipitation_forecast_average': ['Precipitation forecast average', 'mm/h', 'mdi:weather-pouring'], 'precipitation_forecast_total': ['Precipitation forecast total', - 'mm/h', 'mdi:weather-pouring'] + 'mm', 'mdi:weather-pouring'] } CONF_TIMEFRAME = 'timeframe' @@ -61,13 +68,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_TIMEFRAME): cv.positive_int + vol.Optional(CONF_TIMEFRAME, default=60): + vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup the buienradar sensor.""" + """Create the buienradar sensor.""" from homeassistant.components.weather.buienradar import DEFAULT_TIMEFRAME latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -81,6 +89,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} + _LOGGER.debug("Initializing buienradar sensor coordinate %s, timeframe %s", + coordinates, timeframe) + dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'))) @@ -88,7 +99,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): data = BrData(hass, coordinates, timeframe, dev) # schedule the first update in 1 minute from now: - _LOGGER.debug("Start running....") yield from data.schedule_update(1) @@ -97,6 +107,8 @@ class BrSensor(Entity): def __init__(self, sensor_type, client_name): """Initialize the sensor.""" + from buienradar.buienradar import (PRECIPITATION_FORECAST) + self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type @@ -104,16 +116,22 @@ class BrSensor(Entity): self._unit_of_measurement = SENSOR_TYPES[self.type][1] self._entity_picture = None self._attribution = None + self._measured = None self._stationname = None + if self.type.startswith(PRECIPITATION_FORECAST): + self._timeframe = None + def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.buienradar import (ATTRIBUTION, IMAGE, STATIONNAME, - SYMBOL, PRECIPITATION_FORECAST) + from buienradar.buienradar import (ATTRIBUTION, IMAGE, MEASURED, + PRECIPITATION_FORECAST, STATIONNAME, + SYMBOL, TIMEFRAME) self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) + self._measured = data.get(MEASURED) if self.type == SYMBOL: # update weather symbol & status text new_state = data.get(self.type) @@ -124,21 +142,26 @@ class BrSensor(Entity): self._state = new_state self._entity_picture = img return True - elif self.type.startswith(PRECIPITATION_FORECAST): + return False + + if self.type.startswith(PRECIPITATION_FORECAST): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) new_state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) + self._timeframe = nested.get(TIMEFRAME) # pylint: disable=protected-access if new_state != self._state: self._state = new_state return True - else: - # update all other sensors - new_state = data.get(self.type) - # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True + return False + + # update all other sensors + new_state = data.get(self.type) + # pylint: disable=protected-access + if new_state != self._state: + self._state = new_state + return True + return False @property def attribution(self): @@ -167,12 +190,21 @@ class BrSensor(Entity): if self.type != SYMBOL: return None - else: - return self._entity_picture + + return self._entity_picture @property def device_state_attributes(self): """Return the state attributes.""" + from buienradar.buienradar import (PRECIPITATION_FORECAST) + + if self.type.startswith(PRECIPITATION_FORECAST): + result = {ATTR_ATTRIBUTION: self._attribution} + if self._timeframe is not None: + result[TIMEFRAME_LABEL] = "%d min" % (self._timeframe) + + return result + return { ATTR_ATTRIBUTION: self._attribution, SENSOR_TYPES['stationname'][0]: self._stationname, @@ -223,7 +255,7 @@ class BrData(object): @asyncio.coroutine def get_data(self, url): - """Load xmpl data from specified url.""" + """Load data from specified url.""" from buienradar.buienradar import (CONTENT, MESSAGE, STATUS_CODE, SUCCESS) @@ -235,14 +267,20 @@ class BrData(object): with async_timeout.timeout(10, loop=self.hass.loop): resp = yield from websession.get(url) - result[SUCCESS] = (resp.status == 200) result[STATUS_CODE] = resp.status result[CONTENT] = yield from resp.text() + if resp.status == 200: + result[SUCCESS] = True + else: + result[MESSAGE] = "Got http statuscode: %d" % (resp.status) return result except (asyncio.TimeoutError, aiohttp.ClientError) as err: result[MESSAGE] = "%s" % err return result + finally: + if resp is not None: + yield from resp.release() @asyncio.coroutine def async_update(self, *_): @@ -254,6 +292,16 @@ class BrData(object): if not content.get(SUCCESS, False): content = yield from self.get_data('http://api.buienradar.nl') + if content.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning("Unable to retrieve xml data from Buienradar." + "(Msg: %s, status: %s,)", + content.get(MESSAGE), + content.get(STATUS_CODE),) + # schedule new call + yield from self.schedule_update(SCHEDULE_NOK) + return + # rounding coordinates prevents unnecessary redirects/calls rainurl = 'http://gadgets.buienradar.nl/data/raintext/?lat={}&lon={}' rainurl = rainurl.format( @@ -262,28 +310,33 @@ class BrData(object): ) raincontent = yield from self.get_data(rainurl) - if content.get(SUCCESS) and raincontent.get(SUCCESS): - result = parse_data(content.get(CONTENT), - raincontent.get(CONTENT), - self.coordinates[CONF_LATITUDE], - self.coordinates[CONF_LONGITUDE], - self.timeframe) - if result.get(SUCCESS): - self.data = result.get(DATA) - - yield from self.update_devices() - - yield from self.schedule_update(10) - else: - yield from self.schedule_update(2) - else: + if raincontent.get(SUCCESS) is not True: # unable to get the data - _LOGGER.warning("Unable to retrieve data from Buienradar." + _LOGGER.warning("Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", - result.get(MESSAGE), - result.get(STATUS_CODE),) + raincontent.get(MESSAGE), + raincontent.get(STATUS_CODE),) # schedule new call - yield from self.schedule_update(2) + yield from self.schedule_update(SCHEDULE_NOK) + return + + result = parse_data(content.get(CONTENT), + raincontent.get(CONTENT), + self.coordinates[CONF_LATITUDE], + self.coordinates[CONF_LONGITUDE], + self.timeframe) + + _LOGGER.debug("Buienradar parsed data: %s", result) + if result.get(SUCCESS) is not True: + _LOGGER.warning("Unable to parse data from Buienradar." + "(Msg: %s)", + result.get(MESSAGE),) + yield from self.schedule_update(SCHEDULE_NOK) + return + + self.data = result.get(DATA) + yield from self.update_devices() + yield from self.schedule_update(SCHEDULE_OK) @property def attribution(self): diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py new file mode 100644 index 00000000000..15046897732 --- /dev/null +++ b/homeassistant/components/sensor/citybikes.py @@ -0,0 +1,293 @@ +""" +Sensor for the CityBikes data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.citybikes/ +""" +import logging +from datetime import timedelta + +import asyncio +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LOCATION, ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_FRIENDLY_NAME, STATE_UNKNOWN, LENGTH_METERS, LENGTH_FEET) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import location, distance +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_ENDPOINT = 'https://api.citybik.es/{uri}' +NETWORKS_URI = 'v2/networks' +STATIONS_URI = 'v2/networks/{uid}?fields=network.stations' + +REQUEST_TIMEOUT = 5 # In seconds; argument to asyncio.timeout +SCAN_INTERVAL = timedelta(minutes=5) # Timely, and doesn't suffocate the API +DOMAIN = 'citybikes' +MONITORED_NETWORKS = 'monitored-networks' +CONF_NETWORK = 'network' +CONF_RADIUS = 'radius' +CONF_STATIONS_LIST = 'stations' +ATTR_NETWORKS_LIST = 'networks' +ATTR_NETWORK = 'network' +ATTR_STATIONS_LIST = 'stations' +ATTR_ID = 'id' +ATTR_UID = 'uid' +ATTR_NAME = 'name' +ATTR_EXTRA = 'extra' +ATTR_TIMESTAMP = 'timestamp' +ATTR_EMPTY_SLOTS = 'empty_slots' +ATTR_FREE_BIKES = 'free_bikes' +ATTR_TIMESTAMP = 'timestamp' +CITYBIKES_ATTRIBUTION = "Information provided by the CityBikes Project "\ + "(https://citybik.es/#about)" + + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_RADIUS, CONF_STATIONS_LIST), + PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=''): cv.string, + vol.Optional(CONF_NETWORK): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, + vol.Optional(CONF_RADIUS, 'station_filter'): cv.positive_int, + vol.Optional(CONF_STATIONS_LIST, 'station_filter'): + vol.All( + cv.ensure_list, + vol.Length(min=1), + [cv.string]) + })) + +NETWORK_SCHEMA = vol.Schema({ + vol.Required(ATTR_ID): cv.string, + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_LOCATION): vol.Schema({ + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + }, extra=vol.REMOVE_EXTRA), + }, extra=vol.REMOVE_EXTRA) + +NETWORKS_RESPONSE_SCHEMA = vol.Schema({ + vol.Required(ATTR_NETWORKS_LIST): [NETWORK_SCHEMA], + }) + +STATION_SCHEMA = vol.Schema({ + vol.Required(ATTR_FREE_BIKES): cv.positive_int, + vol.Required(ATTR_EMPTY_SLOTS): cv.positive_int, + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.latitude, + vol.Required(ATTR_ID): cv.string, + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_TIMESTAMP): cv.string, + vol.Optional(ATTR_EXTRA): vol.Schema({ + vol.Optional(ATTR_UID): cv.string + }, extra=vol.REMOVE_EXTRA) + }, extra=vol.REMOVE_EXTRA) + +STATIONS_RESPONSE_SCHEMA = vol.Schema({ + vol.Required(ATTR_NETWORK): vol.Schema({ + vol.Required(ATTR_STATIONS_LIST): [STATION_SCHEMA] + }, extra=vol.REMOVE_EXTRA) + }) + + +class CityBikesRequestError(Exception): + """Error to indicate a CityBikes API request has failed.""" + + pass + + +@asyncio.coroutine +def async_citybikes_request(hass, uri, schema): + """Perform a request to CityBikes API endpoint, and parse the response.""" + try: + session = async_get_clientsession(hass) + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + req = yield from session.get(DEFAULT_ENDPOINT.format(uri=uri)) + + json_response = yield from req.json() + return schema(json_response) + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Could not connect to CityBikes API endpoint") + except ValueError: + _LOGGER.error("Received non-JSON data from CityBikes API endpoint") + except vol.Invalid as err: + _LOGGER.error("Received unexpected JSON from CityBikes" + " API endpoint: %s", err) + raise CityBikesRequestError + + +# pylint: disable=unused-argument +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the CityBikes platform.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {MONITORED_NETWORKS: {}} + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + network_id = config.get(CONF_NETWORK) + stations_list = set(config.get(CONF_STATIONS_LIST, [])) + radius = config.get(CONF_RADIUS, 0) + name = config.get(CONF_NAME) + if not hass.config.units.is_metric: + radius = distance.convert(radius, LENGTH_FEET, LENGTH_METERS) + + if not network_id: + network_id = yield from CityBikesNetwork.get_closest_network_id( + hass, latitude, longitude) + + if network_id not in hass.data[DOMAIN][MONITORED_NETWORKS]: + network = CityBikesNetwork(hass, network_id) + hass.data[DOMAIN][MONITORED_NETWORKS][network_id] = network + hass.async_add_job(network.async_refresh) + async_track_time_interval(hass, network.async_refresh, + SCAN_INTERVAL) + else: + network = hass.data[DOMAIN][MONITORED_NETWORKS][network_id] + + yield from network.ready.wait() + + entities = [] + for station in network.stations: + dist = location.distance(latitude, longitude, + station[ATTR_LATITUDE], + station[ATTR_LONGITUDE]) + station_id = station[ATTR_ID] + station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, '')) + + if radius > dist or stations_list.intersection((station_id, + station_uid)): + entities.append(CityBikesStation(network, station_id, name)) + + async_add_entities(entities, True) + + +class CityBikesNetwork: + """Thin wrapper around a CityBikes network object.""" + + NETWORKS_LIST = None + NETWORKS_LIST_LOADING = asyncio.Condition() + + @classmethod + @asyncio.coroutine + def get_closest_network_id(cls, hass, latitude, longitude): + """Return the id of the network closest to provided location.""" + try: + yield from cls.NETWORKS_LIST_LOADING.acquire() + if cls.NETWORKS_LIST is None: + networks = yield from async_citybikes_request( + hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA) + cls.NETWORKS_LIST = networks[ATTR_NETWORKS_LIST] + networks_list = cls.NETWORKS_LIST + network = networks_list[0] + result = network[ATTR_ID] + minimum_dist = location.distance( + latitude, longitude, + network[ATTR_LOCATION][ATTR_LATITUDE], + network[ATTR_LOCATION][ATTR_LONGITUDE]) + for network in networks_list[1:]: + network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE] + network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE] + dist = location.distance(latitude, longitude, + network_latitude, network_longitude) + if dist < minimum_dist: + minimum_dist = dist + result = network[ATTR_ID] + + return result + except CityBikesRequestError: + raise PlatformNotReady + finally: + cls.NETWORKS_LIST_LOADING.release() + + def __init__(self, hass, network_id): + """Initialize the network object.""" + self.hass = hass + self.network_id = network_id + self.stations = [] + self.ready = asyncio.Event() + + @asyncio.coroutine + def async_refresh(self, now=None): + """Refresh the state of the network.""" + try: + network = yield from async_citybikes_request( + self.hass, STATIONS_URI.format(uid=self.network_id), + STATIONS_RESPONSE_SCHEMA) + self.stations = network[ATTR_NETWORK][ATTR_STATIONS_LIST] + self.ready.set() + except CityBikesRequestError: + if now is not None: + self.ready.clear() + else: + raise PlatformNotReady + + +class CityBikesStation(Entity): + """CityBikes API Sensor.""" + + def __init__(self, network, station_id, base_name=''): + """Initialize the sensor.""" + self._network = network + self._station_id = station_id + self._station_data = {} + self._base_name = base_name + + @property + def state(self): + """Return the state of the sensor.""" + return self._station_data.get(ATTR_FREE_BIKES, STATE_UNKNOWN) + + @property + def name(self): + """Return the name of the sensor.""" + if self._base_name: + return "{} {} {}".format(self._network.network_id, self._base_name, + self._station_id) + return "{} {}".format(self._network.network_id, self._station_id) + + @asyncio.coroutine + def async_update(self): + """Update station state.""" + if self._network.ready.is_set(): + for station in self._network.stations: + if station[ATTR_ID] == self._station_id: + self._station_data = station + break + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._station_data: + return { + ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION, + ATTR_UID: self._station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), + ATTR_LATITUDE: self._station_data[ATTR_LATITUDE], + ATTR_LONGITUDE: self._station_data[ATTR_LONGITUDE], + ATTR_EMPTY_SLOTS: self._station_data[ATTR_EMPTY_SLOTS], + ATTR_FRIENDLY_NAME: self._station_data[ATTR_NAME], + ATTR_TIMESTAMP: self._station_data[ATTR_TIMESTAMP], + } + return {ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION} + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return 'bikes' + + @property + def icon(self): + """Return the icon.""" + return 'mdi:bike' diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index eaf0c474994..a10a276bf0f 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -187,9 +187,9 @@ class DarkSkySensor(Entity): """Return the name of the sensor.""" if self.forecast_day == 0: return '{} {}'.format(self.client_name, self._name) - else: - return '{} {} {}'.format( - self.client_name, self._name, self.forecast_day) + + return '{} {} {}'.format( + self.client_name, self._name, self.forecast_day) @property def state(self): @@ -214,8 +214,8 @@ class DarkSkySensor(Entity): if self._icon in CONDITION_PICTURES: return CONDITION_PICTURES[self._icon] - else: - return None + + return None def update_unit_of_measurement(self): """Update units based on unit system.""" diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index a6fc9b10bee..1f24a0ee667 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -26,6 +26,8 @@ _LOGGER = logging.getLogger(__name__) CONF_PIN = 'pin' CONF_SENSOR = 'sensor' +CONF_HUMIDITY_OFFSET = 'humidity_offset' +CONF_TEMPERATURE_OFFSET = 'temperature_offset' DEFAULT_NAME = 'DHT Sensor' @@ -45,6 +47,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TEMPERATURE_OFFSET, default=0): + vol.All(vol.Coerce(float), vol.Range(min=-100, max=100)), + vol.Optional(CONF_HUMIDITY_OFFSET, default=0): + vol.All(vol.Coerce(float), vol.Range(min=-100, max=100)) }) @@ -61,6 +67,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } sensor = available_sensors.get(config.get(CONF_SENSOR)) pin = config.get(CONF_PIN) + temperature_offset = config.get(CONF_TEMPERATURE_OFFSET) + humidity_offset = config.get(CONF_HUMIDITY_OFFSET) if not sensor: _LOGGER.error("DHT sensor type is not supported") @@ -73,7 +81,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: for variable in config[CONF_MONITORED_CONDITIONS]: dev.append(DHTSensor( - data, variable, SENSOR_TYPES[variable][1], name)) + data, variable, SENSOR_TYPES[variable][1], name, + temperature_offset, humidity_offset)) except KeyError: pass @@ -83,13 +92,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DHTSensor(Entity): """Implementation of the DHT sensor.""" - def __init__(self, dht_client, sensor_type, temp_unit, name): + def __init__(self, dht_client, sensor_type, temp_unit, name, + temperature_offset, humidity_offset): """Initialize the sensor.""" self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] self.dht_client = dht_client self.temp_unit = temp_unit self.type = sensor_type + self.temperature_offset = temperature_offset + self.humidity_offset = humidity_offset self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self.update() @@ -112,18 +124,24 @@ class DHTSensor(Entity): def update(self): """Get the latest data from the DHT and updates the states.""" self.dht_client.update() + temperature_offset = self.temperature_offset + humidity_offset = self.humidity_offset data = self.dht_client.data if self.type == SENSOR_TEMPERATURE: - temperature = round(data[SENSOR_TEMPERATURE], 1) + temperature = data[SENSOR_TEMPERATURE] + _LOGGER.debug("Temperature %.1f \u00b0C + offset %.1f", + temperature, temperature_offset) if (temperature >= -20) and (temperature < 80): - self._state = temperature + self._state = round(temperature + temperature_offset, 1) if self.temp_unit == TEMP_FAHRENHEIT: self._state = round(celsius_to_fahrenheit(temperature), 1) elif self.type == SENSOR_HUMIDITY: - humidity = round(data[SENSOR_HUMIDITY], 1) + humidity = data[SENSOR_HUMIDITY] + _LOGGER.debug("Humidity %.1f%% + offset %.1f", + humidity, humidity_offset) if (humidity >= 0) and (humidity <= 100): - self._state = humidity + self._state = round(humidity + humidity_offset, 1) class DHTClient(object): diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 23324fe7360..76fde35410d 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -207,11 +207,11 @@ class DSMREntity(Entity): if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) - else: - if value is not None: - return value - else: - return STATE_UNKNOWN + + if value is not None: + return value + + return STATE_UNKNOWN @property def unit_of_measurement(self): @@ -227,8 +227,8 @@ class DSMREntity(Entity): return 'normal' elif value == '0001': return 'low' - else: - return STATE_UNKNOWN + + return STATE_UNKNOWN class DerivativeDSMREntity(DSMREntity): diff --git a/homeassistant/components/sensor/dublin_bus_transport.py b/homeassistant/components/sensor/dublin_bus_transport.py index f1dda41c05f..f6d791f9fd6 100644 --- a/homeassistant/components/sensor/dublin_bus_transport.py +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -141,7 +141,7 @@ class PublicTransportData(object): params = {} params['stopid'] = self.stop - if len(self.route) > 0: + if self.route: params['routeid'] = self.route params['maxresults'] = 2 diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py index 2f324519566..d3839f847ea 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -1,15 +1,24 @@ -"""Support for Dyson Pure Cool Link Sensors.""" +"""Support for Dyson Pure Cool Link Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.dyson/ +""" import logging import asyncio -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import TEMP_CELSIUS from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.helpers.entity import Entity DEPENDENCIES = ['dyson'] -SENSOR_UNITS = {'filter_life': 'hours'} +SENSOR_UNITS = { + "filter_life": "hours", + "humidity": "%", + "dust": "level", + "air_quality": "level" +} _LOGGER = logging.getLogger(__name__) @@ -18,21 +27,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dyson Sensors.""" _LOGGER.info("Creating new Dyson fans") devices = [] + unit = hass.config.units.temperature_unit # Get Dyson Devices from parent component for device in hass.data[DYSON_DEVICES]: devices.append(DysonFilterLifeSensor(hass, device)) + devices.append(DysonDustSensor(hass, device)) + devices.append(DysonHumiditySensor(hass, device)) + devices.append(DysonTemperatureSensor(hass, device, unit)) + devices.append(DysonAirQualitySensor(hass, device)) add_devices(devices) -class DysonFilterLifeSensor(Entity): - """Representation of Dyson filter life sensor (in hours).""" +class DysonSensor(Entity): + """Representation of Dyson sensor.""" def __init__(self, hass, device): """Create a new Dyson filter life sensor.""" self.hass = hass self._device = device - self._name = "{} filter life".format(self._device.name) self._old_value = None + self._name = None @asyncio.coroutine def async_added_to_hass(self): @@ -42,10 +56,10 @@ class DysonFilterLifeSensor(Entity): def on_message(self, message): """Called when new messages received from the fan.""" - _LOGGER.debug( - "Message received for %s device: %s", self.name, message) # Prevent refreshing if not needed if self._old_value is None or self._old_value != self.state: + _LOGGER.debug("Message received for %s device: %s", self.name, + message) self._old_value = self.state self.schedule_update_ha_state() @@ -54,20 +68,116 @@ class DysonFilterLifeSensor(Entity): """No polling needed.""" return False - @property - def state(self): - """Return filter life in hours..""" - if self._device.state: - return self._device.state.filter_life - else: - return STATE_UNKNOWN - @property def name(self): """Return the name of the dyson sensor name.""" return self._name + +class DysonFilterLifeSensor(DysonSensor): + """Representation of Dyson filter life sensor (in hours).""" + + def __init__(self, hass, device): + """Create a new Dyson filter life sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} filter life".format(self._device.name) + + @property + def state(self): + """Return filter life in hours.""" + if self._device.state: + return int(self._device.state.filter_life) + return None + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_UNITS['filter_life'] + + +class DysonDustSensor(DysonSensor): + """Representation of Dyson Dust sensor (lower is better).""" + + def __init__(self, hass, device): + """Create a new Dyson Dust sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} dust".format(self._device.name) + + @property + def state(self): + """Return Dust value.""" + if self._device.environmental_state: + return self._device.environmental_state.dust + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['dust'] + + +class DysonHumiditySensor(DysonSensor): + """Representation of Dyson Humidity sensor.""" + + def __init__(self, hass, device): + """Create a new Dyson Humidity sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} humidity".format(self._device.name) + + @property + def state(self): + """Return Dust value.""" + if self._device.environmental_state: + return self._device.environmental_state.humidity + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['humidity'] + + +class DysonTemperatureSensor(DysonSensor): + """Representation of Dyson Temperature sensor.""" + + def __init__(self, hass, device, unit): + """Create a new Dyson Temperature sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} temperature".format(self._device.name) + self._unit = unit + + @property + def state(self): + """Return Dust value.""" + if self._device.environmental_state: + temperature_kelvin = self._device.environmental_state.temperature + if self._unit == TEMP_CELSIUS: + return float("{0:.1f}".format(temperature_kelvin - 273.15)) + return float("{0:.1f}".format(temperature_kelvin * 9 / 5 - 459.67)) + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + +class DysonAirQualitySensor(DysonSensor): + """Representation of Dyson Air Quality sensor (lower is better).""" + + def __init__(self, hass, device): + """Create a new Dyson Air Quality sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} air quality".format(self._device.name) + + @property + def state(self): + """Return Air QUality value.""" + if self._device.environmental_state: + return self._device.environmental_state.volatil_organic_compounds + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['air_quality'] diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index 7fd5f14e1af..fe05da3ccdd 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -86,8 +86,7 @@ def get_from_conf(config, config_key, length): _LOGGER.error("Error in config parameter %s: Must be exactly %d " "bytes. Device will not be added", config_key, length/2) return None - else: - return string + return string class EddystoneTemp(Entity): diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py index e3b2f92d4e1..f7d42a9f5bd 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/sensor/eight_sleep.py @@ -155,8 +155,8 @@ class EightUserSensor(EightSleepUserEntity): elif 'bed_temp' in self._sensor: if self._units == 'si': return '°C' - else: - return '°F' + return '°F' + return None @property def icon(self): @@ -264,8 +264,7 @@ class EightRoomSensor(EightSleepUserEntity): """Return the unit the value is expressed in.""" if self._units == 'si': return '°C' - else: - return '°F' + return '°F' @property def icon(self): diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index ea2fcd026b3..a780d999b7e 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -91,10 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _stop_listener ) - if monitor.sock is None: - return False - else: - return True + return monitor.sock is not None class FritzBoxCallSensor(Entity): @@ -118,10 +115,7 @@ class FritzBoxCallSensor(Entity): @property def should_poll(self): """Only poll to update phonebook, if defined.""" - if self.phonebook is None: - return False - else: - return True + return self.phonebook is not None @property def state(self): @@ -142,8 +136,7 @@ class FritzBoxCallSensor(Entity): """Return a name for a given phone number.""" if self.phonebook is None: return 'unknown' - else: - return self.phonebook.get_name(number) + return self.phonebook.get_name(number) def update(self): """Update the phonebook if it is defined.""" diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 09738454bcb..58ac363b98e 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -89,8 +89,7 @@ class GlancesSensor(Entity): """Return the name of the sensor.""" if self._name is None: return SENSOR_TYPES[self.type][0] - else: - return '{} {}'.format(self._name, SENSOR_TYPES[self.type][0]) + return '{} {}'.format(self._name, SENSOR_TYPES[self.type][0]) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 9027802c295..472dd1d70f6 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -94,8 +94,7 @@ class GpsdSensor(Entity): return "3D Fix" elif self.agps_thread.data_stream.mode == 2: return "2D Fix" - else: - return STATE_UNKNOWN + return STATE_UNKNOWN @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index c5313a7c215..9aa9f14663c 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -52,6 +52,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): now = datetime.datetime.now() + offset day_name = now.strftime('%A').lower() now_str = now.strftime('%H:%M:%S') + today = now.strftime('%Y-%m-%d') from sqlalchemy.sql import text @@ -89,11 +90,14 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): AND start_station.stop_id = :origin_station_id AND end_station.stop_id = :end_station_id AND origin_stop_time.stop_sequence < destination_stop_time.stop_sequence + AND calendar.start_date <= :today + AND calendar.end_date >= :today ORDER BY origin_stop_time.departure_time LIMIT 1; """.format(day_name=day_name)) result = sched.engine.execute(sql_query, now_str=now_str, origin_station_id=origin_station.id, - end_station_id=destination_station.id) + end_station_id=destination_station.id, + today=today) item = {} for row in result: item = row @@ -101,7 +105,6 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): if item == {}: return None - today = now.strftime('%Y-%m-%d') departure_time_string = '{} {}'.format(today, item[2]) arrival_time_string = '{} {}'.format(today, item[3]) departure_time = datetime.datetime.strptime(departure_time_string, diff --git a/homeassistant/components/sensor/hddtemp.py b/homeassistant/components/sensor/hddtemp.py index 87647f4d16c..cd84bd8f9a8 100644 --- a/homeassistant/components/sensor/hddtemp.py +++ b/homeassistant/components/sensor/hddtemp.py @@ -77,8 +77,7 @@ class HddTempSensor(Entity): """Return the unit the value is expressed in.""" if self._details[4] == 'C': return TEMP_CELSIUS - else: - return TEMP_FAHRENHEIT + return TEMP_FAHRENHEIT @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/history_stats.py b/homeassistant/components/sensor/history_stats.py index fa000a75875..175bdafd4a9 100644 --- a/homeassistant/components/sensor/history_stats.py +++ b/homeassistant/components/sensor/history_stats.py @@ -296,8 +296,7 @@ class HistoryStatsHelper: return '%dd %dh %dm' % (days, hours, minutes) elif hours > 0: return '%dh %dm' % (hours, minutes) - else: - return '%dm' % minutes + return '%dm' % minutes @staticmethod def pretty_ratio(value, period): diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 30db91bc8b0..771b4a94bd4 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -56,8 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devices = [] - for config in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMSensor(hass, config) + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + new_device = HMSensor(hass, conf) new_device.link_homematic() devices.append(new_device) diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index c3bcbf60828..7bfe2dbd62a 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -121,8 +121,7 @@ class IOSSensor(Entity): if self.type == "state": return returning_icon_state - else: - return returning_icon_level + return returning_icon_level def update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 2f6d7346283..57995a831f3 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -310,8 +310,7 @@ class ISYSensorDevice(isy.ISYDevice): raw_units = self.raw_unit_of_measurement if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS): return self.hass.config.units.temperature_unit - else: - return raw_units + return raw_units class ISYWeatherDevice(isy.ISYDevice): diff --git a/homeassistant/components/sensor/kwb.py b/homeassistant/components/sensor/kwb.py index 0641917145b..f307b0f6102 100644 --- a/homeassistant/components/sensor/kwb.py +++ b/homeassistant/components/sensor/kwb.py @@ -105,8 +105,7 @@ class KWBSensor(Entity): """Return the state of value.""" if self._sensor.value is not None and self._sensor.available: return self._sensor.value - else: - return STATE_UNKNOWN + return STATE_UNKNOWN @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/london_underground.py b/homeassistant/components/sensor/london_underground.py new file mode 100644 index 00000000000..fe13c0db8a7 --- /dev/null +++ b/homeassistant/components/sensor/london_underground.py @@ -0,0 +1,135 @@ +""" +Sensor for checking the status of London Underground tube lines. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.london_underground/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +import requests + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Powered by TfL Open Data" +CONF_LINE = 'line' +SCAN_INTERVAL = timedelta(seconds=30) +TUBE_LINES = [ + 'Bakerloo', + 'Central', + 'Circle', + 'District', + 'DLR', + 'Hammersmith & City', + 'Jubilee', + 'London Overground', + 'Metropolitan', + 'Northern', + 'Piccadilly', + 'TfL Rail', + 'Victoria', + 'Waterloo & City'] +URL = 'https://api.tfl.gov.uk/line/mode/tube,overground,dlr,tflrail/status' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_LINE): + vol.All(cv.ensure_list, [vol.In(list(TUBE_LINES))]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tube sensor.""" + data = TubeData() + data.update() + sensors = [] + for line in config.get(CONF_LINE): + sensors.append(LondonTubeSensor(line, data)) + + add_devices(sensors, True) + + +class LondonTubeSensor(Entity): + """Sensor that reads the status of a line from TubeData.""" + + ICON = 'mdi:subway' + + def __init__(self, name, data): + """Initialize the sensor.""" + self._name = name + self._data = data + self._state = None + self._description = None + + @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 + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self.ICON + + @property + def device_state_attributes(self): + """Return other details about the sensor state.""" + attrs = {} + attrs['Description'] = self._description + return attrs + + def update(self): + """Update the sensor.""" + self._data.update() + self._state = self._data.data[self.name]['State'] + self._description = self._data.data[self.name]['Description'] + + +class TubeData(object): + """Get the latest tube data from TFL.""" + + def __init__(self): + """Initialize the TubeData object.""" + self.data = None + + # Update only once in scan interval. + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from TFL.""" + response = requests.get(URL) + if response.status_code != 200: + _LOGGER.warning("Invalid response from API") + else: + self.data = parse_api_response(response.json()) + + +def parse_api_response(response): + """Take in the TFL API json response.""" + lines = [line['name'] for line in response] + data_dict = dict.fromkeys(lines) + + for line in response: + statuses = [status['statusSeverityDescription'] + for status in line['lineStatuses']] + state = ' + '.join(sorted(set(statuses))) + + if state == 'Good Service': + reason = 'Nothing to report' + else: + reason = ' *** '.join( + [status['reason'] for status in line['lineStatuses']]) + + attr = {'State': state, 'Description': reason} + data_dict[line['name']] = attr + + return data_dict diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index 961b4692e93..25516eda5b1 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -40,6 +40,15 @@ CONDITION_CLASSES = { 'exceptional': [], } +VISIBILTY_CLASSES = { + 'VP': '<1', + 'PO': '1-4', + 'MO': '4-10', + 'GO': '10-20', + 'VG': '20-40', + 'EX': '>40' +} + SCAN_INTERVAL = timedelta(minutes=35) # Sensor types are defined like: Name, units @@ -51,7 +60,8 @@ SENSOR_TYPES = { 'wind_speed': ['Wind Speed', 'm/s'], 'wind_direction': ['Wind Direction', None], 'wind_gust': ['Wind Gust', 'm/s'], - 'visibility': ['Visibility', 'km'], + 'visibility': ['Visibility', None], + 'visibility_distance': ['Visibility Distance', 'km'], 'uv': ['UV', None], 'precipitation': ['Probability of Precipitation', '%'], 'humidity': ['Humidity', '%'] @@ -119,15 +129,16 @@ class MetOfficeCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" + if (self._condition == 'visibility_distance' and + 'visibility' in self.data.data.__dict__.keys()): + return VISIBILTY_CLASSES.get(self.data.data.visibility.value) if self._condition in self.data.data.__dict__.keys(): variable = getattr(self.data.data, self._condition) if self._condition == "weather": return [k for k, v in CONDITION_CLASSES.items() if self.data.data.weather.value in v][0] - else: - return variable.value - else: - return STATE_UNKNOWN + return variable.value + return STATE_UNKNOWN @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index 9d78ffd3f1a..ecea0815e79 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) use_tls = config.get(CONF_SSL) verify_tls = config.get(CONF_VERIFY_SSL) - default_port = use_tls and 6443 or 6080 + default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) from mficlient.client import FailedToLogin, MFiClient @@ -97,10 +97,9 @@ class MfiSensor(Entity): if tag is None: return STATE_OFF elif self._port.model == 'Input Digital': - return self._port.value > 0 and STATE_ON or STATE_OFF - else: - digits = DIGITS.get(self._port.tag, 0) - return round(self._port.value, digits) + return STATE_ON if self._port.value > 0 else STATE_OFF + digits = DIGITS.get(self._port.tag, 0) + return round(self._port.value, digits) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 3ee59e5ae54..9453daea413 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.components.modbus as modbus from homeassistant.const import ( - CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT) + CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE) from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -25,7 +25,6 @@ CONF_PRECISION = 'precision' CONF_REGISTER = 'register' CONF_REGISTERS = 'registers' CONF_SCALE = 'scale' -CONF_SLAVE = 'slave' CONF_DATA_TYPE = 'data_type' CONF_REGISTER_TYPE = 'register_type' @@ -118,17 +117,20 @@ class ModbusRegisterSensor(Entity): self._register, self._count) val = 0 - if not result: + + try: + registers = result.registers + except AttributeError: _LOGGER.error("No response from modbus slave %s register %s", self._slave, self._register) return if self._data_type == DATA_TYPE_FLOAT: byte_string = b''.join( - [x.to_bytes(2, byteorder='big') for x in result.registers] + [x.to_bytes(2, byteorder='big') for x in registers] ) val = struct.unpack(">f", byte_string)[0] elif self._data_type == DATA_TYPE_INT: - for i, res in enumerate(result.registers): + for i, res in enumerate(registers): val += res * (2**(i*16)) self._value = format( self._scale * val + self._offset, '.{}f'.format(self._precision)) diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 4b1b740093e..ccebd93dbec 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -242,10 +242,9 @@ class MoldIndicator(Entity): ATTR_DEWPOINT: self._dewpoint, ATTR_CRITICAL_TEMP: self._crit_temp, } - else: - return { - ATTR_DEWPOINT: - util.temperature.celsius_to_fahrenheit(self._dewpoint), - ATTR_CRITICAL_TEMP: - util.temperature.celsius_to_fahrenheit(self._crit_temp), - } + return { + ATTR_DEWPOINT: + util.temperature.celsius_to_fahrenheit(self._dewpoint), + ATTR_CRITICAL_TEMP: + util.temperature.celsius_to_fahrenheit(self._crit_temp), + } diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py index 9d844292e18..75b8a1f72bd 100644 --- a/homeassistant/components/sensor/moon.py +++ b/homeassistant/components/sensor/moon.py @@ -64,8 +64,7 @@ class MoonSensor(Entity): return 'Waning gibbous' elif self._state == 21: return 'Last quarter' - else: - return 'Waning crescent' + return 'Waning crescent' @property def icon(self): diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index d1f101fc02f..46d79c1121b 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -91,8 +91,7 @@ class MVGLiveSensor(Entity): """Return the name of the sensor.""" if self._name: return self._name - else: - return self._station + return self._station @property def state(self): diff --git a/homeassistant/components/sensor/neato.py b/homeassistant/components/sensor/neato.py index 39d77e736c5..eb1524e1748 100644 --- a/homeassistant/components/sensor/neato.py +++ b/homeassistant/components/sensor/neato.py @@ -136,10 +136,7 @@ class NeatoConnectedSensor(Entity): @property def available(self): """Return True if sensor data is available.""" - if not self._state: - return False - else: - return True + return self._state @property def state(self): diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py index ad7426eb1d3..6cb6ef9a14d 100644 --- a/homeassistant/components/sensor/netdata.py +++ b/homeassistant/components/sensor/netdata.py @@ -14,7 +14,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PORT, STATE_UNKNOWN, CONF_NAME, CONF_RESOURCES) + CONF_HOST, CONF_PORT, CONF_NAME, CONF_RESOURCES) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -110,8 +110,7 @@ class NetdataSensor(Entity): netdata_id = SENSOR_TYPES[self.type][3] if netdata_id in value: return "{0:.{1}f}".format(value[netdata_id], self._precision) - else: - return STATE_UNKNOWN + return None @property def available(self): diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index b029451bd5e..150a97288cc 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -13,13 +13,12 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['octoprint'] - +DOMAIN = "octoprint" DEFAULT_NAME = 'OctoPrint' SENSOR_TYPES = { @@ -38,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available OctoPrint sensors.""" - octoprint = get_component('octoprint') + octoprint_api = hass.data[DOMAIN]["api"] name = config.get(CONF_NAME) monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) @@ -46,16 +45,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): types = ["actual", "target"] for octo_type in monitored_conditions: if octo_type == "Temperatures": - for tool in octoprint.OCTOPRINT.get_tools(): + for tool in octoprint_api.get_tools(): for temp_type in types: new_sensor = OctoPrintSensor( - octoprint.OCTOPRINT, temp_type, temp_type, name, + octoprint_api, temp_type, temp_type, name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], SENSOR_TYPES[octo_type][1], tool) devices.append(new_sensor) else: new_sensor = OctoPrintSensor( - octoprint.OCTOPRINT, octo_type, SENSOR_TYPES[octo_type][2], + octoprint_api, octo_type, SENSOR_TYPES[octo_type][2], name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0], SENSOR_TYPES[octo_type][1]) devices.append(new_sensor) @@ -116,7 +115,3 @@ class OctoPrintSensor(Entity): except requests.exceptions.ConnectionError: # Error calling the api, already logged in api.update() return - - if self._state is None and self.sensor_type != "completion": - _LOGGER.warning("Unable to locate value for %s", self.sensor_type) - return diff --git a/homeassistant/components/sensor/ohmconnect.py b/homeassistant/components/sensor/ohmconnect.py index 9808e6ecef7..cb1a1d3d260 100644 --- a/homeassistant/components/sensor/ohmconnect.py +++ b/homeassistant/components/sensor/ohmconnect.py @@ -60,8 +60,7 @@ class OhmconnectSensor(Entity): """Return the state of the sensor.""" if self._data.get("active") == "True": return "Active" - else: - return "Inactive" + return "Inactive" @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/openhardwaremonitor.py b/homeassistant/components/sensor/openhardwaremonitor.py index 1d805916d97..54ce5dbd6da 100644 --- a/homeassistant/components/sensor/openhardwaremonitor.py +++ b/homeassistant/components/sensor/openhardwaremonitor.py @@ -147,13 +147,13 @@ class OpenHardwareMonitorData(object): """Recursively loop through child objects, finding the values.""" result = devices.copy() - if len(json[OHM_CHILDREN]) > 0: + if json[OHM_CHILDREN]: for child_index in range(0, len(json[OHM_CHILDREN])): child_path = path.copy() child_path.append(child_index) child_names = names.copy() - if len(path) > 0: + if path: child_names.append(json[OHM_NAME]) obj = json[OHM_CHILDREN][child_index] diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 8d55b343781..944ee101d13 100755 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.6.1'] +REQUIREMENTS = ['pyowm==2.7.1'] _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ SENSOR_TYPES = { 'pressure': ['Pressure', 'mbar'], 'clouds': ['Cloud coverage', '%'], 'rain': ['Rain', 'mm'], - 'snow': ['Snow', 'mm'] + 'snow': ['Snow', 'mm'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev.append(OpenWeatherMapSensor( name, data, 'forecast', SENSOR_TYPES['temperature'][1])) - add_devices(dev) + add_devices(dev, True) class OpenWeatherMapSensor(Entity): @@ -100,7 +100,6 @@ class OpenWeatherMapSensor(Entity): self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self.update() @property def name(self): @@ -130,6 +129,9 @@ class OpenWeatherMapSensor(Entity): data = self.owa_client.data fc_data = self.owa_client.fc_data + if data is None or fc_data is None: + return + if self.type == 'weather': self._state = data.get_detailed_status() elif self.type == 'temperature': @@ -188,7 +190,7 @@ class WeatherData(object): except TypeError: obs = None if obs is None: - _LOGGER.warning("Failed to fetch data from OpenWeatherMap") + _LOGGER.warning("Failed to fetch data") return self.data = obs.get_weather() @@ -199,4 +201,4 @@ class WeatherData(object): self.latitude, self.longitude) self.fc_data = obs.get_forecast() except TypeError: - _LOGGER.warning("Failed to fetch forecast from OpenWeatherMap") + _LOGGER.warning("Failed to fetch forecast") diff --git a/homeassistant/components/sensor/otp.py b/homeassistant/components/sensor/otp.py new file mode 100644 index 00000000000..5d7808ea4c7 --- /dev/null +++ b/homeassistant/components/sensor/otp.py @@ -0,0 +1,90 @@ +""" +Support for One-Time Password (OTP). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.otp/ +""" +import time +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_TOKEN) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['pyotp==2.2.6'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'OTP Sensor' + +TIME_STEP = 30 # Default time step assumed by Google Authenticator + +ICON = 'mdi:update' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the OTP sensor.""" + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + async_add_devices([TOTPSensor(name, token)], True) + return True + + +# Only TOTP supported at the moment, HOTP might be added later +class TOTPSensor(Entity): + """Representation of a TOTP sensor.""" + + def __init__(self, name, token): + """Initialize the sensor.""" + import pyotp + self._name = name + self._otp = pyotp.TOTP(token) + self._state = None + self._next_expiration = None + + @asyncio.coroutine + def async_added_to_hass(self): + """Handle when an entity is about to be added to Home Assistant.""" + self._call_loop() + + @callback + def _call_loop(self): + self._state = self._otp.now() + self.hass.async_add_job(self.async_update_ha_state()) + + # Update must occur at even TIME_STEP, e.g. 12:00:00, 12:00:30, + # 12:01:00, etc. in order to have synced time (see RFC6238) + self._next_expiration = TIME_STEP - (time.time() % TIME_STEP) + self.hass.loop.call_later(self._next_expiration, self._call_loop) + + @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 + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON diff --git a/homeassistant/components/sensor/pvoutput.py b/homeassistant/components/sensor/pvoutput.py index cb3d2d1427a..baad452b629 100644 --- a/homeassistant/components/sensor/pvoutput.py +++ b/homeassistant/components/sensor/pvoutput.py @@ -15,8 +15,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_API_KEY, CONF_NAME, STATE_UNKNOWN, ATTR_DATE, - ATTR_TIME) + ATTR_TEMPERATURE, CONF_API_KEY, CONF_NAME, ATTR_DATE, ATTR_TIME) _LOGGER = logging.getLogger(__name__) _ENDPOINT = 'http://pvoutput.org/service/r2/getstatus.jsp' @@ -90,8 +89,7 @@ class PvoutputSensor(Entity): """Return the state of the device.""" if self.pvcoutput is not None: return self.pvcoutput.energy_generation - else: - return STATE_UNKNOWN + return None @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index c72d2d65c6d..42f68a1967a 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -232,8 +232,7 @@ class QNAPSensor(Entity): if self.monitor_device is not None: return "{} {} ({})".format( server_name, self.var_name, self.monitor_device) - else: - return "{} {}".format(server_name, self.var_name) + return "{} {}".format(server_name, self.var_name) @property def icon(self): diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py index f5efe12c449..59408b4f96b 100644 --- a/homeassistant/components/sensor/radarr.py +++ b/homeassistant/components/sensor/radarr.py @@ -216,9 +216,9 @@ def get_date(zone, offset=0): def get_release_date(data): """Get release date.""" - date = data['physicalRelease'] + date = data.get('physicalRelease') if not date: - date = data['inCinemas'] + date = data.get('inCinemas') return date diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index c6660701c21..0d5fc283e32 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -9,9 +9,9 @@ from functools import partial import logging from homeassistant.components.rflink import ( - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, DATA_DEVICE_REGISTER, - DATA_ENTITY_LOOKUP, DOMAIN, EVENT_KEY_ID, EVENT_KEY_SENSOR, EVENT_KEY_UNIT, - RflinkDevice, cv, vol) + CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, + DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, DOMAIN, EVENT_KEY_ID, + EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, cv, remove_deprecated, vol) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_PLATFORM, CONF_UNIT_OF_MEASUREMENT) @@ -36,7 +36,10 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_SENSOR_TYPE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string, - vol.Optional(CONF_ALIASSES, default=[]): + vol.Optional(CONF_ALIASES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + # deprecated config options + vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), }, }), @@ -61,6 +64,7 @@ def devices_from_config(domain_config, hass=None): if not config[ATTR_UNIT_OF_MEASUREMENT]: config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( config[CONF_SENSOR_TYPE]) + remove_deprecated(config) device = RflinkSensor(device_id, hass, **config) devices.append(device) diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index 0df63956f2e..3e2f228423b 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -90,8 +90,7 @@ class SwissHydrologicalDataSensor(Entity): """Return the unit of measurement of this entity, if any.""" if self._state is not STATE_UNKNOWN: return self._unit_of_measurement - else: - return None + return None @property def state(self): diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index eb31381ccef..b52d01a0112 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -177,8 +177,7 @@ class SynoNasSensor(Entity): """Return the name of the sensor, if any.""" if self.monitor_device is not None: return "{} ({})".format(self.var_name, self.monitor_device) - else: - return self.var_name + return self.var_name @property def icon(self): @@ -191,8 +190,7 @@ class SynoNasSensor(Entity): if self.var_id in ['volume_disk_temp_avg', 'volume_disk_temp_max', 'disk_temp']: return self._api.temp_unit - else: - return self.var_units + return self.var_units def update(self): """Get the latest data for the states.""" @@ -238,8 +236,8 @@ class SynoNasStorageSensor(SynoNasSensor): if self._api.temp_unit == TEMP_CELSIUS: return attr - else: - return round(attr * 1.8 + 32.0, 1) - else: - return getattr(self._api.storage, - self.var_id)(self.monitor_device) + + return round(attr * 1.8 + 32.0, 1) + + return getattr(self._api.storage, + self.var_id)(self.monitor_device) diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index b654a4444c4..1d40e4ceb50 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zones = tado.get_zones() except RuntimeError: _LOGGER.error("Unable to get zone info from mytado") - return False + return sensor_items = [] for zone in zones: @@ -47,9 +47,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if sensor_items: add_devices(sensor_items, True) - return True - else: - return False def create_zone_sensor(tado, zone, name, zone_id, variable): diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 68d0bb6535f..c14b20e1099 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -92,8 +92,7 @@ class TelldusLiveSensor(TelldusLiveEntity): return self._value_as_humidity elif self._type == SENSOR_TYPE_LUMINANCE: return self._value_as_luminance - else: - return self._value + return self._value @property def quantity_name(self): diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index c1c4d62dbcb..a59ee01bac2 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -83,8 +83,7 @@ class TimeDateSensor(Entity): return 'mdi:calendar-clock' elif 'date' in self.type: return 'mdi:calendar' - else: - return 'mdi:clock' + return 'mdi:clock' def get_next_interval(self, now=None): """Compute next time an update should occur.""" diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index 2830b8c98e9..17ce1036244 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -9,10 +9,10 @@ from datetime import timedelta import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['uber_rides==0.4.1'] @@ -31,12 +31,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERVER_TOKEN): cv.string, - vol.Required(CONF_START_LATITUDE): cv.latitude, - vol.Required(CONF_START_LONGITUDE): cv.longitude, + vol.Optional(CONF_START_LATITUDE): cv.latitude, + vol.Optional(CONF_START_LONGITUDE): cv.longitude, vol.Optional(CONF_END_LATITUDE): cv.latitude, vol.Optional(CONF_END_LONGITUDE): cv.longitude, - vol.Optional(CONF_PRODUCT_IDS): - vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_PRODUCT_IDS): vol.All(cv.ensure_list, [cv.string]), }) @@ -45,23 +44,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from uber_rides.session import Session session = Session(server_token=config.get(CONF_SERVER_TOKEN)) - + start_latitude = config.get(CONF_START_LATITUDE, hass.config.latitude) + start_longitude = config.get(CONF_START_LONGITUDE, hass.config.longitude) + end_latitude = config.get(CONF_END_LATITUDE) + end_longitude = config.get(CONF_END_LONGITUDE) wanted_product_ids = config.get(CONF_PRODUCT_IDS) dev = [] timeandpriceest = UberEstimate( - session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE], - config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE)) + session, start_latitude, start_longitude, end_latitude, end_longitude) + for product_id, product in timeandpriceest.products.items(): if (wanted_product_ids is not None) and \ (product_id not in wanted_product_ids): continue dev.append(UberSensor('time', timeandpriceest, product_id, product)) - if (product.get('price_details') is not None) and \ - product['price_details']['estimate'] is not 'Metered': + + if product.get('price_details') is not None \ + and product['display_name'] != 'TAXI': dev.append(UberSensor( 'price', timeandpriceest, product_id, product)) - add_devices(dev) + + add_devices(dev, True) class UberSensor(Entity): @@ -73,8 +77,8 @@ class UberSensor(Entity): self._product_id = product_id self._product = product self._sensortype = sensorType - self._name = '{} {}'.format(self._product['display_name'], - self._sensortype) + self._name = '{} {}'.format( + self._product['display_name'], self._sensortype) if self._sensortype == 'time': self._unit_of_measurement = 'min' time_estimate = self._product.get('time_estimate_seconds', 0) @@ -90,7 +94,6 @@ class UberSensor(Entity): self._state = int(price_details.get(statekey, 0)) else: self._state = 0 - self.update() @property def name(self): @@ -214,8 +217,8 @@ class UberEstimate(object): if product.get('price_details') is None: price_details = {} price_details['estimate'] = price.get('estimate', '0') - price_details['high_estimate'] = price.get('high_estimate', - '0') + price_details['high_estimate'] = price.get( + 'high_estimate', '0') price_details['low_estimate'] = price.get('low_estimate', '0') price_details['currency_code'] = price.get('currency_code') surge_multiplier = price.get('surge_multiplier', '0') diff --git a/homeassistant/components/sensor/volvooncall.py b/homeassistant/components/sensor/volvooncall.py index 703315c478c..622261941d6 100644 --- a/homeassistant/components/sensor/volvooncall.py +++ b/homeassistant/components/sensor/volvooncall.py @@ -28,8 +28,7 @@ class VolvoSensor(VolvoEntity): val = getattr(self.vehicle, self._attribute) if self._attribute == 'odometer': return round(val / 1000) # km - else: - return val + return val @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 01bdab24af9..318a22cfa2a 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -4,18 +4,22 @@ Support for the World Air Quality Index service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.waqi/ """ +import asyncio import logging from datetime import timedelta +import aiohttp import voluptuous as vol +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, ATTR_TEMPERATURE, STATE_UNKNOWN, CONF_TOKEN) + ATTR_ATTRIBUTION, ATTR_TIME, ATTR_TEMPERATURE, CONF_TOKEN) +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pwaqi==3.0'] +REQUIREMENTS = ['waqiasync==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -27,6 +31,18 @@ ATTR_PM10 = 'pm_10' ATTR_PM2_5 = 'pm_2_5' ATTR_PRESSURE = 'pressure' ATTR_SULFUR_DIOXIDE = 'sulfur_dioxide' + +KEY_TO_ATTR = { + 'pm25': ATTR_PM2_5, + 'pm10': ATTR_PM10, + 'h': ATTR_HUMIDITY, + 'p': ATTR_PRESSURE, + 't': ATTR_TEMPERATURE, + 'o3': ATTR_OZONE, + 'no2': ATTR_NITROGEN_DIOXIDE, + 'so2': ATTR_SULFUR_DIOXIDE, +} + ATTRIBUTION = 'Data provided by the World Air Quality Index project' CONF_LOCATIONS = 'locations' @@ -34,9 +50,7 @@ CONF_STATIONS = 'stations' SCAN_INTERVAL = timedelta(minutes=5) -SENSOR_TYPES = { - 'aqi': ['AQI', '0-300+', 'mdi:cloud'] -} +TIMEOUT = 10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STATIONS): cv.ensure_list, @@ -45,51 +59,65 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the requested World Air Quality Index locations.""" - import pwaqi + import waqiasync token = config.get(CONF_TOKEN) station_filter = config.get(CONF_STATIONS) locations = config.get(CONF_LOCATIONS) + client = waqiasync.WaqiClient( + token, async_get_clientsession(hass), timeout=TIMEOUT) dev = [] - for location_name in locations: - station_ids = pwaqi.findStationCodesByCity(location_name, token) - _LOGGER.info("The following stations were returned: %s", station_ids) - for station in station_ids: - waqi_sensor = WaqiSensor(WaqiData(station, token), station) - if (not station_filter) or \ - (waqi_sensor.station_name in station_filter): - dev.append(WaqiSensor(WaqiData(station, token), station)) - - add_devices(dev, True) + try: + for location_name in locations: + stations = yield from client.search(location_name) + _LOGGER.debug("The following stations were returned: %s", stations) + for station in stations: + waqi_sensor = WaqiSensor(client, station) + if not station_filter or \ + {waqi_sensor.uid, + waqi_sensor.url, + waqi_sensor.station_name} & set(station_filter): + dev.append(waqi_sensor) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError): + _LOGGER.exception('Failed to connct to WAQI servers.') + raise PlatformNotReady + async_add_devices(dev, True) class WaqiSensor(Entity): """Implementation of a WAQI sensor.""" - def __init__(self, data, station_id): + def __init__(self, client, station): """Initialize the sensor.""" - self.data = data - self._station_id = station_id - self._details = None + self._client = client + try: + self.uid = station['uid'] + except (KeyError, TypeError): + self.uid = None + + try: + self.url = station['station']['url'] + except (KeyError, TypeError): + self.url = None + + try: + self.station_name = station['station']['name'] + except (KeyError, TypeError): + self.station_name = None + + self._data = None @property def name(self): """Return the name of the sensor.""" - try: - return 'WAQI {}'.format(self._details['city']['name']) - except (KeyError, TypeError): - return 'WAQI {}'.format(self._station_id) - - @property - def station_name(self): - """Return the name of the station.""" - try: - return self._details['city']['name'] - except (KeyError, TypeError): - return None + if self.station_name: + return 'WAQI {}'.format(self.station_name) + return 'WAQI {}'.format(self.url if self.url else self.uid) @property def icon(self): @@ -99,10 +127,9 @@ class WaqiSensor(Entity): @property def state(self): """Return the state of the device.""" - if self._details is not None: - return self._details.get('aqi') - else: - return STATE_UNKNOWN + if self._data is not None: + return self._data.get('aqi') + return None @property def unit_of_measurement(self): @@ -114,52 +141,32 @@ class WaqiSensor(Entity): """Return the state attributes of the last update.""" attrs = {} - if self.data is not None: + if self._data is not None: try: - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs[ATTR_TIME] = self._details.get('time') - attrs[ATTR_DOMINENTPOL] = self._details.get('dominentpol') - for values in self._details['iaqi']: - if values['p'] == 'pm25': - attrs[ATTR_PM2_5] = values['cur'] - elif values['p'] == 'pm10': - attrs[ATTR_PM10] = values['cur'] - elif values['p'] == 'h': - attrs[ATTR_HUMIDITY] = values['cur'] - elif values['p'] == 'p': - attrs[ATTR_PRESSURE] = values['cur'] - elif values['p'] == 't': - attrs[ATTR_TEMPERATURE] = values['cur'] - elif values['p'] == 'o3': - attrs[ATTR_OZONE] = values['cur'] - elif values['p'] == 'no2': - attrs[ATTR_NITROGEN_DIOXIDE] = values['cur'] - elif values['p'] == 'so2': - attrs[ATTR_SULFUR_DIOXIDE] = values['cur'] + attrs[ATTR_ATTRIBUTION] = ' and '.join( + [ATTRIBUTION] + [ + v['name'] for v in self._data.get('attributions', [])]) + + attrs[ATTR_TIME] = self._data['time']['s'] + attrs[ATTR_DOMINENTPOL] = self._data.get('dominentpol') + + iaqi = self._data['iaqi'] + for key in iaqi: + if key in KEY_TO_ATTR: + attrs[KEY_TO_ATTR[key]] = iaqi[key]['v'] + else: + attrs[key] = iaqi[key]['v'] return attrs except (IndexError, KeyError): return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and updates the states.""" - self.data.update() - self._details = self.data.data - - -class WaqiData(object): - """Get the latest data and update the states.""" - - def __init__(self, station_id, token): - """Initialize the data object.""" - self._station_id = station_id - self._token = token - self.data = None - - def update(self): - """Get the data from World Air Quality Index and updates the states.""" - import pwaqi - try: - self.data = pwaqi.get_station_observation( - self._station_id, self._token) - except AttributeError: - _LOGGER.exception("Unable to fetch data from WAQI") + if self.uid: + result = yield from self._client.get_station_by_number(self.uid) + elif self.url: + result = yield from self._client.get_station_by_name(self.url) + else: + result = None + self._data = result diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 84f6d2d2ac0..3a72432610c 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -254,8 +254,7 @@ class WUAlertsSensorConfig(WUSensorConfig): feature="alerts", value=lambda wu: len(wu.data['alerts']), icon=lambda wu: "mdi:alert-circle-outline" - if len(wu.data['alerts']) > 0 - else "mdi:check-circle-outline", + if wu.data['alerts'] else "mdi:check-circle-outline", device_state_attributes=self._get_attributes ) diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index 4919d75e8da..2883a396b77 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -15,6 +15,7 @@ from homeassistant.const import ( TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_NAME, STATE_UNKNOWN, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle REQUIREMENTS = ['yahooweather==0.8'] @@ -26,7 +27,7 @@ CONF_WOEID = 'woeid' DEFAULT_NAME = 'Yweather' -SCAN_INTERVAL = timedelta(minutes=10) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SENSOR_TYPES = { 'weather_current': ['Current', None], @@ -181,6 +182,7 @@ class YahooWeatherData(object): """Return Yahoo! API object.""" return self._yahoo + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Yahoo!.""" return self._yahoo.updateWeather() diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 78627674c55..ca4ab6bbff3 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -31,17 +31,16 @@ 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 import zcl - if isinstance(discovery_info['clusters'][0], - zcl.clusters.measurement.TemperatureMeasurement): + from bellows.zigbee.zcl.clusters.measurement import TemperatureMeasurement + in_clusters = discovery_info['in_clusters'] + if TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) else: sensor = Sensor(**discovery_info) - clusters = discovery_info['clusters'] attr = sensor.value_attribute if discovery_info['new_join']: - cluster = clusters[0] + cluster = list(in_clusters.values())[0] yield from cluster.bind() yield from cluster.configure_reporting( attr, 300, 600, sensor.min_reportable_change, @@ -57,10 +56,6 @@ class Sensor(zha.Entity): value_attribute = 0 min_reportable_change = 1 - def __init__(self, **kwargs): - """Initialize ZHA sensor.""" - super().__init__(**kwargs) - @property def state(self) -> str: """Return the state of the entity.""" diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index e9ea0e7512d..fe295d84d49 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -78,8 +78,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): return TEMP_CELSIUS elif self._units == 'F': return TEMP_FAHRENHEIT - else: - return self._units + return self._units class ZWaveAlarmSensor(ZWaveSensor): diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index d81d14fc991..eefcff5bd17 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -470,3 +470,53 @@ axis: param: description: What parameter to operate on. [Required] example: 'package=VideoMotionDetection' + +apple_tv: + apple_tv_authenticate: + description: Start AirPlay device authentication. + + fields: + entity_id: + description: Name(s) of entities to authenticate with. + example: 'media_player.apple_tv' + + apple_tv_scan: + description: Scan for Apple TV devices. + +modbus: + write_register: + description: Write to a modbus holding register + fields: + unit: + description: Address of the modbus unit + example: 21 + address: + description: Address of the holding register to write to + example: 0 + value: + description: Value to write + example: 0 + write_coil: + description: Write to a modbus coil + fields: + unit: + description: Address of the modbus unit + example: 21 + address: + description: Address of the register to read + example: 0 + state: + description: State to write + example: false + +wake_on_lan: + send_magic_packet: + description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. + + fields: + mac: + description: MAC address of the device to wake up. + example: 'aa:bb:cc:dd:ee:ff' + broadcast_address: + description: Optional broadcast IP where to send the magic packet. + example: '192.168.255.255' diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index de54c0239c7..b123de48158 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -108,6 +108,7 @@ class IntentHandler(object): slots = self.parse_slots(response) yield from action.async_run(slots) + # pylint: disable=no-self-use def parse_slots(self, response): """Parse the intent slots.""" parameters = {} diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index eba05c64555..6e31694fd2d 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -102,7 +102,7 @@ class ArestSwitchFunction(ArestSwitchBase): request = requests.get( '{}/{}'.format(self._resource, self._func), timeout=10) - if request.status_code is not 200: + if request.status_code != 200: _LOGGER.error("Can't find function") return @@ -159,7 +159,7 @@ class ArestSwitchPin(ArestSwitchBase): request = requests.get( '{}/mode/{}/o'.format(self._resource, self._pin), timeout=10) - if request.status_code is not 200: + if request.status_code != 200: _LOGGER.error("Can't set mode") self._available = False diff --git a/homeassistant/components/switch/bbb_gpio.py b/homeassistant/components/switch/bbb_gpio.py index ce2d91273f9..6dc5df4ffe3 100644 --- a/homeassistant/components/switch/bbb_gpio.py +++ b/homeassistant/components/switch/bbb_gpio.py @@ -77,13 +77,13 @@ class BBBGPIOSwitch(ToggleEntity): """Return true if device is on.""" return self._state - def turn_on(self): + def turn_on(self, **kwargs): """Turn the device on.""" bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) self._state = True self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self, **kwargs): """Turn the device off.""" bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) self._state = False diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index ffa4aaea615..3a7f3ee0c80 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -21,7 +21,7 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_HOST, CONF_MAC, CONF_TYPE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['broadlink==0.3'] +REQUIREMENTS = ['broadlink==0.5'] _LOGGER = logging.getLogger(__name__) @@ -244,10 +244,6 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): class BroadlinkSP2Switch(BroadlinkSP1Switch): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device): - """Initialize the switch.""" - super().__init__(friendly_name, device) - @property def assumed_state(self): """Return true if unable to access real state of entity.""" diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index d5036f9cb06..b24693da616 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -14,8 +14,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN -REQUIREMENTS = ['https://github.com/LinuxChristian/pyW215/archive/' - 'v0.4.zip#pyW215==0.4'] +REQUIREMENTS = ['pyW215==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index daa4d1f8cd1..dea4285e3a9 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -37,6 +37,7 @@ CONF_MODE = 'mode' MODE_XY = 'xy' MODE_MIRED = 'mired' +MODE_RGB = 'rgb' DEFAULT_MODE = MODE_XY PLATFORM_SCHEMA = vol.Schema({ @@ -55,7 +56,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), vol.Optional(CONF_DISABLE_BRIGTNESS_ADJUST): cv.boolean, vol.Optional(CONF_MODE, default=DEFAULT_MODE): - vol.Any(MODE_XY, MODE_MIRED) + vol.Any(MODE_XY, MODE_MIRED, MODE_RGB) }) @@ -79,6 +80,15 @@ def set_lights_temp(hass, lights, mired, brightness): transition=30) +def set_lights_rgb(hass, lights, rgb): + """Set color of array of lights.""" + for light in lights: + if is_on(hass, light): + turn_on(hass, light, + rgb_color=rgb, + transition=30) + + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Flux switches.""" @@ -194,7 +204,8 @@ class FluxSwitch(SwitchDevice): temp = self._sunset_colortemp - temp_offset else: temp = self._sunset_colortemp + temp_offset - x_val, y_val, b_val = color_RGB_to_xy(*color_temperature_to_rgb(temp)) + rgb = color_temperature_to_rgb(temp) + x_val, y_val, b_val = color_RGB_to_xy(*rgb) brightness = self._brightness if self._brightness else b_val if self._disable_brightness_adjust: brightness = None @@ -205,6 +216,11 @@ class FluxSwitch(SwitchDevice): "of %s cycle complete at %s", x_val, y_val, brightness, round( percentage_complete * 100), time_state, now) + elif self._mode == MODE_RGB: + set_lights_rgb(self.hass, self._lights, rgb) + _LOGGER.info("Lights updated to rgb:%s, %s%% " + "of %s cycle complete at %s", rgb, + round(percentage_complete * 100), time_state, now) else: # Convert to mired and clamp to allowed values mired = color_temperature_kelvin_to_mired(temp) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index e67f293525c..566eff99828 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devices = [] - for config in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMSwitch(hass, config) + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + new_device = HMSwitch(hass, conf) new_device.link_homematic() devices.append(new_device) diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 00fb7fdd909..07425840b9a 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -122,7 +122,7 @@ class HookSmartHome(SwitchDevice): return data['return_value'] == '1' @asyncio.coroutine - def async_turn_on(self): + def async_turn_on(self, **kwargs): """Turn the device on asynchronously.""" _LOGGER.debug("Turning on: %s", self._name) url = '{}{}{}{}'.format( @@ -131,7 +131,7 @@ class HookSmartHome(SwitchDevice): self._state = success @asyncio.coroutine - def async_turn_off(self): + def async_turn_off(self, **kwargs): """Turn the device off asynchronously.""" _LOGGER.debug("Turning off: %s", self._name) url = '{}{}{}{}'.format( diff --git a/homeassistant/components/switch/mfi.py b/homeassistant/components/switch/mfi.py index 6f5dd655ba4..c0dc72440d3 100644 --- a/homeassistant/components/switch/mfi.py +++ b/homeassistant/components/switch/mfi.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) use_tls = config.get(CONF_SSL) verify_tls = config.get(CONF_VERIFY_SSL) - default_port = use_tls and 6443 or 6080 + default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) from mficlient.client import FailedToLogin, MFiClient @@ -100,12 +100,12 @@ class MfiSwitch(SwitchDevice): self._port.data['output'] = float(self._target_state) self._target_state = None - def turn_on(self): + def turn_on(self, **kwargs): """Turn the switch on.""" self._port.control(True) self._target_state = True - def turn_off(self): + def turn_off(self, **kwargs): """Turn the switch off.""" self._port.control(False) self._target_state = False diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index 93406c869d4..e6342617f28 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol import homeassistant.components.modbus as modbus -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_SLAVE from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -18,7 +18,6 @@ DEPENDENCIES = ['modbus'] CONF_COIL = "coil" CONF_COILS = "coils" -CONF_SLAVE = "slave" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COILS): [{ @@ -71,10 +70,10 @@ class ModbusCoilSwitch(ToggleEntity): def update(self): """Update the state of the switch.""" result = modbus.HUB.read_coils(self._slave, self._coil, 1) - if not result: + try: + self._is_on = bool(result.bits[0]) + except AttributeError: _LOGGER.error( 'No response from modbus slave %s coil %s', self._slave, self._coil) - return - self._is_on = bool(result.bits[0]) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index c72ea1e4cfe..38f67ee3ee9 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -135,7 +135,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): return self._values[self.value_type] == STATE_ON return False - def turn_on(self): + def turn_on(self, **kwargs): """Turn the switch on.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1) @@ -144,7 +144,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): self._values[self.value_type] = STATE_ON self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self, **kwargs): """Turn the switch off.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0) @@ -191,7 +191,7 @@ class MySensorsIRSwitch(MySensorsSwitch): # turn off switch after switch was turned on self.turn_off() - def turn_off(self): + def turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq if set_req.V_LIGHT not in self._values: diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index e61a46f78a6..739e4e03fed 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -89,10 +89,7 @@ class NeatoConnectedSwitch(ToggleEntity): @property def available(self): """Return True if entity is available.""" - if not self._state: - return False - else: - return True + return self._state @property def is_on(self): diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index 69b932ecf71..03f9e84b3c8 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -137,8 +137,7 @@ class PAServer(): self._current_module_state) if result and result.group(1).isdigit(): return int(result.group(1)) - else: - return -1 + return -1 class PALoopbackSwitch(SwitchDevice): diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index 63809fd4456..547442a4233 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -17,7 +17,7 @@ DATA_RACHIO = 'rachio' CONF_MANUAL_RUN_MINS = 'manual_run_mins' DEFAULT_MANUAL_RUN_MINS = 10 -MIN_UPDATE_INTERVAL = timedelta(minutes=5) +MIN_UPDATE_INTERVAL = timedelta(seconds=30) MIN_FORCED_UPDATE_INTERVAL = timedelta(seconds=1) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -27,7 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# noinspection PyUnusedLocal def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the component.""" # Get options @@ -52,18 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Get and persist devices devices = _list_devices(rachio, manual_run_mins) - if len(devices) == 0: + if not devices: _LOGGER.error("No Rachio devices found in account " + person['username']) return False - else: - hass.data[DATA_RACHIO] = devices[0] - if len(devices) > 1: - _LOGGER.warning("Multiple Rachio devices found in account, " - "using " + hass.data[DATA_RACHIO].device_id) - else: - _LOGGER.info("Found Rachio device") + hass.data[DATA_RACHIO] = devices[0] + + if len(devices) > 1: + _LOGGER.warning("Multiple Rachio devices found in account, " + "using " + hass.data[DATA_RACHIO].device_id) + else: + _LOGGER.info("Found Rachio device") hass.data[DATA_RACHIO].update() add_devices(hass.data[DATA_RACHIO].list_zones()) @@ -137,9 +136,9 @@ class RachioIro(object): if include_disabled: return self._zones - else: - self.update(no_throttle=True) - return [z for z in self._zones if z.is_enabled] + + self.update(no_throttle=True) + return [z for z in self._zones if z.is_enabled] @util.Throttle(MIN_UPDATE_INTERVAL, MIN_FORCED_UPDATE_INTERVAL) def update(self, **kwargs): @@ -215,14 +214,11 @@ class RachioZone(SwitchDevice): def turn_on(self): """Start the zone.""" - # Convert minutes to seconds - seconds = self._manual_run_secs * 60 - # Stop other zones first self.turn_off() - _LOGGER.info("Watering %s for %d sec", self.name, seconds) - self.rachio.zone.start(self.zone_id, seconds) + _LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs) + self.rachio.zone.start(self.zone_id, self._manual_run_secs) def turn_off(self): """Stop all zones.""" diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py index 58b1e0959af..29e93342f66 100644 --- a/homeassistant/components/switch/rflink.py +++ b/homeassistant/components/switch/rflink.py @@ -8,13 +8,15 @@ import asyncio import logging from homeassistant.components.rflink import ( - CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, - CONF_GROUP, CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASSES, - CONF_SIGNAL_REPETITIONS, DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, - DEVICE_DEFAULTS_SCHEMA, DOMAIN, EVENT_KEY_COMMAND, SwitchableRflinkDevice, - cv, vol) + CONF_ALIASES, CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, + CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES, + CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, + DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, + DOMAIN, EVENT_KEY_COMMAND, SwitchableRflinkDevice, cv, remove_deprecated, + vol) from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.helpers.deprecation import get_deprecated DEPENDENCIES = ['rflink'] @@ -27,15 +29,22 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_DEVICES, default={}): vol.Schema({ cv.string: { vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ALIASSES, default=[]): + vol.Optional(CONF_ALIASES, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES, default=[]): + vol.Optional(CONF_GROUP_ALIASES, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_NOGROUP_ALIASSES, default=[]): + vol.Optional(CONF_NOGROUP_ALIASES, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, + # deprecated config options + vol.Optional(CONF_ALIASSES): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_GROUP_ALIASSES): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_NOGROUP_ALIASSES): + vol.All(cv.ensure_list, [cv.string]), }, }), }) @@ -46,27 +55,30 @@ def devices_from_config(domain_config, hass=None): devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) + remove_deprecated(device_config) device = RflinkSwitch(device_id, hass, **device_config) devices.append(device) - # Register entity (and aliasses) to listen to incoming rflink events - # Device id and normal aliasses respond to normal and group command + # Register entity (and aliases) to listen to incoming rflink events + # Device id and normal aliases respond to normal and group command hass.data[DATA_ENTITY_LOOKUP][ EVENT_KEY_COMMAND][device_id].append(device) if config[CONF_GROUP]: hass.data[DATA_ENTITY_GROUP_LOOKUP][ EVENT_KEY_COMMAND][device_id].append(device) - for _id in config[CONF_ALIASSES]: + for _id in get_deprecated(config, CONF_ALIASES, CONF_ALIASSES): hass.data[DATA_ENTITY_LOOKUP][ EVENT_KEY_COMMAND][_id].append(device) hass.data[DATA_ENTITY_GROUP_LOOKUP][ EVENT_KEY_COMMAND][_id].append(device) - # group_aliasses only respond to group commands - for _id in config[CONF_GROUP_ALIASSES]: + # group_aliases only respond to group commands + for _id in get_deprecated( + config, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES): hass.data[DATA_ENTITY_GROUP_LOOKUP][ EVENT_KEY_COMMAND][_id].append(device) - # nogroup_aliasses only respond to normal commands - for _id in config[CONF_NOGROUP_ALIASSES]: + # nogroup_aliases only respond to normal commands + for _id in get_deprecated( + config, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES): hass.data[DATA_ENTITY_LOOKUP][ EVENT_KEY_COMMAND][_id].append(device) diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index 18d05db2f28..ac38da1c6a7 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -73,13 +73,13 @@ class RPiGPIOSwitch(ToggleEntity): """Return true if device is on.""" return self._state - def turn_on(self): + def turn_on(self, **kwargs): """Turn the device on.""" rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) self._state = True self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self, **kwargs): """Turn the device off.""" rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) self._state = False diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 1c6ad76ceba..7b97ece337b 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -162,8 +162,7 @@ class WemoSwitch(SwitchDevice): return STATE_OFF elif standby_state == WEMO_STANDBY: return STATE_STANDBY - else: - return STATE_UNKNOWN + return STATE_UNKNOWN @property def is_on(self): @@ -186,8 +185,7 @@ class WemoSwitch(SwitchDevice): """Return the icon of device based on its type.""" if self._model_name == 'CoffeeMaker': return 'mdi:coffee' - else: - return super().icon + return None def turn_on(self, **kwargs): """Turn the switch on.""" @@ -195,7 +193,7 @@ class WemoSwitch(SwitchDevice): self.wemo.on() self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self, **kwargs): """Turn the switch off.""" self._state = WEMO_OFF self.wemo.off() diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index b5feac5fc43..0076355665c 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -37,10 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkToggleDevice(WinkDevice, ToggleEntity): """Representation of a Wink toggle device.""" - def __init__(self, wink, hass): - """Initialize the Wink device.""" - super().__init__(wink, hass) - @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" diff --git a/homeassistant/components/switch/xiaomi_vacuum.py b/homeassistant/components/switch/xiaomi_vacuum.py new file mode 100644 index 00000000000..20906dd8df1 --- /dev/null +++ b/homeassistant/components/switch/xiaomi_vacuum.py @@ -0,0 +1,124 @@ +""" +Support for Xiaomi Vacuum cleaner robot. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/switch.xiaomi_vacuum/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import (DEVICE_DEFAULT_NAME, + CONF_NAME, CONF_HOST, CONF_TOKEN) + +_LOGGER = logging.getLogger(__name__) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME): cv.string, +}) + +REQUIREMENTS = ['python-mirobo==0.1.1'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Set up the vacuum from config.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + add_devices_callback([MiroboSwitch(name, host, token)]) + + +class MiroboSwitch(SwitchDevice): + """Representation of a Xiaomi Vacuum.""" + + def __init__(self, name, host, token): + """Initialize the vacuum switch.""" + self._name = name or DEVICE_DEFAULT_NAME + self._icon = 'mdi:broom' + self.host = host + self.token = token + + self._vacuum = None + self._state = None + self._state_attrs = {} + self._is_on = False + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def available(self): + """Return true when state is known.""" + return self._state is not None + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._state_attrs + + @property + def is_on(self): + """Return true if switch is on.""" + return self._is_on + + @property + def vacuum(self): + """Property accessor for vacuum object.""" + if not self._vacuum: + from mirobo import Vacuum + _LOGGER.info("initializing with host %s token %s", + self.host, self.token) + self._vacuum = Vacuum(self.host, self.token) + + return self._vacuum + + def turn_on(self, **kwargs): + """Turn the vacuum on.""" + from mirobo import VacuumException + try: + self.vacuum.start() + self._is_on = True + except VacuumException as ex: + _LOGGER.error("Unable to start the vacuum: %s", ex) + + def turn_off(self, **kwargs): + """Turn the vacuum off and return to home.""" + from mirobo import VacuumException + try: + self.vacuum.stop() + self.vacuum.home() + self._is_on = False + except VacuumException as ex: + _LOGGER.error("Unable to turn off and return home: %s", ex) + + def update(self): + """Fetch state from the device.""" + from mirobo import VacuumException + try: + state = self.vacuum.status() + _LOGGER.debug("got state from the vacuum: %s", state) + + self._state_attrs = { + 'Status': state.state, 'Error': state.error, + 'Battery': state.battery, 'Fan': state.fanspeed, + 'Cleaning time': str(state.clean_time), + 'Cleaned area': state.clean_area} + + self._state = state.state_code + self._is_on = state.is_on + except VacuumException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py index 24712fa2fbe..1f5125d724e 100644 --- a/homeassistant/components/tado.py +++ b/homeassistant/components/tado.py @@ -15,9 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle -REQUIREMENTS = ['https://github.com/wmalgadey/PyTado/archive/' - '0.2.1.zip#' - 'PyTado==0.2.1'] +REQUIREMENTS = ['python-tado==0.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 488fbdfec2b..30d81930b44 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -114,5 +114,4 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): if not self.process_message(data): return self.json_message('Invalid message', HTTP_BAD_REQUEST) - else: - return self.json({}) + return self.json({}) diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index f923e09323c..01ccb981cfa 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -116,10 +116,9 @@ class TelldusLiveClient(object): return 'cover' elif device.methods & TURNON: return 'switch' - else: - _LOGGER.warning( - "Unidentified device type (methods: %d)", device.methods) - return 'switch' + _LOGGER.warning( + "Unidentified device type (methods: %d)", device.methods) + return 'switch' def discover(device_id, component): """Discover the component.""" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 888a1773189..9f36b2fb78f 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -280,7 +280,9 @@ class SpeechManager(object): # Options if provider.default_options and options: - options = provider.default_options.copy().update(options) + merged_options = provider.default_options.copy() + merged_options.update(options) + options = merged_options options = options or provider.default_options if options is not None: invalid_opts = [opt_name for opt_name in options.keys() diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index 9be882a4cb3..a75f71c3463 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -164,7 +164,7 @@ class AmazonPollyProvider(Provider): """Request TTS file from Polly.""" voice_id = options.get(CONF_VOICE, self.default_voice) voice_in_dict = self.all_voices.get(voice_id) - if language is not voice_in_dict.get('LanguageCode'): + if language != voice_in_dict.get('LanguageCode'): _LOGGER.error("%s does not support the %s language", voice_id, language) return (None, None) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 9b12507de36..3ddcc5c716a 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -129,8 +129,7 @@ class GoogleProvider(Provider): if len(fullstring) > MESSAGE_SIZE: idx = fullstring.rfind(' ', 0, MESSAGE_SIZE) return [fullstring[:idx]] + split_by_space(fullstring[idx:]) - else: - return [fullstring] + return [fullstring] msg_parts = [] for part in parts: diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index a058fdae85e..355a6d0a648 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -9,6 +9,8 @@ from urllib.parse import urlsplit import voluptuous as vol +import homeassistant.loader as loader + from homeassistant.const import (EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery @@ -23,8 +25,12 @@ DOMAIN = 'upnp' DATA_UPNP = 'UPNP' CONF_ENABLE_PORT_MAPPING = 'port_mapping' +CONF_EXTERNAL_PORT = 'external_port' CONF_UNITS = 'unit' +NOTIFICATION_ID = 'upnp_notification' +NOTIFICATION_TITLE = 'UPnP Setup' + UNITS = { "Bytes": 1, "KBytes": 1024, @@ -35,6 +41,7 @@ UNITS = { CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, + vol.Optional(CONF_EXTERNAL_PORT, default=0): cv.positive_int, vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), }), }, extra=vol.ALLOW_EXTRA) @@ -65,15 +72,33 @@ def setup(hass, config): base_url = urlsplit(hass.config.api.base_url) host = base_url.hostname - external_port = internal_port = base_url.port + internal_port = base_url.port + external_port = int(config[DOMAIN].get(CONF_EXTERNAL_PORT)) - upnp.addportmapping( - external_port, 'TCP', host, internal_port, 'Home Assistant', '') + if external_port == 0: + external_port = internal_port - def deregister_port(event): - """De-register the UPnP port mapping.""" - upnp.deleteportmapping(hass.config.api.port, 'TCP') + persistent_notification = loader.get_component('persistent_notification') - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port) + try: + upnp.addportmapping( + external_port, 'TCP', host, internal_port, 'Home Assistant', '') + def deregister_port(event): + """De-register the UPnP port mapping.""" + upnp.deleteportmapping(external_port, 'TCP') + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port) + + except Exception as ex: + _LOGGER.error("UPnP failed to configure port mapping: %s", str(ex)) + persistent_notification.create( + hass, 'ERROR: tcp port {} is already mapped in your router.' + '
Please disable port_mapping in the upnp ' + 'configuration section.
' + 'You will need to restart hass after fixing.' + ''.format(external_port), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False return True diff --git a/homeassistant/components/velux.py b/homeassistant/components/velux.py new file mode 100644 index 00000000000..b0c902aa83e --- /dev/null +++ b/homeassistant/components/velux.py @@ -0,0 +1,67 @@ +""" +Connects to VELUX KLF 200 interface. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/velux/ +""" +import logging +import asyncio + +import voluptuous as vol + +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_HOST, CONF_PASSWORD) + +DOMAIN = "velux" +DATA_VELUX = "data_velux" +SUPPORTED_DOMAINS = ['scene'] +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pyvlx==0.1.3'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the velux component.""" + from pyvlx import PyVLXException + try: + hass.data[DATA_VELUX] = VeluxModule(hass, config) + yield from hass.data[DATA_VELUX].async_start() + + except PyVLXException as ex: + _LOGGER.exception("Can't connect to velux interface: %s", ex) + return False + + for component in SUPPORTED_DOMAINS: + hass.async_add_job( + discovery.async_load_platform(hass, component, DOMAIN, {}, config)) + return True + + +class VeluxModule: + """Abstraction for velux component.""" + + def __init__(self, hass, config): + """Initialize for velux component.""" + from pyvlx import PyVLX + self.initialized = False + host = config[DOMAIN].get(CONF_HOST) + password = config[DOMAIN].get(CONF_PASSWORD) + self.pyvlx = PyVLX( + host=host, + password=password) + self.hass = hass + + @asyncio.coroutine + def async_start(self): + """Start velux component.""" + yield from self.pyvlx.load_scenes() + self.initialized = True diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index cef185bc21f..b43cea3fcea 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -20,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.33'] +REQUIREMENTS = ['pyvera==0.2.34'] _LOGGER = logging.getLogger(__name__) @@ -122,8 +122,7 @@ def map_vera_device(vera_device, remap): if isinstance(vera_device, veraApi.VeraSwitch): if vera_device.device_id in remap: return 'light' - else: - return 'switch' + return 'switch' return None diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 1ec1f9e537d..3ed6efc25d7 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -18,7 +18,7 @@ from homeassistant.util import Throttle import homeassistant.config as conf_util import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['vsure==1.3.6', 'jsonpath==0.75'] +REQUIREMENTS = ['vsure==1.3.7', 'jsonpath==0.75'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py new file mode 100644 index 00000000000..ab72aa989d7 --- /dev/null +++ b/homeassistant/components/wake_on_lan.py @@ -0,0 +1,62 @@ +""" +Component to wake up devices sending Wake-On-LAN magic packets. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/wake_on_lan/ +""" +import asyncio +from functools import partial +import logging +import os + +import voluptuous as vol + +from homeassistant.config import load_yaml_config_file +from homeassistant.const import CONF_MAC +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['wakeonlan==0.2.2'] + +DOMAIN = "wake_on_lan" +_LOGGER = logging.getLogger(__name__) + +CONF_BROADCAST_ADDRESS = 'broadcast_address' + +SERVICE_SEND_MAGIC_PACKET = 'send_magic_packet' + +WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema({ + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, +}) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the wake on LAN component.""" + from wakeonlan import wol + + @asyncio.coroutine + def send_magic_packet(call): + """Send magic packet to wake up a device.""" + mac_address = call.data.get(CONF_MAC) + broadcast_address = call.data.get(CONF_BROADCAST_ADDRESS) + _LOGGER.info("Send magic packet to mac %s (broadcast: %s)", + mac_address, broadcast_address) + if broadcast_address is not None: + yield from hass.async_add_job( + partial(wol.send_magic_packet, mac_address, + ip_address=broadcast_address)) + else: + yield from hass.async_add_job( + partial(wol.send_magic_packet, mac_address)) + + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, send_magic_packet, + description=descriptions.get(DOMAIN).get(SERVICE_SEND_MAGIC_PACKET), + schema=WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA) + + return True diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 17a47fbc522..9e927da893e 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -165,6 +165,5 @@ class WeatherEntity(Entity): if hass_unit == TEMP_CELSIUS: return round(value, 1) - else: - # Users of fahrenheit generally expect integer units. - return round(value) + # Users of fahrenheit generally expect integer units. + return round(value) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index 5b425c0a42b..c6563509e71 100755 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/weather.buienradar/ """ import logging import asyncio -from datetime import timedelta from homeassistant.components.weather import ( WeatherEntity, PLATFORM_SCHEMA) from homeassistant.const import \ @@ -15,10 +14,10 @@ from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation from homeassistant.components.sensor.buienradar import ( BrData) -from homeassistant.helpers.event import ( - async_track_time_interval) import voluptuous as vol +REQUIREMENTS = ['buienradar==0.7'] + _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEFRAME = 60 @@ -49,14 +48,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # create weather data: data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, None) # create weather device: + _LOGGER.debug("Initializing buienradar weather: coordinates %s", + coordinates) async_add_devices([BrWeather(data, config.get(CONF_FORECAST, True), config.get(CONF_NAME, None))]) - # Update weather every 10 minutes, since - # the data gets updated every 10 minutes - async_track_time_interval(hass, data.async_update, timedelta(minutes=10)) # schedule the first update in 1 minute from now: - data.schedule_update(1) + yield from data.schedule_update(1) class BrWeather(WeatherEntity): diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 088ca359cc1..6442a0342a5 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -16,7 +16,7 @@ from homeassistant.const import (CONF_API_KEY, CONF_NAME, CONF_LATITUDE, import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.6.1'] +REQUIREMENTS = ['pyowm==2.7.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 6566a20814b..e9f567c04d3 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -29,7 +29,7 @@ from homeassistant.components.http.ban import process_wrong_login DOMAIN = 'websocket_api' URL = '/api/websocket' -DEPENDENCIES = 'http', +DEPENDENCIES = ('http',) MAX_PENDING_MSG = 512 diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index c33e3b14502..1c0410a4aa0 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -277,5 +277,4 @@ class WinkDevice(Entity): """Return the devices tamper status.""" if hasattr(self.wink, 'tamper_detected'): return self.wink.tamper_detected() - else: - return None + return None diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 8c84fe166f0..e397b7d042a 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -14,7 +14,7 @@ from homeassistant import const as ha_const from homeassistant.helpers import discovery, entity from homeassistant.util import slugify -REQUIREMENTS = ['bellows==0.2.7'] +REQUIREMENTS = ['bellows==0.3.2'] DOMAIN = 'zha' @@ -128,6 +128,10 @@ class ApplicationListener: """Handle device joined and basic information discovered.""" self._hass.async_add_job(self.async_device_initialized(device, True)) + def device_left(self, device): + """Handle device leaving the network.""" + pass + @asyncio.coroutine def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" @@ -142,7 +146,7 @@ class ApplicationListener: discovered_info = yield from _discover_endpoint_info(endpoint) component = None - used_clusters = [] + profile_clusters = ([], []) device_key = '%s-%s' % (str(device.ieee), endpoint_id) node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get( device_key, {}) @@ -152,20 +156,25 @@ class ApplicationListener: if zha_const.DEVICE_CLASS.get(endpoint.profile_id, {}).get(endpoint.device_type, None): - used_clusters = profile.CLUSTERS[endpoint.device_type] + profile_clusters = profile.CLUSTERS[endpoint.device_type] profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id] component = profile_info[endpoint.device_type] if ha_const.CONF_TYPE in node_config: component = node_config[ha_const.CONF_TYPE] - used_clusters = zha_const.COMPONENT_CLUSTERS[component] + profile_clusters = zha_const.COMPONENT_CLUSTERS[component] if component: - clusters = [endpoint.clusters[c] for c in used_clusters if c in - endpoint.clusters] + in_clusters = [endpoint.in_clusters[c] + for c in profile_clusters[0] + if c in endpoint.in_clusters] + out_clusters = [endpoint.out_clusters[c] + for c in profile_clusters[1] + if c in endpoint.out_clusters] discovery_info = { 'endpoint': endpoint, - 'clusters': clusters, + 'in_clusters': {c.cluster_id: c for c in in_clusters}, + 'out_clusters': {c.cluster_id: c for c in out_clusters}, 'new_join': join, } discovery_info.update(discovered_info) @@ -179,9 +188,9 @@ class ApplicationListener: self._config, ) - for cluster_id, cluster in endpoint.clusters.items(): + for cluster_id, cluster in endpoint.in_clusters.items(): cluster_type = type(cluster) - if cluster_id in used_clusters: + if cluster_id in profile_clusters[0]: continue if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS: continue @@ -189,7 +198,8 @@ class ApplicationListener: component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type] discovery_info = { 'endpoint': endpoint, - 'clusters': [cluster], + 'in_clusters': {cluster.cluster_id: cluster}, + 'out_clusters': {}, 'new_join': join, } discovery_info.update(discovered_info) @@ -210,7 +220,8 @@ class Entity(entity.Entity): _domain = None # Must be overriden by subclasses - def __init__(self, endpoint, clusters, manufacturer, model, **kwargs): + def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, + model, **kwargs): """Init ZHA entity.""" self._device_state_attributes = {} ieeetail = ''.join([ @@ -234,10 +245,13 @@ class Entity(entity.Entity): ieeetail, endpoint.endpoint_id, ) - for cluster in clusters: + for cluster in in_clusters.values(): + cluster.add_listener(self) + for cluster in out_clusters.values(): cluster.add_listener(self) self._endpoint = endpoint - self._clusters = {c.cluster_id: c for c in clusters} + self._in_clusters = in_clusters + self._out_clusters = out_clusters self._state = ha_const.STATE_UNKNOWN def attribute_updated(self, attribute, value): @@ -261,19 +275,28 @@ def _discover_endpoint_info(endpoint): 'manufacturer': None, 'model': None, } - if 0 not in endpoint.clusters: + if 0 not in endpoint.in_clusters: return extra_info - result, _ = yield from endpoint.clusters[0].read_attributes( - ['manufacturer', 'model'], - allow_cache=True, - ) - extra_info.update(result) + @asyncio.coroutine + def read(attributes): + """Read attributes and update extra_info convenience function.""" + result, _ = yield from endpoint.in_clusters[0].read_attributes( + attributes, + allow_cache=True, + ) + extra_info.update(result) + + yield from read(['manufacturer', 'model']) + if extra_info['manufacturer'] is None or extra_info['model'] is None: + # Some devices fail at returning multiple results. Attempt separately. + yield from read(['manufacturer']) + yield from read(['model']) for key, value in extra_info.items(): if isinstance(value, bytes): try: - extra_info[key] = value.decode('ascii') + extra_info[key] = value.decode('ascii').strip() except UnicodeDecodeError: # Unsure what the best behaviour here is. Unset the key? pass diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index ed06f18c1f5..b1659536e32 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -46,6 +46,7 @@ def populate_data(): profile = PROFILES[profile_id] for device_type, component in classes.items(): if component not in COMPONENT_CLUSTERS: - COMPONENT_CLUSTERS[component] = set() + COMPONENT_CLUSTERS[component] = (set(), set()) clusters = profile.CLUSTERS[device_type] - COMPONENT_CLUSTERS[component].update(clusters) + COMPONENT_CLUSTERS[component][0].update(clusters[0]) + COMPONENT_CLUSTERS[component][1].update(clusters[1]) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index ba970f94e95..8670ae7b9e7 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -450,12 +450,11 @@ def setup(hass, config): "with selection %s", param, node_id, selection) return - else: - value.data = int(selection) - _LOGGER.info("Setting config parameter %s on Node %s " - "with selection %s", param, node_id, - selection) - return + value.data = int(selection) + _LOGGER.info("Setting config parameter %s on Node %s " + "with selection %s", param, node_id, + selection) + return node.set_config_param(param, selection, size) _LOGGER.info("Setting unknown config parameter %s on Node %s " "with selection %s", param, node_id, diff --git a/homeassistant/const.py b/homeassistant/const.py index 8399729f4ce..f7df04be7f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 48 -PATCH_VERSION = 1 +MINOR_VERSION = 49 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) @@ -179,6 +179,7 @@ EVENT_COMPONENT_LOADED = 'component_loaded' EVENT_SERVICE_REGISTERED = 'service_registered' EVENT_SERVICE_REMOVED = 'service_removed' EVENT_LOGBOOK_ENTRY = 'logbook_entry' +EVENT_THEMES_UPDATED = 'themes_updated' # #### STATES #### STATE_ON = 'on' diff --git a/homeassistant/core.py b/homeassistant/core.py index c65566b42fa..d1779fe420d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -335,9 +335,9 @@ class Event(object): return "".format( self.event_type, str(self.origin)[0], util.repr_helper(self.data)) - else: - return "".format(self.event_type, - str(self.origin)[0]) + + return "".format(self.event_type, + str(self.origin)[0]) def __eq__(self, other): """Return the comparison.""" @@ -783,8 +783,8 @@ class ServiceCall(object): if self.data: return "".format( self.domain, self.service, util.repr_helper(self.data)) - else: - return "".format(self.domain, self.service) + + return "".format(self.domain, self.service) class ServiceRegistry(object): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index dc6c29ce735..49f250c65fa 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -172,6 +172,7 @@ class Entity(object): if async_update is None: return + # pylint: disable=not-callable run_coroutine_threadsafe(async_update(), self.hass.loop).result() # DO NOT OVERWRITE @@ -377,19 +378,18 @@ class ToggleEntity(Entity): return self.hass.async_add_job( ft.partial(self.turn_off, **kwargs)) - def toggle(self) -> None: + def toggle(self, **kwargs) -> None: """Toggle the entity.""" if self.is_on: - self.turn_off() + self.turn_off(**kwargs) else: - self.turn_on() + self.turn_on(**kwargs) - def async_toggle(self): + def async_toggle(self, **kwargs): """Toggle the entity. This method must be run in the event loop and returns a coroutine. """ if self.is_on: - return self.async_turn_off() - else: - return self.async_turn_on() + return self.async_turn_off(**kwargs) + return self.async_turn_on(**kwargs) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d3ad93d3646..9b64c08af18 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -312,8 +312,7 @@ def _process_state_match(parameter): return MATCH_ALL elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): return (parameter,) - else: - return tuple(parameter) + return tuple(parameter) def _process_time_match(parameter): @@ -324,8 +323,7 @@ def _process_time_match(parameter): return parameter elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): return (parameter,) - else: - return tuple(parameter) + return tuple(parameter) def _matcher(subject, pattern): diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 7715e49880d..19113f243d2 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -25,15 +25,16 @@ from homeassistant.components.climate.ecobee import ( ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME, ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, + 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_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED, - STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN, STATE_UNLOCKED, SERVICE_SELECT_OPTION, ATTR_OPTION) + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, + STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_UNLOCKED, + SERVICE_SELECT_OPTION) from homeassistant.core import State from homeassistant.util.async import run_coroutine_threadsafe @@ -203,10 +204,10 @@ def state_as_number(state): Raises ValueError if this is not possible. """ if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON, - STATE_OPEN): + STATE_OPEN, STATE_HOME): return 1 elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN, - STATE_BELOW_HORIZON, STATE_CLOSED): + STATE_BELOW_HORIZON, STATE_CLOSED, STATE_NOT_HOME): return 0 return float(state.state) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 77d0819e10d..6c74c49424e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -47,7 +47,7 @@ def extract_entities(template): return MATCH_ALL extraction = _RE_GET_ENTITIES.findall(template) - if len(extraction) > 0: + if extraction: return list(set(extraction)) return MATCH_ALL diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f4230db153e..1de3671a296 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.2.0 +aiohttp==2.2.3 async_timeout==1.2.1 -chardet==3.0.2 +chardet==3.0.4 astral==1.4 diff --git a/homeassistant/remote.py b/homeassistant/remote.py index b65b3f3de22..c8fe62f64d9 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -92,10 +92,10 @@ class API(object): if method == METHOD_GET: return requests.get( url, params=data, timeout=timeout, headers=self._headers) - else: - return requests.request( - method, url, data=data, timeout=timeout, - headers=self._headers) + + return requests.request( + method, url, data=data, timeout=timeout, + headers=self._headers) except requests.exceptions.ConnectionError: _LOGGER.exception("Error connecting to server") @@ -116,29 +116,29 @@ class JSONEncoder(json.JSONEncoder): """JSONEncoder that supports Home Assistant objects.""" # pylint: disable=method-hidden - def default(self, obj): + def default(self, o): """Convert Home Assistant objects. Hand other objects to the original method. """ - if isinstance(obj, datetime): - return obj.isoformat() - elif isinstance(obj, set): - return list(obj) - elif hasattr(obj, 'as_dict'): - return obj.as_dict() + if isinstance(o, datetime): + return o.isoformat() + elif isinstance(o, set): + return list(o) + elif hasattr(o, 'as_dict'): + return o.as_dict() try: - return json.JSONEncoder.default(self, obj) + return json.JSONEncoder.default(self, o) except TypeError: # If the JSON serializer couldn't serialize it # it might be a generator, convert it to a list try: return [self.default(child_obj) - for child_obj in obj] + for child_obj in o] except TypeError: # Ok, we're lost, cause the original error - return json.JSONEncoder.default(self, obj) + return json.JSONEncoder.default(self, o) def validate_api(api): @@ -152,8 +152,7 @@ def validate_api(api): elif req.status_code == 401: return APIStatus.INVALID_PASSWORD - else: - return APIStatus.UNKNOWN + return APIStatus.UNKNOWN except HomeAssistantError: return APIStatus.CANNOT_CONNECT @@ -259,8 +258,8 @@ def set_state(api, entity_id, new_state, attributes=None, force_update=False): _LOGGER.error("Error changing state: %d - %s", req.status_code, req.text) return False - else: - return True + + return True except HomeAssistantError: _LOGGER.exception("Error setting state") diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 1e06f96b3e4..05cf4d646f6 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -288,7 +288,7 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): indent_str = indent_str[:-1] + '-' if isinstance(layer, Dict): for key, value in sorted(layer.items(), key=sort_dict_key): - if isinstance(value, dict) or isinstance(value, list): + if isinstance(value, (dict, list)): print(indent_str, key + ':', line_info(value, **kwargs)) dump_dict(value, indent_count + 2) else: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 616b9100815..fc9116dda52 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -56,8 +56,8 @@ def repr_helper(inp: Any) -> str: in inp.items()) elif isinstance(inp, datetime): return as_local(inp).isoformat() - else: - return str(inp) + + return str(inp) def convert(value: T, to_type: Callable[[T], U], @@ -303,8 +303,8 @@ class Throttle(object): result = method(*args, **kwargs) throttle[1] = utcnow() return result - else: - return None + + return None finally: throttle[0].release() diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index adaa8afcb57..b7e2412f293 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -26,5 +26,4 @@ def convert(temperature: float, from_unit: str, to_unit: str) -> float: return temperature elif from_unit == TEMP_CELSIUS: return celsius_to_fahrenheit(temperature) - else: - return fahrenheit_to_celsius(temperature) + return fahrenheit_to_celsius(temperature) diff --git a/requirements_all.txt b/requirements_all.txt index 6f596cc53ae..58ef8d070d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,9 +6,9 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.2.0 +aiohttp==2.2.3 async_timeout==1.2.1 -chardet==3.0.2 +chardet==3.0.4 astral==1.4 # homeassistant.components.nuimo_controller @@ -49,10 +49,10 @@ aiodns==1.1.1 aiohttp_cors==0.5.3 # homeassistant.components.light.lifx -aiolifx==0.5.0 +aiolifx==0.5.2 # homeassistant.components.light.lifx -aiolifx_effects==0.1.0 +aiolifx_effects==0.1.1 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.4 @@ -60,21 +60,20 @@ aiopvapi==1.4 # homeassistant.components.alarmdecoder alarmdecoder==0.12.1.0 -# homeassistant.components.camera.amcrest -# homeassistant.components.sensor.amcrest +# homeassistant.components.amcrest amcrest==1.2.0 # homeassistant.components.media_player.anthemav anthemav==1.1.8 # homeassistant.components.apcupsd -apcaccess==0.0.10 +apcaccess==0.0.13 # homeassistant.components.notify.apns apns2==0.1.1 # homeassistant.components.light.avion -# avion==0.6 +# avion==0.7 # homeassistant.components.axis axis==8 @@ -93,7 +92,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.2.7 +bellows==0.3.2 # homeassistant.components.blink blinkpy==0.6.0 @@ -115,10 +114,11 @@ boto3==1.4.3 # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink -broadlink==0.3 +broadlink==0.5 # homeassistant.components.sensor.buienradar -buienradar==0.6 +# homeassistant.components.weather.buienradar +buienradar==0.7 # homeassistant.components.notify.ciscospark ciscosparkapi==0.4.2 @@ -147,13 +147,13 @@ datapoint==0.4.3 # decora==0.6 # homeassistant.components.media_player.denonavr -denonavr==0.5.1 +denonavr==0.5.2 # homeassistant.components.media_player.directv directpy==0.1 # homeassistant.components.notify.discord -discord.py==0.16.0 +discord.py==0.16.8 # homeassistant.components.updater distro==1.0.4 @@ -191,7 +191,7 @@ evohomeclient==0.2.5 # homeassistant.components.image_processing.dlib_face_detect # homeassistant.components.image_processing.dlib_face_identify -# face_recognition==0.1.14 +# face_recognition==0.2.0 # homeassistant.components.sensor.fastdotcom fastdotcom==0.0.1 @@ -273,9 +273,6 @@ holidays==0.8.1 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a -# homeassistant.components.switch.dlink -https://github.com/LinuxChristian/pyW215/archive/v0.4.zip#pyW215==0.4 - # homeassistant.components.sensor.dht # https://github.com/adafruit/Adafruit_Python_DHT/archive/da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip#Adafruit_DHT==1.3.0 @@ -309,9 +306,6 @@ https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4 # homeassistant.components.light.osramlightify https://github.com/tfriedel/python-lightify/archive/1bb1db0e7bd5b14304d7bb267e2398cd5160df46.zip#lightify==1.0.5 -# homeassistant.components.tado -https://github.com/wmalgadey/PyTado/archive/0.2.1.zip#PyTado==0.2.1 - # homeassistant.components.media_player.lg_netcast https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 @@ -344,19 +338,19 @@ jsonrpc-websocket==0.5 keyring>=9.3,<10.0 # homeassistant.components.knx -knxip==0.3.3 +knxip==0.4 # homeassistant.components.device_tracker.owntracks libnacl==1.5.1 # homeassistant.components.dyson -libpurecoollink==0.1.5 +libpurecoollink==0.2.0 # homeassistant.components.device_tracker.mikrotik librouteros==1.0.2 # homeassistant.components.media_player.soundtouch -libsoundtouch==0.6.2 +libsoundtouch==0.7.2 # homeassistant.components.light.lifx_legacy liffylights==0.9.4 @@ -367,6 +361,10 @@ limitlessled==1.0.8 # homeassistant.components.media_player.liveboxplaytv liveboxplaytv==1.4.9 +# homeassistant.components.lametric +# homeassistant.components.notify.lametric +lmnotify==0.0.4 + # homeassistant.components.sensor.lyft lyft_rides==0.1.0b0 @@ -446,7 +444,7 @@ pdunehd==1.3 pexpect==4.0.1 # homeassistant.components.light.hue -phue==0.9 +phue==1.0 # homeassistant.components.rpi_pfio pifacecommon==4.1.2 @@ -474,6 +472,9 @@ pocketcasts==0.1 # homeassistant.components.climate.proliphix proliphix==0.4.1 +# homeassistant.components.prometheus +prometheus_client==0.0.19 + # homeassistant.components.sensor.systemmonitor psutil==5.2.2 @@ -487,9 +488,6 @@ pushbullet.py==0.10.0 # homeassistant.components.notify.pushetta pushetta==1.0.15 -# homeassistant.components.sensor.waqi -pwaqi==3.0 - # homeassistant.components.light.rpi_gpio_pwm pwmled==1.1.1 @@ -503,7 +501,10 @@ pyCEC==0.4.13 pyHS100==0.2.4.2 # homeassistant.components.rfxtrx -pyRFXtrx==0.18.0 +pyRFXtrx==0.19.0 + +# homeassistant.components.switch.dlink +pyW215==0.5.1 # homeassistant.components.alarm_control_panel.alarmdotcom pyalarmdotcom==0.3.0 @@ -517,8 +518,8 @@ pyasn1-modules==0.0.9 # homeassistant.components.notify.xmpp pyasn1==0.2.3 -# homeassistant.components.media_player.apple_tv -pyatv==0.2.1 +# homeassistant.components.apple_tv +pyatv==0.3.2 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox @@ -552,7 +553,7 @@ pyebox==0.1.0 pyeight==0.0.7 # homeassistant.components.media_player.emby -pyemby==1.3 +pyemby==1.4 # homeassistant.components.envisalink pyenvisalink==2.1 @@ -573,10 +574,10 @@ pygatt==3.1.1 pyharmony==1.0.16 # homeassistant.components.binary_sensor.hikvision -pyhik==0.1.2 +pyhik==0.1.3 # homeassistant.components.homematic -pyhomematic==0.1.28 +pyhomematic==0.1.29 # homeassistant.components.sensor.hydroquebec pyhydroquebec==1.2.0 @@ -622,7 +623,7 @@ pymailgunner==1.4 pymochad==0.1.1 # homeassistant.components.modbus -pymodbus==1.3.0rc1 +pymodbus==1.3.1 # homeassistant.components.cover.myq pymyq==0.0.8 @@ -646,9 +647,12 @@ pynut2==2.1.2 # homeassistant.components.binary_sensor.nx584 pynx584==0.4 +# homeassistant.components.sensor.otp +pyotp==2.2.6 + # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap -pyowm==2.6.1 +pyowm==2.7.1 # homeassistant.components.qwikswitch pyqwikswitch==0.4 @@ -707,6 +711,9 @@ python-juicenet==0.0.5 # homeassistant.components.lirc # python-lirc==1.2.3 +# homeassistant.components.switch.xiaomi_vacuum +python-mirobo==0.1.1 + # homeassistant.components.media_player.mpd python-mpd2==0.5.5 @@ -732,6 +739,9 @@ python-roku==3.1.3 # homeassistant.components.sensor.synologydsm python-synology==0.1.0 +# homeassistant.components.tado +python-tado==0.2.2 + # homeassistant.components.telegram_bot python-telegram-bot==6.1.0 @@ -760,7 +770,13 @@ pyunifi==2.13 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.33 +pyvera==0.2.34 + +# homeassistant.components.media_player.vizio +pyvizio==0.0.2 + +# homeassistant.components.velux +pyvlx==0.1.3 # homeassistant.components.notify.html5 pywebpush==1.0.5 @@ -880,7 +896,7 @@ thingspeak==0.4.1 tikteck==0.4 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.7 +total_connect_client==0.11 # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission @@ -902,17 +918,21 @@ uvcclient==0.10.0 volvooncall==0.3.3 # homeassistant.components.verisure -vsure==1.3.6 +vsure==1.3.7 # homeassistant.components.sensor.vasttrafik vtjp==0.1.14 +# 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 +# homeassistant.components.sensor.waqi +waqiasync==1.0.0 + # homeassistant.components.media_player.gpmdp websocket-client==0.37.0 @@ -944,6 +964,9 @@ yeelight==0.3.0 # homeassistant.components.light.yeelightsunflower yeelightsunflower==0.0.8 +# homeassistant.components.media_extractor +youtube_dl==2017.7.9 + # homeassistant.components.light.zengge zengge==0.2 diff --git a/requirements_docs.txt b/requirements_docs.txt index eb217ec94ec..da5e4159de9 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.6.2 +Sphinx==1.6.3 sphinx-autodoc-typehints==1.2.0 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7b74c3b9d8..58fdcecf63c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,10 +62,10 @@ holidays==0.8.1 influxdb==3.0.0 # homeassistant.components.dyson -libpurecoollink==0.1.5 +libpurecoollink==0.2.0 # homeassistant.components.media_player.soundtouch -libsoundtouch==0.6.2 +libsoundtouch==0.7.2 # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi @@ -88,6 +88,9 @@ pilight==0.1.1 # homeassistant.components.sensor.serial_pm pmsensor==0.4 +# homeassistant.components.prometheus +prometheus_client==0.0.19 + # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/script/build_frontend b/script/build_frontend index a81d3ca9eb0..f687c10a31c 100755 --- a/script/build_frontend +++ b/script/build_frontend @@ -24,7 +24,7 @@ cp build/service_worker.js .. cd .. # Pack frontend -gzip -f -k -9 *.html *.js ./panels/*.html +gzip -f -n -k -9 *.html *.js ./panels/*.html cd ../../../.. # Generate the MD5 hash of the new frontend diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f1f0678c60f..7e2f1d99f2a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -69,6 +69,7 @@ TEST_REQUIREMENTS = ( 'PyJWT', 'restrictedpython', 'pyunifi', + 'prometheus_client', ) IGNORE_PACKAGES = ( diff --git a/setup.py b/setup.py index 3a37874a08f..4476bc2f9f0 100755 --- a/setup.py +++ b/setup.py @@ -22,9 +22,9 @@ REQUIRES = [ 'jinja2>=2.9.5', 'voluptuous==0.10.5', 'typing>=3,<4', - 'aiohttp==2.2.0', + 'aiohttp==2.2.3', 'async_timeout==1.2.1', - 'chardet==3.0.2', + 'chardet==3.0.4', 'astral==1.4', ] diff --git a/tests/components/binary_sensor/test_zwave.py b/tests/components/binary_sensor/test_zwave.py index 13f3934f964..eb52dc0c825 100644 --- a/tests/components/binary_sensor/test_zwave.py +++ b/tests/components/binary_sensor/test_zwave.py @@ -83,15 +83,13 @@ def test_trigger_sensor_value_changed(hass, mock_openzwave): assert not device.is_on value.data = True - yield from hass.loop.run_in_executor(None, value_changed, value) - yield from hass.async_block_till_done() + yield from hass.async_add_job(value_changed, value) assert device.invalidate_after is None device.hass = hass value.data = True - yield from hass.loop.run_in_executor(None, value_changed, value) - yield from hass.async_block_till_done() + yield from hass.async_add_job(value_changed, value) assert device.is_on test_time = device.invalidate_after - datetime.timedelta(seconds=1) diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index 52bc3d9e048..84eaf107d70 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -2,7 +2,7 @@ import asyncio from unittest import mock -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.setup import async_setup_component @asyncio.coroutine @@ -10,18 +10,14 @@ def test_fetching_url(aioclient_mock, hass, test_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com', text='hello world') - def setup_platform(): - """Setup the platform.""" - assert setup_component(hass, 'camera', { - 'camera': { - 'name': 'config_test', - 'platform': 'generic', - 'still_image_url': 'http://example.com', - 'username': 'user', - 'password': 'pass' - }}) - - yield from hass.loop.run_in_executor(None, setup_platform) + yield from async_setup_component(hass, 'camera', { + 'camera': { + 'name': 'config_test', + 'platform': 'generic', + 'still_image_url': 'http://example.com', + 'username': 'user', + 'password': 'pass' + }}) client = yield from test_client(hass.http.app) @@ -44,18 +40,14 @@ def test_limit_refetch(aioclient_mock, hass, test_client): aioclient_mock.get('http://example.com/15a', text='hello planet') aioclient_mock.get('http://example.com/20a', status=404) - def setup_platform(): - """Setup the platform.""" - assert setup_component(hass, 'camera', { - 'camera': { - 'name': 'config_test', - 'platform': 'generic', - 'still_image_url': - 'http://example.com/{{ states.sensor.temp.state + "a" }}', - 'limit_refetch_to_url_change': True, - }}) - - yield from hass.loop.run_in_executor(None, setup_platform) + yield from async_setup_component(hass, 'camera', { + 'camera': { + 'name': 'config_test', + 'platform': 'generic', + 'still_image_url': + 'http://example.com/{{ states.sensor.temp.state + "a" }}', + 'limit_refetch_to_url_change': True, + }}) client = yield from test_client(hass.http.app) diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 06e7e5e3515..812dd399a48 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -6,28 +6,21 @@ from unittest import mock # https://bugs.python.org/issue23004 from mock_open import MockOpen -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import mock_http_component -import logging +from homeassistant.setup import async_setup_component @asyncio.coroutine def test_loading_file(hass, test_client): """Test that it loads image from disk.""" - @mock.patch('os.path.isfile', mock.Mock(return_value=True)) - @mock.patch('os.access', mock.Mock(return_value=True)) - def setup_platform(): - """Setup platform inside callback.""" - assert setup_component(hass, 'camera', { + 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', { 'camera': { 'name': 'config_test', 'platform': 'local_file', 'file_path': 'mock.file', }}) - yield from hass.loop.run_in_executor(None, setup_platform) - client = yield from test_client(hass.http.app) m_open = MockOpen(read_data=b'hello') @@ -45,26 +38,18 @@ def test_loading_file(hass, test_client): @asyncio.coroutine def test_file_not_readable(hass, caplog): """Test a warning is shown setup when file is not readable.""" - mock_http_component(hass) - - @mock.patch('os.path.isfile', mock.Mock(return_value=True)) - @mock.patch('os.access', mock.Mock(return_value=False)) - def run_test(): - - caplog.set_level( - logging.WARNING, logger='requests.packages.urllib3.connectionpool') - - assert setup_component(hass, 'camera', { + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ + mock.patch('os.access', mock.Mock(return_value=False)): + yield from async_setup_component(hass, 'camera', { 'camera': { 'name': 'config_test', 'platform': 'local_file', 'file_path': 'mock.file', }}) - assert 'Could not read' in caplog.text - assert 'config_test' in caplog.text - assert 'mock.file' in caplog.text - yield from hass.loop.run_in_executor(None, run_test) + assert 'Could not read' in caplog.text + assert 'config_test' in caplog.text + assert 'mock.file' in caplog.text @asyncio.coroutine diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 16dbe5ae895..15fc3f6a982 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -108,6 +108,12 @@ class TestClimateGenericThermostat(unittest.TestCase): self.assertEqual(35, state.attributes.get('max_temp')) self.assertEqual(None, state.attributes.get('temperature')) + def test_get_operation_modes(self): + """Test that the operation list returns the correct modes.""" + state = self.hass.states.get(ENTITY) + modes = state.attributes.get('operation_list') + self.assertEqual([climate.STATE_AUTO, STATE_OFF], modes) + def test_set_target_temp(self): """Test the setting of the target temperature.""" climate.set_temperature(self.hass, 30) @@ -211,6 +217,30 @@ class TestClimateGenericThermostat(unittest.TestCase): self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def test_running_when_operating_mode_is_off(self): + """Test that the switch turns off when enabled is set False.""" + self._setup_switch(True) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + climate.set_operation_mode(self.hass, STATE_OFF) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def test_no_state_change_when_operation_mode_off(self): + """Test that the switch doesn't turn on when enabled is False.""" + self._setup_switch(False) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + climate.set_operation_mode(self.hass, STATE_OFF) + self.hass.block_till_done() + self._setup_sensor(25) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): """Setup the test sensor.""" self.hass.states.set(ENT_SENSOR, temp, { @@ -321,6 +351,30 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self.assertEqual(SERVICE_TURN_ON, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def test_running_when_operating_mode_is_off(self): + """Test that the switch turns off when enabled is set False.""" + self._setup_switch(True) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + climate.set_operation_mode(self.hass, STATE_OFF) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def test_no_state_change_when_operation_mode_off(self): + """Test that the switch doesn't turn on when enabled is False.""" + self._setup_switch(False) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + climate.set_operation_mode(self.hass, STATE_OFF) + self.hass.block_till_done() + self._setup_sensor(35) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): """Setup the test sensor.""" self.hass.states.set(ENT_SENSOR, temp, { diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index 35ec21bfbdf..cd2120e71e6 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -54,10 +54,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, } } } @@ -79,7 +75,7 @@ class TestTemplateCover(unittest.TestCase): assert state.state == STATE_CLOSED def test_template_state_boolean(self): - """Test the state text of a template.""" + """Test the value_template attribute.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -96,10 +92,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, } } } @@ -112,7 +104,7 @@ class TestTemplateCover(unittest.TestCase): assert state.state == STATE_OPEN def test_template_position(self): - """Test the state text of a template.""" + """Test the position_template attribute.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -129,10 +121,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test' - }, } } } @@ -170,7 +158,7 @@ class TestTemplateCover(unittest.TestCase): assert state.state == STATE_CLOSED def test_template_tilt(self): - """Test the state text of a template.""" + """Test the tilt_template attribute.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -189,10 +177,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, } } } @@ -205,7 +189,7 @@ class TestTemplateCover(unittest.TestCase): assert state.attributes.get('current_tilt_position') == 42.0 def test_template_out_of_bounds(self): - """Test the state text of a template.""" + """Test template out-of-bounds condition.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -224,10 +208,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, } } } @@ -260,10 +240,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, 'icon_template': "{% if states.cover.test_state.state %}" "mdi:check" @@ -294,14 +270,26 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, - 'icon_template': - "{% if states.cover.test_state.state %}" - "mdi:check" - "{% endif %}" + }, + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_template_open_or_position(self): + """Test that at least one of open_cover or set_position is used.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ 1 == 1 }}", } } } @@ -312,8 +300,32 @@ 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.""" + with assert_setup_component(0, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ 1 == 1 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + }, + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + def test_template_non_numeric(self): - """Test the state text of a template.""" + """Test that tilt_template values are numeric.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -336,10 +348,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, } } } @@ -353,7 +361,7 @@ class TestTemplateCover(unittest.TestCase): assert state.attributes.get('current_position') is None def test_open_action(self): - """Test the state text of a template.""" + """Test the open_cover command.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -369,10 +377,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, } } } @@ -390,7 +394,7 @@ class TestTemplateCover(unittest.TestCase): assert len(self.calls) == 1 def test_close_stop_action(self): - """Test the state text of a template.""" + """Test the close-cover and stop_cover commands.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -429,29 +433,30 @@ class TestTemplateCover(unittest.TestCase): assert len(self.calls) == 2 def test_set_position(self): - """Test the state text of a template.""" + """Test the set_position command.""" with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'input_slider', { + 'input_slider': { + 'test': { + 'min': '0', + 'max': '100', + 'initial': '42', + } + } + }) assert setup.setup_component(self.hass, 'cover', { 'cover': { 'platform': 'template', 'covers': { 'test_template_cover': { 'position_template': - "{{ 100 }}", - 'open_cover': { - 'service': 'cover.open_cover', - 'entity_id': 'cover.test_state' - }, - 'close_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, - 'stop_cover': { - 'service': 'cover.stop_cover', - 'entity_id': 'cover.test_state' - }, + "{{ states.input_slider.test.state | int }}", 'set_cover_position': { - 'service': 'test.automation', + 'service': 'input_slider.select_value', + 'entity_id': 'input_slider.test', + 'data_template': { + 'value': '{{ position }}' + }, }, } } @@ -461,17 +466,29 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() + state = self.hass.states.set('input_slider.test', 42) + self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN - cover.set_cover_position(self.hass, 42, + cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') == 100.0 + + cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') == 0.0 + + cover.set_cover_position(self.hass, 25, 'cover.test_template_cover') self.hass.block_till_done() - - assert len(self.calls) == 1 + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') == 25.0 def test_set_tilt_position(self): - """Test the state text of a template.""" + """Test the set_tilt_position command.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -488,10 +505,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.stop_cover', - 'entity_id': 'cover.test_state' - }, 'set_cover_tilt_position': { 'service': 'test.automation', }, @@ -510,7 +523,7 @@ class TestTemplateCover(unittest.TestCase): assert len(self.calls) == 1 def test_open_tilt_action(self): - """Test the state text of a template.""" + """Test the open_cover_tilt command.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -527,10 +540,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.stop_cover', - 'entity_id': 'cover.test_state' - }, 'set_cover_tilt_position': { 'service': 'test.automation', }, @@ -548,7 +557,7 @@ class TestTemplateCover(unittest.TestCase): assert len(self.calls) == 1 def test_close_tilt_action(self): - """Test the state text of a template.""" + """Test the close_cover_tilt command.""" with assert_setup_component(1, 'cover'): assert setup.setup_component(self.hass, 'cover', { 'cover': { @@ -565,10 +574,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.stop_cover', - 'entity_id': 'cover.test_state' - }, 'set_cover_tilt_position': { 'service': 'test.automation', }, @@ -603,10 +608,6 @@ class TestTemplateCover(unittest.TestCase): 'service': 'cover.close_cover', 'entity_id': 'cover.test_state' }, - 'stop_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, 'icon_template': "{% if states.cover.test_state.state %}" "mdi:check" diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index dea53b16559..1ef3aefa6a4 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -7,7 +7,7 @@ import logging from homeassistant.setup import setup_component from homeassistant.components import device_tracker from homeassistant.const import ( - CONF_PLATFORM, CONF_HOST, CONF_PASSWORD) + CONF_PLATFORM, CONF_HOST) from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.device_tracker.upc_connect as platform from homeassistant.util.async import run_coroutine_threadsafe @@ -62,43 +62,10 @@ class TestUPCConnect(object): assert setup_component( self.hass, DOMAIN, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }}) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' - - @patch('homeassistant.components.device_tracker._LOGGER.error') - def test_setup_platform_error_webservice(self, mock_error, aioclient_mock): - """Setup a platform with api error.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - status=404 - ) - - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' - }}) - - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' - - assert 'Error setting up platform' in \ - str(mock_error.call_args_list[-1]) + assert len(aioclient_mock.mock_calls) == 1 @patch('homeassistant.components.device_tracker._LOGGER.error') def test_setup_platform_timeout_webservice(self, mock_error, @@ -106,10 +73,7 @@ class TestUPCConnect(object): """Setup a platform with api timeout.""" aioclient_mock.get( "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), + cookies={'sessionToken': '654321'}, content=b'successful', exc=asyncio.TimeoutError() ) @@ -118,14 +82,10 @@ class TestUPCConnect(object): assert setup_component( self.hass, DOMAIN, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }}) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + assert len(aioclient_mock.mock_calls) == 1 assert 'Error setting up platform' in \ str(mock_error.call_args_list[-1]) @@ -147,8 +107,7 @@ class TestUPCConnect(object): assert setup_component( self.hass, DOMAIN, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }}) assert len(aioclient_mock.mock_calls) == 1 @@ -171,14 +130,11 @@ class TestUPCConnect(object): scanner = run_coroutine_threadsafe(platform.async_get_scanner( self.hass, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }} ), self.hass.loop).result() - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + assert len(aioclient_mock.mock_calls) == 1 aioclient_mock.clear_requests() aioclient_mock.post( @@ -191,8 +147,7 @@ class TestUPCConnect(object): scanner.async_scan_devices(), self.hass.loop).result() assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2]['fun'] == 123 - assert scanner.token == '1235678' + assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', '70:EE:50:27:A1:38'] @@ -211,14 +166,11 @@ class TestUPCConnect(object): scanner = run_coroutine_threadsafe(platform.async_get_scanner( self.hass, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }} ), self.hass.loop).result() - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + assert len(aioclient_mock.mock_calls) == 1 aioclient_mock.clear_requests() aioclient_mock.get( @@ -235,8 +187,8 @@ class TestUPCConnect(object): mac_list = run_coroutine_threadsafe( scanner.async_scan_devices(), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 3 - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', '70:EE:50:27:A1:38'] @@ -255,14 +207,11 @@ class TestUPCConnect(object): scanner = run_coroutine_threadsafe(platform.async_get_scanner( self.hass, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }} ), self.hass.loop).result() - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + assert len(aioclient_mock.mock_calls) == 1 aioclient_mock.clear_requests() aioclient_mock.get( @@ -280,7 +229,7 @@ class TestUPCConnect(object): scanner.async_scan_devices(), self.hass.loop).result() assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' assert mac_list == [] def test_scan_devices_parse_error(self, aioclient_mock): @@ -298,14 +247,11 @@ class TestUPCConnect(object): scanner = run_coroutine_threadsafe(platform.async_get_scanner( self.hass, {DOMAIN: { CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host, - CONF_PASSWORD: '123456' + CONF_HOST: self.host }} ), self.hass.loop).result() - assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' - assert aioclient_mock.mock_calls[1][2]['fun'] == 15 - assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + assert len(aioclient_mock.mock_calls) == 1 aioclient_mock.clear_requests() aioclient_mock.post( @@ -318,6 +264,6 @@ class TestUPCConnect(object): scanner.async_scan_devices(), self.hass.loop).result() assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2]['fun'] == 123 + assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' assert scanner.token is None assert mac_list == [] diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py index 4548b12434b..e388f31e664 100644 --- a/tests/components/fan/test_dyson.py +++ b/tests/components/fan/test_dyson.py @@ -6,6 +6,15 @@ from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.components.fan import dyson from tests.common import get_test_home_assistant from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation +from libpurecoollink.dyson import DysonState + + +class MockDysonState(DysonState): + """Mock Dyson state.""" + + def __init__(self): + """Create new Mock Dyson State.""" + pass def _get_device_with_no_state(): @@ -257,7 +266,7 @@ class DysonTest(unittest.TestCase): component = dyson.DysonPureCoolLinkDevice(self.hass, device) component.entity_id = "entity_id" component.schedule_update_ha_state = mock.Mock() - component.on_message("Message") + component.on_message(MockDysonState()) component.schedule_update_ha_state.assert_called_with() def test_service_set_night_mode(self): diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index 03180c47a4a..25f83b1d123 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -27,7 +27,7 @@ CONFIG = { 'devices': { 'protocol_0_0': { 'name': 'test', - 'aliasses': ['test_alias_0_0'], + 'aliases': ['test_alias_0_0'], }, 'dimmable_0_0': { 'name': 'dim_test', @@ -58,7 +58,7 @@ def test_default_setup(hass, monkeypatch): assert light_initial.attributes['assumed_state'] # light should follow state of the hardware device by interpreting - # incoming events for its name and aliasses + # incoming events for its name and aliases # mock incoming command event for this device event_callback({ @@ -100,7 +100,7 @@ def test_default_setup(hass, monkeypatch): assert hass.states.get(DOMAIN + '.test').state == 'off' - # test following aliasses + # test following aliases # mock incoming command event for this device alias event_callback({ 'id': 'test_alias_0_0', @@ -185,7 +185,7 @@ def test_firing_bus_event(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'aliasses': ['test_alias_0_0'], + 'aliases': ['test_alias_0_0'], 'fire_event': True, }, }, @@ -418,7 +418,7 @@ def test_group_alias(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'group_aliasses': ['test_group_0_0'], + 'group_aliases': ['test_group_0_0'], }, }, }, @@ -461,7 +461,7 @@ def test_nogroup_alias(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'nogroup_aliasses': ['test_nogroup_0_0'], + 'nogroup_aliases': ['test_nogroup_0_0'], }, }, }, diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py index 4958f5ee263..a8242b39f7f 100644 --- a/tests/components/media_player/test_soundtouch.py +++ b/tests/components/media_player/test_soundtouch.py @@ -584,6 +584,24 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_presets.call_count, 2) self.assertEqual(mocked_select_preset.call_count, 1) + @mock.patch('libsoundtouch.device.SoundTouchDevice.play_url') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @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, + 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_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + all_devices[0].play_media('MUSIC', "http://fqdn/file.mp3") + mocked_play_url.assert_called_with("http://fqdn/file.mp3") + @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status') diff --git a/tests/components/sensor/test_dyson.py b/tests/components/sensor/test_dyson.py index 8dc76c70147..8599346f769 100644 --- a/tests/components/sensor/test_dyson.py +++ b/tests/components/sensor/test_dyson.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.sensor import dyson from tests.common import get_test_home_assistant @@ -12,6 +12,7 @@ def _get_device_without_state(): device = mock.Mock() device.name = "Device_name" device.state = None + device.environmental_state = None return device @@ -21,6 +22,12 @@ def _get_with_state(): device.name = "Device_name" device.state = mock.Mock() device.state.filter_life = 100 + device.environmental_state = mock.Mock() + device.environmental_state.dust = 5 + device.environmental_state.humidity = 45 + device.environmental_state.temperature = 295 + device.environmental_state.volatil_organic_compounds = 2 + return device @@ -45,27 +52,31 @@ class DysonTest(unittest.TestCase): def test_setup_component(self): """Test setup component with devices.""" def _add_device(devices): - assert len(devices) == 1 + assert len(devices) == 5 assert devices[0].name == "Device_name filter life" + assert devices[1].name == "Device_name dust" + assert devices[2].name == "Device_name humidity" + assert devices[3].name == "Device_name temperature" + assert devices[4].name == "Device_name air quality" device = _get_device_without_state() self.hass.data[dyson.DYSON_DEVICES] = [device] dyson.setup_platform(self.hass, None, _add_device) def test_dyson_filter_life_sensor(self): - """Test sensor with no value.""" + """Test filter life sensor with no value.""" sensor = dyson.DysonFilterLifeSensor(self.hass, _get_device_without_state()) sensor.entity_id = "sensor.dyson_1" self.assertFalse(sensor.should_poll) - self.assertEqual(sensor.state, STATE_UNKNOWN) + self.assertIsNone(sensor.state) self.assertEqual(sensor.unit_of_measurement, "hours") self.assertEqual(sensor.name, "Device_name filter life") self.assertEqual(sensor.entity_id, "sensor.dyson_1") sensor.on_message('message') def test_dyson_filter_life_sensor_with_values(self): - """Test sensor with values.""" + """Test filter sensor with values.""" sensor = dyson.DysonFilterLifeSensor(self.hass, _get_with_state()) sensor.entity_id = "sensor.dyson_1" self.assertFalse(sensor.should_poll) @@ -74,3 +85,100 @@ class DysonTest(unittest.TestCase): self.assertEqual(sensor.name, "Device_name filter life") self.assertEqual(sensor.entity_id, "sensor.dyson_1") sensor.on_message('message') + + def test_dyson_dust_sensor(self): + """Test dust sensor with no value.""" + sensor = dyson.DysonDustSensor(self.hass, + _get_device_without_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name dust") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_dust_sensor_with_values(self): + """Test dust sensor with values.""" + sensor = dyson.DysonDustSensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 5) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name dust") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_humidity_sensor(self): + """Test humidity sensor with no value.""" + sensor = dyson.DysonHumiditySensor(self.hass, + _get_device_without_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, '%') + self.assertEqual(sensor.name, "Device_name humidity") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_humidity_sensor_with_values(self): + """Test humidity sensor with values.""" + sensor = dyson.DysonHumiditySensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 45) + self.assertEqual(sensor.unit_of_measurement, '%') + self.assertEqual(sensor.name, "Device_name humidity") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_temperature_sensor(self): + """Test temperature sensor with no value.""" + sensor = dyson.DysonTemperatureSensor(self.hass, + _get_device_without_state(), + TEMP_CELSIUS) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, '°C') + self.assertEqual(sensor.name, "Device_name temperature") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_temperature_sensor_with_values(self): + """Test temperature sensor with values.""" + sensor = dyson.DysonTemperatureSensor(self.hass, + _get_with_state(), + TEMP_CELSIUS) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 21.9) + self.assertEqual(sensor.unit_of_measurement, '°C') + self.assertEqual(sensor.name, "Device_name temperature") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + sensor = dyson.DysonTemperatureSensor(self.hass, + _get_with_state(), + TEMP_FAHRENHEIT) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 71.3) + self.assertEqual(sensor.unit_of_measurement, '°F') + self.assertEqual(sensor.name, "Device_name temperature") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_air_quality_sensor(self): + """Test air quality sensor with no value.""" + sensor = dyson.DysonAirQualitySensor(self.hass, + _get_device_without_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name air quality") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_air_quality_sensor_with_values(self): + """Test air quality sensor with values.""" + sensor = dyson.DysonAirQualitySensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 2) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name air quality") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") diff --git a/tests/components/sensor/test_london_underground.py b/tests/components/sensor/test_london_underground.py new file mode 100644 index 00000000000..fbffcbd1d8f --- /dev/null +++ b/tests/components/sensor/test_london_underground.py @@ -0,0 +1,38 @@ +"""The tests for the tube_state platform.""" +import unittest +import requests_mock + +from homeassistant.components.sensor.london_underground import CONF_LINE, URL +from homeassistant.setup import setup_component +from tests.common import load_fixture, get_test_home_assistant + +VALID_CONFIG = { + 'platform': 'london_underground', + CONF_LINE: [ + 'London Overground', + ] +} + + +class TestLondonTubeSensor(unittest.TestCase): + """Test the tube_state platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker() + def test_setup(self, mock_req): + """Test for operational tube_state sensor with proper attributes.""" + mock_req.get(URL, text=load_fixture('london_underground.json')) + self.assertTrue( + setup_component(self.hass, 'sensor', {'sensor': self.config})) + + state = self.hass.states.get('sensor.london_overground') + assert state.state == 'Minor Delays' + assert state.attributes.get('Description') == 'something' diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index 2422f0ea334..d529e8c3f56 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -557,3 +557,50 @@ class TestSwitchFlux(unittest.TestCase): self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_COLOR_TEMP], 269) + + def test_flux_with_rgb(self): + """Test the flux switch´s mode rgb.""" + platform = loader.get_component('light.test') + platform.init() + self.assertTrue( + setup_component(self.hass, light.DOMAIN, + {light.DOMAIN: {CONF_PLATFORM: 'test'}})) + + dev1 = platform.DEVICES[0] + + # Verify initial state of light + state = self.hass.states.get(dev1.entity_id) + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('color_temp')) + + test_time = dt_util.now().replace(hour=8, minute=30, second=0) + sunset_time = test_time.replace(hour=17, minute=0, second=0) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time + + with patch('homeassistant.util.dt.now', return_value=test_time): + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'mode': 'rgb' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() + call = turn_on_calls[-1] + rgb = (255, 198, 152) + rounded_call = tuple(map(round, call.data[light.ATTR_RGB_COLOR])) + self.assertEqual(rounded_call, rgb) diff --git a/tests/components/switch/test_rflink.py b/tests/components/switch/test_rflink.py index d48c9aca7a4..b261d9c9b49 100644 --- a/tests/components/switch/test_rflink.py +++ b/tests/components/switch/test_rflink.py @@ -24,7 +24,7 @@ CONFIG = { 'devices': { 'protocol_0_0': { 'name': 'test', - 'aliasses': ['test_alias_0_0'], + 'aliases': ['test_alias_0_0'], }, }, }, @@ -47,7 +47,7 @@ def test_default_setup(hass, monkeypatch): assert switch_initial.attributes['assumed_state'] # switch should follow state of the hardware device by interpreting - # incoming events for its name and aliasses + # incoming events for its name and aliases # mock incoming command event for this device event_callback({ @@ -70,7 +70,7 @@ def test_default_setup(hass, monkeypatch): assert hass.states.get('switch.test').state == 'off' - # test following aliasses + # test following aliases # mock incoming command event for this device alias event_callback({ 'id': 'test_alias_0_0', @@ -112,7 +112,7 @@ def test_group_alias(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'group_aliasses': ['test_group_0_0'], + 'group_aliases': ['test_group_0_0'], }, }, }, @@ -155,7 +155,7 @@ def test_nogroup_alias(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'nogroup_aliasses': ['test_nogroup_0_0'], + 'nogroup_aliases': ['test_nogroup_0_0'], }, }, }, diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index ce6fce03e83..3682e0a2c14 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,10 +1,12 @@ """The tests for Home Assistant frontend.""" import asyncio import re +from unittest.mock import patch import pytest from homeassistant.setup import async_setup_component +from homeassistant.components.frontend import DOMAIN, ATTR_THEMES @pytest.fixture @@ -14,6 +16,20 @@ def mock_http_client(hass, test_client): return hass.loop.run_until_complete(test_client(hass.http.app)) +@pytest.fixture +def mock_http_client_with_themes(hass, test_client): + """Start the Hass HTTP component.""" + hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { + DOMAIN: { + ATTR_THEMES: { + 'happy': { + 'primary-color': 'red' + } + } + }})) + return hass.loop.run_until_complete(test_client(hass.http.app)) + + @asyncio.coroutine def test_frontend_and_static(mock_http_client): """Test if we can get the frontend.""" @@ -56,14 +72,74 @@ def test_we_cannot_POST_to_root(mock_http_client): @asyncio.coroutine -def test_states_routes(hass, mock_http_client): +def test_states_routes(mock_http_client): """All served by index.""" resp = yield from mock_http_client.get('/states') assert resp.status == 200 - resp = yield from mock_http_client.get('/states/group.non_existing') - assert resp.status == 404 - - hass.states.async_set('group.existing', 'on', {'view': True}) resp = yield from mock_http_client.get('/states/group.existing') assert resp.status == 200 + + +@asyncio.coroutine +def test_themes_api(mock_http_client_with_themes): + """Test that /api/themes returns correct data.""" + resp = yield from mock_http_client_with_themes.get('/api/themes') + json = yield from resp.json() + assert json['default_theme'] == 'default' + assert json['themes'] == {'happy': {'primary-color': 'red'}} + + +@asyncio.coroutine +def test_themes_set_theme(hass, mock_http_client_with_themes): + """Test frontend.set_theme service.""" + yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'happy'}) + yield from hass.async_block_till_done() + resp = yield from mock_http_client_with_themes.get('/api/themes') + json = yield from resp.json() + assert json['default_theme'] == 'happy' + + yield from hass.services.async_call( + DOMAIN, 'set_theme', {'name': 'default'}) + yield from hass.async_block_till_done() + resp = yield from mock_http_client_with_themes.get('/api/themes') + json = yield from resp.json() + assert json['default_theme'] == 'default' + + +@asyncio.coroutine +def test_themes_set_theme_wrong_name(hass, mock_http_client_with_themes): + """Test frontend.set_theme service called with wrong name.""" + yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'wrong'}) + yield from hass.async_block_till_done() + resp = yield from mock_http_client_with_themes.get('/api/themes') + json = yield from resp.json() + assert json['default_theme'] == 'default' + + +@asyncio.coroutine +def test_themes_reload_themes(hass, mock_http_client_with_themes): + """Test frontend.reload_themes service.""" + with patch('homeassistant.components.frontend.load_yaml_config_file', + return_value={DOMAIN: { + ATTR_THEMES: { + 'sad': {'primary-color': 'blue'} + }}}): + yield from hass.services.async_call(DOMAIN, 'set_theme', + {'name': 'happy'}) + yield from hass.services.async_call(DOMAIN, 'reload_themes') + yield from hass.async_block_till_done() + resp = yield from mock_http_client_with_themes.get('/api/themes') + json = yield from resp.json() + assert json['themes'] == {'sad': {'primary-color': 'blue'}} + assert json['default_theme'] == 'default' + + +@asyncio.coroutine +def test_missing_themes(mock_http_client): + """Test that themes API works when themes are not defined.""" + resp = yield from mock_http_client.get('/api/themes') + assert resp.status == 200 + json = yield from resp.json() + assert json['default_theme'] == 'default' + assert json['themes'] == {} diff --git a/tests/components/test_group.py b/tests/components/test_group.py index d94ccaa385c..7371ecf6e56 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -150,6 +150,20 @@ class TestComponentsGroup(unittest.TestCase): sorted(group.expand_entity_ids( self.hass, ['light.bowl', test_group.entity_id]))) + def test_expand_entity_ids_recursive(self): + """Test expand_entity_ids method with a group that contains itself.""" + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) + test_group = group.Group.create_group( + self.hass, + 'init_group', + ['light.Bowl', 'light.Ceiling', 'group.init_group'], + False) + + self.assertEqual(sorted(['light.ceiling', 'light.bowl']), + sorted(group.expand_entity_ids( + self.hass, [test_group.entity_id]))) + def test_expand_entity_ids_ignores_non_strings(self): """Test that non string elements in lists are ignored.""" self.assertEqual([], group.expand_entity_ids(self.hass, [5, True])) @@ -226,11 +240,11 @@ class TestComponentsGroup(unittest.TestCase): group_conf = OrderedDict() group_conf['second_group'] = { - 'entities': 'light.Bowl, ' + test_group.entity_id, - 'icon': 'mdi:work', - 'view': True, - 'control': 'hidden', - } + 'entities': 'light.Bowl, ' + test_group.entity_id, + 'icon': 'mdi:work', + 'view': True, + 'control': 'hidden', + } group_conf['test_group'] = 'hello.world,sensor.happy' group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None} @@ -275,8 +289,8 @@ class TestComponentsGroup(unittest.TestCase): self.hass, 'light', ['light.test_1', 'light.test_2']) group.Group.create_group( self.hass, 'switch', ['switch.test_1', 'switch.test_2']) - group.Group.create_group(self.hass, 'group_of_groups', ['group.light', - 'group.switch']) + group.Group.create_group( + self.hass, 'group_of_groups', ['group.light', 'group.switch']) self.assertEqual( ['light.test_1', 'light.test_2', 'switch.test_1', 'switch.test_2'], @@ -325,27 +339,26 @@ class TestComponentsGroup(unittest.TestCase): def test_reloading_groups(self): """Test reloading the group config.""" assert setup_component(self.hass, 'group', {'group': { - 'second_group': { - 'entities': 'light.Bowl', - 'icon': 'mdi:work', - 'view': True, - }, - 'test_group': 'hello.world,sensor.happy', - 'empty_group': {'name': 'Empty Group', 'entities': None}, - } - }) + 'second_group': { + 'entities': 'light.Bowl', + 'icon': 'mdi:work', + 'view': True, + }, + 'test_group': 'hello.world,sensor.happy', + 'empty_group': {'name': 'Empty Group', 'entities': None}, + }}) assert sorted(self.hass.states.entity_ids()) == \ ['group.empty_group', 'group.second_group', 'group.test_group'] assert self.hass.bus.listeners['state_changed'] == 3 with patch('homeassistant.config.load_yaml_config_file', return_value={ - 'group': { - 'hello': { - 'entities': 'light.Bowl', - 'icon': 'mdi:work', - 'view': True, - }}}): + 'group': { + 'hello': { + 'entities': 'light.Bowl', + 'icon': 'mdi:work', + 'view': True, + }}}): group.reload(self.hass) self.hass.block_till_done() @@ -395,6 +408,7 @@ def test_service_group_services(hass): assert hass.services.has_service('group', group.SERVICE_REMOVE) +# pylint: disable=invalid-name @asyncio.coroutine def test_service_group_set_group_remove_group(hass): """Check if service are available.""" diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index ec1e5bf3650..5f9cdcfa57c 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -53,9 +53,7 @@ class TestPanelIframe(unittest.TestCase): }, }) - # 5 dev tools + map are automatically loaded + 2 iframe panels - assert len(self.hass.data[frontend.DATA_PANELS]) == 8 - assert self.hass.data[frontend.DATA_PANELS]['router'] == { + assert self.hass.data[frontend.DATA_PANELS].get('router') == { 'component_name': 'iframe', 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', @@ -64,7 +62,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'router' } - assert self.hass.data[frontend.DATA_PANELS]['weather'] == { + assert self.hass.data[frontend.DATA_PANELS].get('weather') == { 'component_name': 'iframe', 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py new file mode 100644 index 00000000000..dd8cbfe55e0 --- /dev/null +++ b/tests/components/test_prometheus.py @@ -0,0 +1,33 @@ +"""The tests for the Prometheus exporter.""" +import asyncio +import pytest + +from homeassistant.setup import async_setup_component +import homeassistant.components.prometheus as prometheus + + +@pytest.fixture +def prometheus_client(loop, hass, test_client): + """Initialize a test_client with Prometheus component.""" + assert loop.run_until_complete(async_setup_component( + hass, + prometheus.DOMAIN, + {}, + )) + return loop.run_until_complete(test_client(hass.http.app)) + + +@asyncio.coroutine +def test_view(prometheus_client): # pylint: disable=redefined-outer-name + """Test prometheus metrics view.""" + resp = yield from prometheus_client.get(prometheus.API_ENDPOINT) + + assert resp.status == 200 + assert resp.headers['content-type'] == 'text/plain' + body = yield from resp.text() + body = body.split("\n") + + assert len(body) > 3 # At least two comment lines and a metric + for line in body: + if line: + assert line.startswith('# ') or line.startswith('process_') diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py index 9a83644dcfd..ce6b473b465 100644 --- a/tests/components/test_rflink.py +++ b/tests/components/test_rflink.py @@ -99,7 +99,7 @@ def test_send_no_wait(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'aliasses': ['test_alias_0_0'], + 'aliases': ['test_alias_0_0'], }, }, }, @@ -192,7 +192,7 @@ def test_error_when_not_connected(hass, monkeypatch): 'devices': { 'protocol_0_0': { 'name': 'test', - 'aliasses': ['test_alias_0_0'], + 'aliases': ['test_alias_0_0'], }, }, }, diff --git a/tests/components/test_wake_on_lan.py b/tests/components/test_wake_on_lan.py new file mode 100644 index 00000000000..abaf7dd6d14 --- /dev/null +++ b/tests/components/test_wake_on_lan.py @@ -0,0 +1,47 @@ +"""Tests for Wake On LAN component.""" +import asyncio +from unittest import mock + +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.wake_on_lan import ( + DOMAIN, SERVICE_SEND_MAGIC_PACKET) + + +@pytest.fixture +def mock_wakeonlan(): + """Mock mock_wakeonlan.""" + module = mock.MagicMock() + with mock.patch.dict('sys.modules', { + 'wakeonlan': module, + }): + yield module + + +@asyncio.coroutine +def test_send_magic_packet(hass, caplog, mock_wakeonlan): + """Test of send magic packet service call.""" + mac = "aa:bb:cc:dd:ee:ff" + bc_ip = "192.168.255.255" + + yield from async_setup_component(hass, DOMAIN, {}) + + yield from hass.services.async_call( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, + {"mac": mac, "broadcast_address": bc_ip}, blocking=True) + assert len(mock_wakeonlan.mock_calls) == 1 + assert mock_wakeonlan.mock_calls[-1][1][0] == mac + assert mock_wakeonlan.mock_calls[-1][2]['ip_address'] == bc_ip + + yield from hass.services.async_call( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, + {"broadcast_address": bc_ip}, blocking=True) + assert 'ERROR' in caplog.text + assert len(mock_wakeonlan.mock_calls) == 1 + + yield from hass.services.async_call( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True) + assert len(mock_wakeonlan.mock_calls) == 2 + assert mock_wakeonlan.mock_calls[-1][1][0] == mac + assert not mock_wakeonlan.mock_calls[-1][2] diff --git a/tests/fixtures/london_underground.json b/tests/fixtures/london_underground.json new file mode 100644 index 00000000000..fddae7e89e2 --- /dev/null +++ b/tests/fixtures/london_underground.json @@ -0,0 +1,465 @@ +[ + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "bakerloo", + "name": "Bakerloo", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.703Z", + "modified": "2017-06-28T11:43:10.703Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Bakerloo&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "central", + "name": "Central", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.623Z", + "modified": "2017-06-28T11:43:10.623Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Central&serviceTypes=Regular" + }, + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Night", + "uri": "/Line/Route?ids=Central&serviceTypes=Night" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "circle", + "name": "Circle", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.733Z", + "modified": "2017-06-28T11:43:10.733Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Circle&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "district", + "name": "District", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.623Z", + "modified": "2017-06-28T11:43:10.623Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=District&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "dlr", + "name": "DLR", + "modeName": "dlr", + "disruptions": [], + "created": "2017-06-28T11:43:10.703Z", + "modified": "2017-06-28T11:43:10.703Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=DLR&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "hammersmith-city", + "name": "Hammersmith & City", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.67Z", + "modified": "2017-06-28T11:43:10.67Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Hammersmith & City&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "jubilee", + "name": "Jubilee", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.623Z", + "modified": "2017-06-28T11:43:10.623Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Jubilee&serviceTypes=Regular" + }, + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Night", + "uri": "/Line/Route?ids=Jubilee&serviceTypes=Night" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "london-overground", + "name": "London Overground", + "modeName": "overground", + "disruptions": [], + "created": "2017-06-28T11:43:10.607Z", + "modified": "2017-06-28T11:43:10.607Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "lineId": "london-overground", + "statusSeverity": 9, + "statusSeverityDescription": "Minor Delays", + "reason": "something", + "created": "0001-01-01T00:00:00", + "validityPeriods": [ + { + "$type": "Tfl.Api.Presentation.Entities.ValidityPeriod, Tfl.Api.Presentation.Entities", + "fromDate": "2017-06-29T06:27:21Z", + "toDate": "2017-06-30T01:29:00Z", + "isNow": true + } + ], + "disruption": { + "$type": "Tfl.Api.Presentation.Entities.Disruption, Tfl.Api.Presentation.Entities", + "category": "RealTime", + "categoryDescription": "RealTime", + "description": "London Overground: Minor delays Richmond to Stratford and Willesden Junction to Clapham Junction while we fix a faulty train at Richmond, GOOD SERVICE all other routes. ", + "affectedRoutes": [], + "affectedStops": [], + "closureText": "minorDelays" + } + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=London Overground&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "metropolitan", + "name": "Metropolitan", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.703Z", + "modified": "2017-06-28T11:43:10.703Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Metropolitan&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "northern", + "name": "Northern", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.67Z", + "modified": "2017-06-28T11:43:10.67Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Northern&serviceTypes=Regular" + }, + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Night", + "uri": "/Line/Route?ids=Northern&serviceTypes=Night" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "piccadilly", + "name": "Piccadilly", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.67Z", + "modified": "2017-06-28T11:43:10.67Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Piccadilly&serviceTypes=Regular" + }, + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Night", + "uri": "/Line/Route?ids=Piccadilly&serviceTypes=Night" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "tfl-rail", + "name": "TfL Rail", + "modeName": "tflrail", + "disruptions": [], + "created": "2017-06-28T11:43:10.657Z", + "modified": "2017-06-28T11:43:10.657Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=TfL Rail&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "victoria", + "name": "Victoria", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.607Z", + "modified": "2017-06-28T11:43:10.607Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Victoria&serviceTypes=Regular" + }, + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Night", + "uri": "/Line/Route?ids=Victoria&serviceTypes=Night" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + }, + { + "$type": "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities", + "id": "waterloo-city", + "name": "Waterloo & City", + "modeName": "tube", + "disruptions": [], + "created": "2017-06-28T11:43:10.703Z", + "modified": "2017-06-28T11:43:10.703Z", + "lineStatuses": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities", + "id": 0, + "statusSeverity": 10, + "statusSeverityDescription": "Good Service", + "created": "0001-01-01T00:00:00", + "validityPeriods": [] + } + ], + "routeSections": [], + "serviceTypes": [ + { + "$type": "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities", + "name": "Regular", + "uri": "/Line/Route?ids=Waterloo & City&serviceTypes=Regular" + } + ], + "crowding": { + "$type": "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities" + } + } +] diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index e9d163ad471..cc42bc8d7f8 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -13,7 +13,8 @@ from homeassistant.helpers import state from homeassistant.const import ( STATE_OPEN, STATE_CLOSED, STATE_LOCKED, STATE_UNLOCKED, - STATE_ON, STATE_OFF) + STATE_ON, STATE_OFF, + STATE_HOME, STATE_NOT_HOME) from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE) from homeassistant.components.sun import (STATE_ABOVE_HORIZON, @@ -258,8 +259,9 @@ class TestStateHelpers(unittest.TestCase): def test_as_number_states(self): """Test state_as_number with states.""" zero_states = (STATE_OFF, STATE_CLOSED, STATE_UNLOCKED, - STATE_BELOW_HORIZON) - one_states = (STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_ABOVE_HORIZON) + STATE_BELOW_HORIZON, STATE_NOT_HOME) + one_states = (STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_ABOVE_HORIZON, + STATE_HOME) for _state in zero_states: self.assertEqual(0, state.state_as_number( ha.State('domain.test', _state, {})))