diff --git a/.coveragerc b/.coveragerc index 73a79c2d87b..3d369eed073 100644 --- a/.coveragerc +++ b/.coveragerc @@ -251,6 +251,9 @@ omit = homeassistant/components/scsgate.py homeassistant/components/*/scsgate.py + homeassistant/components/sisyphus.py + homeassistant/components/*/sisyphus.py + homeassistant/components/skybell.py homeassistant/components/*/skybell.py @@ -346,6 +349,9 @@ omit = homeassistant/components/tuya.py homeassistant/components/*/tuya.py + homeassistant/components/spider.py + homeassistant/components/*/spider.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py @@ -398,6 +404,8 @@ omit = homeassistant/components/climate/touchline.py homeassistant/components/climate/venstar.py homeassistant/components/climate/zhong_hong.py + homeassistant/components/cover/aladdin_connect.py + homeassistant/components/cover/brunt.py homeassistant/components/cover/garadget.py homeassistant/components/cover/gogogate2.py homeassistant/components/cover/homematic.py @@ -461,6 +469,7 @@ omit = homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora.py homeassistant/components/light/flux_led.py + homeassistant/components/light/futurenow.py homeassistant/components/light/greenwave.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py @@ -657,6 +666,7 @@ omit = homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/luftdaten.py homeassistant/components/sensor/lyft.py + homeassistant/components/sensor/magicseaweed.py homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py homeassistant/components/sensor/mitemp_bt.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ad922d7045..86e212bb11d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Home Assistant -Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them? +Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spend a couple of hours and help to integrate them? The process is straight-forward. diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 496308598dc..65b1cd2ae1a 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -8,7 +8,7 @@ import subprocess import sys import threading -from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import +from typing import List, Dict, Any # noqa pylint: disable=unused-import from homeassistant import monkey_patch @@ -20,7 +20,7 @@ from homeassistant.const import ( ) -def attempt_use_uvloop(): +def attempt_use_uvloop() -> None: """Attempt to use uvloop.""" import asyncio @@ -280,11 +280,11 @@ def setup_and_run_hass(config_dir: str, # Imported here to avoid importing asyncio before monkey patch from homeassistant.util.async_ import run_callback_threadsafe - def open_browser(event): - """Open the webinterface in a browser.""" - if hass.config.api is not None: + def open_browser(_: Any) -> None: + """Open the web interface in a browser.""" + if hass.config.api is not None: # type: ignore import webbrowser - webbrowser.open(hass.config.api.base_url) + webbrowser.open(hass.config.api.base_url) # type: ignore run_callback_threadsafe( hass.loop, diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index d24110a4736..e9693b09634 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -3,6 +3,7 @@ import base64 from collections import OrderedDict import hashlib import hmac +from typing import Dict # noqa: F401 pylint: disable=unused-import import voluptuous as vol @@ -68,12 +69,12 @@ class Data: """Return users.""" return self._data['users'] - def validate_login(self, username, password): + def validate_login(self, username: str, password: str) -> None: """Validate a username and password. Raises InvalidAuth if auth invalid. """ - password = self.hash_password(password) + hashed = self.hash_password(password) found = None @@ -84,33 +85,33 @@ class Data: if found is None: # Do one more compare to make timing the same as if user was found. - hmac.compare_digest(password, password) + hmac.compare_digest(hashed, hashed) raise InvalidAuth - if not hmac.compare_digest(password, + if not hmac.compare_digest(hashed, base64.b64decode(found['password'])): raise InvalidAuth - def hash_password(self, password, for_storage=False): + def hash_password(self, password: str, for_storage: bool = False) -> bytes: """Encode a password.""" hashed = hashlib.pbkdf2_hmac( 'sha512', password.encode(), self._data['salt'].encode(), 100000) if for_storage: - hashed = base64.b64encode(hashed).decode() + hashed = base64.b64encode(hashed) return hashed - def add_auth(self, username, password): + def add_auth(self, username: str, password: str) -> None: """Add a new authenticated user/pass.""" if any(user['username'] == username for user in self.users): raise InvalidUser self.users.append({ 'username': username, - 'password': self.hash_password(password, True), + 'password': self.hash_password(password, True).decode(), }) @callback - def async_remove_auth(self, username): + def async_remove_auth(self, username: str) -> None: """Remove authentication.""" index = None for i, user in enumerate(self.users): @@ -123,14 +124,15 @@ class Data: self.users.pop(index) - def change_password(self, username, new_password): + def change_password(self, username: str, new_password: str) -> None: """Update the password. Raises InvalidUser if user cannot be found. """ for user in self.users: if user['username'] == username: - user['password'] = self.hash_password(new_password, True) + user['password'] = self.hash_password( + new_password, True).decode() break else: raise InvalidUser @@ -160,7 +162,7 @@ class HassAuthProvider(AuthProvider): """Return a flow to login.""" return LoginFlow(self) - async def async_validate_login(self, username, password): + async def async_validate_login(self, username: str, password: str): """Helper to validate a username and password.""" if self.data is None: await self.async_initialize() @@ -225,7 +227,7 @@ class LoginFlow(data_entry_flow.FlowHandler): data=user_input ) - schema = OrderedDict() + schema = OrderedDict() # type: Dict[str, type] schema['username'] = str schema['password'] = str diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a190aea9fa8..43c7168dd2e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -221,8 +221,8 @@ async def async_from_config_file(config_path: str, @core.callback def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False, - log_rotate_days=None, - log_file=None, + log_rotate_days: Optional[int] = None, + log_file: Optional[str] = None, log_no_color: bool = False) -> None: """Set up the logging. @@ -291,9 +291,9 @@ def async_enable_logging(hass: core.HomeAssistant, async_handler = AsyncHandler(hass.loop, err_handler) - async def async_stop_async_handler(event): + async def async_stop_async_handler(_: Any) -> None: """Cleanup async handler.""" - logging.getLogger('').removeHandler(async_handler) + logging.getLogger('').removeHandler(async_handler) # type: ignore await async_handler.async_close(blocking=True) hass.bus.async_listen_once( diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index f0c4f7bb3e2..06e28c42b13 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -167,7 +167,7 @@ def async_setup(hass, config): def async_handle_core_service(call): """Service handler for handling core services.""" if call.service == SERVICE_HOMEASSISTANT_STOP: - hass.async_add_job(hass.async_stop()) + hass.async_create_task(hass.async_stop()) return try: @@ -183,7 +183,7 @@ def async_setup(hass, config): return if call.service == SERVICE_HOMEASSISTANT_RESTART: - hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) + hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) hass.services.async_register( ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 6d5feb87dc2..bafbc0781ca 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -85,7 +85,7 @@ ABODE_PLATFORMS = [ ] -class AbodeSystem(object): +class AbodeSystem: """Abode System class.""" def __init__(self, username, password, cache, diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index d603843f51f..100444c0211 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -110,7 +110,7 @@ NotificationItem = namedtuple( ) -class AdsHub(object): +class AdsHub: """Representation of an ADS connection.""" def __init__(self, ads_client): diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 84a72945a7e..0a4dd6bde78 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -187,7 +187,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(self.alarm_disarm, code) + return self.hass.async_add_executor_job(self.alarm_disarm, code) def alarm_arm_home(self, code=None): """Send arm home command.""" @@ -198,7 +198,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(self.alarm_arm_home, code) + return self.hass.async_add_executor_job(self.alarm_arm_home, code) def alarm_arm_away(self, code=None): """Send arm away command.""" @@ -209,7 +209,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(self.alarm_arm_away, code) + return self.hass.async_add_executor_job(self.alarm_arm_away, code) def alarm_arm_night(self, code=None): """Send arm night command.""" @@ -220,7 +220,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(self.alarm_arm_night, code) + return self.hass.async_add_executor_job(self.alarm_arm_night, code) def alarm_trigger(self, code=None): """Send alarm trigger command.""" @@ -231,7 +231,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(self.alarm_trigger, code) + return self.hass.async_add_executor_job(self.alarm_trigger, code) def alarm_arm_custom_bypass(self, code=None): """Send arm custom bypass command.""" @@ -242,7 +242,8 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(self.alarm_arm_custom_bypass, code) + return self.hass.async_add_executor_job( + self.alarm_arm_custom_bypass, code) @property def state_attributes(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 87e85f09da0..736334c956a 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -83,7 +83,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): """Return one or more digits/characters.""" if self._code is None: return None - elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' return 'Any' @@ -92,9 +92,9 @@ class AlarmDotCom(alarm.AlarmControlPanel): """Return the state of the device.""" if self._alarm.state.lower() == 'disarmed': return STATE_ALARM_DISARMED - elif self._alarm.state.lower() == 'armed stay': + if self._alarm.state.lower() == 'armed stay': return STATE_ALARM_ARMED_HOME - elif self._alarm.state.lower() == 'armed away': + if self._alarm.state.lower() == 'armed away': return STATE_ALARM_ARMED_AWAY return STATE_UNKNOWN diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py index 20887157cb4..0f8913f85a0 100644 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ b/homeassistant/components/alarm_control_panel/arlo.py @@ -122,10 +122,10 @@ class ArloBaseStation(AlarmControlPanel): """Convert Arlo mode to Home Assistant state.""" if mode == ARMED: return STATE_ALARM_ARMED_AWAY - elif mode == DISARMED: + if mode == DISARMED: return STATE_ALARM_DISARMED - elif mode == self._home_mode_name: + if mode == self._home_mode_name: return STATE_ALARM_ARMED_HOME - elif mode == self._away_mode_name: + if mode == self._away_mode_name: return STATE_ALARM_ARMED_AWAY return mode diff --git a/homeassistant/components/alarm_control_panel/canary.py b/homeassistant/components/alarm_control_panel/canary.py index 2e0e9994e10..3cd44dcc84c 100644 --- a/homeassistant/components/alarm_control_panel/canary.py +++ b/homeassistant/components/alarm_control_panel/canary.py @@ -55,9 +55,9 @@ class CanaryAlarm(AlarmControlPanel): mode = location.mode if mode.name == LOCATION_MODE_AWAY: return STATE_ALARM_ARMED_AWAY - elif mode.name == LOCATION_MODE_HOME: + if mode.name == LOCATION_MODE_HOME: return STATE_ALARM_ARMED_HOME - elif mode.name == LOCATION_MODE_NIGHT: + if mode.name == LOCATION_MODE_NIGHT: return STATE_ALARM_ARMED_NIGHT return None diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index c080a136c08..d2366e5836c 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ import datetime -import homeassistant.components.alarm_control_panel.manual as manual +from homeassistant.components.alarm_control_panel import manual from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, diff --git a/homeassistant/components/alarm_control_panel/homematicip_cloud.py b/homeassistant/components/alarm_control_panel/homematicip_cloud.py index 893fa76c44b..79f872951db 100644 --- a/homeassistant/components/alarm_control_panel/homematicip_cloud.py +++ b/homeassistant/components/alarm_control_panel/homematicip_cloud.py @@ -20,7 +20,6 @@ DEPENDENCIES = ['homematicip_cloud'] _LOGGER = logging.getLogger(__name__) -HMIP_OPEN = 'OPEN' HMIP_ZONE_AWAY = 'EXTERNAL' HMIP_ZONE_HOME = 'INTERNAL' @@ -57,14 +56,18 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): @property def state(self): """Return the state of the device.""" + from homematicip.base.enums import WindowState + if self._device.active: if (self._device.sabotage or self._device.motionDetected or - self._device.windowState == HMIP_OPEN): + self._device.windowState == WindowState.OPEN): return STATE_ALARM_TRIGGERED - if self._device.label == HMIP_ZONE_HOME: + active = self._home.get_security_zones_activation() + if active == (True, True): + return STATE_ALARM_ARMED_AWAY + if active == (False, True): return STATE_ALARM_ARMED_HOME - return STATE_ALARM_ARMED_AWAY return STATE_ALARM_DISARMED @@ -79,10 +82,3 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): async def async_alarm_arm_away(self, code=None): """Send arm away command.""" await self._home.set_security_zones_activation(True, True) - - @property - def device_state_attributes(self): - """Return the state attributes of the alarm control device.""" - # The base class is loading the battery property, but device doesn't - # have this property - base class needs clean-up. - return None diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 209c5367c92..9941f70a2e4 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -128,7 +128,7 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Return one or more digits/characters.""" if self._code is None: return None - elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' return 'Any' diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 2f2f89b9dfc..b2b7c45d410 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -205,7 +205,7 @@ class ManualAlarm(alarm.AlarmControlPanel): """Return one or more digits/characters.""" if self._code is None: return None - elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' return 'Any' diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 895f5edd5da..942d0dc159a 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.helpers.event import async_track_state_change from homeassistant.core import callback @@ -241,7 +241,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): """Return one or more digits/characters.""" if self._code is None: return None - elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' return 'Any' diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 9f2a4176ed8..54b85ffbe23 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.components.alarm_control_panel as alarm -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, @@ -49,6 +49,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT Alarm Control Panel platform.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + async_add_devices([MqttAlarm( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -123,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): """Return one or more digits/characters.""" if self._code is None: return None - elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' return 'Any' diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index b4906acba3c..b400a927b5e 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -9,23 +9,22 @@ import re import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + PLATFORM_SCHEMA, AlarmControlPanel) from homeassistant.const import ( CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['simplisafe-python==1.0.5'] +REQUIREMENTS = ['simplisafe-python==2.0.2'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'SimpliSafe' -DOMAIN = 'simplisafe' -NOTIFICATION_ID = 'simplisafe_notification' -NOTIFICATION_TITLE = 'SimpliSafe Setup' +ATTR_ALARM_ACTIVE = "alarm_active" +ATTR_TEMPERATURE = "temperature" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, @@ -37,36 +36,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the SimpliSafe platform.""" - from simplipy.api import SimpliSafeApiInterface, get_systems + from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException name = config.get(CONF_NAME) code = config.get(CONF_CODE) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - simplisafe = SimpliSafeApiInterface() - status = simplisafe.set_credentials(username, password) - if status: - hass.data[DOMAIN] = simplisafe - locations = get_systems(simplisafe) - for location in locations: - add_devices([SimpliSafeAlarm(location, name, code)]) - else: - message = 'Failed to log into SimpliSafe. Check credentials.' - _LOGGER.error(message) - hass.components.persistent_notification.create( - message, - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False + try: + simplisafe = SimpliSafeApiInterface(username, password) + except SimpliSafeAPIException: + _LOGGER.error("Failed to setup SimpliSafe") + return - def logout(event): - """Logout of the SimpliSafe API.""" - hass.data[DOMAIN].logout() + systems = [] - hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout) + for system in simplisafe.get_systems(): + systems.append(SimpliSafeAlarm(system, name, code)) + + add_devices(systems) -class SimpliSafeAlarm(alarm.AlarmControlPanel): +class SimpliSafeAlarm(AlarmControlPanel): """Representation of a SimpliSafe alarm.""" def __init__(self, simplisafe, name, code): @@ -75,31 +65,37 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): self._name = name self._code = str(code) if code else None + @property + def unique_id(self): + """Return the unique ID.""" + return self.simplisafe.location_id + @property def name(self): """Return the name of the device.""" if self._name is not None: return self._name - return 'Alarm {}'.format(self.simplisafe.location_id()) + return 'Alarm {}'.format(self.simplisafe.location_id) @property def code_format(self): """Return one or more digits/characters.""" if self._code is None: return None - elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' return 'Any' @property def state(self): """Return the state of the device.""" - status = self.simplisafe.state() - if status == 'off': + status = self.simplisafe.state + if status.lower() == 'off': state = STATE_ALARM_DISARMED - elif status == 'home': + elif status.lower() == 'home' or status.lower() == 'home_count': state = STATE_ALARM_ARMED_HOME - elif status == 'away': + elif (status.lower() == 'away' or status.lower() == 'exitDelay' or + status.lower() == 'away_count'): state = STATE_ALARM_ARMED_AWAY else: state = STATE_UNKNOWN @@ -108,14 +104,13 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): @property def device_state_attributes(self): """Return the state attributes.""" - return { - 'alarm': self.simplisafe.alarm(), - 'co': self.simplisafe.carbon_monoxide(), - 'fire': self.simplisafe.fire(), - 'flood': self.simplisafe.flood(), - 'last_event': self.simplisafe.last_event(), - 'temperature': self.simplisafe.temperature(), - } + attributes = {} + + attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active + if self.simplisafe.temperature is not None: + attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature + + return attributes def update(self): """Update alarm status.""" diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py index bc7f1910803..1377b2a6c3a 100644 --- a/homeassistant/components/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder.py @@ -34,6 +34,8 @@ CONF_ZONE_NAME = 'name' CONF_ZONE_TYPE = 'type' CONF_ZONE_RFID = 'rfid' CONF_ZONES = 'zones' +CONF_RELAY_ADDR = 'relayaddr' +CONF_RELAY_CHAN = 'relaychan' DEFAULT_DEVICE_TYPE = 'socket' DEFAULT_DEVICE_HOST = 'localhost' @@ -53,6 +55,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message' +SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message' DEVICE_SOCKET_SCHEMA = vol.Schema({ vol.Required(CONF_DEVICE_TYPE): 'socket', @@ -71,7 +74,11 @@ ZONE_SCHEMA = vol.Schema({ vol.Required(CONF_ZONE_NAME): cv.string, vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), - vol.Optional(CONF_ZONE_RFID): cv.string}) + vol.Optional(CONF_ZONE_RFID): cv.string, + vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation', + 'Relay address and channel must exist together'): cv.byte, + vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation', + 'Relay address and channel must exist together'): cv.byte}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -153,6 +160,11 @@ def setup(hass, config): hass.helpers.dispatcher.dispatcher_send( SIGNAL_ZONE_RESTORE, zone) + def handle_rel_message(sender, message): + """Handle relay message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_REL_MESSAGE, message) + controller = False if device_type == 'socket': host = device.get(CONF_DEVICE_HOST) @@ -171,6 +183,7 @@ def setup(hass, config): controller.on_zone_fault += zone_fault_callback controller.on_zone_restore += zone_restore_callback controller.on_close += handle_closed_connection + controller.on_relay_changed += handle_rel_message hass.data[DATA_AD] = controller diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 9d47e4bd322..80a02b3275d 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -68,7 +68,7 @@ def turn_on(hass, entity_id): def async_turn_on(hass, entity_id): """Async reset the alert.""" data = {ATTR_ENTITY_ID: entity_id} - hass.async_add_job( + hass.async_create_task( hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) @@ -81,7 +81,7 @@ def turn_off(hass, entity_id): def async_turn_off(hass, entity_id): """Async acknowledge the alert.""" data = {ATTR_ENTITY_ID: entity_id} - hass.async_add_job( + hass.async_create_task( hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) @@ -94,7 +94,7 @@ def toggle(hass, entity_id): def async_toggle(hass, entity_id): """Async toggle acknowledgement of alert.""" data = {ATTR_ENTITY_ID: entity_id} - hass.async_add_job( + hass.async_create_task( hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) @@ -217,7 +217,7 @@ class Alert(ToggleEntity): else: yield from self._schedule_notify() - self.hass.async_add_job(self.async_update_ha_state) + self.async_schedule_update_ha_state() @asyncio.coroutine def end_alerting(self): @@ -228,7 +228,7 @@ class Alert(ToggleEntity): self._firing = False if self._done_message and self._send_done_message: yield from self._notify_done_message() - self.hass.async_add_job(self.async_update_ha_state) + self.async_schedule_update_ha_state() @asyncio.coroutine def _schedule_notify(self): diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index b6d406bd550..8d4520d74e8 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -210,7 +210,7 @@ def resolve_slot_synonyms(key, request): return resolved_value -class AlexaResponse(object): +class AlexaResponse: """Help generating the response for Alexa.""" def __init__(self, hass, intent_info): diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 9b7da71a293..042d878fceb 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -55,7 +55,7 @@ HANDLERS = Registry() ENTITY_ADAPTERS = Registry() -class _DisplayCategory(object): +class _DisplayCategory: """Possible display categories for Discovery response. https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories @@ -153,7 +153,7 @@ class _UnsupportedProperty(Exception): """This entity does not support the requested Smart Home API property.""" -class _AlexaEntity(object): +class _AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. The API handlers should manipulate entities only through this interface. @@ -208,7 +208,7 @@ class _AlexaEntity(object): raise NotImplementedError -class _AlexaInterface(object): +class _AlexaInterface: def __init__(self, entity): self.entity = entity @@ -315,7 +315,7 @@ class _AlexaLockController(_AlexaInterface): if self.entity.state == STATE_LOCKED: return 'LOCKED' - elif self.entity.state == STATE_UNLOCKED: + if self.entity.state == STATE_UNLOCKED: return 'UNLOCKED' return 'JAMMED' @@ -615,7 +615,7 @@ class _SensorCapabilities(_AlexaEntity): yield _AlexaTemperatureSensor(self.entity) -class _Cause(object): +class _Cause: """Possible causes for property changes. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index 820ca41ad2e..bcd0c38c3bd 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -164,7 +164,7 @@ def setup(hass, config): return True -class AmcrestDevice(object): +class AmcrestDevice: """Representation of a base Amcrest discovery device.""" def __init__(self, camera, name, authentication, ffmpeg_arguments, diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 13fa64438d3..5da117e74c3 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -214,11 +214,11 @@ def async_setup(hass, config): CONF_PASSWORD: password }) - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'camera', 'mjpeg', mjpeg_camera, config)) if sensors: - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'sensor', DOMAIN, { CONF_NAME: name, CONF_HOST: host, @@ -226,7 +226,7 @@ def async_setup(hass, config): }, config)) if switches: - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'switch', DOMAIN, { CONF_NAME: name, CONF_HOST: host, @@ -234,7 +234,7 @@ def async_setup(hass, config): }, config)) if motion: - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'binary_sensor', DOMAIN, { CONF_HOST: host, CONF_NAME: name, diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index 7e2b4cda28f..8808cee79a3 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -58,7 +58,7 @@ def setup(hass, config): return True -class APCUPSdData(object): +class APCUPSdData: """Stores the data retrieved from APCUPSd. For each entity to use, acts as the single point responsible for fetching diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b80a5716061..de28eeff5ca 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -220,7 +220,8 @@ class APIEntityStateView(HomeAssistantView): is_new_state = hass.states.get(entity_id) is None # Write state - hass.states.async_set(entity_id, new_state, attributes, force_update) + hass.states.async_set(entity_id, new_state, attributes, force_update, + self.context(request)) # Read the state back for our response status_code = HTTP_CREATED if is_new_state else 200 @@ -279,7 +280,8 @@ class APIEventView(HomeAssistantView): event_data[key] = state request.app['hass'].bus.async_fire( - event_type, event_data, ha.EventOrigin.remote) + event_type, event_data, ha.EventOrigin.remote, + self.context(request)) return self.json_message("Event {} fired.".format(event_type)) @@ -316,7 +318,8 @@ class APIDomainServicesView(HomeAssistantView): "Data should be valid JSON.", HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: - await hass.services.async_call(domain, service, data, True) + await hass.services.async_call( + domain, service, data, True, self.context(request)) return self.json(changed_states) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 68445092db7..97fb2363024 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -45,7 +45,7 @@ NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication' NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' -T = TypeVar('T') +T = TypeVar('T') # pylint: disable=invalid-name # This version of ensure_list interprets an empty dict as no value @@ -218,10 +218,10 @@ def _setup_atv(hass, atv_config): ATTR_POWER: power } - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'media_player', DOMAIN, atv_config)) - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'remote', DOMAIN, atv_config)) diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index 8625685c057..785f8c57f94 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -62,7 +62,7 @@ def setup(hass, config): return True -class ArduinoBoard(object): +class ArduinoBoard: """Representation of an Arduino board.""" def __init__(self, port): diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 475e43e55a4..c6a414b9d91 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.1.9'] +REQUIREMENTS = ['pyarlo==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py index 0b5e7c1e1d7..e273d7d6f6a 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -48,7 +48,7 @@ def setup(hass, config): return True -class AsteriskData(object): +class AsteriskData: """Store Asterisk mailbox data.""" def __init__(self, hass, host, port, password): diff --git a/homeassistant/components/august.py b/homeassistant/components/august.py index 2a7da86c6cf..eb25ee8fb08 100644 --- a/homeassistant/components/august.py +++ b/homeassistant/components/august.py @@ -123,9 +123,9 @@ def setup_august(hass, config, api, authenticator): discovery.load_platform(hass, component, DOMAIN, {}, config) return True - elif state == AuthenticationState.BAD_PASSWORD: + if state == AuthenticationState.BAD_PASSWORD: return False - elif state == AuthenticationState.REQUIRES_VALIDATION: + if state == AuthenticationState.REQUIRES_VALIDATION: request_configuration(hass, config, api, authenticator) return True diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 435555c2e31..0b2b4fb1a2e 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -1,62 +1,5 @@ """Component to allow users to login and get tokens. -All requests will require passing in a valid client ID and secret via HTTP -Basic Auth. - -# GET /auth/providers - -Return a list of auth providers. Example: - -[ - { - "name": "Local", - "id": null, - "type": "local_provider", - } -] - -# POST /auth/login_flow - -Create a login flow. Will return the first step of the flow. - -Pass in parameter 'handler' to specify the auth provider to use. Auth providers -are identified by type and id. - -{ - "handler": ["local_provider", null] -} - -Return value will be a step in a data entry flow. See the docs for data entry -flow for details. - -{ - "data_schema": [ - {"name": "username", "type": "string"}, - {"name": "password", "type": "string"} - ], - "errors": {}, - "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", - "handler": ["insecure_example", null], - "step_id": "init", - "type": "form" -} - -# POST /auth/login_flow/{flow_id} - -Progress the flow. Most flows will be 1 page, but could optionally add extra -login challenges, like TFA. Once the flow has finished, the returned step will -have type "create_entry" and "result" key will contain an authorization code. - -{ - "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", - "handler": ["insecure_example", null], - "result": "411ee2f916e648d691e937ae9344681e", - "source": "user", - "title": "Example", - "type": "create_entry", - "version": 1 -} - # POST /auth/token This is an OAuth2 endpoint for granting tokens. We currently support the grant @@ -102,24 +45,20 @@ a limited expiration. "token_type": "Bearer" } """ -from datetime import timedelta import logging import uuid +from datetime import timedelta -import aiohttp.web import voluptuous as vol -from homeassistant import data_entry_flow -from homeassistant.core import callback -from homeassistant.helpers.data_entry_flow import ( - FlowManagerIndexView, FlowManagerResourceView) from homeassistant.components import websocket_api -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import callback from homeassistant.util import dt as dt_util - from . import indieauth - +from . import login_flow DOMAIN = 'auth' DEPENDENCIES = ['http'] @@ -136,10 +75,6 @@ async def async_setup(hass, config): """Component to allow users to login.""" store_credentials, retrieve_credentials = _create_cred_store() - hass.http.register_view(AuthProvidersView) - hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) - hass.http.register_view( - LoginFlowResourceView(hass.auth.login_flow, store_credentials)) hass.http.register_view(GrantTokenView(retrieve_credentials)) hass.http.register_view(LinkUserView(retrieve_credentials)) @@ -148,93 +83,11 @@ async def async_setup(hass, config): SCHEMA_WS_CURRENT_USER ) + await login_flow.async_setup(hass, store_credentials) + return True -class AuthProvidersView(HomeAssistantView): - """View to get available auth providers.""" - - url = '/auth/providers' - name = 'api:auth:providers' - requires_auth = False - - async def get(self, request): - """Get available auth providers.""" - return self.json([{ - 'name': provider.name, - 'id': provider.id, - 'type': provider.type, - } for provider in request.app['hass'].auth.auth_providers]) - - -class LoginFlowIndexView(FlowManagerIndexView): - """View to create a config flow.""" - - url = '/auth/login_flow' - name = 'api:auth:login_flow' - requires_auth = False - - async def get(self, request): - """Do not allow index of flows in progress.""" - return aiohttp.web.Response(status=405) - - @RequestDataValidator(vol.Schema({ - vol.Required('client_id'): str, - vol.Required('handler'): vol.Any(str, list), - vol.Required('redirect_uri'): str, - })) - async def post(self, request, data): - """Create a new login flow.""" - if not indieauth.verify_redirect_uri(data['client_id'], - data['redirect_uri']): - return self.json_message('invalid client id or redirect uri', 400) - - # pylint: disable=no-value-for-parameter - return await super().post(request) - - -class LoginFlowResourceView(FlowManagerResourceView): - """View to interact with the flow manager.""" - - url = '/auth/login_flow/{flow_id}' - name = 'api:auth:login_flow:resource' - requires_auth = False - - def __init__(self, flow_mgr, store_credentials): - """Initialize the login flow resource view.""" - super().__init__(flow_mgr) - self._store_credentials = store_credentials - - async def get(self, request, flow_id): - """Do not allow getting status of a flow in progress.""" - return self.json_message('Invalid flow specified', 404) - - @RequestDataValidator(vol.Schema({ - 'client_id': str - }, extra=vol.ALLOW_EXTRA)) - async def post(self, request, flow_id, data): - """Handle progressing a login flow request.""" - client_id = data.pop('client_id') - - if not indieauth.verify_client_id(client_id): - return self.json_message('Invalid client id', 400) - - try: - result = await self._flow_mgr.async_configure(flow_id, data) - except data_entry_flow.UnknownFlow: - return self.json_message('Invalid flow specified', 404) - except vol.Invalid: - return self.json_message('User input malformed', 400) - - if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return self.json(self._prepare_result_json(result)) - - result.pop('data') - result['result'] = self._store_credentials(client_id, result['result']) - - return self.json(result) - - class GrantTokenView(HomeAssistantView): """View to grant tokens.""" @@ -247,11 +100,26 @@ class GrantTokenView(HomeAssistantView): """Initialize the grant token view.""" self._retrieve_credentials = retrieve_credentials + @log_invalid_auth async def post(self, request): """Grant a token.""" hass = request.app['hass'] data = await request.post() + grant_type = data.get('grant_type') + + if grant_type == 'authorization_code': + return await self._async_handle_auth_code(hass, data) + + if grant_type == 'refresh_token': + return await self._async_handle_refresh_token(hass, data) + + return self.json({ + 'error': 'unsupported_grant_type', + }, status_code=400) + + async def _async_handle_auth_code(self, hass, data): + """Handle authorization code request.""" client_id = data.get('client_id') if client_id is None or not indieauth.verify_client_id(client_id): return self.json({ @@ -259,21 +127,6 @@ class GrantTokenView(HomeAssistantView): 'error_description': 'Invalid client id', }, status_code=400) - grant_type = data.get('grant_type') - - if grant_type == 'authorization_code': - return await self._async_handle_auth_code(hass, client_id, data) - - elif grant_type == 'refresh_token': - return await self._async_handle_refresh_token( - hass, client_id, data) - - return self.json({ - 'error': 'unsupported_grant_type', - }, status_code=400) - - async def _async_handle_auth_code(self, hass, client_id, data): - """Handle authorization code request.""" code = data.get('code') if code is None: @@ -309,8 +162,15 @@ class GrantTokenView(HomeAssistantView): int(refresh_token.access_token_expiration.total_seconds()), }) - async def _async_handle_refresh_token(self, hass, client_id, data): + async def _async_handle_refresh_token(self, hass, data): """Handle authorization code request.""" + client_id = data.get('client_id') + if client_id is not None and not indieauth.verify_client_id(client_id): + return self.json({ + 'error': 'invalid_request', + 'error_description': 'Invalid client id', + }, status_code=400) + token = data.get('refresh_token') if token is None: @@ -320,11 +180,16 @@ class GrantTokenView(HomeAssistantView): refresh_token = await hass.auth.async_get_refresh_token(token) - if refresh_token is None or refresh_token.client_id != client_id: + if refresh_token is None: return self.json({ 'error': 'invalid_grant', }, status_code=400) + if refresh_token.client_id != client_id: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ @@ -412,4 +277,7 @@ def websocket_current_user(hass, connection, msg): 'id': user.id, 'name': user.name, 'is_owner': user.is_owner, + 'credentials': [{'auth_provider_type': c.auth_provider_type, + 'auth_provider_id': c.auth_provider_id} + for c in user.credentials] })) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py new file mode 100644 index 00000000000..6d1b6cf4ecf --- /dev/null +++ b/homeassistant/components/auth/login_flow.py @@ -0,0 +1,172 @@ +"""HTTP views handle login flow. + +# GET /auth/providers + +Return a list of auth providers. Example: + +[ + { + "name": "Local", + "id": null, + "type": "local_provider", + } +] + + +# POST /auth/login_flow + +Create a login flow. Will return the first step of the flow. + +Pass in parameter 'client_id' and 'redirect_url' validate by indieauth. + +Pass in parameter 'handler' to specify the auth provider to use. Auth providers +are identified by type and id. + +{ + "client_id": "https://hassbian.local:8123/", + "handler": ["local_provider", null], + "redirect_url": "https://hassbian.local:8123/" +} + +Return value will be a step in a data entry flow. See the docs for data entry +flow for details. + +{ + "data_schema": [ + {"name": "username", "type": "string"}, + {"name": "password", "type": "string"} + ], + "errors": {}, + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "step_id": "init", + "type": "form" +} + + +# POST /auth/login_flow/{flow_id} + +Progress the flow. Most flows will be 1 page, but could optionally add extra +login challenges, like TFA. Once the flow has finished, the returned step will +have type "create_entry" and "result" key will contain an authorization code. + +{ + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "result": "411ee2f916e648d691e937ae9344681e", + "source": "user", + "title": "Example", + "type": "create_entry", + "version": 1 +} +""" +import aiohttp.web +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.http.ban import process_wrong_login, \ + log_invalid_auth +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, FlowManagerResourceView) +from . import indieauth + + +async def async_setup(hass, store_credentials): + """Component to allow users to login.""" + hass.http.register_view(AuthProvidersView) + hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) + hass.http.register_view( + LoginFlowResourceView(hass.auth.login_flow, store_credentials)) + + +class AuthProvidersView(HomeAssistantView): + """View to get available auth providers.""" + + url = '/auth/providers' + name = 'api:auth:providers' + requires_auth = False + + async def get(self, request): + """Get available auth providers.""" + return self.json([{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in request.app['hass'].auth.auth_providers]) + + +class LoginFlowIndexView(FlowManagerIndexView): + """View to create a config flow.""" + + url = '/auth/login_flow' + name = 'api:auth:login_flow' + requires_auth = False + + async def get(self, request): + """Do not allow index of flows in progress.""" + return aiohttp.web.Response(status=405) + + @RequestDataValidator(vol.Schema({ + vol.Required('client_id'): str, + vol.Required('handler'): vol.Any(str, list), + vol.Required('redirect_uri'): str, + })) + @log_invalid_auth + async def post(self, request, data): + """Create a new login flow.""" + if not indieauth.verify_redirect_uri(data['client_id'], + data['redirect_uri']): + return self.json_message('invalid client id or redirect uri', 400) + + # pylint: disable=no-value-for-parameter + return await super().post(request) + + +class LoginFlowResourceView(FlowManagerResourceView): + """View to interact with the flow manager.""" + + url = '/auth/login_flow/{flow_id}' + name = 'api:auth:login_flow:resource' + requires_auth = False + + def __init__(self, flow_mgr, store_credentials): + """Initialize the login flow resource view.""" + super().__init__(flow_mgr) + self._store_credentials = store_credentials + + async def get(self, request, flow_id): + """Do not allow getting status of a flow in progress.""" + return self.json_message('Invalid flow specified', 404) + + @RequestDataValidator(vol.Schema({ + 'client_id': str + }, extra=vol.ALLOW_EXTRA)) + @log_invalid_auth + async def post(self, request, flow_id, data): + """Handle progressing a login flow request.""" + client_id = data.pop('client_id') + + if not indieauth.verify_client_id(client_id): + return self.json_message('Invalid client id', 400) + + try: + result = await self._flow_mgr.async_configure(flow_id, data) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + except vol.Invalid: + return self.json_message('User input malformed', 400) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + # @log_invalid_auth does not work here since it returns HTTP 200 + # need manually log failed login attempts + if result['errors'] is not None and \ + result['errors'].get('base') == 'invalid_auth': + await process_wrong_login(request) + return self.json(self._prepare_result_json(result)) + + result.pop('data') + result['result'] = self._store_credentials(client_id, result['result']) + + return self.json(result) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 2a7a3887b34..8b1cd3cad84 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -297,7 +297,7 @@ class AutomationEntity(ToggleEntity): return # HomeAssistant is starting up - elif self.hass.state == CoreState.not_running: + if self.hass.state == CoreState.not_running: @asyncio.coroutine def async_enable_automation(event): """Start automation on startup.""" diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 6b8ee577a09..74cf195bc61 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -44,7 +44,7 @@ def async_trigger(hass, config, action): # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. - elif hass.state == CoreState.starting: + if hass.state == CoreState.starting: hass.async_run_job(action, { 'trigger': { 'platform': 'homeassistant', diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 172a368225d..60c33ca9b0e 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -10,7 +10,7 @@ import json import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio.py index f932f239969..e3f327f1d5c 100644 --- a/homeassistant/components/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio.py @@ -19,7 +19,7 @@ DOMAIN = 'bbb_gpio' def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error - import Adafruit_BBIO.GPIO as GPIO + from Adafruit_BBIO import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -36,14 +36,14 @@ def setup(hass, config): def setup_output(pin): """Set up a GPIO as output.""" # pylint: disable=import-error - import Adafruit_BBIO.GPIO as GPIO + from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" # pylint: disable=import-error - import Adafruit_BBIO.GPIO as GPIO + from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP) @@ -52,20 +52,20 @@ def setup_input(pin, pull_mode): def write_output(pin, value): """Write a value to a GPIO.""" # pylint: disable=import-error - import Adafruit_BBIO.GPIO as GPIO + from Adafruit_BBIO import GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" # pylint: disable=import-error - import Adafruit_BBIO.GPIO as GPIO + from Adafruit_BBIO import GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" # pylint: disable=import-error - import Adafruit_BBIO.GPIO as GPIO + from Adafruit_BBIO import GPIO GPIO.add_event_detect( pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index f0c8ec2d97c..fcc77d474e1 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -11,7 +11,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.alarmdecoder import ( ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, - SIGNAL_RFX_MESSAGE) + SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR, + CONF_RELAY_CHAN) DEPENDENCIES = ['alarmdecoder'] @@ -37,8 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] zone_rfid = device_config_data.get(CONF_ZONE_RFID) + relay_addr = device_config_data.get(CONF_RELAY_ADDR) + relay_chan = device_config_data.get(CONF_RELAY_CHAN) device = AlarmDecoderBinarySensor( - zone_num, zone_name, zone_type, zone_rfid) + zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan) devices.append(device) add_devices(devices) @@ -49,7 +52,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class AlarmDecoderBinarySensor(BinarySensorDevice): """Representation of an AlarmDecoder binary sensor.""" - def __init__(self, zone_number, zone_name, zone_type, zone_rfid): + def __init__(self, zone_number, zone_name, zone_type, zone_rfid, + relay_addr, relay_chan): """Initialize the binary_sensor.""" self._zone_number = zone_number self._zone_type = zone_type @@ -57,6 +61,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self._name = zone_name self._rfid = zone_rfid self._rfstate = None + self._relay_addr = relay_addr + self._relay_chan = relay_chan @asyncio.coroutine def async_added_to_hass(self): @@ -70,6 +76,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_RFX_MESSAGE, self._rfx_message_callback) + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_REL_MESSAGE, self._rel_message_callback) + @property def name(self): """Return the name of the entity.""" @@ -122,3 +131,12 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): if self._rfid and message and message.serial_number == self._rfid: self._rfstate = message.value self.schedule_update_ha_state() + + def _rel_message_callback(self, message): + """Update relay state.""" + if (self._relay_addr == message.address and + self._relay_chan == message.channel): + _LOGGER.debug("Relay %d:%d value:%d", message.address, + message.channel, message.value) + self._state = message.value + self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 73751ef14bb..0366f753ba6 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -89,7 +89,7 @@ class ArestBinarySensor(BinarySensorDevice): self.arest.update() -class ArestData(object): +class ArestData: """Class for handling the data retrieval for pins.""" def __init__(self, resource, pin): diff --git a/homeassistant/components/binary_sensor/aurora.py b/homeassistant/components/binary_sensor/aurora.py index 772792f5785..0c33877854f 100644 --- a/homeassistant/components/binary_sensor/aurora.py +++ b/homeassistant/components/binary_sensor/aurora.py @@ -99,7 +99,7 @@ class AuroraSensor(BinarySensorDevice): self.aurora_data.update() -class AuroraData(object): +class AuroraData: """Get aurora forecast.""" def __init__(self, latitude, longitude, threshold): diff --git a/homeassistant/components/binary_sensor/bbb_gpio.py b/homeassistant/components/binary_sensor/bbb_gpio.py index 785b178969f..690d1651db9 100644 --- a/homeassistant/components/binary_sensor/bbb_gpio.py +++ b/homeassistant/components/binary_sensor/bbb_gpio.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.bbb_gpio as bbb_gpio +from homeassistant.components import bbb_gpio from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 480786b2c2c..c2045c2df5e 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -25,6 +25,9 @@ DEFAULT_PAYLOAD_OFF = 'OFF' SCAN_INTERVAL = timedelta(seconds=60) +CONF_COMMAND_TIMEOUT = 'command_timeout' +DEFAULT_TIMEOUT = 15 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -32,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional( + CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) @@ -43,9 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): payload_on = config.get(CONF_PAYLOAD_ON) device_class = config.get(CONF_DEVICE_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) + command_timeout = config.get(CONF_COMMAND_TIMEOUT) if value_template is not None: value_template.hass = hass - data = CommandSensorData(hass, command) + data = CommandSensorData(hass, command, command_timeout) add_devices([CommandBinarySensor( hass, data, name, device_class, payload_on, payload_off, diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index f9ff4ac0a7a..de6ad8223d7 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -117,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities) -class HikvisionData(object): +class HikvisionData: """Hikvision device event stream object.""" def __init__(self, hass, url, port, name, username, password): diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/binary_sensor/ihc.py index 96efa6e6c19..25435d373fd 100644 --- a/homeassistant/components/binary_sensor/ihc.py +++ b/homeassistant/components/binary_sensor/ihc.py @@ -3,8 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ihc/ """ -from xml.etree.ElementTree import Element - import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -70,7 +68,7 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice): def __init__(self, ihc_controller, name, ihc_id: int, info: bool, sensor_type: str, inverting: bool, - product: Element = None) -> None: + product=None) -> None: """Initialize the IHC binary sensor.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._state = None diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 9cb87b31749..25fc3fb5d73 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', - 'wetLeakSensor': 'moisture'} + 'wetLeakSensor': 'moisture', + 'lightSensor': 'light', + 'batterySensor': 'battery'} @asyncio.coroutine @@ -54,4 +56,9 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @property def is_on(self): """Return the boolean response if the node is on.""" - return bool(self._insteon_device_state.value) + on_val = bool(self._insteon_device_state.value) + + if self._insteon_device_state.name == 'lightSensor': + return not on_val + + return on_val diff --git a/homeassistant/components/binary_sensor/iss.py b/homeassistant/components/binary_sensor/iss.py index d35c36a012e..d0654317248 100644 --- a/homeassistant/components/binary_sensor/iss.py +++ b/homeassistant/components/binary_sensor/iss.py @@ -101,7 +101,7 @@ class IssBinarySensor(BinarySensorDevice): self.iss_data.update() -class IssData(object): +class IssData: """Get data from the ISS API.""" def __init__(self, latitude, longitude): diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index deaa118f51c..b6d582b7793 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -55,7 +55,7 @@ def setup_platform(hass, config: ConfigType, else: device_type = _detect_device_type(node) subnode_id = int(node.nid[-1]) - if (device_type == 'opening' or device_type == 'moisture'): + if device_type in ('opening', 'moisture'): # These sensors use an optional "negative" subnode 2 to snag # all state changes if subnode_id == 2: diff --git a/homeassistant/components/binary_sensor/modbus.py b/homeassistant/components/binary_sensor/modbus.py index 00dc588a468..1a45235f15a 100644 --- a/homeassistant/components/binary_sensor/modbus.py +++ b/homeassistant/components/binary_sensor/modbus.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.modbus/ import logging import voluptuous as vol -import homeassistant.components.modbus as modbus +from homeassistant.components import modbus from homeassistant.const import CONF_NAME, CONF_SLAVE from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index d2533eb8f5b..cb943ac3f18 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -11,7 +11,7 @@ from typing import Optional import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 7c3a3e1dd30..73a373a15ff 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -142,7 +142,7 @@ class NetatmoBinarySensor(BinarySensorDevice): """Return the class of this sensor, from DEVICE_CLASSES.""" if self._cameratype == 'NACamera': return WELCOME_SENSOR_TYPES.get(self._sensor_name) - elif self._cameratype == 'NOC': + if self._cameratype == 'NOC': return PRESENCE_SENSOR_TYPES.get(self._sensor_name) return TAG_SENSOR_TYPES.get(self._sensor_name) diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py index 0830d86dc2a..bb597f208e6 100644 --- a/homeassistant/components/binary_sensor/ping.py +++ b/homeassistant/components/binary_sensor/ping.py @@ -96,7 +96,7 @@ class PingBinarySensor(BinarySensorDevice): self.ping.update() -class PingData(object): +class PingData: """The Class for handling the data retrieval.""" def __init__(self, host, count): diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py index cc3079c6e53..59bf8a21064 100644 --- a/homeassistant/components/binary_sensor/rachio.py +++ b/homeassistant/components/binary_sensor/rachio.py @@ -111,11 +111,10 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): if data[KEY_STATUS] == STATUS_ONLINE: return True - elif data[KEY_STATUS] == STATUS_OFFLINE: + if data[KEY_STATUS] == STATUS_OFFLINE: return False - else: - _LOGGER.warning('"%s" reported in unknown state "%s"', self.name, - data[KEY_STATUS]) + _LOGGER.warning('"%s" reported in unknown state "%s"', self.name, + data[KEY_STATUS]) def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/binary_sensor/raincloud.py index 288b46c2370..3cbd179154f 100644 --- a/homeassistant/components/binary_sensor/raincloud.py +++ b/homeassistant/components/binary_sensor/raincloud.py @@ -67,6 +67,6 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice): """Return the icon of this device.""" if self._sensor_type == 'is_watering': return 'mdi:water' if self.is_on else 'mdi:water-off' - elif self._sensor_type == 'status': + if self._sensor_type == 'status': return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected' return ICON_MAP.get(self._sensor_type) diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 4072f4ae234..31a518dc1dc 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.rpi_gpio as rpi_gpio +from homeassistant.components import rpi_gpio from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/binary_sensor/rpi_pfio.py b/homeassistant/components/binary_sensor/rpi_pfio.py index 1abfa25c82b..a1126bdd2f9 100644 --- a/homeassistant/components/binary_sensor/rpi_pfio.py +++ b/homeassistant/components/binary_sensor/rpi_pfio.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorDevice) -import homeassistant.components.rpi_pfio as rpi_pfio +from homeassistant.components import rpi_pfio from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/binary_sensor/tahoma.py b/homeassistant/components/binary_sensor/tahoma.py new file mode 100644 index 00000000000..efcfb629f39 --- /dev/null +++ b/homeassistant/components/binary_sensor/tahoma.py @@ -0,0 +1,98 @@ +""" +Support for Tahoma binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.tahoma/ +""" + +import logging +from datetime import timedelta + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice) +from homeassistant.components.tahoma import ( + DOMAIN as TAHOMA_DOMAIN, TahomaDevice) +from homeassistant.const import (STATE_OFF, STATE_ON, ATTR_BATTERY_LEVEL) + +DEPENDENCIES = ['tahoma'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=120) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tahoma controller devices.""" + _LOGGER.debug("Setup Tahoma Binary sensor platform") + controller = hass.data[TAHOMA_DOMAIN]['controller'] + devices = [] + for device in hass.data[TAHOMA_DOMAIN]['devices']['smoke']: + devices.append(TahomaBinarySensor(device, controller)) + add_devices(devices, True) + + +class TahomaBinarySensor(TahomaDevice, BinarySensorDevice): + """Representation of a Tahoma Binary Sensor.""" + + def __init__(self, tahoma_device, controller): + """Initialize the sensor.""" + super().__init__(tahoma_device, controller) + + self._state = None + self._icon = None + self._battery = None + + @property + def is_on(self): + """Return the state of the sensor.""" + return bool(self._state == STATE_ON) + + @property + def device_class(self): + """Return the class of the device.""" + if self.tahoma_device.type == 'rtds:RTDSSmokeSensor': + return 'smoke' + return None + + @property + def icon(self): + """Icon for device by its type.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if self._battery is not None: + attr[ATTR_BATTERY_LEVEL] = self._battery + return attr + + def update(self): + """Update the state.""" + self.controller.get_states([self.tahoma_device]) + if self.tahoma_device.type == 'rtds:RTDSSmokeSensor': + if self.tahoma_device.active_states['core:SmokeState']\ + == 'notDetected': + self._state = STATE_OFF + else: + self._state = STATE_ON + + if 'core:SensorDefectState' in self.tahoma_device.active_states: + # Set to 'lowBattery' for low battery warning. + self._battery = self.tahoma_device.active_states[ + 'core:SensorDefectState'] + else: + self._battery = None + + if self._state == STATE_ON: + self._icon = "mdi:fire" + elif self._battery == 'lowBattery': + self._icon = "mdi:battery-alert" + else: + self._icon = None + + _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/binary_sensor/tapsaff.py b/homeassistant/components/binary_sensor/tapsaff.py index c0f6ca3f112..5b8e133b5f4 100644 --- a/homeassistant/components/binary_sensor/tapsaff.py +++ b/homeassistant/components/binary_sensor/tapsaff.py @@ -63,7 +63,7 @@ class TapsAffSensor(BinarySensorDevice): self.data.update() -class TapsAffData(object): +class TapsAffData: """Class for handling the data retrieval for pins.""" def __init__(self, location): diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 79c36fb2ef2..360671d1cea 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -129,9 +129,9 @@ class ThresholdSensor(BinarySensorDevice): if self._threshold_lower is not None and \ self._threshold_upper is not None: return TYPE_RANGE - elif self._threshold_lower is not None: + if self._threshold_lower is not None: return TYPE_LOWER - elif self._threshold_upper is not None: + if self._threshold_upper is not None: return TYPE_UPPER @property diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 6a53569798b..78f471d125b 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.5'] +REQUIREMENTS = ['numpy==1.15.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/volvooncall.py b/homeassistant/components/binary_sensor/volvooncall.py index 39f520ddc6d..402feefa99f 100644 --- a/homeassistant/components/binary_sensor/volvooncall.py +++ b/homeassistant/components/binary_sensor/volvooncall.py @@ -28,7 +28,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice): val = getattr(self.vehicle, self._attribute) if self._attribute == 'bulb_failures': return bool(val) - elif self._attribute in ['doors', 'windows']: + if self._attribute in ['doors', 'windows']: return any([val[key] for key in val if 'Open' in key]) return val != 'Normal' diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index e6eff0d9bb5..a589ab4e8c8 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Register discovered WeMo binary sensors.""" - import pywemo.discovery as discovery + from pywemo import discovery if discovery_info is not None: location = discovery_info['ssdp_description'] diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index b37be3f6cb6..00d2a95e356 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -135,7 +135,7 @@ class IsWorkdaySensor(BinarySensorDevice): """Check if given day is in the includes list.""" if day in self._workdays: return True - elif 'holiday' in self._workdays and now in self._obj_holidays: + if 'holiday' in self._workdays and now in self._obj_holidays: return True return False @@ -144,7 +144,7 @@ class IsWorkdaySensor(BinarySensorDevice): """Check if given day is in the excludes list.""" if day in self._excludes: return True - elif 'holiday' in self._excludes and now in self._obj_holidays: + if 'holiday' in self._excludes and now in self._obj_holidays: return True return False diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index be5d9a689d1..2a9746b4a01 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -124,7 +124,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): return False self._state = True return True - elif value == '0': + if value == '0': if self._state: self._state = False return True @@ -184,7 +184,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor): return False self._state = True return True - elif value == NO_MOTION: + if value == NO_MOTION: if not self._state: return False self._state = False @@ -224,7 +224,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor): return False self._state = True return True - elif value == 'close': + if value == 'close': self._open_since = 0 if self._state: self._state = False @@ -254,7 +254,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): return False self._state = True return True - elif value == 'no_leak': + if value == 'no_leak': if self._state: self._state = False return True @@ -290,7 +290,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): return False self._state = True return True - elif value == '0': + if value == '0': if self._state: self._state = False return True diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index fc18648f907..784a96d8615 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_time from homeassistant.components import zwave from homeassistant.components.zwave import workaround -from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDevice) diff --git a/homeassistant/components/blink.py b/homeassistant/components/blink.py index a44f0163787..e84643711eb 100644 --- a/homeassistant/components/blink.py +++ b/homeassistant/components/blink.py @@ -40,7 +40,7 @@ SNAP_PICTURE_SCHEMA = vol.Schema({ }) -class BlinkSystem(object): +class BlinkSystem: """Blink System class.""" def __init__(self, config_info): diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index bc9d3acf54f..00377b3f12b 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -50,7 +50,7 @@ def setup(hass, config): return True -class BloomSky(object): +class BloomSky: """Handle all communication with the BloomSky API.""" # API documentation at http://weatherlution.com/bloomsky-api/ diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index a7ed262ac2c..061b09c1b3b 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -118,7 +118,7 @@ def setup_account(account_config: dict, hass, name: str) \ return cd_account -class BMWConnectedDriveAccount(object): +class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" def __init__(self, username: str, password: str, region_str: str, diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 35566b0cbed..9d105fb02d0 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -130,7 +130,7 @@ class CalendarEventDevice(Entity): now = dt.now() - if start <= now and end > now: + if start <= now < end: return STATE_ON if now >= end: diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 9c30d1481f8..3db24790aaf 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -125,7 +125,7 @@ class WebDavCalendarEventDevice(CalendarEventDevice): return await self.data.async_get_events(hass, start_date, end_date) -class WebDavCalendarData(object): +class WebDavCalendarData: """Class to utilize the calendar dav client object to get next event.""" def __init__(self, calendar, include_all_day, search): diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py index 53129d3316c..0bf09f6f2c7 100644 --- a/homeassistant/components/calendar/demo.py +++ b/homeassistant/components/calendar/demo.py @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -class DemoGoogleCalendarData(object): +class DemoGoogleCalendarData: """Representation of a Demo Calendar element.""" event = {} diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 279fb1e2694..925bbcacddf 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -55,7 +55,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): return await self.data.async_get_events(hass, start_date, end_date) -class GoogleCalendarData(object): +class GoogleCalendarData: """Class to utilize calendar service object to get next event.""" def __init__(self, calendar_service, calendar_id, search, diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index 71a6a17de10..30c5a6177b4 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -26,6 +26,9 @@ CONF_PROJECT_DUE_DATE = 'due_date_days' CONF_PROJECT_LABEL_WHITELIST = 'labels' CONF_PROJECT_WHITELIST = 'include_projects' +# https://github.com/PyCQA/pylint/pull/2320 +# pylint: disable=fixme + # Calendar Platform: Does this calendar event last all day? ALL_DAY = 'all_day' # Attribute: All tasks in this project @@ -280,7 +283,7 @@ class TodoistProjectDevice(CalendarEventDevice): return attributes -class TodoistProjectData(object): +class TodoistProjectData: """ Class used by the Task Device service object to hold all Todoist Tasks. @@ -503,7 +506,7 @@ class TodoistProjectData(object): time_format = '%a %d %b %Y %H:%M:%S %z' for task in project_task_data: due_date = datetime.strptime(task['due_date_utc'], time_format) - if due_date > start_date and due_date < end_date: + if start_date < due_date < end_date: event = { 'uid': task['id'], 'title': task['content'], diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 22354b51956..736bcec1e9c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -19,7 +19,8 @@ import async_timeout import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \ + SERVICE_TURN_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity @@ -47,6 +48,9 @@ STATE_RECORDING = 'recording' STATE_STREAMING = 'streaming' STATE_IDLE = 'idle' +# Bitfield of features supported by the camera entity +SUPPORT_ON_OFF = 1 + DEFAULT_CONTENT_TYPE = 'image/jpeg' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' @@ -79,6 +83,35 @@ class Image: content = attr.ib(type=bytes) +@bind_hass +def turn_off(hass, entity_id=None): + """Turn off camera.""" + hass.add_job(async_turn_off, hass, entity_id) + + +@bind_hass +async def async_turn_off(hass, entity_id=None): + """Turn off camera.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn on camera.""" + hass.add_job(async_turn_on, hass, entity_id) + + +@bind_hass +async def async_turn_on(hass, entity_id=None): + """Turn on camera, and set operation mode.""" + data = {} + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) + + @bind_hass def enable_motion_detection(hass, entity_id=None): """Enable Motion Detection.""" @@ -119,6 +152,9 @@ async def async_get_image(hass, entity_id, timeout=10): if camera is None: raise HomeAssistantError('Camera not found') + if not camera.is_on: + raise HomeAssistantError('Camera is off') + with suppress(asyncio.CancelledError, asyncio.TimeoutError): with async_timeout.timeout(timeout, loop=hass.loop): image = await camera.async_camera_image() @@ -163,6 +199,12 @@ async def async_setup(hass, config): await camera.async_enable_motion_detection() elif service.service == SERVICE_DISABLE_MOTION: await camera.async_disable_motion_detection() + elif service.service == SERVICE_TURN_OFF and \ + camera.supported_features & SUPPORT_ON_OFF: + await camera.async_turn_off() + elif service.service == SERVICE_TURN_ON and \ + camera.supported_features & SUPPORT_ON_OFF: + await camera.async_turn_on() if not camera.should_poll: continue @@ -200,6 +242,12 @@ async def async_setup(hass, config): except OSError as err: _LOGGER.error("Can't write image to file: %s", err) + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_handle_camera_service, + schema=CAMERA_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_camera_service, + schema=CAMERA_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service, schema=CAMERA_SERVICE_SCHEMA) @@ -243,6 +291,11 @@ class Camera(Entity): """Return a link to the camera feed as entity picture.""" return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) + @property + def supported_features(self): + """Flag supported features.""" + return 0 + @property def is_recording(self): """Return true if the device is recording.""" @@ -301,32 +354,23 @@ class Camera(Entity): last_image = None - try: - while True: - img_bytes = await self.async_camera_image() - if not img_bytes: - break + while True: + img_bytes = await self.async_camera_image() + if not img_bytes: + break - if img_bytes and img_bytes != last_image: + if img_bytes and img_bytes != last_image: + await write_to_mjpeg_stream(img_bytes) + + # Chrome seems to always ignore first picture, + # print it twice. + if last_image is None: await write_to_mjpeg_stream(img_bytes) + last_image = img_bytes - # Chrome seems to always ignore first picture, - # print it twice. - if last_image is None: - await write_to_mjpeg_stream(img_bytes) + await asyncio.sleep(interval) - last_image = img_bytes - - await asyncio.sleep(interval) - - except asyncio.CancelledError: - _LOGGER.debug("Stream closed by frontend.") - response = None - raise - - finally: - if response is not None: - await response.write_eof() + return response async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera. @@ -342,14 +386,38 @@ class Camera(Entity): """Return the camera state.""" if self.is_recording: return STATE_RECORDING - elif self.is_streaming: + if self.is_streaming: return STATE_STREAMING return STATE_IDLE + @property + def is_on(self): + """Return true if on.""" + return True + + def turn_off(self): + """Turn off camera.""" + raise NotImplementedError() + + @callback + def async_turn_off(self): + """Turn off camera.""" + return self.hass.async_add_job(self.turn_off) + + def turn_on(self): + """Turn off camera.""" + raise NotImplementedError() + + @callback + def async_turn_on(self): + """Turn off camera.""" + return self.hass.async_add_job(self.turn_on) + def enable_motion_detection(self): """Enable motion detection in the camera.""" raise NotImplementedError() + @callback def async_enable_motion_detection(self): """Call the job and enable motion detection.""" return self.hass.async_add_job(self.enable_motion_detection) @@ -358,6 +426,7 @@ class Camera(Entity): """Disable motion detection in camera.""" raise NotImplementedError() + @callback def async_disable_motion_detection(self): """Call the job and disable motion detection.""" return self.hass.async_add_job(self.disable_motion_detection) @@ -402,17 +471,19 @@ class CameraView(HomeAssistantView): camera = self.component.get_entity(entity_id) if camera is None: - status = 404 if request[KEY_AUTHENTICATED] else 401 - return web.Response(status=status) + raise web.HTTPNotFound() authenticated = (request[KEY_AUTHENTICATED] or request.query.get('token') in camera.access_tokens) if not authenticated: - return web.Response(status=401) + raise web.HTTPUnauthorized() - response = await self.handle(request, camera) - return response + if not camera.is_on: + _LOGGER.debug('Camera is off.') + raise web.HTTPServiceUnavailable() + + return await self.handle(request, camera) async def handle(self, request, camera): """Handle the camera request.""" @@ -435,7 +506,7 @@ class CameraImageView(CameraView): return web.Response(body=image, content_type=camera.content_type) - return web.Response(status=500) + raise web.HTTPInternalServerError() class CameraMjpegStream(CameraView): @@ -448,8 +519,7 @@ class CameraMjpegStream(CameraView): """Serve camera stream, possibly with interval.""" interval = request.query.get('interval') if interval is None: - await camera.handle_async_mjpeg_stream(request) - return + return await camera.handle_async_mjpeg_stream(request) try: # Compose camera stream from stills @@ -457,10 +527,9 @@ class CameraMjpegStream(CameraView): if interval < MIN_STREAM_INTERVAL: raise ValueError("Stream interval must be be > {}" .format(MIN_STREAM_INTERVAL)) - await camera.handle_async_still_stream(request, interval) - return + return await camera.handle_async_still_stream(request, interval) except ValueError: - return web.Response(status=400) + raise web.HTTPBadRequest() @callback diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 3c63e56b319..4cb218bc019 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -64,7 +64,7 @@ class AmcrestCam(Camera): yield from super().handle_async_mjpeg_stream(request) return - elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']: + if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: # stream an MJPEG image stream directly from the camera websession = async_get_clientsession(self.hass) streaming_url = self._camera.mjpeg_url(typeno=self._resolution) diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/camera/axis.py index 51c3bc89b05..5b39718939a 100644 --- a/homeassistant/components/camera/axis.py +++ b/homeassistant/components/camera/axis.py @@ -23,7 +23,7 @@ def _get_image_url(host, port, mode): """Set the URL to get the image.""" if mode == 'mjpeg': return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port) - elif mode == 'single': + if mode == 'single': return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port) diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py index 3c1477d1828..0e77e6e95ad 100644 --- a/homeassistant/components/camera/demo.py +++ b/homeassistant/components/camera/demo.py @@ -4,10 +4,10 @@ Demo camera platform that has a fake camera. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -import os import logging -import homeassistant.util.dt as dt_util -from homeassistant.components.camera import Camera +import os + +from homeassistant.components.camera import Camera, SUPPORT_ON_OFF _LOGGER = logging.getLogger(__name__) @@ -16,26 +16,29 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Demo camera platform.""" async_add_devices([ - DemoCamera(hass, config, 'Demo camera') + DemoCamera('Demo camera') ]) class DemoCamera(Camera): """The representation of a Demo camera.""" - def __init__(self, hass, config, name): + def __init__(self, name): """Initialize demo camera component.""" super().__init__() - self._parent = hass self._name = name self._motion_status = False + self.is_streaming = True + self._images_index = 0 def camera_image(self): """Return a faked still image response.""" - now = dt_util.utcnow() + self._images_index = (self._images_index + 1) % 4 image_path = os.path.join( - os.path.dirname(__file__), 'demo_{}.jpg'.format(now.second % 4)) + os.path.dirname(__file__), + 'demo_{}.jpg'.format(self._images_index)) + _LOGGER.debug('Loading camera_image: %s', image_path) with open(image_path, 'rb') as file: return file.read() @@ -46,8 +49,21 @@ class DemoCamera(Camera): @property def should_poll(self): - """Camera should poll periodically.""" - return True + """Demo camera doesn't need poll. + + Need explicitly call schedule_update_ha_state() after state changed. + """ + return False + + @property + def supported_features(self): + """Camera support turn on/off features.""" + return SUPPORT_ON_OFF + + @property + def is_on(self): + """Whether camera is on (streaming).""" + return self.is_streaming @property def motion_detection_enabled(self): @@ -57,7 +73,19 @@ class DemoCamera(Camera): def enable_motion_detection(self): """Enable the Motion detection in base station (Arm).""" self._motion_status = True + self.schedule_update_ha_state() def disable_motion_detection(self): """Disable the motion detection in base station (Disarm).""" self._motion_status = False + self.schedule_update_ha_state() + + def turn_off(self): + """Turn off camera.""" + self.is_streaming = False + self.schedule_update_ha_state() + + def turn_on(self): + """Turn on camera.""" + self.is_streaming = True + self.schedule_update_ha_state() diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 1bbd263e585..3da0f19fbf0 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -29,8 +29,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a FFmpeg camera.""" if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return @@ -49,30 +49,30 @@ class FFmpegCamera(Camera): self._input = config.get(CONF_INPUT) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments), loop=self.hass.loop) return image - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._input, extra_cmd=self._extra_arguments) - yield from async_aiohttp_proxy_stream( - self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + try: + return await async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + finally: + await stream.close() @property def name(self): diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index a5ed0cdc02c..757a1b5fc09 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -123,19 +123,18 @@ class MjpegCamera(Camera): with closing(req) as response: return extract_image_from_mjpeg(response.iter_content(102400)) - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" # aiohttp don't support DigestAuth -> Fallback if self._authentication == HTTP_DIGEST_AUTHENTICATION: - yield from super().handle_async_mjpeg_stream(request) + await super().handle_async_mjpeg_stream(request) return # connect to stream websession = async_get_clientsession(self.hass) stream_coro = websession.get(self._mjpeg_url, auth=self._auth) - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + return await async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index b2a27230a02..dc991644b8e 100644 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -11,7 +11,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.const import CONF_NAME from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py index ab26df5caf0..bf6700371fd 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -9,8 +9,9 @@ from datetime import timedelta import requests -import homeassistant.components.nest as nest -from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) +from homeassistant.components import nest +from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera, + SUPPORT_ON_OFF) from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -76,7 +77,36 @@ class NestCamera(Camera): """Return the brand of the camera.""" return NEST_BRAND - # This doesn't seem to be getting called regularly, for some reason + @property + def supported_features(self): + """Nest Cam support turn on and off.""" + return SUPPORT_ON_OFF + + @property + def is_on(self): + """Return true if on.""" + return self._online and self._is_streaming + + def turn_off(self): + """Turn off camera.""" + _LOGGER.debug('Turn off camera %s', self._name) + # Calling Nest API in is_streaming setter. + # device.is_streaming would not immediately change until the process + # finished in Nest Cam. + self.device.is_streaming = False + + def turn_on(self): + """Turn on camera.""" + if not self._online: + _LOGGER.error('Camera %s is offline.', self._name) + return + + _LOGGER.debug('Turn on camera %s', self._name) + # Calling Nest API in is_streaming setter. + # device.is_streaming would not immediately change until the process + # finished in Nest Cam. + self.device.is_streaming = True + def update(self): """Cache value from Python-nest.""" self._location = self.device.where diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 34a78e19f9f..1c7dc4c7ce0 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -105,6 +105,6 @@ class NetatmoCamera(Camera): """Return the camera model.""" if self._cameratype == "NOC": return "Presence" - elif self._cameratype == "NACamera": + if self._cameratype == "NACamera": return "Welcome" return None diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 447f4e1e56a..a695848d1fa 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -2,56 +2,53 @@ Proxy camera platform that enables image processing of camera data. For more details about this platform, please refer to the documentation -https://home-assistant.io/components/proxy +https://www.home-assistant.io/components/camera.proxy/ """ -import logging import asyncio +import logging + import aiohttp import async_timeout - import voluptuous as vol -from homeassistant.util.async_ import run_coroutine_threadsafe +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH from homeassistant.helpers import config_validation as cv - -import homeassistant.util.dt as dt_util -from homeassistant.const import ( - CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH) -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, Camera) from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_aiohttp_proxy_web) + async_aiohttp_proxy_web, async_get_clientsession) +from homeassistant.util.async_ import run_coroutine_threadsafe +import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pillow==5.0.0'] +REQUIREMENTS = ['pillow==5.2.0'] _LOGGER = logging.getLogger(__name__) -CONF_MAX_IMAGE_WIDTH = "max_image_width" -CONF_IMAGE_QUALITY = "image_quality" -CONF_IMAGE_REFRESH_RATE = "image_refresh_rate" -CONF_FORCE_RESIZE = "force_resize" -CONF_MAX_STREAM_WIDTH = "max_stream_width" -CONF_STREAM_QUALITY = "stream_quality" -CONF_CACHE_IMAGES = "cache_images" +CONF_CACHE_IMAGES = 'cache_images' +CONF_FORCE_RESIZE = 'force_resize' +CONF_IMAGE_QUALITY = 'image_quality' +CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate' +CONF_MAX_IMAGE_WIDTH = 'max_image_width' +CONF_MAX_STREAM_WIDTH = 'max_stream_width' +CONF_STREAM_QUALITY = 'stream_quality' DEFAULT_BASENAME = "Camera Proxy" DEFAULT_QUALITY = 75 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MAX_IMAGE_WIDTH): int, + vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean, + vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean, vol.Optional(CONF_IMAGE_QUALITY): int, vol.Optional(CONF_IMAGE_REFRESH_RATE): float, - vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean, - vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean, + vol.Optional(CONF_MAX_IMAGE_WIDTH): int, vol.Optional(CONF_MAX_STREAM_WIDTH): int, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_STREAM_QUALITY): int, }) -async def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Proxy camera platform.""" async_add_devices([ProxyCamera(hass, config)]) @@ -69,7 +66,7 @@ def _resize_image(image, opts): img = Image.open(io.BytesIO(image)) imgfmt = str(img.format) - if imgfmt != 'PNG' and imgfmt != 'JPEG': + if imgfmt not in ('PNG', 'JPEG'): _LOGGER.debug("Image is of unsupported type: %s", imgfmt) return image @@ -77,7 +74,7 @@ def _resize_image(image, opts): old_size = len(image) if old_width <= new_width: if opts.quality is None: - _LOGGER.debug("Image is smaller-than / equal-to requested width") + _LOGGER.debug("Image is smaller-than/equal-to requested width") return image new_width = old_width @@ -86,7 +83,7 @@ def _resize_image(image, opts): img = img.resize((new_width, new_height), Image.ANTIALIAS) imgbuf = io.BytesIO() - img.save(imgbuf, "JPEG", optimize=True, quality=quality) + img.save(imgbuf, 'JPEG', optimize=True, quality=quality) newimage = imgbuf.getvalue() if not opts.force_resize and len(newimage) >= old_size: _LOGGER.debug("Using original image(%d bytes) " @@ -94,11 +91,9 @@ def _resize_image(image, opts): old_size, len(newimage)) return image - _LOGGER.debug("Resized image " - "from (%dx%d - %d bytes) " - "to (%dx%d - %d bytes)", - old_width, old_height, old_size, - new_width, new_height, len(newimage)) + _LOGGER.debug( + "Resized image from (%dx%d - %d bytes) to (%dx%d - %d bytes)", + old_width, old_height, old_size, new_width, new_height, len(newimage)) return newimage @@ -112,7 +107,7 @@ class ImageOpts(): self.force_resize = force_resize def __bool__(self): - """Bool evalution rules.""" + """Bool evaluation rules.""" return bool(self.max_width or self.quality) @@ -133,8 +128,7 @@ class ProxyCamera(Camera): config.get(CONF_FORCE_RESIZE)) self._stream_opts = ImageOpts( - config.get(CONF_MAX_STREAM_WIDTH), - config.get(CONF_STREAM_QUALITY), + config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_STREAM_QUALITY), True) self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE) @@ -145,8 +139,7 @@ class ProxyCamera(Camera): self._last_image = None self._headers = ( {HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password} - if self.hass.config.api.api_password is not None - else None) + if self.hass.config.api.api_password is not None else None) def camera_image(self): """Return camera image.""" @@ -191,12 +184,12 @@ class ProxyCamera(Camera): stream_coro = websession.get(url, headers=self._headers) if not self._stream_opts: - await async_aiohttp_proxy_web(self.hass, request, stream_coro) - return + return await async_aiohttp_proxy_web( + self.hass, request, stream_coro) response = aiohttp.web.StreamResponse() - response.content_type = ('multipart/x-mixed-replace; ' - 'boundary=--frameboundary') + response.content_type = ( + 'multipart/x-mixed-replace; boundary=--frameboundary') await response.prepare(request) async def write(img_bytes): @@ -229,15 +222,10 @@ class ProxyCamera(Camera): _resize_image, image, self._stream_opts) await write(image) data = data[jpg_end + 2:] - except asyncio.CancelledError: - _LOGGER.debug("Stream closed by frontend.") - req.close() - response = None - raise - finally: - if response is not None: - await response.write_eof() + req.close() + + return response @property def name(self): diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 544fd0e6b8a..b977fcd5c52 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -1,5 +1,19 @@ # Describes the format for available camera services +turn_off: + description: Turn off camera. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +turn_on: + description: Turn on camera. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + enable_motion_detection: description: Enable the motion detection in a camera. fields: diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index e992020e2b2..b5306c31c84 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -171,10 +171,9 @@ class UnifiVideoCamera(Camera): if retry: self._login() return _get_image(retry=False) - else: - _LOGGER.error( - "Unable to log into camera, unable to get snapshot") - raise + _LOGGER.error( + "Unable to log into camera, unable to get snapshot") + raise return _get_image() diff --git a/homeassistant/components/camera/verisure.py b/homeassistant/components/camera/verisure.py index b637858303e..554f877d0bd 100644 --- a/homeassistant/components/camera/verisure.py +++ b/homeassistant/components/camera/verisure.py @@ -66,8 +66,7 @@ class VerisureSmartcam(Camera): if not image_ids: return new_image_id = image_ids[0] - if (new_image_id == '-1' or - self._image_id == new_image_id): + if new_image_id in ('-1', self._image_id): _LOGGER.debug("The image is the same, or loading image_id") return _LOGGER.debug("Download new image %s", new_image_id) diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index 90ef08c24fe..be59a1c1f50 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_NAME from homeassistant.components.camera.mjpeg import ( CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) -import homeassistant.components.zoneminder as zoneminder +from homeassistant.components import zoneminder _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py index 4d0fbe617b2..04c33d83f3d 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary.py @@ -65,7 +65,7 @@ def setup(hass, config): return True -class CanaryData(object): +class CanaryData: """Get the latest data and update the states.""" def __init__(self, username, password, timeout): diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 86c6152b6e2..aadf0103c5a 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -22,7 +22,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Cast from a config entry.""" - hass.async_add_job(hass.config_entries.async_forward_entry_setup( + hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, 'media_player')) return True @@ -31,7 +31,7 @@ async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" from pychromecast.discovery import discover_chromecasts - return await hass.async_add_job(discover_chromecasts) + return await hass.async_add_executor_job(discover_chromecasts) config_entry_flow.register_discovery_flow( diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index 2c49b25a39d..50501025f0c 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -145,7 +145,7 @@ class DaikinClimate(ClimateDevice): if value is None: _LOGGER.error("Invalid value requested for key %s", key) else: - if value == "-" or value == "--": + if value in ("-", "--"): value = None elif cast_to_float: try: diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index e64c2d5000e..71878827153 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -177,7 +177,7 @@ class Thermostat(ClimateDevice): return None if self.current_operation == STATE_HEAT: return self.thermostat['runtime']['desiredHeat'] / 10.0 - elif self.current_operation == STATE_COOL: + if self.current_operation == STATE_COOL: return self.thermostat['runtime']['desiredCool'] / 10.0 return None @@ -217,15 +217,15 @@ class Thermostat(ClimateDevice): return 'away' # A permanent hold from away climate return AWAY_MODE - elif event['holdClimateRef'] != "": + if event['holdClimateRef'] != "": # Any other hold based on climate return event['holdClimateRef'] # Any hold not based on a climate is a temp hold return TEMPERATURE_HOLD - elif event['type'].startswith('auto'): + if event['type'].startswith('auto'): # All auto modes are treated as holds return event['type'][4:].lower() - elif event['type'] == 'vacation': + if event['type'] == 'vacation': self.vacation = event['name'] return VACATION_HOLD return None @@ -317,7 +317,7 @@ class Thermostat(ClimateDevice): if hold == hold_mode: # no change, so no action required return - elif hold_mode == 'None' or hold_mode is None: + if hold_mode == 'None' or hold_mode is None: if hold == VACATION_HOLD: self.data.ecobee.delete_vacation( self.thermostat_index, self.vacation) diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py index 565e913319f..6c340e4a5f0 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE) -import homeassistant.components.modbus as modbus +from homeassistant.components import modbus import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyflexit==0.3'] diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index 92e363228a8..12057e88647 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): HeatmiserV3Thermostat( heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport) ]) - return class HeatmiserV3Thermostat(ClimateDevice): diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index b8fb7a984fa..a2725f6f3aa 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -87,7 +87,7 @@ class HMThermostat(HMDevice, ClimateDevice): # HM ip etrv 2 uses the set_point_mode to say if its # auto or manual - elif not set_point_mode == -1: + if not set_point_mode == -1: code = set_point_mode # Other devices use the control_mode else: diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 11a507aded2..04d705d6b49 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -165,7 +165,7 @@ class RoundThermostat(ClimateDevice): self.client.set_temperature(self._name, temperature) @property - def current_operation(self: ClimateDevice) -> str: + def current_operation(self) -> str: """Get the current operation of the system.""" return getattr(self.client, ATTR_SYSTEM_MODE, None) @@ -174,7 +174,7 @@ class RoundThermostat(ClimateDevice): """Return true if away mode is on.""" return self._away - def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: + def set_operation_mode(self, operation_mode: str) -> None: """Set the HVAC mode for the thermostat.""" if hasattr(self.client, ATTR_SYSTEM_MODE): self.client.system_mode = operation_mode @@ -280,7 +280,7 @@ class HoneywellUSThermostat(ClimateDevice): return self._device.setpoint_heat @property - def current_operation(self: ClimateDevice) -> str: + def current_operation(self) -> str: """Return current operation ie. heat, cool, idle.""" oper = getattr(self._device, ATTR_CURRENT_OPERATION, None) if oper == "off": @@ -373,7 +373,7 @@ class HoneywellUSThermostat(ClimateDevice): except somecomfort.SomeComfortError: _LOGGER.error('Can not stop hold mode') - def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: + def set_operation_mode(self, operation_mode: str) -> None: """Set the system mode (Cool, Heat, etc).""" if hasattr(self._device, ATTR_SYSTEM_MODE): self._device.system_mode = operation_mode diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py index 9c005b62dcc..a0adc12bfbf 100644 --- a/homeassistant/components/climate/melissa.py +++ b/homeassistant/components/climate/melissa.py @@ -192,9 +192,9 @@ class MelissaClimate(ClimateDevice): """Translate Melissa states to hass states.""" if state == self._api.STATE_ON: return STATE_ON - elif state == self._api.STATE_OFF: + if state == self._api.STATE_OFF: return STATE_OFF - elif state == self._api.STATE_IDLE: + if state == self._api.STATE_IDLE: return STATE_IDLE return None @@ -202,11 +202,11 @@ class MelissaClimate(ClimateDevice): """Translate Melissa modes to hass states.""" if mode == self._api.MODE_HEAT: return STATE_HEAT - elif mode == self._api.MODE_COOL: + if mode == self._api.MODE_COOL: return STATE_COOL - elif mode == self._api.MODE_DRY: + if mode == self._api.MODE_DRY: return STATE_DRY - elif mode == self._api.MODE_FAN: + if mode == self._api.MODE_FAN: return STATE_FAN_ONLY _LOGGER.warning( "Operation mode %s could not be mapped to hass", mode) @@ -216,11 +216,11 @@ class MelissaClimate(ClimateDevice): """Translate Melissa fan modes to hass modes.""" if fan == self._api.FAN_AUTO: return STATE_AUTO - elif fan == self._api.FAN_LOW: + if fan == self._api.FAN_LOW: return SPEED_LOW - elif fan == self._api.FAN_MEDIUM: + if fan == self._api.FAN_MEDIUM: return SPEED_MEDIUM - elif fan == self._api.FAN_HIGH: + if fan == self._api.FAN_HIGH: return SPEED_HIGH _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) return None @@ -229,24 +229,22 @@ class MelissaClimate(ClimateDevice): """Translate hass states to melissa modes.""" if mode == STATE_HEAT: return self._api.MODE_HEAT - elif mode == STATE_COOL: + if mode == STATE_COOL: return self._api.MODE_COOL - elif mode == STATE_DRY: + if mode == STATE_DRY: return self._api.MODE_DRY - elif mode == STATE_FAN_ONLY: + if mode == STATE_FAN_ONLY: return self._api.MODE_FAN - else: - _LOGGER.warning("Melissa have no setting for %s mode", mode) + _LOGGER.warning("Melissa have no setting for %s mode", mode) def hass_fan_to_melissa(self, fan): """Translate hass fan modes to melissa modes.""" if fan == STATE_AUTO: return self._api.FAN_AUTO - elif fan == SPEED_LOW: + if fan == SPEED_LOW: return self._api.FAN_LOW - elif fan == SPEED_MEDIUM: + if fan == SPEED_MEDIUM: return self._api.FAN_MEDIUM - elif fan == SPEED_HIGH: + if fan == SPEED_HIGH: return self._api.FAN_HIGH - else: - _LOGGER.warning("Melissa have no setting for %s fan mode", fan) + _LOGGER.warning("Melissa have no setting for %s fan mode", fan) diff --git a/homeassistant/components/climate/modbus.py b/homeassistant/components/climate/modbus.py index 7d392e5a40f..e567340efc9 100644 --- a/homeassistant/components/climate/modbus.py +++ b/homeassistant/components/climate/modbus.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) -import homeassistant.components.modbus as modbus +from homeassistant.components import modbus import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['modbus'] diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index fbe5460979b..1426ff31af9 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index dc1f74613bc..fa3943c3e27 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -147,7 +147,7 @@ class NestThermostat(ClimateDevice): """Return current operation ie. heat, cool, idle.""" if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: return self._mode - elif self._mode == NEST_MODE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return STATE_AUTO return STATE_UNKNOWN diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index a4b921037db..b4bed367878 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -99,7 +99,7 @@ class NetatmoThermostat(ClimateDevice): state = self._data.thermostatdata.relay_cmd if state == 0: return STATE_IDLE - elif state == 100: + if state == 100: return STATE_HEAT @property @@ -140,7 +140,7 @@ class NetatmoThermostat(ClimateDevice): self._away = self._data.setpoint_mode == 'away' -class ThermostatData(object): +class ThermostatData: """Get the latest data from Netatmo.""" def __init__(self, auth, device=None): diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 34fcfd667b6..9338c219fe5 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -102,9 +102,9 @@ class ProliphixThermostat(ClimateDevice): state = self._pdp.hvac_state if state in (1, 2): return STATE_IDLE - elif state == 3: + if state == 3: return STATE_HEAT - elif state == 6: + if state == 6: return STATE_COOL def set_temperature(self, **kwargs): diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 032d85637ef..b3043689f8c 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -308,7 +308,7 @@ class RadioThermostat(ClimateDevice): def set_operation_mode(self, operation_mode): """Set operation mode (auto, cool, heat, off).""" - if operation_mode == STATE_OFF or operation_mode == STATE_AUTO: + if operation_mode in (STATE_OFF, STATE_AUTO): self.device.tmode = TEMP_MODE_TO_CODE[operation_mode] # Setting t_cool or t_heat automatically changes tmode. diff --git a/homeassistant/components/climate/spider.py b/homeassistant/components/climate/spider.py new file mode 100644 index 00000000000..a6916b22a25 --- /dev/null +++ b/homeassistant/components/climate/spider.py @@ -0,0 +1,127 @@ +""" +Support for Spider thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.spider/ +""" + +import logging + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, STATE_COOL, STATE_HEAT, STATE_IDLE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN +from homeassistant.const import TEMP_CELSIUS + +DEPENDENCIES = ['spider'] + +OPERATION_LIST = [ + STATE_HEAT, + STATE_COOL, +] + +HA_STATE_TO_SPIDER = { + STATE_COOL: 'Cool', + STATE_HEAT: 'Heat', + STATE_IDLE: 'Idle' +} + +SPIDER_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_SPIDER.items()} + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Spider thermostat.""" + if discovery_info is None: + return + + devices = [SpiderThermostat(hass.data[SPIDER_DOMAIN]['controller'], device) + for device in hass.data[SPIDER_DOMAIN]['thermostats']] + add_devices(devices, True) + + +class SpiderThermostat(ClimateDevice): + """Representation of a thermostat.""" + + def __init__(self, api, thermostat): + """Initialize the thermostat.""" + self.api = api + self.thermostat = thermostat + + @property + def supported_features(self): + """Return the list of supported features.""" + supports = SUPPORT_TARGET_TEMPERATURE + + if self.thermostat.has_operation_mode: + supports = supports | SUPPORT_OPERATION_MODE + + return supports + + @property + def unique_id(self): + """Return the id of the thermostat, if any.""" + return self.thermostat.id + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self.thermostat.name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.thermostat.current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.thermostat.target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self.thermostat.temperature_steps + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.thermostat.minimum_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.thermostat.maximum_temperature + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return SPIDER_STATE_TO_HA[self.thermostat.operation_mode] + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return OPERATION_LIST + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + self.thermostat.set_temperature(temperature) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + self.thermostat.set_operation_mode( + HA_STATE_TO_SPIDER.get(operation_mode)) + + def update(self): + """Get the latest data.""" + self.thermostat = self.api.get_thermostat(self.unique_id) diff --git a/homeassistant/components/climate/tuya.py b/homeassistant/components/climate/tuya.py new file mode 100644 index 00000000000..19267d693a0 --- /dev/null +++ b/homeassistant/components/climate/tuya.py @@ -0,0 +1,173 @@ +""" +Support for the Tuya climate devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.tuya/ +""" + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO, + STATE_ELECTRIC, STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, STATE_PERFORMANCE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH +from homeassistant.components.tuya import DATA_TUYA, TuyaDevice + +from homeassistant.const import ( + PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT) + +DEPENDENCIES = ['tuya'] +DEVICE_TYPE = 'climate' + +HA_STATE_TO_TUYA = { + STATE_AUTO: 'auto', + STATE_COOL: 'cold', + STATE_ECO: 'eco', + STATE_ELECTRIC: 'electric', + STATE_FAN_ONLY: 'wind', + STATE_GAS: 'gas', + STATE_HEAT: 'hot', + STATE_HEAT_PUMP: 'heat_pump', + STATE_HIGH_DEMAND: 'high_demand', + STATE_PERFORMANCE: 'performance', +} + +TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} + +FAN_MODES = {SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tuya Climate devices.""" + if discovery_info is None: + return + tuya = hass.data[DATA_TUYA] + dev_ids = discovery_info.get('dev_ids') + devices = [] + for dev_id in dev_ids: + device = tuya.get_device_by_id(dev_id) + if device is None: + continue + devices.append(TuyaClimateDevice(device)) + add_devices(devices) + + +class TuyaClimateDevice(TuyaDevice, ClimateDevice): + """Tuya climate devices,include air conditioner,heater.""" + + def __init__(self, tuya): + """Init climate device.""" + super().__init__(tuya) + self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + self.operations = [] + + async def async_added_to_hass(self): + """Create operation list when add to hass.""" + await super().async_added_to_hass() + modes = self.tuya.operation_list() + if modes is None: + return + for mode in modes: + if mode in TUYA_STATE_TO_HA: + self.operations.append(TUYA_STATE_TO_HA[mode]) + + @property + def is_on(self): + """Return true if climate is on.""" + return self.tuya.state() + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + unit = self.tuya.temperature_unit() + if unit == 'CELSIUS': + return TEMP_CELSIUS + if unit == 'FAHRENHEIT': + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + mode = self.tuya.current_operation() + if mode is None: + return None + return TUYA_STATE_TO_HA.get(mode) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self.operations + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.tuya.current_temperature() + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.tuya.target_temperature() + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self.tuya.target_temperature_step() + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.tuya.current_fan_mode() + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return self.tuya.fan_list() + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs: + self.tuya.set_temperature(kwargs[ATTR_TEMPERATURE]) + + def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + self.tuya.set_fan_mode(fan_mode) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + self.tuya.set_operation_mode(HA_STATE_TO_TUYA.get(operation_mode)) + + def turn_on(self): + """Turn device on.""" + self.tuya.turn_on() + + def turn_off(self): + """Turn device off.""" + self.tuya.turn_off() + + @property + def supported_features(self): + """Return the list of supported features.""" + supports = SUPPORT_ON_OFF + if self.tuya.support_target_temperature(): + supports = supports | SUPPORT_TARGET_TEMPERATURE + if self.tuya.support_mode(): + supports = supports | SUPPORT_OPERATION_MODE + if self.tuya.support_wind_speed(): + supports = supports | SUPPORT_FAN_MODE + return supports + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.tuya.min_temp() + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.tuya.max_temp() diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index c2b82e1cc84..4bacf64cf9e 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -152,9 +152,9 @@ class VenstarThermostat(ClimateDevice): """Return current operation ie. heat, cool, idle.""" if self._client.mode == self._client.MODE_HEAT: return STATE_HEAT - elif self._client.mode == self._client.MODE_COOL: + if self._client.mode == self._client.MODE_COOL: return STATE_COOL - elif self._client.mode == self._client.MODE_AUTO: + if self._client.mode == self._client.MODE_AUTO: return STATE_AUTO return STATE_OFF @@ -178,7 +178,7 @@ class VenstarThermostat(ClimateDevice): """Return the target temperature we try to reach.""" if self._client.mode == self._client.MODE_HEAT: return self._client.heattemp - elif self._client.mode == self._client.MODE_COOL: + if self._client.mode == self._client.MODE_COOL: return self._client.cooltemp return None diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py index 4deb4d9ea2e..0f89b15e5a1 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -55,11 +55,11 @@ class VeraThermostat(VeraDevice, ClimateDevice): mode = self.vera_device.get_hvac_mode() if mode == 'HeatOn': return OPERATION_LIST[0] # heat - elif mode == 'CoolOn': + if mode == 'CoolOn': return OPERATION_LIST[1] # cool - elif mode == 'AutoChangeOver': + if mode == 'AutoChangeOver': return OPERATION_LIST[2] # auto - elif mode == 'Off': + if mode == 'Off': return OPERATION_LIST[3] # off return 'Off' @@ -74,9 +74,9 @@ class VeraThermostat(VeraDevice, ClimateDevice): mode = self.vera_device.get_fan_mode() if mode == "ContinuousOn": return FAN_OPERATION_LIST[0] # on - elif mode == "Auto": + if mode == "Auto": return FAN_OPERATION_LIST[1] # auto - elif mode == "PeriodicOn": + if mode == "PeriodicOn": return FAN_OPERATION_LIST[2] # cycle return "Auto" diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 12a6960f833..15e555db8b9 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -224,7 +224,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): if self.current_operation != STATE_AUTO and not self.is_away_mode_on: if self.current_operation == STATE_COOL: return self.wink.current_max_set_point() - elif self.current_operation == STATE_HEAT: + if self.current_operation == STATE_HEAT: return self.wink.current_min_set_point() return None @@ -311,7 +311,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): """Return whether the fan is on.""" if self.wink.current_fan_mode() == 'on': return STATE_ON - elif self.wink.current_fan_mode() == 'auto': + if self.wink.current_fan_mode() == 'auto': return STATE_AUTO # No Fan available so disable slider return None @@ -483,7 +483,7 @@ class WinkAC(WinkDevice, ClimateDevice): speed = self.wink.current_fan_speed() if speed <= 0.33: return SPEED_LOW - elif speed <= 0.66: + if speed <= 0.66: return SPEED_MEDIUM return SPEED_HIGH diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 52c544256b6..f87f2e83f5d 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) from homeassistant.components.zwave import ZWaveDeviceEntity -from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) @@ -186,7 +186,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Return the unit of measurement.""" if self._unit == 'C': return TEMP_CELSIUS - elif self._unit == 'F': + if self._unit == 'F': return TEMP_FAHRENHEIT return self._unit diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 12b81c9003b..f4ce7bb3d1a 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -253,5 +253,3 @@ def async_handle_cloud(hass, cloud, payload): payload['reason']) else: _LOGGER.warning("Received unknown cloud action: %s", action) - - return None diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py index c40bd99b542..154320b4abd 100644 --- a/homeassistant/components/coinbase.py +++ b/homeassistant/components/coinbase.py @@ -69,7 +69,7 @@ def setup(hass, config): return True -class CoinbaseData(object): +class CoinbaseData: """Get the latest data and update the states.""" def __init__(self, api_key, api_secret): diff --git a/homeassistant/components/comfoconnect.py b/homeassistant/components/comfoconnect.py index 425ed6f9c9a..69d88274f29 100644 --- a/homeassistant/components/comfoconnect.py +++ b/homeassistant/components/comfoconnect.py @@ -88,7 +88,7 @@ def setup(hass, config): return True -class ComfoConnectBridge(object): +class ComfoConnectBridge: """Representation of a ComfoConnect bridge.""" def __init__(self, hass, bridge, name, token, friendly_name, pin): diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index c594bf1f99e..7c0867e3852 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -20,6 +20,7 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('entity_id'): cv.entity_id, # If passed in, we update value. Passing None will remove old value. vol.Optional('name'): vol.Any(str, None), + vol.Optional('new_entity_id'): str, }) @@ -74,13 +75,28 @@ def websocket_update_entity(hass, connection, msg): msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found')) return - entry = registry.async_update_entity( - msg['entity_id'], name=msg['name']) - connection.send_message_outside(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) + changes = {} - hass.async_add_job(update_entity()) + if 'name' in msg: + changes['name'] = msg['name'] + + if 'new_entity_id' in msg: + changes['new_entity_id'] = msg['new_entity_id'] + + try: + if changes: + entry = registry.async_update_entity( + msg['entity_id'], **changes) + except ValueError as err: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'invalid_info', str(err) + )) + else: + connection.send_message_outside(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) + + hass.async_create_task(update_entity()) @callback diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index c839ab7bc6e..84927712741 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -29,6 +29,7 @@ def async_setup(hass): hass.http.register_view(ZWaveUserCodeView) hass.http.register_view(ZWaveLogView) hass.http.register_view(ZWaveConfigWriteView) + hass.http.register_view(ZWaveProtectionView) return True @@ -196,3 +197,59 @@ class ZWaveUserCodeView(HomeAssistantView): 'label': value.label, 'length': len(value.data)} return self.json(usercodes) + + +class ZWaveProtectionView(HomeAssistantView): + """View for the protection commandclass of a node.""" + + url = r"/api/zwave/protection/{node_id:\d+}" + name = "api:zwave:protection" + + async def get(self, request, node_id): + """Retrieve the protection commandclass options of node.""" + nodeid = int(node_id) + hass = request.app['hass'] + network = hass.data.get(const.DATA_NETWORK) + + def _fetch_protection(): + """Helper to get protection data.""" + node = network.nodes.get(nodeid) + if node is None: + return self.json_message('Node not found', HTTP_NOT_FOUND) + protection_options = {} + if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): + return self.json(protection_options) + protections = node.get_protections() + protection_options = { + 'value_id': '{0:d}'.format(list(protections)[0]), + 'selected': node.get_protection_item(list(protections)[0]), + 'options': node.get_protection_items(list(protections)[0])} + return self.json(protection_options) + + return await hass.async_add_executor_job(_fetch_protection) + + async def post(self, request, node_id): + """Change the selected option in protection commandclass.""" + nodeid = int(node_id) + hass = request.app['hass'] + network = hass.data.get(const.DATA_NETWORK) + protection_data = await request.json() + + def _set_protection(): + """Helper to get protection data.""" + node = network.nodes.get(nodeid) + selection = protection_data["selection"] + value_id = int(protection_data[const.ATTR_VALUE_ID]) + if node is None: + return self.json_message('Node not found', HTTP_NOT_FOUND) + if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): + return self.json_message( + 'No protection commandclass on this node', HTTP_NOT_FOUND) + state = node.set_protection(value_id, selection) + if not state: + return self.json_message( + 'Protection setting did not complete', 202) + return self.json_message( + 'Protection setting succsessfully set', HTTP_OK) + + return await hass.async_add_executor_job(_set_protection) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 2c159633a9b..56fb7b4247b 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -128,7 +128,7 @@ def async_setup(hass, config): return True -class Configurator(object): +class Configurator: """The class to keep track of current configuration requests.""" def __init__(self, hass): diff --git a/homeassistant/components/cover/aladdin_connect.py b/homeassistant/components/cover/aladdin_connect.py new file mode 100644 index 00000000000..efaea39bb86 --- /dev/null +++ b/homeassistant/components/cover/aladdin_connect.py @@ -0,0 +1,115 @@ +""" +Platform for the Aladdin Connect cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/cover.aladdin_connect/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA, + SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, + STATE_OPENING, STATE_CLOSING, STATE_OPEN) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['aladdin_connect==0.1'] + +_LOGGER = logging.getLogger(__name__) + +NOTIFICATION_ID = 'aladdin_notification' +NOTIFICATION_TITLE = 'Aladdin Connect Cover Setup' + +STATES_MAP = { + 'open': STATE_OPEN, + 'opening': STATE_OPENING, + 'closed': STATE_CLOSED, + 'closing': STATE_CLOSING +} + +SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Aladdin Connect platform.""" + from aladdin_connect import AladdinConnectClient + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + acc = AladdinConnectClient(username, password) + + try: + if not acc.login(): + raise ValueError("Username or Password is incorrect") + add_devices(AladdinDevice(acc, door) for door in acc.get_doors()) + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + +class AladdinDevice(CoverDevice): + """Representation of Aladdin Connect cover.""" + + def __init__(self, acc, device): + """Initialize the cover.""" + self._acc = acc + self._device_id = device['device_id'] + self._number = device['door_number'] + self._name = device['name'] + self._status = STATES_MAP.get(device['status']) + + @property + def device_class(self): + """Define this cover as a garage door.""" + return 'garage' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES + + @property + def name(self): + """Return the name of the garage door.""" + return self._name + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._status == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._status == STATE_CLOSING + + @property + def is_closed(self): + """Return None if status is unknown, True if closed, else False.""" + if self._status is None: + return None + return self._status == STATE_CLOSED + + def close_cover(self, **kwargs): + """Issue close command to cover.""" + self._acc.close_door(self._device_id, self._number) + + def open_cover(self, **kwargs): + """Issue open command to cover.""" + self._acc.open_door(self._device_id, self._number) + + def update(self): + """Update status of cover.""" + acc_status = self._acc.get_door_status(self._device_id, self._number) + self._status = STATES_MAP.get(acc_status) diff --git a/homeassistant/components/cover/brunt.py b/homeassistant/components/cover/brunt.py new file mode 100644 index 00000000000..713f06db735 --- /dev/null +++ b/homeassistant/components/cover/brunt.py @@ -0,0 +1,182 @@ +""" +Support for Brunt Blind Engine covers. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/cover.brunt +""" + +import logging + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.components.cover import ( + ATTR_POSITION, CoverDevice, + PLATFORM_SCHEMA, SUPPORT_CLOSE, + SUPPORT_OPEN, SUPPORT_SET_POSITION +) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['brunt==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION +DEVICE_CLASS = 'window' + +ATTR_REQUEST_POSITION = 'request_position' +NOTIFICATION_ID = 'brunt_notification' +NOTIFICATION_TITLE = 'Brunt Cover Setup' +ATTRIBUTION = 'Based on an unofficial Brunt SDK.' + +CLOSED_POSITION = 0 +OPEN_POSITION = 100 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the brunt platform.""" + # pylint: disable=no-name-in-module + from brunt import BruntAPI + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + bapi = BruntAPI(username=username, password=password) + try: + things = bapi.getThings()['things'] + if not things: + _LOGGER.error("No things present in account.") + else: + add_devices([BruntDevice( + bapi, thing['NAME'], + thing['thingUri']) for thing in things], True) + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + +class BruntDevice(CoverDevice): + """ + Representation of a Brunt cover device. + + Contains the common logic for all Brunt devices. + """ + + def __init__(self, bapi, name, thing_uri): + """Init the Brunt device.""" + self._bapi = bapi + self._name = name + self._thing_uri = thing_uri + + self._state = {} + self._available = None + + @property + def name(self): + """Return the name of the device as reported by tellcore.""" + return self._name + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._available + + @property + def current_cover_position(self): + """ + Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + pos = self._state.get('currentPosition') + return int(pos) if pos else None + + @property + def request_cover_position(self): + """ + Return request position of cover. + + The request position is the position of the last request + to Brunt, at times there is a diff of 1 to current + None is unknown, 0 is closed, 100 is fully open. + """ + pos = self._state.get('requestPosition') + return int(pos) if pos else None + + @property + def move_state(self): + """ + Return current moving state of cover. + + None is unknown, 0 when stopped, 1 when opening, 2 when closing + """ + mov = self._state.get('moveState') + return int(mov) if mov else None + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self.move_state == 1 + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self.move_state == 2 + + @property + def device_state_attributes(self): + """Return the detailed device state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_REQUEST_POSITION: self.request_cover_position + } + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS + + @property + def supported_features(self): + """Flag supported features.""" + return COVER_FEATURES + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return self.current_cover_position == CLOSED_POSITION + + def update(self): + """Poll the current state of the device.""" + try: + self._state = self._bapi.getState( + thingUri=self._thing_uri).get('thing') + self._available = True + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + self._available = False + + def open_cover(self, **kwargs): + """Set the cover to the open position.""" + self._bapi.changeRequestPosition( + OPEN_POSITION, thingUri=self._thing_uri) + + def close_cover(self, **kwargs): + """Set the cover to the closed position.""" + self._bapi.changeRequestPosition( + CLOSED_POSITION, thingUri=self._thing_uri) + + def set_cover_position(self, **kwargs): + """Set the cover to a specific position.""" + self._bapi.changeRequestPosition( + kwargs[ATTR_POSITION], thingUri=self._thing_uri) diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index b1533bd68c8..b81ac4e45e1 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -97,7 +97,7 @@ class DemoCover(CoverDevice): """Close the cover.""" if self._position == 0: return - elif self._position is None: + if self._position is None: self._closed = True self.schedule_update_ha_state() return @@ -119,7 +119,7 @@ class DemoCover(CoverDevice): """Open the cover.""" if self._position == 100: return - elif self._position is None: + if self._position is None: self._closed = False self.schedule_update_ha_state() return diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 62e1069e18b..e1775e2f968 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.cover import ( CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index aefb7ab89d7..5079a3b60c2 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/cover.rfxtrx/ """ import voluptuous as vol -import homeassistant.components.rfxtrx as rfxtrx +from homeassistant.components import rfxtrx from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME from homeassistant.components.rfxtrx import ( diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 384f96f3f52..2f6951cfc0d 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -import homeassistant.components.rpi_gpio as rpi_gpio +from homeassistant.components import rpi_gpio import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py index ac4fddf98bb..04bf0ef1d32 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/cover/scsgate.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.scsgate as scsgate +from homeassistant.components import scsgate from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_DEVICES, CONF_NAME) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/cover/tuya.py b/homeassistant/components/cover/tuya.py new file mode 100644 index 00000000000..7b5fefee58a --- /dev/null +++ b/homeassistant/components/cover/tuya.py @@ -0,0 +1,60 @@ +""" +Support for Tuya cover. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.tuya/ +""" +from homeassistant.components.cover import ( + CoverDevice, ENTITY_ID_FORMAT, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP) +from homeassistant.components.tuya import DATA_TUYA, TuyaDevice + +DEPENDENCIES = ['tuya'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tuya cover devices.""" + if discovery_info is None: + return + tuya = hass.data[DATA_TUYA] + dev_ids = discovery_info.get('dev_ids') + devices = [] + for dev_id in dev_ids: + device = tuya.get_device_by_id(dev_id) + if device is None: + continue + devices.append(TuyaCover(device)) + add_devices(devices) + + +class TuyaCover(TuyaDevice, CoverDevice): + """Tuya cover devices.""" + + def __init__(self, tuya): + """Init tuya cover device.""" + super().__init__(tuya) + self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + if self.tuya.support_stop(): + supported_features |= SUPPORT_STOP + return supported_features + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return None + + def open_cover(self, **kwargs): + """Open the cover.""" + self.tuya.open_cover() + + def close_cover(self, **kwargs): + """Close cover.""" + self.tuya.close_cover() + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self.tuya.stop_cover() diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index c29c11c5b6b..8c8c88ecb87 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -11,7 +11,7 @@ from homeassistant.components.cover import ( DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION) from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components import zwave -from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import from homeassistant.components.zwave import workaround from homeassistant.components.cover import CoverDevice @@ -27,11 +27,10 @@ def get_device(hass, values, node_config, **kwargs): zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and values.primary.index == 0): return ZwaveRollershutter(hass, values, invert_buttons) - elif (values.primary.command_class == - zwave.const.COMMAND_CLASS_SWITCH_BINARY): + if values.primary.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY: return ZwaveGarageDoorSwitch(values) - elif (values.primary.command_class == - zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): + if values.primary.command_class == \ + zwave.const.COMMAND_CLASS_BARRIER_OPERATOR: return ZwaveGarageDoorBarrier(values) return None @@ -84,7 +83,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): if self._current_position is not None: if self._current_position <= 5: return 0 - elif self._current_position >= 95: + if self._current_position >= 95: return 100 return self._current_position diff --git a/homeassistant/components/daikin.py b/homeassistant/components/daikin.py index 5808528ca5a..8983ecf82d8 100644 --- a/homeassistant/components/daikin.py +++ b/homeassistant/components/daikin.py @@ -115,7 +115,7 @@ def daikin_api_setup(hass, host, name=None): return api -class DaikinApi(object): +class DaikinApi: """Keep the Daikin instance in one place and centralize the update.""" def __init__(self, device, name): diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 88174b9d612..e0982c65f33 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -97,7 +97,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DATA_DECONZ_UNSUB] = [] for component in ['binary_sensor', 'light', 'scene', 'sensor']: - hass.async_add_job(hass.config_entries.async_forward_entry_setup( + hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, component)) @callback @@ -178,7 +178,7 @@ async def async_unload_entry(hass, config_entry): return True -class DeconzEvent(object): +class DeconzEvent: """When you want signals instead of entities. Stateless sensors such as remotes are expected to generate an event diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index b67d32508be..a6f67506227 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -57,7 +57,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): if len(self.bridges) == 1: self.deconz_config = self.bridges[0] return await self.async_step_link() - elif len(self.bridges) > 1: + if len(self.bridges) > 1: hosts = [] for bridge in self.bridges: hosts.append(bridge[CONF_HOST]) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 64ce3cda073..c2c7866148f 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/demo/ import asyncio import time -import homeassistant.bootstrap as bootstrap +from homeassistant import bootstrap import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 580c0272e46..74cb0a77fef 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv -import homeassistant.util as util +from homeassistant import util from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.yaml import dump @@ -231,7 +231,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): return True -class DeviceTracker(object): +class DeviceTracker: """Representation of a device tracker.""" def __init__(self, hass: HomeAssistantType, consider_home: timedelta, @@ -537,7 +537,7 @@ class Device(Entity): """ if not self.last_seen: return - elif self.location_name: + if self.location_name: self._state = self.location_name elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: zone_state = async_active_zone( @@ -577,7 +577,7 @@ class Device(Entity): state.attributes[ATTR_LONGITUDE]) -class DeviceScanner(object): +class DeviceScanner: """Device scanner object.""" hass = None # type: HomeAssistantType diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index 61eee99e721..142842b12d2 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -94,10 +94,10 @@ class ArubaDeviceScanner(DeviceScanner): if query == 1: _LOGGER.error("Timeout") return - elif query == 2: + if query == 2: _LOGGER.error("Unexpected response from router") return - elif query == 3: + if query == 3: ssh.sendline('yes') ssh.expect('password:') elif query == 4: diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 607f236f920..4fcc550d7db 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -193,7 +193,7 @@ class AutomaticAuthCallbackView(HomeAssistantView): return response -class AutomaticData(object): +class AutomaticData: """A class representing an Automatic cloud service connection.""" def __init__(self, hass, client, session, devices, async_see): diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py index f36afc622ee..02a12653180 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/device_tracker/bmw_connected_drive.py @@ -27,7 +27,7 @@ def setup_scanner(hass, config, see, discovery_info=None): return True -class BMWDeviceTracker(object): +class BMWDeviceTracker: """BMW Connected Drive device tracker.""" def __init__(self, see, vehicle): diff --git a/homeassistant/components/device_tracker/bt_home_hub_5.py b/homeassistant/components/device_tracker/bt_home_hub_5.py index 707850d2215..93bc9270650 100644 --- a/homeassistant/components/device_tracker/bt_home_hub_5.py +++ b/homeassistant/components/device_tracker/bt_home_hub_5.py @@ -94,8 +94,7 @@ def _get_homehub_data(url): return if response.status_code == 200: return _parse_homehub_response(response.text) - else: - _LOGGER.error("Invalid response from Home Hub: %s", response) + _LOGGER.error("Invalid response from Home Hub: %s", response) def _parse_homehub_response(data_str): diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 3e17fdd3329..539d4fde5ef 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -131,13 +131,12 @@ class DdWrtDeviceScanner(DeviceScanner): return if response.status_code == 200: return _parse_ddwrt_response(response.text) - elif response.status_code == 401: + if response.status_code == 401: # Authentication error _LOGGER.exception( "Failed to authenticate, check your username and password") return - else: - _LOGGER.error("Invalid response from DD-WRT: %s", response) + _LOGGER.error("Invalid response from DD-WRT: %s", response) def _parse_ddwrt_response(data_str): diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index b278c421925..2cac81fd405 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -62,7 +62,7 @@ def _build_device(device_dict): device_dict['l3connectivities'][0]['addr']) -class FreeboxDeviceScanner(object): +class FreeboxDeviceScanner: """This class scans for devices connected to the Freebox.""" def __init__(self, hass, config, async_see): diff --git a/homeassistant/components/device_tracker/geofency.py b/homeassistant/components/device_tracker/geofency.py index adb5c6f6d28..7231c5127be 100644 --- a/homeassistant/components/device_tracker/geofency.py +++ b/homeassistant/components/device_tracker/geofency.py @@ -70,16 +70,15 @@ class GeofencyView(HomeAssistantView): if self._is_mobile_beacon(data): return (yield from self._set_location(hass, data, None)) + if data['entry'] == LOCATION_ENTRY: + location_name = data['name'] else: - if data['entry'] == LOCATION_ENTRY: - location_name = data['name'] - else: - location_name = STATE_NOT_HOME - if ATTR_CURRENT_LATITUDE in data: - data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE] - data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE] + location_name = STATE_NOT_HOME + if ATTR_CURRENT_LATITUDE in data: + data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE] + data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE] - return (yield from self._set_location(hass, data, location_name)) + return (yield from self._set_location(hass, data, location_name)) @staticmethod def _validate_data(data): diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 5f06946fc44..d0669ab4967 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -42,7 +42,7 @@ def setup_scanner(hass, config: ConfigType, see, discovery_info=None): return scanner.success_init -class GoogleMapsScanner(object): +class GoogleMapsScanner: """Representation of an Google Maps location sharing account.""" def __init__(self, hass, config: ConfigType, see) -> None: diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index bf3916f3abe..a2a371163fd 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -19,7 +19,7 @@ from homeassistant.const import ( INTERFACES = 2 DEFAULT_TIMEOUT = 10 -REQUIREMENTS = ['beautifulsoup4==4.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index aee584aa953..354d3b0980c 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -84,7 +84,7 @@ class LocativeView(HomeAssistantView): gps=gps_location)) return 'Setting location to {}'.format(location_name) - elif direction == 'exit': + if direction == 'exit': current_state = hass.states.get( '{}.{}'.format(DOMAIN, device)) @@ -102,7 +102,7 @@ class LocativeView(HomeAssistantView): return 'Ignoring exit from {} (already in {})'.format( location_name, current_state) - elif direction == 'test': + if 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.' diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py index 9bbc6bf9ffe..c996b7e643b 100644 --- a/homeassistant/components/device_tracker/meraki.py +++ b/homeassistant/components/device_tracker/meraki.py @@ -74,17 +74,16 @@ class MerakiView(HomeAssistantView): _LOGGER.error("Invalid Secret received from Meraki") return self.json_message('Invalid secret', HTTP_UNPROCESSABLE_ENTITY) - elif data['version'] != VERSION: + if data['version'] != VERSION: _LOGGER.error("Invalid API version: %s", data['version']) return self.json_message('Invalid version', HTTP_UNPROCESSABLE_ENTITY) - else: - _LOGGER.debug('Valid Secret') - if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'): - _LOGGER.error("Unknown Device %s", data['type']) - return self.json_message('Invalid device type', - HTTP_UNPROCESSABLE_ENTITY) - _LOGGER.debug("Processing %s", data['type']) + _LOGGER.debug('Valid Secret') + if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'): + _LOGGER.error("Unknown Device %s", data['type']) + return self.json_message('Invalid device type', + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.debug("Processing %s", data['type']) if not data["data"]["observations"]: _LOGGER.debug("No observations found") return diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 2e2d9b10d98..b5031e8ccfb 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.core import callback from homeassistant.const import CONF_DEVICES from homeassistant.components.mqtt import CONF_QOS diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 9a5532fc9f4..7e5ae7c9227 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.core import callback from homeassistant.components.mqtt import CONF_QOS from homeassistant.components.device_tracker import PLATFORM_SCHEMA diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index e99524c36db..2d7f1e80406 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -12,7 +12,7 @@ from collections import defaultdict import voluptuous as vol -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import ( diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index 6a0cb18d55e..d09e1930d4f 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -class Host(object): +class Host: """Host object with ping detection.""" def __init__(self, ip_address, dev_id, hass, config): diff --git a/homeassistant/components/device_tracker/sky_hub.py b/homeassistant/components/device_tracker/sky_hub.py index 0c289ce9a82..deab486ec6e 100644 --- a/homeassistant/components/device_tracker/sky_hub.py +++ b/homeassistant/components/device_tracker/sky_hub.py @@ -91,8 +91,7 @@ def _get_skyhub_data(url): return if response.status_code == 200: return _parse_skyhub_response(response.text) - else: - _LOGGER.error("Invalid response from Sky Hub: %s", response) + _LOGGER.error("Invalid response from Sky Hub: %s", response) def _parse_skyhub_response(data_str): diff --git a/homeassistant/components/device_tracker/tesla.py b/homeassistant/components/device_tracker/tesla.py index ba9bc8c2631..c08ddb4047b 100644 --- a/homeassistant/components/device_tracker/tesla.py +++ b/homeassistant/components/device_tracker/tesla.py @@ -23,7 +23,7 @@ def setup_scanner(hass, config, see, discovery_info=None): return True -class TeslaDeviceTracker(object): +class TeslaDeviceTracker: """A class representing a Tesla device.""" def __init__(self, hass, config, see, tesla_devices): diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index 6df9f3c9974..07f15e7e88a 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -74,7 +74,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): return await scanner.async_init() -class TileScanner(object): +class TileScanner: """Define an object to retrieve Tile data.""" def __init__(self, client, hass, async_see, types, show_inactive): @@ -109,7 +109,7 @@ class TileScanner(object): _LOGGER.debug('Updating Tile data') try: - await self._client.asayn_init() + await self._client.async_init() tiles = await self._client.tiles.all( whitelist=self._types, show_inactive=self._show_inactive) except SessionExpiredError: diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 01ae2977f6d..718adad4212 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -102,12 +102,12 @@ class TomatoDeviceScanner(DeviceScanner): for param, value in \ self.parse_api_pattern.findall(response.text): - if param == 'wldev' or param == 'dhcpd_lease': + if param in ('wldev', 'dhcpd_lease'): self.last_results[param] = \ json.loads(value.replace("'", '"')) return True - elif response.status_code == 401: + if response.status_code == 401: # Authentication error _LOGGER.exception(( "Failed to authenticate, " diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 5266b9c6f57..346f381db34 100644 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -22,6 +22,8 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_HEADER_X_REQUESTED_WITH) import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['tplink==0.2.1'] + _LOGGER = logging.getLogger(__name__) HTTP_HEADER_NO_CACHE = 'no-cache' @@ -34,10 +36,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_scanner(hass, config): - """Validate the configuration and return a TP-Link scanner.""" - for cls in [Tplink5DeviceScanner, Tplink4DeviceScanner, - Tplink3DeviceScanner, Tplink2DeviceScanner, - TplinkDeviceScanner]: + """ + Validate the configuration and return a TP-Link scanner. + + The default way of integrating devices is to use a pypi + + package, The TplinkDeviceScanner has been refactored + + to depend on a pypi package, the other implementations + + should be gradually migrated in the pypi package + + """ + for cls in [ + TplinkDeviceScanner, Tplink5DeviceScanner, Tplink4DeviceScanner, + Tplink3DeviceScanner, Tplink2DeviceScanner, Tplink1DeviceScanner + ]: scanner = cls(config[DOMAIN]) if scanner.success_init: return scanner @@ -46,6 +60,46 @@ def get_scanner(hass, config): class TplinkDeviceScanner(DeviceScanner): + """Queries the router for connected devices.""" + + def __init__(self, config): + """Initialize the scanner.""" + from tplink.tplink import TpLinkClient + host = config[CONF_HOST] + password = config[CONF_PASSWORD] + username = config[CONF_USERNAME] + + self.tplink_client = TpLinkClient( + password, host=host, username=username) + + self.last_results = {} + self.success_init = self._update_info() + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return self.last_results.keys() + + def get_device_name(self, device): + """Get the name of the device.""" + return self.last_results.get(device) + + def _update_info(self): + """Ensure the information from the TP-Link router is up to date. + + Return boolean if scanning successful. + """ + _LOGGER.info("Loading wireless clients...") + result = self.tplink_client.get_connected_devices() + + if result: + self.last_results = result + return True + + return False + + +class Tplink1DeviceScanner(DeviceScanner): """This class queries a wireless router running TP-Link firmware.""" def __init__(self, config): @@ -94,7 +148,7 @@ class TplinkDeviceScanner(DeviceScanner): return False -class Tplink2DeviceScanner(TplinkDeviceScanner): +class Tplink2DeviceScanner(Tplink1DeviceScanner): """This class queries a router with newer version of TP-Link firmware.""" def scan_devices(self): @@ -147,7 +201,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): return False -class Tplink3DeviceScanner(TplinkDeviceScanner): +class Tplink3DeviceScanner(Tplink1DeviceScanner): """This class queries the Archer C9 router with version 150811 or high.""" def __init__(self, config): @@ -256,7 +310,7 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): self.sysauth = '' -class Tplink4DeviceScanner(TplinkDeviceScanner): +class Tplink4DeviceScanner(Tplink1DeviceScanner): """This class queries an Archer C7 router with TP-Link firmware 150427.""" def __init__(self, config): @@ -337,7 +391,7 @@ class Tplink4DeviceScanner(TplinkDeviceScanner): return True -class Tplink5DeviceScanner(TplinkDeviceScanner): +class Tplink5DeviceScanner(Tplink1DeviceScanner): """This class queries a TP-Link EAP-225 AP with newer TP-Link FW.""" def scan_devices(self): diff --git a/homeassistant/components/device_tracker/trackr.py b/homeassistant/components/device_tracker/trackr.py index 84fb449c070..08d3a4c9445 100644 --- a/homeassistant/components/device_tracker/trackr.py +++ b/homeassistant/components/device_tracker/trackr.py @@ -30,7 +30,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): return True -class TrackRDeviceScanner(object): +class TrackRDeviceScanner: """A class representing a TrackR device.""" def __init__(self, hass, config: dict, see) -> None: diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index f265014657b..94e3b407d13 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -56,7 +56,7 @@ def _refresh_on_access_denied(func): try: return func(self, *args, **kwargs) except PermissionError: - _LOGGER.warning("Invalid session detected." + + _LOGGER.warning("Invalid session detected." " Trying to refresh session_id and re-run RPC") self.session_id = _get_session_id( self.url, self.username, self.password) diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py index 28b3a05e403..0f275a7fe66 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow.py @@ -119,7 +119,7 @@ async def async_handle_message(hass, message): return dialogflow_response.as_dict() -class DialogflowResponse(object): +class DialogflowResponse: """Help generating the response for Dialogflow.""" def __init__(self, parameters): diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index a0f50842649..c0c9d95586c 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -65,7 +65,7 @@ def setup(hass, config): return True -class DigitalOcean(object): +class DigitalOcean: """Handle all communication with the Digital Ocean API.""" def __init__(self, access_token): diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 96f094b527d..3829c2caebd 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -84,7 +84,7 @@ def setup_ecobee(hass, network, config): discovery.load_platform(hass, 'weather', DOMAIN, {}, config) -class EcobeeData(object): +class EcobeeData: """Get the latest data and update the states.""" def __init__(self, config_file): diff --git a/homeassistant/components/egardia.py b/homeassistant/components/egardia.py index f350ea56bb4..b7da671bb15 100644 --- a/homeassistant/components/egardia.py +++ b/homeassistant/components/egardia.py @@ -81,7 +81,7 @@ def setup(hass, config): device = hass.data[EGARDIA_DEVICE] = egardiadevice.EgardiaDevice( host, port, username, password, '', version) except requests.exceptions.RequestException: - _LOGGER.error("An error occurred accessing your Egardia device. " + + _LOGGER.error("An error occurred accessing your Egardia device. " "Please check config.") return False except egardiadevice.UnauthorizedError: @@ -108,7 +108,7 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) except IOError: - _LOGGER.error("Binding error occurred while starting " + + _LOGGER.error("Binding error occurred while starting " "EgardiaServer.") return False diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 704eab1846b..209fa7ba879 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -143,12 +143,12 @@ async def async_setup(hass, config): # No users, cannot continue return False - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'sensor', DOMAIN, { CONF_SENSORS: sensors, }, config)) - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'binary_sensor', DOMAIN, { CONF_BINARY_SENSORS: binary_sensors, }, config)) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 36ce1c392f9..8a67b933b9f 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -136,7 +136,7 @@ def setup(hass, yaml_config): return True -class Config(object): +class Config: """Hold configuration variables for the emulated hue bridge.""" def __init__(self, hass, conf): diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 2b74984e4ca..f7fbe2e15e3 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -237,11 +237,11 @@ class HueOneLightChangeView(HomeAssistantView): # Convert 0-100 to a fan speed if brightness == 0: data[ATTR_SPEED] = SPEED_OFF - elif brightness <= 33.3 and brightness > 0: + elif 0 < brightness <= 33.3: data[ATTR_SPEED] = SPEED_LOW - elif brightness <= 66.6 and brightness > 33.3: + elif 33.3 < brightness <= 66.6: data[ATTR_SPEED] = SPEED_MEDIUM - elif brightness <= 100 and brightness > 66.6: + elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH if entity.domain in config.off_maps_to_on_domains: diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 5ffd97ef0e3..7dd4e7dc32a 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -167,21 +167,21 @@ def async_setup(hass, config): # Load sub-components for Envisalink if partitions: - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'alarm_control_panel', 'envisalink', { CONF_PARTITIONS: partitions, CONF_CODE: code, CONF_PANIC: panic_type }, config )) - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'sensor', 'envisalink', { CONF_PARTITIONS: partitions, CONF_CODE: code }, config )) if zones: - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'binary_sensor', 'envisalink', { CONF_ZONES: zones }, config diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 66790d02687..db0e8c590fd 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -235,11 +235,11 @@ def async_setup(hass, config: dict): class FanEntity(ToggleEntity): """Representation of a fan.""" - def set_speed(self: ToggleEntity, speed: str) -> None: + def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" raise NotImplementedError() - def async_set_speed(self: ToggleEntity, speed: str): + def async_set_speed(self, speed: str): """Set the speed of the fan. This method must be run in the event loop and returns a coroutine. @@ -248,11 +248,11 @@ class FanEntity(ToggleEntity): return self.async_turn_off() return self.hass.async_add_job(self.set_speed, speed) - def set_direction(self: ToggleEntity, direction: str) -> None: + def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" raise NotImplementedError() - def async_set_direction(self: ToggleEntity, direction: str): + def async_set_direction(self, direction: str): """Set the direction of the fan. This method must be run in the event loop and returns a coroutine. @@ -260,12 +260,12 @@ class FanEntity(ToggleEntity): return self.hass.async_add_job(self.set_direction, direction) # pylint: disable=arguments-differ - def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" raise NotImplementedError() # pylint: disable=arguments-differ - def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs): + def async_turn_on(self, speed: str = None, **kwargs): """Turn on the fan. This method must be run in the event loop and returns a coroutine. @@ -275,11 +275,11 @@ class FanEntity(ToggleEntity): return self.hass.async_add_job( ft.partial(self.turn_on, speed, **kwargs)) - def oscillate(self: ToggleEntity, oscillating: bool) -> None: + def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" pass - def async_oscillate(self: ToggleEntity, oscillating: bool): + def async_oscillate(self, oscillating: bool): """Oscillate the fan. This method must be run in the event loop and returns a coroutine. @@ -297,7 +297,7 @@ class FanEntity(ToggleEntity): return None @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" return [] @@ -307,7 +307,7 @@ class FanEntity(ToggleEntity): return None @property - def state_attributes(self: ToggleEntity) -> dict: + def state_attributes(self) -> dict: """Return optional state attributes.""" data = {} # type: dict @@ -322,6 +322,6 @@ class FanEntity(ToggleEntity): return data @property - def supported_features(self: ToggleEntity) -> int: + def supported_features(self) -> int: """Flag supported features.""" return 0 diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py index 12dc0b1104f..fd3265b8230 100644 --- a/homeassistant/components/fan/comfoconnect.py +++ b/homeassistant/components/fan/comfoconnect.py @@ -31,7 +31,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ccb = hass.data[DOMAIN] add_devices([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True) - return class ComfoConnectFan(FanEntity): diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index 5b689ece6ed..fbe9ffc948c 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -8,12 +8,11 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.components.fan import ( DOMAIN, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity) from homeassistant.const import CONF_ENTITY_ID -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -100,7 +99,7 @@ class DysonPureCoolLinkDevice(FanEntity): """Return the display name of this fan.""" return self._device.name - def set_speed(self: ToggleEntity, speed: str) -> None: + def set_speed(self, speed: str) -> None: """Set the speed of the fan. Never called ??.""" from libpurecoollink.const import FanSpeed, FanMode @@ -113,7 +112,7 @@ class DysonPureCoolLinkDevice(FanEntity): self._device.set_configuration( fan_mode=FanMode.FAN, fan_speed=fan_speed) - def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" from libpurecoollink.const import FanSpeed, FanMode @@ -129,14 +128,14 @@ class DysonPureCoolLinkDevice(FanEntity): # Speed not set, just turn on self._device.set_configuration(fan_mode=FanMode.FAN) - def turn_off(self: ToggleEntity, **kwargs) -> None: + def turn_off(self, **kwargs) -> None: """Turn off the fan.""" from libpurecoollink.const import FanMode _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) - def oscillate(self: ToggleEntity, oscillating: bool) -> None: + def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" from libpurecoollink.const import Oscillation @@ -183,7 +182,7 @@ class DysonPureCoolLinkDevice(FanEntity): """Return Night mode.""" return self._device.state.night_mode == "ON" - def night_mode(self: ToggleEntity, night_mode: bool) -> None: + def night_mode(self, night_mode: bool) -> None: """Turn fan in night mode.""" from libpurecoollink.const import NightMode @@ -198,7 +197,7 @@ class DysonPureCoolLinkDevice(FanEntity): """Return auto mode.""" return self._device.state.fan_mode == "AUTO" - def auto_mode(self: ToggleEntity, auto_mode: bool) -> None: + def auto_mode(self, auto_mode: bool) -> None: """Turn fan in auto mode.""" from libpurecoollink.const import FanMode @@ -209,7 +208,7 @@ class DysonPureCoolLinkDevice(FanEntity): self._device.set_configuration(fan_mode=FanMode.FAN) @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" from libpurecoollink.const import FanSpeed @@ -230,6 +229,6 @@ class DysonPureCoolLinkDevice(FanEntity): return supported_speeds @property - def supported_features(self: ToggleEntity) -> int: + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index b8a5c99add4..28b93c86ed7 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -7,11 +7,10 @@ https://home-assistant.io/components/fan.insteon_local/ import logging from datetime import timedelta +from homeassistant import util from homeassistant.components.fan import ( ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, FanEntity) -from homeassistant.helpers.entity import ToggleEntity -import homeassistant.util as util _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -68,7 +67,7 @@ class InsteonLocalFanDevice(FanEntity): return self._speed @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] @@ -91,21 +90,18 @@ class InsteonLocalFanDevice(FanEntity): """Flag supported features.""" return SUPPORT_INSTEON_LOCAL - def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: str = None, **kwargs) -> None: """Turn device on.""" if speed is None: - if ATTR_SPEED in kwargs: - speed = kwargs[ATTR_SPEED] - else: - speed = SPEED_MEDIUM + speed = kwargs.get(ATTR_SPEED, SPEED_MEDIUM) self.set_speed(speed) - def turn_off(self: ToggleEntity, **kwargs) -> None: + def turn_off(self, **kwargs) -> None: """Turn device off.""" self.node.off() - def set_speed(self: ToggleEntity, speed: str) -> None: + def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" if self.node.on(speed): self._speed = speed diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 6fa506edec6..5faa735801d 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index a40437e719b..74fb73dae1d 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -8,21 +8,17 @@ import logging import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import ( - CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, - STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN) - -from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.fan import ( SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT, SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION) - +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, + STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN) +from homeassistant.core import callback +from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script @@ -65,7 +61,7 @@ FAN_SCHEMA = vol.Schema({ vol.Optional(CONF_ENTITY_ID): cv.entity_ids }) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), }) @@ -196,7 +192,7 @@ class TemplateFan(FanEntity): return self._supported_features @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" return self._speed_list @@ -250,8 +246,7 @@ class TemplateFan(FanEntity): await self._set_speed_script.async_run({ATTR_SPEED: speed}) else: _LOGGER.error( - 'Received invalid speed: %s. ' + - 'Expected: %s.', + 'Received invalid speed: %s. Expected: %s.', speed, self._speed_list) async def async_oscillate(self, oscillating: bool) -> None: @@ -265,8 +260,7 @@ class TemplateFan(FanEntity): {ATTR_OSCILLATING: oscillating}) else: _LOGGER.error( - 'Received invalid oscillating value: %s. ' + - 'Expected: %s.', + 'Received invalid oscillating value: %s. Expected: %s.', oscillating, ', '.join(_VALID_OSC)) async def async_set_direction(self, direction: str) -> None: @@ -280,8 +274,7 @@ class TemplateFan(FanEntity): {ATTR_DIRECTION: direction}) else: _LOGGER.error( - 'Received invalid direction: %s. ' + - 'Expected: %s.', + 'Received invalid direction: %s. Expected: %s.', direction, ', '.join(_VALID_DIRECTIONS)) async def async_added_to_hass(self): @@ -319,8 +312,7 @@ class TemplateFan(FanEntity): self._state = None else: _LOGGER.error( - 'Received invalid fan is_on state: %s. ' + - 'Expected: %s.', + 'Received invalid fan is_on state: %s. Expected: %s.', state, ', '.join(_VALID_STATES)) self._state = None @@ -340,8 +332,7 @@ class TemplateFan(FanEntity): self._speed = None else: _LOGGER.error( - 'Received invalid speed: %s. ' + - 'Expected: %s.', + 'Received invalid speed: %s. Expected: %s.', speed, self._speed_list) self._speed = None @@ -363,8 +354,8 @@ class TemplateFan(FanEntity): self._oscillating = None else: _LOGGER.error( - 'Received invalid oscillating: %s. ' + - 'Expected: True/False.', oscillating) + 'Received invalid oscillating: %s. Expected: True/False.', + oscillating) self._oscillating = None # Update direction if 'direction_template' is configured @@ -383,7 +374,6 @@ class TemplateFan(FanEntity): self._direction = None else: _LOGGER.error( - 'Received invalid direction: %s. ' + - 'Expected: %s.', + 'Received invalid direction: %s. Expected: %s.', direction, ', '.join(_VALID_DIRECTIONS)) self._direction = None diff --git a/homeassistant/components/fan/tuya.py b/homeassistant/components/fan/tuya.py new file mode 100644 index 00000000000..f19a9e5a5f7 --- /dev/null +++ b/homeassistant/components/fan/tuya.py @@ -0,0 +1,99 @@ +""" +Support for Tuya fans. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.tuya/ +""" + +from homeassistant.components.fan import ( + ENTITY_ID_FORMAT, FanEntity, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) +from homeassistant.components.tuya import DATA_TUYA, TuyaDevice +from homeassistant.const import STATE_OFF + +DEPENDENCIES = ['tuya'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tuya fan platform.""" + if discovery_info is None: + return + tuya = hass.data[DATA_TUYA] + dev_ids = discovery_info.get('dev_ids') + devices = [] + for dev_id in dev_ids: + device = tuya.get_device_by_id(dev_id) + if device is None: + continue + devices.append(TuyaFanDevice(device)) + add_devices(devices) + + +class TuyaFanDevice(TuyaDevice, FanEntity): + """Tuya fan devices.""" + + def __init__(self, tuya): + """Init Tuya fan device.""" + super().__init__(tuya) + self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + self.speeds = [STATE_OFF] + + async def async_added_to_hass(self): + """Create fan list when add to hass.""" + await super().async_added_to_hass() + self.speeds.extend(self.tuya.speed_list()) + + def set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if speed == STATE_OFF: + self.turn_off() + else: + self.tuya.set_speed(speed) + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn on the fan.""" + if speed is not None: + self.set_speed(speed) + else: + self.tuya.turn_on() + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + self.tuya.turn_off() + + def oscillate(self, oscillating) -> None: + """Oscillate the fan.""" + self.tuya.oscillate(oscillating) + + @property + def oscillating(self): + """Return current oscillating status.""" + if self.supported_features & SUPPORT_OSCILLATE == 0: + return None + if self.speed == STATE_OFF: + return False + return self.tuya.oscillating() + + @property + def is_on(self): + """Return true if the entity is on.""" + return self.tuya.state() + + @property + def speed(self) -> str: + """Return the current speed.""" + if self.is_on: + return self.tuya.speed() + return STATE_OFF + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return self.speeds + + @property + def supported_features(self) -> int: + """Flag supported features.""" + supports = SUPPORT_SET_SPEED + if self.tuya.support_oscillate(): + supports = supports | SUPPORT_OSCILLATE + return supports diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 0cebd9cb9f8..4eebacbbbf2 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -11,7 +11,6 @@ from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, STATE_UNKNOWN, SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity) from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -39,19 +38,19 @@ class WinkFanDevice(WinkDevice, FanEntity): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['fan'].append(self) - def set_direction(self: ToggleEntity, direction: str) -> None: + def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" self.wink.set_fan_direction(direction) - def set_speed(self: ToggleEntity, speed: str) -> None: + def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" self.wink.set_state(True, speed) - def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" self.wink.set_state(True, speed) - def turn_off(self: ToggleEntity, **kwargs) -> None: + def turn_off(self, **kwargs) -> None: """Turn off the fan.""" self.wink.set_state(False) @@ -82,7 +81,7 @@ class WinkFanDevice(WinkDevice, FanEntity): return self.wink.current_fan_direction() @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" wink_supported_speeds = self.wink.fan_speeds() supported_speeds = [] @@ -99,6 +98,6 @@ class WinkFanDevice(WinkDevice, FanEntity): return supported_speeds @property - def supported_features(self: ToggleEntity) -> int: + def supported_features(self) -> int: """Flag supported features.""" return SUPPORTED_FEATURES diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py index 01b1d0a92cf..983bc3a79d7 100644 --- a/homeassistant/components/fan/zha.py +++ b/homeassistant/components/fan/zha.py @@ -89,7 +89,7 @@ class ZhaFan(zha.Entity, FanEntity): yield from self.async_set_speed(SPEED_OFF) @asyncio.coroutine - def async_set_speed(self: FanEntity, speed: str) -> None: + def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" yield from self._endpoint.fan.write_attributes({ 'fan_mode': SPEED_TO_VALUE[speed]}) diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py index 364306ff8dd..645cb033e13 100644 --- a/homeassistant/components/fan/zwave.py +++ b/homeassistant/components/fan/zwave.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) from homeassistant.components import zwave -from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 73ab9e8123c..782fd8ac8dd 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -53,7 +53,7 @@ def setup(hass, config): return len(feeds) > 0 -class FeedManager(object): +class FeedManager: """Abstraction over Feedparser module.""" def __init__(self, url, scan_interval, max_entries, hass, storage): @@ -170,7 +170,7 @@ class FeedManager(object): self._firstrun = False -class StoredData(object): +class StoredData: """Abstraction over pickle data storage.""" def __init__(self, data_file): @@ -189,7 +189,7 @@ class StoredData(object): with self._lock, open(self._data_file, 'rb') as myfile: self._data = pickle.load(myfile) or {} self._cache_outdated = False - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except _LOGGER.error("Error loading data from pickled file %s", self._data_file) @@ -207,7 +207,7 @@ class StoredData(object): feed_id, self._data_file) try: pickle.dump(self._data, myfile) - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except _LOGGER.error( "Error saving pickled data to %s", self._data_file) self._cache_outdated = True diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index e083affe92b..9aaae16ee21 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -116,7 +116,7 @@ def async_setup(hass, config): return True -class FFmpegManager(object): +class FFmpegManager: """Helper for ha-ffmpeg.""" def __init__(self, hass, ffmpeg_bin, run_test): diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index fb59d6254b0..5bf2b193957 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180720.0'] +REQUIREMENTS = ['home-assistant-frontend==20180726.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', @@ -352,7 +352,7 @@ class IndexView(HomeAssistantView): def get_template(self, latest): """Get template.""" if self.repo_path is not None: - root = self.repo_path + root = os.path.join(self.repo_path, 'hass_frontend') elif latest: import hass_frontend root = hass_frontend.where() diff --git a/homeassistant/components/gc100.py b/homeassistant/components/gc100.py index 25bcb5b0f79..0d4b19da030 100644 --- a/homeassistant/components/gc100.py +++ b/homeassistant/components/gc100.py @@ -53,7 +53,7 @@ def setup(hass, base_config): return True -class GC100Device(object): +class GC100Device: """The GC100 component.""" def __init__(self, hass, gc_device): diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index fdbc3382072..e37b3ba7ff7 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -231,7 +231,7 @@ def do_setup(hass, config): return True -class GoogleCalendarService(object): +class GoogleCalendarService: """Calendar service interface to Google.""" def __init__(self, token_file): diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 927139a483e..63a3e641170 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,5 +1,5 @@ """Support for Google Assistant Smart Home API.""" -import collections +from collections.abc import Mapping from itertools import product import logging @@ -50,7 +50,7 @@ DOMAIN_TO_GOOGLE_TYPES = { def deep_update(target, source): """Update a nested dictionary with another nested dictionary.""" for key, value in source.items(): - if isinstance(value, collections.Mapping): + if isinstance(value, Mapping): target[key] = deep_update(target.get(key, {}), value) else: target[key] = value diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 2f60f226042..74ee55c5e93 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -106,9 +106,9 @@ class BrightnessTrait(_Trait): """Test if state is supported.""" if domain == light.DOMAIN: return features & light.SUPPORT_BRIGHTNESS - elif domain == cover.DOMAIN: + if domain == cover.DOMAIN: return features & cover.SUPPORT_SET_POSITION - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: return features & media_player.SUPPORT_VOLUME_SET return False diff --git a/homeassistant/components/graphite.py b/homeassistant/components/graphite.py index e4626d0f016..2b768bc3786 100644 --- a/homeassistant/components/graphite.py +++ b/homeassistant/components/graphite.py @@ -137,8 +137,8 @@ class GraphiteFeeder(threading.Thread): _LOGGER.debug("Event processing thread stopped") self._queue.task_done() return - elif (event.event_type == EVENT_STATE_CHANGED and - event.data.get('new_state')): + if event.event_type == EVENT_STATE_CHANGED and \ + event.data.get('new_state'): _LOGGER.debug("Processing STATE_CHANGED event for %s", event.data['entity_id']) try: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6ab86435371..3a3e19fb484 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -27,6 +27,8 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'hassio' DEPENDENCIES = ['http'] +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 CONF_FRONTEND_REPO = 'development_repo' @@ -140,7 +142,7 @@ def async_check_config(hass): if not result: return "Hass.io config check API error" - elif result['result'] == "error": + if result['result'] == "error": return result['message'] return None @@ -167,6 +169,21 @@ def async_setup(hass, config): _LOGGER.error("Not connected with Hass.io") return False + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + data = yield from store.async_load() + + if data is None: + data = {} + + if 'hassio_user' in data: + user = yield from hass.auth.async_get_user(data['hassio_user']) + refresh_token = list(user.refresh_tokens.values())[0] + else: + user = yield from hass.auth.async_create_system_user('Hass.io') + refresh_token = yield from hass.auth.async_create_refresh_token(user) + data['hassio_user'] = user.id + yield from store.async_save(data) + # This overrides the normal API call that would be forwarded development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) if development_repo is not None: @@ -186,8 +203,13 @@ def async_setup(hass, config): embed_iframe=True, ) - if 'http' in config: - yield from hassio.update_hass_api(config['http']) + # Temporary. No refresh token tells supervisor to use API password. + if hass.auth.active: + token = refresh_token.token + else: + token = None + + yield from hassio.update_hass_api(config.get('http', {}), token) if 'homeassistant' in config: yield from hassio.update_hass_timezone(config['homeassistant']) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index c3caf40ba62..d75529a99b0 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -23,10 +23,9 @@ X_HASSIO = 'X-HASSIO-KEY' def _api_bool(funct): """Return a boolean.""" - @asyncio.coroutine - def _wrapper(*argv, **kwargs): + async def _wrapper(*argv, **kwargs): """Wrap function.""" - data = yield from funct(*argv, **kwargs) + data = await funct(*argv, **kwargs) return data and data['result'] == "ok" return _wrapper @@ -34,10 +33,9 @@ def _api_bool(funct): def _api_data(funct): """Return data of an api.""" - @asyncio.coroutine - def _wrapper(*argv, **kwargs): + async def _wrapper(*argv, **kwargs): """Wrap function.""" - data = yield from funct(*argv, **kwargs) + data = await funct(*argv, **kwargs) if data and data['result'] == "ok": return data['data'] return None @@ -45,7 +43,7 @@ def _api_data(funct): return _wrapper -class HassIO(object): +class HassIO: """Small API wrapper for Hass.io.""" def __init__(self, loop, websession, ip): @@ -94,24 +92,23 @@ class HassIO(object): return self.send_command("/homeassistant/check", timeout=300) @_api_bool - def update_hass_api(self, http_config): - """Update Home Assistant API data on Hass.io. - - This method return a coroutine. - """ + async def update_hass_api(self, http_config, refresh_token): + """Update Home Assistant API data on Hass.io.""" port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT options = { 'ssl': CONF_SSL_CERTIFICATE in http_config, 'port': port, 'password': http_config.get(CONF_API_PASSWORD), 'watchdog': True, + 'refresh_token': refresh_token, } if CONF_SERVER_HOST in http_config: options['watchdog'] = False _LOGGER.warning("Don't use 'server_host' options with Hass.io") - return self.send_command("/homeassistant/options", payload=options) + return await self.send_command("/homeassistant/options", + payload=options) @_api_bool def update_hass_timezone(self, core_config): diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 7ee1c70487f..21d4cdc6e56 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -353,7 +353,7 @@ class HistoryPeriodView(HomeAssistantView): return await hass.async_add_job(self.json, result) -class Filters(object): +class Filters: """Container for the configured include and exclude filters.""" def __init__(self): diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 611043c1b16..ad2f8b4ac6d 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -9,7 +9,7 @@ from zlib import adler32 import voluptuous as vol -import homeassistant.components.cover as cover +from homeassistant.components import cover from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, @@ -173,7 +173,7 @@ def get_accessory(hass, driver, state, aid, config): def generate_aid(entity_id): """Generate accessory aid with zlib adler32.""" aid = adler32(entity_id.encode('utf-8')) - if aid == 0 or aid == 1: + if aid in (0, 1): return None return aid diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 373c1188f2d..d4c2cb58209 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -181,6 +181,6 @@ class BinarySensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" state = new_state.state - detected = (state == STATE_ON) or (state == STATE_HOME) + detected = state in (STATE_ON, STATE_HOME) self.char_detected.set_value(detected) _LOGGER.debug('%s: Set to %d', self.entity_id, detected) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 73a29990fba..8517122f6a8 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -124,8 +124,7 @@ class Thermostat(HomeAccessory): if hass_value == STATE_OFF: self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) return - else: - self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OPERATION_MODE: hass_value} self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 6a43a0c6228..23a907d43f7 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -import homeassistant.components.media_player as media_player +from homeassistant.components import media_player from homeassistant.core import split_entity_id from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) @@ -142,10 +142,10 @@ def density_to_air_quality(density): """Map PM2.5 density to HomeKit AirQuality level.""" if density <= 35: return 1 - elif density <= 75: + if density <= 75: return 2 - elif density <= 115: + if density <= 115: return 3 - elif density <= 150: + if density <= 150: return 4 return 5 diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 6754db05f77..f737e2ad7d2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.45'] +REQUIREMENTS = ['pyhomematic==0.1.46'] _LOGGER = logging.getLogger(__name__) @@ -61,7 +61,8 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'], + 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic', + 'IPKeySwitchPowermeter'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_SENSORS: [ 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', @@ -71,7 +72,8 @@ HM_DEVICE_TYPES = { 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', - 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor'], + 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', + 'IPKeySwitchPowermeter'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -80,7 +82,8 @@ HM_DEVICE_TYPES = { 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', - 'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor'], + 'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor', + 'SmartwareMotion'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], DISCOVER_LOCKS: ['KeyMatic'] } diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 9e5356d914a..3be89172e27 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -70,8 +70,7 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler): HMIPC_NAME: self.auth.config.get(HMIPC_NAME) }) return self.async_abort(reason='conection_aborted') - else: - errors['base'] = 'press_the_button' + errors['base'] = 'press_the_button' return self.async_show_form(step_id='link', errors=errors) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 94fe5f40be8..bb21e1df3d5 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -62,10 +62,21 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach + @property + def icon(self): + """Return the icon.""" + if hasattr(self._device, 'lowBat') and self._device.lowBat: + return 'mdi:battery-outline' + if hasattr(self._device, 'sabotage') and self._device.sabotage: + return 'mdi:alert' + return None + @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - return { - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_MODEL_TYPE: self._device.modelType - } + attr = {ATTR_MODEL_TYPE: self._device.modelType} + if hasattr(self._device, 'lowBat') and self._device.lowBat: + attr.update({ATTR_LOW_BATTERY: self._device.lowBat}) + if hasattr(self._device, 'sabotage') and self._device.sabotage: + attr.update({ATTR_SABOTAGE: self._device.sabotage}) + return attr diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index a4e3e78e860..9715a5fc024 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -14,7 +14,7 @@ from .errors import HmipcConnectionError _LOGGER = logging.getLogger(__name__) -class HomematicipAuth(object): +class HomematicipAuth: """Manages HomematicIP client registration.""" def __init__(self, hass, config): @@ -73,7 +73,7 @@ class HomematicipAuth(object): return auth -class HomematicipHAP(object): +class HomematicipHAP: """Manages HomematicIP http and websocket connection.""" def __init__(self, hass, config_entry): @@ -117,7 +117,7 @@ class HomematicipHAP(object): self.config_entry.data.get(HMIPC_HAPID)) for component in COMPONENTS: - self.hass.async_add_job( + self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( self.config_entry, component) ) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0cbee628a8a..42629f752ad 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -152,7 +152,7 @@ async def async_setup(hass, config): return True -class HomeAssistantHTTP(object): +class HomeAssistantHTTP: """HTTP server for Home Assistant.""" def __init__(self, hass, api_password, diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 2cc62dce38e..77621e3bc7c 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -20,6 +20,8 @@ _LOGGER = logging.getLogger(__name__) def setup_auth(app, trusted_networks, use_auth, support_legacy=False, api_password=None): """Create auth middleware for the app.""" + old_auth_warning = set() + @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" @@ -27,8 +29,10 @@ def setup_auth(app, trusted_networks, use_auth, if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query): - _LOGGER.warning('Please change to use bearer token access %s', - request.path) + if request.path not in old_auth_warning: + _LOGGER.warning('Please change to use bearer token access %s', + request.path) + old_auth_warning.add(request.path) legacy_auth = (not use_auth or support_legacy) and api_password if (hdrs.AUTHORIZATION in request.headers and @@ -109,7 +113,7 @@ async def async_validate_auth_header(request, api_password=None): request['hass_user'] = access_token.refresh_token.user return True - elif auth_type == 'Basic' and api_password is not None: + if auth_type == 'Basic' and api_password is not None: decoded = base64.b64decode(auth_val).decode('utf-8') try: username, password = decoded.split(':', 1) @@ -123,5 +127,4 @@ async def async_validate_auth_header(request, api_password=None): return hmac.compare_digest(api_password.encode('utf-8'), password.encode('utf-8')) - else: - return False + return False diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index e05f951322e..ab582066a22 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -1,5 +1,4 @@ """Ban logic for HTTP component.""" - from collections import defaultdict from datetime import datetime from ipaddress import ip_address @@ -71,6 +70,17 @@ async def ban_middleware(request, handler): raise +def log_invalid_auth(func): + """Decorator to handle invalid auth or failed login attempts.""" + async def handle_req(view, request, *args, **kwargs): + """Try to log failed login attempts if response status >= 400.""" + resp = await func(view, request, *args, **kwargs) + if resp.status >= 400: + await process_wrong_login(request) + return resp + return handle_req + + async def process_wrong_login(request): """Process a wrong login attempt. diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index cd07ab6df69..8b28a7cf288 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -31,10 +31,9 @@ class CachingStaticResource(StaticResource): if filepath.is_dir(): return await super()._handle(request) - elif filepath.is_file(): + if filepath.is_file(): return CachingFileResponse(filepath, chunk_size=self._chunk_size) - else: - raise HTTPNotFound + raise HTTPNotFound # pylint: disable=too-many-ancestors diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 98f5a5b0a84..22ef34de54a 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -13,7 +13,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError import homeassistant.remote as rem from homeassistant.components.http.ban import process_success_login -from homeassistant.core import is_callback +from homeassistant.core import Context, is_callback from homeassistant.const import CONTENT_TYPE_JSON from .const import KEY_AUTHENTICATED, KEY_REAL_IP @@ -22,7 +22,7 @@ from .const import KEY_AUTHENTICATED, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) -class HomeAssistantView(object): +class HomeAssistantView: """Base view for all views.""" url = None @@ -32,6 +32,14 @@ class HomeAssistantView(object): cors_allowed = False # pylint: disable=no-self-use + def context(self, request): + """Generate a context from a request.""" + user = request.get('hass_user') + if user is None: + return Context() + + return Context(user_id=user.id) + def json(self, result, status_code=200, headers=None): """Return a JSON response.""" try: diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 8710b2561b0..b7cf0e1de07 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -19,7 +19,7 @@ SCENE_SCHEMA = vol.Schema({ }) -class HueBridge(object): +class HueBridge: """Manages a single Hue bridge.""" def __init__(self, hass, config_entry, allow_unreachable, allow_groups): @@ -78,7 +78,7 @@ class HueBridge(object): host) return False - hass.async_add_job(hass.config_entries.async_forward_entry_setup( + hass.async_create_task(hass.config_entries.async_forward_entry_setup( self.config_entry, 'light')) hass.services.async_register( diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index af67a594495..a7fe3ff04e0 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -84,7 +84,7 @@ class HueFlowHandler(data_entry_flow.FlowHandler): reason='all_configured' ) - elif len(hosts) == 1: + if len(hosts) == 1: self.host = hosts[0] return await self.async_step_link() diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise.py index a60e3d5b8fc..0c4db63034e 100644 --- a/homeassistant/components/hydrawise.py +++ b/homeassistant/components/hydrawise.py @@ -101,7 +101,7 @@ def setup(hass, config): return True -class HydrawiseHub(object): +class HydrawiseHub: """Representation of a base Hydrawise device.""" def __init__(self, data): diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py index 0a4ad66ce56..9497282ab21 100644 --- a/homeassistant/components/ifttt.py +++ b/homeassistant/components/ifttt.py @@ -63,7 +63,7 @@ def setup(hass, config): value3 = call.data.get(ATTR_VALUE3) try: - import pyfttt as pyfttt + import pyfttt pyfttt.send_event(key, event, value1, value2, value3) except requests.exceptions.RequestException: _LOGGER.exception("Error communicating with IFTTT") diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 0c0100bc9f5..672964f765e 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -167,7 +167,10 @@ def get_discovery_info(component_setup, groups): name = '{}_{}'.format(groupname, ihc_id) device = { 'ihc_id': ihc_id, - 'product': product, + 'product': { + 'name': product.attrib['name'], + 'note': product.attrib['note'], + 'position': product.attrib['position']}, 'product_cfg': product_cfg} discovery_data[name] = device return discovery_data diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index de6db875def..2ccca366d90 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,6 +1,5 @@ """Implementation of a base class for all IHC devices.""" import asyncio -from xml.etree.ElementTree import Element from homeassistant.helpers.entity import Entity @@ -14,16 +13,16 @@ class IHCDevice(Entity): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - product: Element = None) -> None: + product=None) -> None: """Initialize IHC attributes.""" self.ihc_controller = ihc_controller self._name = name self.ihc_id = ihc_id self.info = info if product: - self.ihc_name = product.attrib['name'] - self.ihc_note = product.attrib['note'] - self.ihc_position = product.attrib['position'] + self.ihc_name = product['name'] + self.ihc_note = product['note'] + self.ihc_position = product['position'] else: self.ihc_name = '' self.ihc_note = '' diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index bda0e1bc550..0b57dba8bca 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -103,7 +103,7 @@ class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): _LOGGER.error("Can't process image on microsoft face: %s", err) return - if face_data is None or len(face_data) < 1: + if not face_data: return faces = [] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 8984f25cdf2..9479a804a44 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -90,7 +90,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): face_data = yield from self._api.call_api( 'post', 'detect', image, binary=True) - if face_data is None or len(face_data) < 1: + if not face_data: return face_ids = [data['faceId'] for data in face_data] diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index ca0f3527f73..00ae01f1123 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.5'] +REQUIREMENTS = ['numpy==1.15.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 82fc6b02266..055015b74f5 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.11.3'] +REQUIREMENTS = ['insteonplm==0.11.7'] _LOGGER = logging.getLogger(__name__) @@ -55,6 +55,11 @@ SRV_HOUSECODE = 'housecode' HOUSECODES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'] +BUTTON_PRESSED_STATE_NAME = 'onLevelButton' +EVENT_BUTTON_ON = 'insteon_plm.button_on' +EVENT_BUTTON_OFF = 'insteon_plm.button_off' +EVENT_CONF_BUTTON = 'button' + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ vol.Required(CONF_ADDRESS): cv.string, @@ -130,15 +135,20 @@ def async_setup(hass, config): """Detect device from transport to be delegated to platform.""" for state_key in device.states: platform_info = ipdb[device.states[state_key]] - if platform_info: + if platform_info and platform_info.platform: platform = platform_info.platform - if platform: + + if platform == 'on_off_events': + device.states[state_key].register_updates( + _fire_button_on_off_event) + + else: _LOGGER.info("New INSTEON PLM device: %s (%s) %s", device.address, device.states[state_key].name, platform) - hass.async_add_job( + hass.async_create_task( discovery.async_load_platform( hass, platform, DOMAIN, discovered={'address': device.address.id, @@ -223,6 +233,23 @@ def async_setup(hass, config): schema=X10_HOUSECODE_SCHEMA) _LOGGER.debug("Insteon_plm Services registered") + def _fire_button_on_off_event(address, group, val): + # Firing an event when a button is pressed. + device = plm.devices[address.hex] + state_name = device.states[group].name + button = ("" if state_name == BUTTON_PRESSED_STATE_NAME + else state_name[-1].lower()) + schema = {CONF_ADDRESS: address.hex} + if button != "": + schema[EVENT_CONF_BUTTON] = button + if val: + event = EVENT_BUTTON_ON + else: + event = EVENT_BUTTON_OFF + _LOGGER.debug('Firing event %s with address %s and button %s', + event, address.hex, button) + hass.bus.fire(event, schema) + _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( device=port, @@ -289,7 +316,7 @@ def async_setup(hass, config): State = collections.namedtuple('Product', 'stateType platform') -class IPDB(object): +class IPDB: """Embodies the INSTEON Product Database static data and access methods.""" def __init__(self): @@ -329,7 +356,7 @@ class IPDB(object): State(DimmableSwitch_Fan, 'fan'), State(DimmableSwitch, 'light'), - State(DimmableRemote, 'binary_sensor'), + State(DimmableRemote, 'on_off_events'), State(X10DimmableSwitch, 'light'), State(X10OnOffSwitch, 'switch'), diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index bbd7bc44082..9a7cc7caecb 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -163,7 +163,7 @@ class KeyboardRemoteThread(threading.Thread): ) -class KeyboardRemote(object): +class KeyboardRemote: """Sets up one thread per device.""" def __init__(self, hass, config): diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index 61f8ca90137..5b3af3029b4 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -107,7 +107,7 @@ async def async_setup(hass, config): ('scene', 'Scene'), ('notify', 'Notification')): found_devices = _get_devices(hass, discovery_type) - hass.async_add_job( + hass.async_create_task( discovery.async_load_platform(hass, component, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices }, config)) @@ -129,7 +129,7 @@ def _get_devices(hass, discovery_type): hass.data[DATA_KNX].xknx.devices))) -class KNXModule(object): +class KNXModule: """Representation of KNX Object.""" def __init__(self, hass, config): @@ -172,7 +172,7 @@ class KNXModule(object): """Return the connection_config.""" if CONF_KNX_TUNNELING in self.config[DOMAIN]: return self.connection_config_tunneling() - elif CONF_KNX_ROUTING in self.config[DOMAIN]: + if CONF_KNX_ROUTING in self.config[DOMAIN]: return self.connection_config_routing() return self.connection_config_auto() @@ -284,7 +284,7 @@ class KNXAutomation(): device.actions.append(self.action) -class KNXExposeTime(object): +class KNXExposeTime: """Object to Expose Time/Date object to KNX bus.""" def __init__(self, xknx, expose_type, address): @@ -308,7 +308,7 @@ class KNXExposeTime(object): self.xknx.devices.add(self.device) -class KNXExposeSensor(object): +class KNXExposeSensor: """Object to Expose HASS entity to KNX bus.""" def __init__(self, hass, xknx, expose_type, entity_id, address): diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 26fe356d772..a3e9ff86ed0 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -114,7 +114,7 @@ async def async_setup(hass, config): return True -class KonnectedDevice(object): +class KonnectedDevice: """A representation of a single Konnected device.""" def __init__(self, hass, host, port, config): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b8a97607215..8b4b2137711 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -83,17 +83,6 @@ COLOR_GROUP = "Color descriptors" LIGHT_PROFILES_FILE = "light_profiles.csv" -PROP_TO_ATTR = { - 'brightness': ATTR_BRIGHTNESS, - 'color_temp': ATTR_COLOR_TEMP, - 'min_mireds': ATTR_MIN_MIREDS, - 'max_mireds': ATTR_MAX_MIREDS, - 'hs_color': ATTR_HS_COLOR, - 'white_value': ATTR_WHITE_VALUE, - 'effect_list': ATTR_EFFECT_LIST, - 'effect': ATTR_EFFECT, -} - # Service call validation schemas VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) @@ -357,7 +346,12 @@ async def async_setup(hass, config): update_tasks = [] for light in target_lights: if service.service == SERVICE_TURN_ON: - await light.async_turn_on(**params) + pars = params + if not pars: + pars = params.copy() + pars[ATTR_PROFILE] = Profiles.get_default(light.entity_id) + preprocess_turn_on_alternatives(pars) + await light.async_turn_on(**pars) elif service.service == SERVICE_TURN_OFF: await light.async_turn_off(**params) else: @@ -365,7 +359,9 @@ async def async_setup(hass, config): if not light.should_poll: continue - update_tasks.append(light.async_update_ha_state(True)) + + update_tasks.append( + light.async_update_ha_state(True, service.context)) if update_tasks: await asyncio.wait(update_tasks, loop=hass.loop) @@ -442,6 +438,18 @@ class Profiles: """Return a named profile.""" return cls._all.get(name) + @classmethod + def get_default(cls, entity_id): + """Return the default turn-on profile for the given light.""" + # pylint: disable=unsupported-membership-test + name = entity_id + ".default" + if name in cls._all: + return name + name = ENTITY_ID_ALL_LIGHTS + ".default" + if name in cls._all: + return name + return None + class Light(ToggleEntity): """Representation of a light.""" @@ -494,29 +502,37 @@ class Light(ToggleEntity): def state_attributes(self): """Return optional state attributes.""" data = {} + supported_features = self.supported_features - if self.supported_features & SUPPORT_COLOR_TEMP: + if supported_features & SUPPORT_COLOR_TEMP: data[ATTR_MIN_MIREDS] = self.min_mireds data[ATTR_MAX_MIREDS] = self.max_mireds if self.is_on: - for prop, attr in PROP_TO_ATTR.items(): - value = getattr(self, prop) - if value is not None: - data[attr] = value + if supported_features & SUPPORT_BRIGHTNESS: + data[ATTR_BRIGHTNESS] = self.brightness - # Expose current color also as RGB and XY - if ATTR_HS_COLOR in data: - data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB( - *data[ATTR_HS_COLOR]) - data[ATTR_XY_COLOR] = color_util.color_hs_to_xy( - *data[ATTR_HS_COLOR]) + if supported_features & SUPPORT_COLOR_TEMP: + data[ATTR_COLOR_TEMP] = self.color_temp + + if self.supported_features & SUPPORT_COLOR and self.hs_color: + # pylint: disable=unsubscriptable-object,not-an-iterable + hs_color = self.hs_color data[ATTR_HS_COLOR] = ( - round(data[ATTR_HS_COLOR][0], 3), - round(data[ATTR_HS_COLOR][1], 3), + round(hs_color[0], 3), + round(hs_color[1], 3), ) + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) - return data + if supported_features & SUPPORT_WHITE_VALUE: + data[ATTR_WHITE_VALUE] = self.white_value + + if supported_features & SUPPORT_EFFECT: + data[ATTR_EFFECT_LIST] = self.effect_list + data[ATTR_EFFECT] = self.effect + + return {key: val for key, val in data.items() if val is not None} @property def supported_features(self): diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py index 8b7e09d86bc..431f5d12ff0 100644 --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/light/abode.py @@ -88,7 +88,7 @@ class AbodeLight(AbodeDevice, Light): """Flag supported features.""" if self._device.is_dimmable and self._device.has_color: return SUPPORT_BRIGHTNESS | SUPPORT_COLOR - elif self._device.is_dimmable: + if self._device.is_dimmable: return SUPPORT_BRIGHTNESS return 0 diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index c5cd9a8c4fd..2b53fb65054 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -245,7 +245,7 @@ class FluxLight(Light): return # Effect selection - elif effect in EFFECT_MAP: + if effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) return diff --git a/homeassistant/components/light/futurenow.py b/homeassistant/components/light/futurenow.py new file mode 100644 index 00000000000..1777376881e --- /dev/null +++ b/homeassistant/components/light/futurenow.py @@ -0,0 +1,130 @@ +""" +Support for FutureNow Ethernet unit outputs as Lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.futurenow/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, + PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyfnip==0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DRIVER = 'driver' +CONF_DRIVER_FNIP6X10AD = 'FNIP6x10ad' +CONF_DRIVER_FNIP8X10A = 'FNIP8x10a' +CONF_DRIVER_TYPES = [CONF_DRIVER_FNIP6X10AD, CONF_DRIVER_FNIP8X10A] + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional('dimmable', default=False): cv.boolean, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_DEVICES): {cv.string: DEVICE_SCHEMA}, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the light platform for each FutureNow unit.""" + lights = [] + for channel, device_config in config[CONF_DEVICES].items(): + device = {} + device['name'] = device_config[CONF_NAME] + device['dimmable'] = device_config['dimmable'] + device['channel'] = channel + device['driver'] = config[CONF_DRIVER] + device['host'] = config[CONF_HOST] + device['port'] = config[CONF_PORT] + lights.append(FutureNowLight(device)) + + add_devices(lights, True) + + +def to_futurenow_level(level): + """Convert the given HASS light level (0-255) to FutureNow (0-100).""" + return int((level * 100) / 255) + + +def to_hass_level(level): + """Convert the given FutureNow (0-100) light level to HASS (0-255).""" + return int((level * 255) / 100) + + +class FutureNowLight(Light): + """Representation of an FutureNow light.""" + + def __init__(self, device): + """Initialize the light.""" + import pyfnip + + self._name = device['name'] + self._dimmable = device['dimmable'] + self._channel = device['channel'] + self._brightness = None + self._last_brightness = 255 + self._state = None + + if device['driver'] == CONF_DRIVER_FNIP6X10AD: + self._light = pyfnip.FNIP6x2adOutput(device['host'], + device['port'], + self._channel) + if device['driver'] == CONF_DRIVER_FNIP8X10A: + self._light = pyfnip.FNIP8x10aOutput(device['host'], + device['port'], + self._channel) + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + if self._dimmable: + return SUPPORT_BRIGHTNESS + return 0 + + def turn_on(self, **kwargs): + """Turn the light on.""" + if self._dimmable: + level = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) + else: + level = 255 + self._light.turn_on(to_futurenow_level(level)) + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._light.turn_off() + if self._brightness: + self._last_brightness = self._brightness + + def update(self): + """Fetch new state data for this light.""" + state = int(self._light.is_on()) + self._state = bool(state) + self._brightness = to_hass_level(state) diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py index 8e9d93657ce..52a70532005 100644 --- a/homeassistant/components/light/greenwave.py +++ b/homeassistant/components/light/greenwave.py @@ -121,7 +121,7 @@ class GreenwaveLight(Light): self._name = bulbs[self._did]['name'] -class GatewayData(object): +class GatewayData: """Handle Gateway data and limit updates.""" def __init__(self, host, token): diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 837a6f82510..0da59b6f100 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -11,7 +11,7 @@ import random import async_timeout -import homeassistant.components.hue as hue +from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_TRANSITION, ATTR_HS_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 8ba2329af7e..cbac8cf4e20 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -146,10 +146,7 @@ class Hyperion(Light): else: rgb_color = self._rgb_mem - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - else: - brightness = self._brightness + brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) if ATTR_EFFECT in kwargs: self._skip_update = True diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/light/ihc.py index c9ceda8651a..5a7e85d50dc 100644 --- a/homeassistant/components/light/ihc.py +++ b/homeassistant/components/light/ihc.py @@ -3,8 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.ihc/ """ -from xml.etree.ElementTree import Element - import voluptuous as vol from homeassistant.components.ihc import ( @@ -64,7 +62,7 @@ class IhcLight(IHCDevice, Light): """ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - dimmable=False, product: Element = None) -> None: + dimmable=False, product=None) -> None: """Initialize the light.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._brightness = 0 diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index bd7814df8f3..e2bc54de517 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -9,7 +9,7 @@ from datetime import timedelta from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -import homeassistant.util as util +from homeassistant import util _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 9b2c183c1d1..3738fd8f004 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -204,7 +204,7 @@ def merge_hsbk(base, change): return [b if c is None else c for b, c in zip(base, change)] -class LIFXManager(object): +class LIFXManager: """Representation of all known LIFX entities.""" def __init__(self, hass, async_add_devices): diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py index 182d7536dc4..3ad75a1cea4 100644 --- a/homeassistant/components/light/lifx_legacy.py +++ b/homeassistant/components/light/lifx_legacy.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): lifx_library.probe() -class LIFX(object): +class LIFX: """Representation of a LIFX light.""" def __init__(self, add_devices_callback, server_addr=None, diff --git a/homeassistant/components/light/litejet.py b/homeassistant/components/light/litejet.py index 2ebe766c8c5..b8491b6f0f5 100644 --- a/homeassistant/components/light/litejet.py +++ b/homeassistant/components/light/litejet.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/light.litejet/ """ import logging -import homeassistant.components.litejet as litejet +from homeassistant.components import litejet from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py index 09f0a337cc3..29186b8fcd2 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/light/lutron_caseta.py @@ -48,10 +48,7 @@ class LutronCasetaLight(LutronCasetaDevice, Light): @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - else: - brightness = 255 + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._smartbridge.set_value(self._device_id, to_lutron_level(brightness)) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index c0e363f85d6..09fa094c1b2 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, @@ -205,7 +205,7 @@ class MqttLight(MqttAvailability, Light): topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and SUPPORT_COLOR_TEMP) self._supported_features |= ( - topic[CONF_EFFECT_STATE_TOPIC] is not None and + topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT) self._supported_features |= ( topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 705e106fdff..d17c7dd73bf 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -9,7 +9,7 @@ import json import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index f6b3fbe8b70..ffa73aca915 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index cdfe2fe5671..293783ee3ab 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.rfxtrx as rfxtrx +from homeassistant.components import rfxtrx from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py index 214a2d99449..3d567afe09e 100644 --- a/homeassistant/components/light/scsgate.py +++ b/homeassistant/components/light/scsgate.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.scsgate as scsgate +from homeassistant.components import scsgate from homeassistant.components.light import (Light, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME) diff --git a/homeassistant/components/light/sisyphus.py b/homeassistant/components/light/sisyphus.py new file mode 100644 index 00000000000..ded78716317 --- /dev/null +++ b/homeassistant/components/light/sisyphus.py @@ -0,0 +1,78 @@ +""" +Support for the light on the Sisyphus Kinetic Art Table. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.sisyphus/ +""" +import logging + +from homeassistant.const import CONF_NAME +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.components.sisyphus import DATA_SISYPHUS + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['sisyphus'] + +SUPPORTED_FEATURES = SUPPORT_BRIGHTNESS + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a single Sisyphus table.""" + name = discovery_info[CONF_NAME] + add_devices( + [SisyphusLight(name, hass.data[DATA_SISYPHUS][name])], + update_before_add=True) + + +class SisyphusLight(Light): + """Represents a Sisyphus table as a light.""" + + def __init__(self, name, table): + """ + Constructor. + + :param name: name of the table + :param table: sisyphus-control Table object + """ + self._name = name + self._table = table + + async def async_added_to_hass(self): + """Add listeners after this object has been initialized.""" + self._table.add_listener( + lambda: self.async_schedule_update_ha_state(False)) + + @property + def name(self): + """Return the ame of the table.""" + return self._name + + @property + def is_on(self): + """Return True if the table is on.""" + return not self._table.is_sleeping + + @property + def brightness(self): + """Return the current brightness of the table's ring light.""" + return self._table.brightness * 255 + + @property + def supported_features(self): + """Return the features supported by the table; i.e. brightness.""" + return SUPPORTED_FEATURES + + async def async_turn_off(self, **kwargs): + """Put the table to sleep.""" + await self._table.sleep() + _LOGGER.debug("Sisyphus table %s: sleep") + + async def async_turn_on(self, **kwargs): + """Wake up the table if necessary, optionally changes brightness.""" + if not self.is_on: + await self._table.wakeup() + _LOGGER.debug("Sisyphus table %s: wakeup") + + if "brightness" in kwargs: + await self._table.set_brightness(kwargs["brightness"] / 255.0) diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 38cac649a1a..ad77b734fbb 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -248,8 +248,7 @@ class LightTemplate(Light): self._state = state in ('true', STATE_ON) else: _LOGGER.error( - 'Received invalid light is_on state: %s. ' + - 'Expected: %s', + 'Received invalid light is_on state: %s. Expected: %s', state, ', '.join(_VALID_STATES)) self._state = None @@ -264,8 +263,7 @@ class LightTemplate(Light): self._brightness = int(brightness) else: _LOGGER.error( - 'Received invalid brightness : %s' + - 'Expected: 0-255', + 'Received invalid brightness : %s. Expected: 0-255', brightness) self._brightness = None diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 669901f5b57..9374c1418f0 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -140,8 +140,6 @@ class TPLinkSmartBulb(Light): """Update the TP-Link Bulb's state.""" from pyHS100 import SmartDeviceException try: - self._available = True - if self._supported_features == 0: self.get_features() @@ -182,9 +180,13 @@ class TPLinkSmartBulb(Light): # device returned no daily/monthly history pass + self._available = True + except (SmartDeviceException, OSError) as ex: - _LOGGER.warning("Could not read state for %s: %s", self._name, ex) - self._available = False + if self._available: + _LOGGER.warning( + "Could not read state for %s: %s", self._name, ex) + self._available = False @property def supported_features(self): diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 4cd34b698da..4c912d60fb7 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -8,7 +8,7 @@ import asyncio import logging from datetime import timedelta -import homeassistant.util as util +from homeassistant import util from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) @@ -27,7 +27,7 @@ SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | def setup_platform(hass, config, add_devices, discovery_info=None): """Set up discovered WeMo switches.""" - import pywemo.discovery as discovery + from pywemo import discovery if discovery_info is not None: location = discovery_info['ssdp_description'] diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index f468e8c25ef..55feef496f8 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light) from homeassistant.components import zwave -from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import from homeassistant.const import STATE_OFF, STATE_ON import homeassistant.util.color as color_util diff --git a/homeassistant/components/linode.py b/homeassistant/components/linode.py index 962e30774b8..c98ef16c7ed 100644 --- a/homeassistant/components/linode.py +++ b/homeassistant/components/linode.py @@ -62,7 +62,7 @@ def setup(hass, config): return True -class Linode(object): +class Linode: """Handle all communication with the Linode API.""" def __init__(self, access_token): diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index d8af22cd5c3..45029e679a5 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index eb2e8391221..c4fcf53a9c1 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -140,7 +140,7 @@ class LogbookView(HomeAssistantView): return await hass.async_add_job(json_events) -class Entry(object): +class Entry: """A human readable version of the log.""" def __init__(self, when=None, name=None, message=None, domain=None, @@ -380,16 +380,16 @@ def _entry_message_from_state(domain, state): return 'is away' return 'is at {}'.format(state.state) - elif domain == 'sun': + if domain == 'sun': if state.state == sun.STATE_ABOVE_HORIZON: return 'has risen' return 'has set' - elif state.state == STATE_ON: + if state.state == STATE_ON: # Future: combine groups and its entity entries ? return "turned on" - elif state.state == STATE_OFF: + if state.state == STATE_OFF: return "turned off" return "changed to {}".format(state.state) diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py index 7b1b7417cfd..2535fb76120 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta.py @@ -63,8 +63,8 @@ def async_setup(hass, base_config): _LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST]) for component in LUTRON_CASETA_COMPONENTS: - hass.async_add_job(discovery.async_load_platform(hass, component, - DOMAIN, {}, config)) + hass.async_create_task(discovery.async_load_platform( + hass, component, DOMAIN, {}, config)) return True diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 8ff3746889e..6a648e4dc47 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -132,7 +132,7 @@ class MailboxEntity(Entity): self.message_count = len(messages) -class Mailbox(object): +class Mailbox: """Represent a mailbox device.""" def __init__(self, hass, name): diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py index ccb371de2f8..8096a4fabb7 100644 --- a/homeassistant/components/mailbox/demo.py +++ b/homeassistant/components/mailbox/demo.py @@ -9,7 +9,7 @@ import logging import os from hashlib import sha1 -import homeassistant.util.dt as dt +from homeassistant.util import dt from homeassistant.components.mailbox import (Mailbox, CONTENT_TYPE_MPEG, StreamError) diff --git a/homeassistant/components/mailgun.py b/homeassistant/components/mailgun.py index ec480ac12d6..7cb7ef7151d 100644 --- a/homeassistant/components/mailgun.py +++ b/homeassistant/components/mailgun.py @@ -48,4 +48,3 @@ class MailgunReceiveMessageView(HomeAssistantView): hass = request.app['hass'] data = yield from request.post() hass.bus.async_fire(MESSAGE_RECEIVED, dict(data)) - return diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py index b2805c994e8..5f6c30aaeba 100644 --- a/homeassistant/components/matrix.py +++ b/homeassistant/components/matrix.py @@ -96,7 +96,7 @@ def setup(hass, config): return True -class MatrixBot(object): +class MatrixBot: """The Matrix Bot.""" def __init__(self, hass, config_file, homeserver, verify_ssl, diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index bca7a1b4ab7..b574f0bcb15 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -79,7 +79,7 @@ def setup(hass, config): return True -class MaxCubeHandle(object): +class MaxCubeHandle: """Keep the cube instance in one place and centralize the update.""" def __init__(self, cube, scan_interval): diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 21accdf84b3..029d10ea00a 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.07.04'] +REQUIREMENTS = ['youtube_dl==2018.07.29'] _LOGGER = logging.getLogger(__name__) @@ -58,7 +58,7 @@ class MEQueryException(Exception): pass -class MediaExtractor(object): +class MediaExtractor: """Class which encapsulates all extraction logic.""" def __init__(self, hass, component_config, call_data): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d314dec65ea..c475291227a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -6,12 +6,13 @@ https://home-assistant.io/components/media_player/ """ import asyncio import base64 +import collections from datetime import timedelta import functools as ft -import collections import hashlib import logging from random import SystemRandom +from urllib.parse import urlparse from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL @@ -956,6 +957,9 @@ async def _async_fetch_image(hass, url): cache_images = ENTITY_IMAGE_CACHE[CACHE_IMAGES] cache_maxsize = ENTITY_IMAGE_CACHE[CACHE_MAXSIZE] + if urlparse(url).hostname is None: + url = hass.config.api.base_url + url + if url not in cache_images: cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)} diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 474751c2574..a74629917b3 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -100,7 +100,7 @@ class AnthemAVR(MediaPlayerDevice): if pwrstate is True: return STATE_ON - elif pwrstate is False: + if pwrstate is False: return STATE_OFF return STATE_UNKNOWN diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 37a50b39e95..d4a7ad19807 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -100,15 +100,14 @@ class AppleTvDevice(MediaPlayerDevice): if self._playing: from pyatv import const state = self._playing.play_state - if state == const.PLAY_STATE_IDLE or \ - state == const.PLAY_STATE_NO_MEDIA or \ - state == const.PLAY_STATE_LOADING: + if state in (const.PLAY_STATE_IDLE, const.PLAY_STATE_NO_MEDIA, + const.PLAY_STATE_LOADING): return STATE_IDLE - elif state == const.PLAY_STATE_PLAYING: + if 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: + if state in (const.PLAY_STATE_PAUSED, + const.PLAY_STATE_FAST_FORWARD, + const.PLAY_STATE_FAST_BACKWARD): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED return STATE_STANDBY # Bad or unknown state? @@ -141,9 +140,9 @@ class AppleTvDevice(MediaPlayerDevice): media_type = self._playing.media_type if media_type == const.MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO - elif media_type == const.MEDIA_TYPE_MUSIC: + if media_type == const.MEDIA_TYPE_MUSIC: return MEDIA_TYPE_MUSIC - elif media_type == const.MEDIA_TYPE_TV: + if media_type == const.MEDIA_TYPE_TV: return MEDIA_TYPE_TVSHOW @property @@ -162,7 +161,7 @@ class AppleTvDevice(MediaPlayerDevice): def media_position_updated_at(self): """Last valid time of media position.""" state = self.state - if state == STATE_PLAYING or state == STATE_PAUSED: + if state in (STATE_PLAYING, STATE_PAUSED): return dt_util.utcnow() @asyncio.coroutine @@ -222,7 +221,7 @@ class AppleTvDevice(MediaPlayerDevice): state = self.state if state == STATE_PAUSED: return self.atv.remote_control.play() - elif state == STATE_PLAYING: + if state == STATE_PLAYING: return self.atv.remote_control.pause() def async_media_play(self): diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index 283c4af032e..5631ec06cf1 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -216,12 +216,8 @@ class BluesoundPlayer(MediaPlayerDevice): async def force_update_sync_status( self, on_updated_cb=None, raise_timeout=False): """Update the internal status.""" - resp = None - try: - resp = await self.send_bluesound_command( - 'SyncStatus', raise_timeout, raise_timeout) - except Exception: - raise + resp = await self.send_bluesound_command( + 'SyncStatus', raise_timeout, raise_timeout) if not resp: return None @@ -333,10 +329,10 @@ class BluesoundPlayer(MediaPlayerDevice): if response.status == 200: result = await response.text() - if len(result) < 1: - data = None - else: + if result: data = xmltodict.parse(result) + else: + data = None elif response.status == 595: _LOGGER.info("Status 595 returned, treating as timeout") raise BluesoundPlayer._TimeoutException() @@ -528,9 +524,9 @@ class BluesoundPlayer(MediaPlayerDevice): return STATE_GROUPED status = self._status.get('state', None) - if status == 'pause' or status == 'stop': + if status in ('pause', 'stop'): return STATE_PAUSED - elif status == 'stream' or status == 'play': + if status in ('stream', 'play'): return STATE_PLAYING return STATE_IDLE @@ -640,7 +636,7 @@ class BluesoundPlayer(MediaPlayerDevice): volume = self.volume_level if not volume: return None - return volume < 0.001 and volume >= 0 + return 0 <= volume < 0.001 @property def name(self): @@ -847,12 +843,12 @@ class BluesoundPlayer(MediaPlayerDevice): items = [x for x in self._preset_items if x['title'] == source] - if len(items) < 1: + if not items: items = [x for x in self._services_items if x['title'] == source] - if len(items) < 1: + if not items: items = [x for x in self._capture_items if x['title'] == source] - if len(items) < 1: + if not items: return selected_source = items[0] @@ -974,6 +970,5 @@ class BluesoundPlayer(MediaPlayerDevice): if volume > 0: self._lastvol = volume return await self.send_bluesound_command('Volume?level=0') - else: - return await self.send_bluesound_command( - 'Volume?level=' + str(float(self._lastvol) * 100)) + return await self.send_bluesound_command( + 'Volume?level=' + str(float(self._lastvol) * 100)) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 464baed1686..07a379db45c 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -88,23 +88,23 @@ def setup_bravia(config, pin, hass, add_devices): if pin is None: request_configuration(config, hass, add_devices) return - else: - mac = _get_mac_address(host) - if mac is not None: - mac = mac.decode('utf8') - # If we came here and configuring this host, mark as done - if host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.info("Discovery configuration done") - # Save config - save_json( - hass.config.path(BRAVIA_CONFIG_FILE), - {host: {'pin': pin, 'host': host, 'mac': mac}}) + mac = _get_mac_address(host) + if mac is not None: + mac = mac.decode('utf8') + # If we came here and configuring this host, mark as done + if host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = hass.components.configurator + configurator.request_done(request_id) + _LOGGER.info("Discovery configuration done") - add_devices([BraviaTVDevice(host, mac, name, pin)]) + # Save config + save_json( + hass.config.path(BRAVIA_CONFIG_FILE), + {host: {'pin': pin, 'host': host, 'mac': mac}}) + + add_devices([BraviaTVDevice(host, mac, name, pin)]) def request_configuration(config, hass, add_devices): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 4e24d5f2f71..099b365c50b 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -63,7 +63,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @attr.s(slots=True, frozen=True) -class ChromecastInfo(object): +class ChromecastInfo: """Class to hold all data about a chromecast for creating connections. This also has the same attributes as the mDNS fields by zeroconf. @@ -258,7 +258,7 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, hass.async_add_job(_discover_chromecast, hass, info) -class CastStatusListener(object): +class CastStatusListener: """Helper class to handle pychromecast status callbacks. Necessary because a CastDevice entity can create a new socket client @@ -499,13 +499,13 @@ class CastDevice(MediaPlayerDevice): """Return the state of the player.""" if self.media_status is None: return None - elif self.media_status.player_is_playing: + if self.media_status.player_is_playing: return STATE_PLAYING - elif self.media_status.player_is_paused: + if self.media_status.player_is_paused: return STATE_PAUSED - elif self.media_status.player_is_idle: + if self.media_status.player_is_idle: return STATE_IDLE - elif self._chromecast is not None and self._chromecast.is_idle: + if self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF return None @@ -534,11 +534,11 @@ class CastDevice(MediaPlayerDevice): """Content type of current playing media.""" if self.media_status is None: return None - elif self.media_status.media_is_tvshow: + if self.media_status.media_is_tvshow: return MEDIA_TYPE_TVSHOW - elif self.media_status.media_is_movie: + if self.media_status.media_is_movie: return MEDIA_TYPE_MOVIE - elif self.media_status.media_is_musictrack: + if self.media_status.media_is_musictrack: return MEDIA_TYPE_MUSIC return None diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index 41713e0c5bc..6ccc6061703 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -217,7 +217,7 @@ class ChannelsPlayer(MediaPlayerDevice): """Image url of current playing media.""" if self.now_playing_image_url: return self.now_playing_image_url - elif self.channel_image_url: + if self.channel_image_url: return self.channel_image_url return 'https://getchannels.com/assets/img/icon-1024.png' diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 0758b5f3058..978a1088aa6 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -91,7 +91,7 @@ class CmusDevice(MediaPlayerDevice): """Return the media state.""" if self.status.get('status') == 'playing': return STATE_PLAYING - elif self.status.get('status') == 'paused': + if self.status.get('status') == 'paused': return STATE_PAUSED return STATE_OFF diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 2b2b9eb5c28..604fb91451e 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -21,7 +21,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.7.4'] +REQUIREMENTS = ['denonavr==0.7.5'] _LOGGER = logging.getLogger(__name__) @@ -261,7 +261,7 @@ class DenonDevice(MediaPlayerDevice): """Title of current playing media.""" if self._current_source not in self._receiver.playing_func_list: return self._current_source - elif self._title is not None: + if self._title is not None: return self._title return self._frequency diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 0adb02b6a65..89547892550 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -140,7 +140,7 @@ class DirecTvDevice(MediaPlayerDevice): """Return the title of current episode of TV show.""" if self._is_standby: return None - elif 'episodeTitle' in self._current: + if 'episodeTitle' in self._current: return self._current['episodeTitle'] return None diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 4f9a4019268..1dfb19a33be 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -206,11 +206,11 @@ class EmbyDevice(MediaPlayerDevice): state = self.device.state if state == 'Paused': return STATE_PAUSED - elif state == 'Playing': + if state == 'Playing': return STATE_PLAYING - elif state == 'Idle': + if state == 'Idle': return STATE_IDLE - elif state == 'Off': + if state == 'Off': return STATE_OFF @property @@ -230,15 +230,15 @@ class EmbyDevice(MediaPlayerDevice): media_type = self.device.media_type if media_type == 'Episode': return MEDIA_TYPE_TVSHOW - elif media_type == 'Movie': + if media_type == 'Movie': return MEDIA_TYPE_MOVIE - elif media_type == 'Trailer': + if media_type == 'Trailer': return MEDIA_TYPE_TRAILER - elif media_type == 'Music': + if media_type == 'Music': return MEDIA_TYPE_MUSIC - elif media_type == 'Video': + if media_type == 'Video': return MEDIA_TYPE_GENERIC_VIDEO - elif media_type == 'Audio': + if media_type == 'Audio': return MEDIA_TYPE_MUSIC return None diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 280a84f0828..979aec57c74 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Could not connect to firetv-server at %s", host) -class FireTV(object): +class FireTV: """The firetv-server client. Should a native Python 3 ADB module become available, python-firetv can diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py index 4b0f9d0cf21..9be4143ef2b 100644 --- a/homeassistant/components/media_player/horizon.py +++ b/homeassistant/components/media_player/horizon.py @@ -18,7 +18,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -import homeassistant.util as util +from homeassistant import util REQUIREMENTS = ['einder==0.3.1'] diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index ca0979f1752..e5f7a2f9432 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -class Itunes(object): +class Itunes: """The iTunes API client.""" def __init__(self, host, port, use_ssl): diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 7fa8d5b3fe8..8758e969db1 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -749,7 +749,7 @@ class KodiDevice(MediaPlayerDevice): if media_type == "CHANNEL": return self.server.Player.Open( {"item": {"channelid": int(media_id)}}) - elif media_type == "PLAYLIST": + if media_type == "PLAYLIST": return self.server.Player.Open( {"item": {"playlistid": int(media_id)}}) @@ -759,7 +759,7 @@ class KodiDevice(MediaPlayerDevice): @asyncio.coroutine def async_set_shuffle(self, shuffle): """Set shuffle mode, for the first player.""" - if len(self._players) < 1: + if not self._players: raise RuntimeError("Error: No active player.") yield from self.server.Player.SetShuffle( {"playerid": self._players[0]['playerid'], "shuffle": shuffle}) diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index df1ee662124..955ba7ccb32 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -18,7 +18,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN, STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) -import homeassistant.util as util +from homeassistant import util REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0'] diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 6b161f86ab0..1b5948c964a 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -202,7 +202,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): state = self._client.media_state if state == 'PLAY': return STATE_PLAYING - elif state == 'PAUSE': + if state == 'PAUSE': return STATE_PAUSED return STATE_ON if self._client.is_on else STATE_OFF diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index ad8dd0bf056..773825e0d57 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -93,7 +93,7 @@ class MpcHcDevice(MediaPlayerDevice): return STATE_OFF if state == 'playing': return STATE_PLAYING - elif state == 'paused': + if state == 'paused': return STATE_PAUSED return STATE_IDLE diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 73417e5f25d..4b3dfc2ccbb 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -141,11 +141,11 @@ class MpdDevice(MediaPlayerDevice): """Return the media state.""" if self._status is None: return STATE_OFF - elif self._status['state'] == 'play': + if self._status['state'] == 'play': return STATE_PLAYING - elif self._status['state'] == 'pause': + if self._status['state'] == 'pause': return STATE_PAUSED - elif self._status['state'] == 'stop': + if self._status['state'] == 'stop': return STATE_OFF return STATE_OFF @@ -182,9 +182,9 @@ class MpdDevice(MediaPlayerDevice): if file_name is None: return "None" return os.path.basename(file_name) - elif name is None: + if name is None: return title - elif title is None: + if title is None: return name return '{}: {}'.format(name, title) diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index 90638cd9dfc..c4d8b778095 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -253,9 +253,11 @@ class PandoraMediaPlayer(MediaPlayerDevice): _LOGGER.warning("On unexpected station list page") self._pianobar.sendcontrol('m') # press enter self._pianobar.sendcontrol('m') # do it again b/c an 'i' got in + # pylint: disable=assignment-from-none response = self.update_playing_status() elif match_idx == 3: _LOGGER.debug("Received new playlist list") + # pylint: disable=assignment-from-none response = self.update_playing_status() else: response = self._pianobar.before.decode('utf-8') @@ -294,8 +296,7 @@ class PandoraMediaPlayer(MediaPlayerDevice): time_remaining = int(cur_minutes) * 60 + int(cur_seconds) self._media_duration = int(total_minutes) * 60 + int(total_seconds) - if (time_remaining != self._time_remaining and - time_remaining != self._media_duration): + if time_remaining not in (self._time_remaining, self._media_duration): self._player_state = STATE_PLAYING elif self._player_state == STATE_PLAYING: self._player_state = STATE_PAUSED diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index ca6b9722a49..e3c6f453c35 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -573,11 +573,11 @@ class PlexClient(MediaPlayerDevice): _LOGGER.debug("Clip content type detected, " "compatibility may vary: %s", self.entity_id) return MEDIA_TYPE_TVSHOW - elif self._session_type == 'episode': + if self._session_type == 'episode': return MEDIA_TYPE_TVSHOW - elif self._session_type == 'movie': + if self._session_type == 'movie': return MEDIA_TYPE_MOVIE - elif self._session_type == 'track': + if self._session_type == 'track': return MEDIA_TYPE_MUSIC return None @@ -654,7 +654,7 @@ class PlexClient(MediaPlayerDevice): if not self._make: return None # no mute support - elif self.make.lower() == "shield android tv": + if self.make.lower() == "shield android tv": _LOGGER.debug( "Shield Android TV client detected, disabling mute " "controls: %s", self.entity_id) @@ -663,7 +663,7 @@ class PlexClient(MediaPlayerDevice): SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_TURN_OFF) # Only supports play,pause,stop (and off which really is stop) - elif self.make.lower().startswith("tivo"): + if self.make.lower().startswith("tivo"): _LOGGER.debug( "Tivo client detected, only enabling pause, play, " "stop, and off controls: %s", self.entity_id) @@ -671,7 +671,7 @@ class PlexClient(MediaPlayerDevice): SUPPORT_TURN_OFF) # Not all devices support playback functionality # Playback includes volume, stop/play/pause, etc. - elif self.device and 'playback' in self._device_protocol_capabilities: + if self.device and 'playback' in self._device_protocol_capabilities: return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY | diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index a46e781de59..5f28660f4bd 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -134,9 +134,9 @@ class RokuDevice(MediaPlayerDevice): if (self.current_app.name == "Power Saver" or self.current_app.is_screensaver): return STATE_IDLE - elif self.current_app.name == "Roku": + if self.current_app.name == "Roku": return STATE_HOME - elif self.current_app.name is not None: + if self.current_app.name is not None: return STATE_PLAYING return STATE_UNKNOWN @@ -156,9 +156,9 @@ class RokuDevice(MediaPlayerDevice): """Content type of current playing media.""" if self.current_app is None: return None - elif self.current_app.name == "Power Saver": + if self.current_app.name == "Power Saver": return None - elif self.current_app.name == "Roku": + if self.current_app.name == "Roku": return None return MEDIA_TYPE_MOVIE @@ -167,11 +167,11 @@ class RokuDevice(MediaPlayerDevice): """Image url of current playing media.""" if self.current_app is None: return None - elif self.current_app.name == "Roku": + if self.current_app.name == "Roku": return None - elif self.current_app.name == "Power Saver": + if self.current_app.name == "Power Saver": return None - elif self.current_app.id is None: + if self.current_app.id is None: return None return 'http://{0}:{1}/query/icon/{2}'.format( diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py index 31b04ceb3cd..e9f8ab5f199 100644 --- a/homeassistant/components/media_player/russound_rio.py +++ b/homeassistant/components/media_player/russound_rio.py @@ -100,8 +100,7 @@ class RussoundZoneDevice(MediaPlayerDevice): if value in (None, "", "------"): return None return value - else: - return None + return None def _zone_callback_handler(self, zone_id, *args): if zone_id == self._zone_id: @@ -134,7 +133,7 @@ class RussoundZoneDevice(MediaPlayerDevice): status = self._zone_var('status', "OFF") if status == 'ON': return STATE_ON - elif status == 'OFF': + if status == 'OFF': return STATE_OFF @property diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index c3de341d607..55b3fb0ea4f 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -153,7 +153,7 @@ class SamsungTVDevice(MediaPlayerDevice): def send_key(self, key): """Send a key to the tv and handles exceptions.""" if self._power_off_in_progress() \ - and not (key == 'KEY_POWER' or key == 'KEY_POWEROFF'): + and key not in ('KEY_POWER', 'KEY_POWEROFF'): _LOGGER.info("TV is powering off, not sending command: %s", key) return try: diff --git a/homeassistant/components/media_player/sisyphus.py b/homeassistant/components/media_player/sisyphus.py new file mode 100644 index 00000000000..9a94da158a1 --- /dev/null +++ b/homeassistant/components/media_player/sisyphus.py @@ -0,0 +1,197 @@ +""" +Support for track controls on the Sisyphus Kinetic Art Table. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.sisyphus/ +""" +import logging + +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SHUFFLE_SET, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + MediaPlayerDevice) +from homeassistant.components.sisyphus import DATA_SISYPHUS +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_PLAYING, \ + STATE_PAUSED, STATE_IDLE, STATE_OFF + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['sisyphus'] + +MEDIA_TYPE_TRACK = "sisyphus_track" + +SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE \ + | SUPPORT_VOLUME_SET \ + | SUPPORT_TURN_OFF \ + | SUPPORT_TURN_ON \ + | SUPPORT_PAUSE \ + | SUPPORT_SHUFFLE_SET \ + | SUPPORT_PREVIOUS_TRACK \ + | SUPPORT_NEXT_TRACK \ + | SUPPORT_PLAY + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a media player entity for a Sisyphus table.""" + name = discovery_info[CONF_NAME] + host = discovery_info[CONF_HOST] + add_devices( + [SisyphusPlayer(name, host, hass.data[DATA_SISYPHUS][name])], + update_before_add=True) + + +class SisyphusPlayer(MediaPlayerDevice): + """Represents a single Sisyphus table as a media player device.""" + + def __init__(self, name, host, table): + """ + Constructor. + + :param name: name of the table + :param host: hostname or ip address + :param table: sisyphus-control Table object + """ + self._name = name + self._host = host + self._table = table + + async def async_added_to_hass(self): + """Add listeners after this object has been initialized.""" + self._table.add_listener( + lambda: self.async_schedule_update_ha_state(False)) + + @property + def name(self): + """Return the name of the table.""" + return self._name + + @property + def state(self): + """Return the current state of the table; sleeping maps to off.""" + if self._table.state in ["homing", "playing"]: + return STATE_PLAYING + if self._table.state == "paused": + if self._table.is_sleeping: + return STATE_OFF + + return STATE_PAUSED + if self._table.state == "waiting": + return STATE_IDLE + + return None + + @property + def volume_level(self): + """Return the current playback speed (0..1).""" + return self._table.speed + + @property + def shuffle(self): + """Return True if the current playlist is in shuffle mode.""" + return self._table.is_shuffle + + async def async_set_shuffle(self, shuffle): + """ + Change the shuffle mode of the current playlist. + + :param shuffle: True to shuffle, False not to + """ + await self._table.set_shuffle(shuffle) + + @property + def media_playlist(self): + """Return the name of the current playlist.""" + return self._table.active_playlist.name \ + if self._table.active_playlist \ + else None + + @property + def media_title(self): + """Return the title of the current track.""" + return self._table.active_track.name \ + if self._table.active_track \ + else None + + @property + def media_content_type(self): + """Return the content type currently playing; i.e. a Sisyphus track.""" + return MEDIA_TYPE_TRACK + + @property + def media_content_id(self): + """Return the track ID of the current track.""" + return self._table.active_track.id \ + if self._table.active_track \ + else None + + @property + def supported_features(self): + """Return the features supported by this table.""" + return SUPPORTED_FEATURES + + @property + def media_image_url(self): + """Return the URL for a thumbnail image of the current track.""" + from sisyphus_control import Track + if self._table.active_track: + return self._table.active_track.get_thumbnail_url( + Track.ThumbnailSize.LARGE) + + return super.media_image_url() + + async def async_turn_on(self): + """Wake up a sleeping table.""" + await self._table.wakeup() + + async def async_turn_off(self): + """Put the table to sleep.""" + await self._table.sleep() + + async def async_volume_down(self): + """Slow down playback.""" + await self._table.set_speed(max(0, self._table.speed - 0.1)) + + async def async_volume_up(self): + """Speed up playback.""" + await self._table.set_speed(min(1.0, self._table.speed + 0.1)) + + async def async_set_volume_level(self, volume): + """Set playback speed (0..1).""" + await self._table.set_speed(volume) + + async def async_media_play(self): + """Start playing.""" + await self._table.play() + + async def async_media_pause(self): + """Pause.""" + await self._table.pause() + + async def async_media_next_track(self): + """Skip to next track.""" + cur_track_index = self._get_current_track_index() + + await self._table.active_playlist.play( + self._table.active_playlist.tracks[cur_track_index + 1]) + + async def async_media_previous_track(self): + """Skip to previous track.""" + cur_track_index = self._get_current_track_index() + + await self._table.active_playlist.play( + self._table.active_playlist.tracks[cur_track_index - 1]) + + def _get_current_track_index(self): + for index, track in enumerate(self._table.active_playlist.tracks): + if track.id == self._table.active_track.id: + return index + + return -1 diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 8a92d89ce67..5375001f75c 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -447,11 +447,15 @@ class SonosDevice(MediaPlayerDevice): self.update_volume() - self._favorites = [] + self._set_favorites() + + def _set_favorites(self): + """Set available favorites.""" # SoCo 0.14 raises a generic Exception on invalid xml in favorites. # Filter those out now so our list is safe to use. # pylint: disable=broad-except try: + self._favorites = [] for fav in self.soco.music_library.get_sonos_favorites(): try: if fav.reference.get_uri(): @@ -493,6 +497,9 @@ class SonosDevice(MediaPlayerDevice): queue = _ProcessSonosEventQueue(self.update_groups) player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) + queue = _ProcessSonosEventQueue(self.update_content) + player.contentDirectory.subscribe(auto_renew=True, event_queue=queue) + def update(self): """Retrieve latest state.""" available = self._check_available() @@ -735,6 +742,11 @@ class SonosDevice(MediaPlayerDevice): slave._sonos_group = sonos_group slave.schedule_update_ha_state() + def update_content(self, event=None): + """Update information about available content.""" + self._set_favorites() + self.schedule_update_ha_state() + @property def volume_level(self): """Volume level of the media player (0..1).""" diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 9c4a0e9fa17..8f14031481a 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -269,7 +269,7 @@ class SoundTouchDevice(MediaPlayerDevice): """Title of current playing media.""" if self._status.station_name is not None: return self._status.station_name - elif self._status.artist is not None: + if self._status.artist is not None: return self._status.artist + " - " + self._status.track return None diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 371ad890364..8eb4c85f6b2 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -143,7 +143,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return True -class LogitechMediaServer(object): +class LogitechMediaServer: """Representation of a Logitech media server.""" def __init__(self, hass, host, port, username, password): diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 81e4c3541d3..046aecbb92e 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers import config_validation as cv -import homeassistant.util as util +from homeassistant import util REQUIREMENTS = ['pyvizio==0.0.3'] @@ -95,7 +95,7 @@ class VizioDevice(MediaPlayerDevice): if is_on is None: self._state = STATE_UNKNOWN return - elif is_on is False: + if is_on is False: self._state = STATE_OFF else: self._state = STATE_ON diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 11ab1615617..c4ddd38fc4f 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -142,7 +142,7 @@ class Volumio(MediaPlayerDevice): status = self._state.get('status', None) if status == 'pause': return STATE_PAUSED - elif status == 'play': + if status == 'play': return STATE_PLAYING return STATE_IDLE diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 42d0ae85ab3..362095daee6 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -24,7 +24,7 @@ from homeassistant.const import ( STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -import homeassistant.util as util +from homeassistant import util REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2'] diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 847f4131f43..e0e0e716d2e 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -289,7 +289,7 @@ class MicrosoftFaceGroupEntity(Entity): return attr -class MicrosoftFace(object): +class MicrosoftFace: """Microsoft Face api for HomeAssistant.""" def __init__(self, hass, server_loc, api_key, timeout, entities): diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad.py index 9f53f84e020..7e6738b95f8 100644 --- a/homeassistant/components/mochad.py +++ b/homeassistant/components/mochad.py @@ -61,7 +61,7 @@ def setup(hass, config): return True -class MochadCtrl(object): +class MochadCtrl: """Mochad controller.""" def __init__(self, host, port): diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index fc6db96e029..f484cb31a6c 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -157,7 +157,7 @@ def setup(hass, config): return True -class ModbusHub(object): +class ModbusHub: """Thread safe wrapper class for pymodbus.""" def __init__(self, modbus_client): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 55d99a0817e..3928eb945aa 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -462,7 +462,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: @attr.s(slots=True, frozen=True) -class Subscription(object): +class Subscription: """Class to hold data about an active subscription.""" topic = attr.ib(type=str) @@ -472,7 +472,7 @@ class Subscription(object): @attr.s(slots=True, frozen=True) -class Message(object): +class Message: """MQTT Message.""" topic = attr.ib(type=str) @@ -481,7 +481,7 @@ class Message(object): retain = attr.ib(type=bool, default=False) -class MQTT(object): +class MQTT: """Home Assistant MQTT client.""" def __init__(self, hass: HomeAssistantType, broker: str, port: int, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3916714b8d1..128c45f1311 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -8,7 +8,7 @@ import json import logging import re -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.helpers.discovery import async_load_platform from homeassistant.const import CONF_PLATFORM from homeassistant.components.mqtt import CONF_STATE_TOPIC @@ -21,7 +21,8 @@ TOPIC_MATCHER = re.compile( SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', - 'light', 'sensor', 'switch', 'lock', 'climate'] + 'light', 'sensor', 'switch', 'lock', 'climate', + 'alarm_control_panel'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], @@ -33,6 +34,7 @@ ALLOWED_PLATFORMS = { 'sensor': ['mqtt'], 'switch': ['mqtt'], 'climate': ['mqtt'], + 'alarm_control_panel': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy.py index 3531c6b4919..292e56418fc 100644 --- a/homeassistant/components/mychevy.py +++ b/homeassistant/components/mychevy.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -class EVSensorConfig(object): +class EVSensorConfig: """The EV sensor configuration.""" def __init__(self, name, attr, unit_of_measurement=None, icon=None): @@ -52,7 +52,7 @@ class EVSensorConfig(object): self.icon = icon -class EVBinarySensorConfig(object): +class EVBinarySensorConfig: """The EV binary sensor configuration.""" def __init__(self, name, attr, device_class=None): diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 3066819638f..980efcf5805 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -52,9 +52,8 @@ def is_persistence_file(value): """Validate that persistence file path ends in either .pickle or .json.""" if value.endswith(('.json', '.pickle')): return value - else: - raise vol.Invalid( - '{} does not end in either `.json` or `.pickle`'.format(value)) + raise vol.Invalid( + '{} does not end in either `.json` or `.pickle`'.format(value)) def deprecated(key): diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index b0770f90c1d..3ae99f61d17 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -25,7 +25,7 @@ def get_mysensors_devices(hass, domain): return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] -class MySensorsDevice(object): +class MySensorsDevice: """Representation of a MySensors device.""" def __init__(self, gateway, node_id, child_id, name, value_type): diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index a7719a80d99..8c80604d188 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -38,10 +38,8 @@ def is_serial_port(value): ports = ('COM{}'.format(idx + 1) for idx in range(256)) if value in ports: return value - else: - raise vol.Invalid('{} is not a serial port'.format(value)) - else: - return cv.isdevice(value) + raise vol.Invalid('{} is not a serial port'.format(value)) + return cv.isdevice(value) def is_socket_address(value): @@ -80,7 +78,7 @@ async def setup_gateways(hass, config): async def _get_gateway(hass, config, gateway_conf, persistence_file): """Return gateway after setup of the gateway.""" - import mysensors.mysensors as mysensors + from mysensors import mysensors conf = config[DOMAIN] persistence = conf[CONF_PERSISTENCE] @@ -180,7 +178,7 @@ async def _discover_persistent_devices(hass, gateway): @callback def _discover_mysensors_platform(hass, platform, new_devices): """Discover a MySensors platform.""" - task = hass.async_add_job(discovery.async_load_platform( + task = hass.async_create_task(discovery.async_load_platform( hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) return task diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index fc407de0a6b..25da38e7f75 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,7 +17,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybotvac==0.0.7'] +REQUIREMENTS = ['pybotvac==0.0.9'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' @@ -118,7 +118,7 @@ def setup(hass, config): return True -class NeatoHub(object): +class NeatoHub: """A My Neato hub wrapper class.""" def __init__(self, hass, domain_config, neato): diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 58fa1953ef0..4406062c821 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -127,7 +127,7 @@ async def async_setup_entry(hass, entry): return False for component in 'climate', 'camera', 'sensor', 'binary_sensor': - hass.async_add_job(hass.config_entries.async_forward_entry_setup( + hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, component)) def set_mode(service): @@ -183,7 +183,7 @@ async def async_setup_entry(hass, entry): return True -class NestDevice(object): +class NestDevice: """Structure Nest functions for hass.""" def __init__(self, hass, conf, nest): diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index b5c095f34b8..f97e0dc8ff5 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -65,14 +65,14 @@ class NestFlowHandler(data_entry_flow.FlowHandler): if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason='already_setup') - elif not flows: + if not flows: return self.async_abort(reason='no_flows') - elif len(flows) == 1: + if len(flows) == 1: self.flow_impl = list(flows)[0] return await self.async_step_link() - elif user_input is not None: + if user_input is not None: self.flow_impl = user_input['flow_impl'] return await self.async_step_link() diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index a635d1820db..c25b57fbd62 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyatmo==1.0.0'] +REQUIREMENTS = ['pyatmo==1.1.1'] _LOGGER = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def setup(hass, config): return True -class CameraData(object): +class CameraData: """Get the latest data from Netatmo.""" def __init__(self, auth, home=None): diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py index 23a01d37c2b..7f54e6fd6f9 100644 --- a/homeassistant/components/netgear_lte.py +++ b/homeassistant/components/netgear_lte.py @@ -17,7 +17,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util import Throttle -REQUIREMENTS = ['eternalegypt==0.0.2'] +REQUIREMENTS = ['eternalegypt==0.0.3'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -37,6 +37,7 @@ class ModemData: """Class for modem state.""" modem = attr.ib() + serial_number = attr.ib(init=False) unread_count = attr.ib(init=False) usage = attr.ib(init=False) @@ -44,6 +45,7 @@ class ModemData: async def async_update(self): """Call the API to update the data.""" information = await self.modem.information() + self.serial_number = information.serial_number self.unread_count = sum(1 for x in information.sms if x.unread) self.usage = information.usage @@ -59,7 +61,7 @@ class LTEData: """Get the requested or the only modem_data value.""" if CONF_HOST in config: return self.modem_data.get(config[CONF_HOST]) - elif len(self.modem_data) == 1: + if len(self.modem_data) == 1: return next(iter(self.modem_data.values())) return None diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 41198d1f296..13cd6203ed4 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -174,7 +174,7 @@ def async_setup(hass, config): return True -class BaseNotificationService(object): +class BaseNotificationService: """An abstract class for notification services.""" hass = None diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 9cca81e1485..8fabfc3aefb 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -56,7 +56,7 @@ def get_service(hass, config, discovery_info=None): return service -class ApnsDevice(object): +class ApnsDevice: """ The APNS Device class. diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index a98bb6c2317..94856c730b1 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.group/ """ import asyncio -import collections +from collections.abc import Mapping from copy import deepcopy import logging import voluptuous as vol @@ -33,7 +33,7 @@ def update(input_dict, update_source): Async friendly. """ for key, val in update_source.items(): - if isinstance(val, collections.Mapping): + if isinstance(val, Mapping): recurse = update(input_dict.get(key, {}), val) input_dict[key] = recurse else: diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 7529608387d..e280aa67e40 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -280,7 +280,7 @@ class HTML5PushCallbackView(HomeAssistantView): return self.json_message('Authorization header must ' 'start with Bearer', status_code=HTTP_UNAUTHORIZED) - elif len(parts) != 2: + if len(parts) != 2: return self.json_message('Authorization header must ' 'be Bearer token', status_code=HTTP_UNAUTHORIZED) diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py index 40b09dc3c72..dd35f986f78 100644 --- a/homeassistant/components/notify/rest.py +++ b/homeassistant/components/notify/rest.py @@ -95,7 +95,7 @@ class RestNotificationService(BaseNotificationService): """Recursive template creator helper function.""" if isinstance(value, list): return [_data_template_creator(item) for item in value] - elif isinstance(value, dict): + if isinstance(value, dict): return {key: _data_template_creator(item) for key, item in value.items()} value.hass = self._hass diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 899ccf9b09a..b012506acd9 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -73,7 +73,7 @@ class TelegramNotificationService(BaseNotificationService): self.hass.services.call( DOMAIN, 'send_photo', service_data=service_data) return - elif data is not None and ATTR_VIDEO in data: + if data is not None and ATTR_VIDEO in data: videos = data.get(ATTR_VIDEO, None) videos = videos if isinstance(videos, list) else [videos] for video_data in videos: @@ -81,11 +81,11 @@ class TelegramNotificationService(BaseNotificationService): self.hass.services.call( DOMAIN, 'send_video', service_data=service_data) return - elif data is not None and ATTR_LOCATION in data: + if data is not None and ATTR_LOCATION in data: service_data.update(data.get(ATTR_LOCATION)) return self.hass.services.call( DOMAIN, 'send_location', service_data=service_data) - elif data is not None and ATTR_DOCUMENT in data: + if data is not None and ATTR_DOCUMENT in data: service_data.update(data.get(ATTR_DOCUMENT)) return self.hass.services.call( DOMAIN, 'send_document', service_data=service_data) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index e38e7fcaa0f..6076cd5393a 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -194,9 +194,9 @@ class TwitterNotificationService(BaseNotificationService): if media_type.startswith('image/gif'): return 'tweet_gif' - elif media_type.startswith('video/'): + if media_type.startswith('video/'): return 'tweet_video' - elif media_type.startswith('image/'): + if media_type.startswith('image/'): return 'tweet_image' return None diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 12ddf49fca8..c5678dff351 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -110,6 +110,5 @@ def send_message(sender, password, recipient, use_tls, def discard_ssl_invalid_cert(event): """Do nothing if ssl certificate is invalid.""" _LOGGER.info('Ignoring invalid ssl certificate as requested.') - return SendNotificationBot() diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py index e7ab86a5f35..0f8fbb39073 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller.py @@ -52,7 +52,7 @@ def setup(hass, config): return True -class NuimoLogger(object): +class NuimoLogger: """Handle Nuimo Controller event callbacks.""" def __init__(self, hass, name): @@ -167,7 +167,7 @@ HOMEASSIST_LOGO = ( ".........") -class DiscoveryLogger(object): +class DiscoveryLogger: """Handle Nuimo Discovery callbacks.""" # pylint: disable=no-self-use diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index c1059227f7a..ff52ad94d8b 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -50,7 +50,7 @@ def setup(hass, config): return True -class OctoPrintAPI(object): +class OctoPrintAPI: """Simple JSON wrapper for OctoPrint's API.""" def __init__(self, api_url, key, bed, number_of_tools): @@ -154,7 +154,7 @@ def get_value_from_json(json_dict, sensor_type, group, tool): return 0 return json_dict[group][sensor_type] - elif tool is not None: + if tool is not None: if sensor_type in json_dict[group][tool]: return json_dict[group][tool][sensor_type] diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 344c750c0ec..d307a428e0e 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -118,7 +118,7 @@ def setup(hass, config): return True -class CallRateDelayThrottle(object): +class CallRateDelayThrottle: """Helper class to provide service call rate throttling. This class provides a decorator to decorate service methods that need diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 048851e97f5..84dc8402742 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -324,7 +324,7 @@ class Plant(Entity): return attrib -class DailyHistory(object): +class DailyHistory: """Stores one measurement per day for a maximum number of days. At the moment only the maximum value per day is kept. diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 0a6c959f243..da986f024a4 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -55,7 +55,7 @@ def setup(hass, config): return True -class PrometheusMetrics(object): +class PrometheusMetrics: """Model all of the metrics which should be exposed to Prometheus.""" def __init__(self, prometheus_client, entity_filter, namespace): diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py index 3a804c50c74..0e67e15d5c0 100644 --- a/homeassistant/components/rachio.py +++ b/homeassistant/components/rachio.py @@ -9,7 +9,7 @@ import logging from aiohttp import web import voluptuous as vol - +from typing import Optional from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API @@ -122,15 +122,14 @@ def setup(hass, config) -> bool: _LOGGER.error("No Rachio devices found in account %s", person.username) return False - else: - _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) + _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) # Enable component hass.data[DOMAIN] = person return True -class RachioPerson(object): +class RachioPerson: """Represent a Rachio user.""" def __init__(self, hass, rachio): @@ -162,7 +161,7 @@ class RachioPerson(object): return self._controllers -class RachioIro(object): +class RachioIro: """Represent a Rachio Iro.""" def __init__(self, hass, rachio, data): @@ -242,7 +241,7 @@ class RachioIro(object): # Only enabled zones return [z for z in self._zones if z[KEY_ENABLED]] - def get_zone(self, zone_id) -> dict or None: + def get_zone(self, zone_id) -> Optional[dict]: """Return the zone with the given ID.""" for zone in self.list_zones(include_disabled=True): if zone[KEY_ID] == zone_id: diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index a04f4926b76..53cd8e79d7e 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -124,7 +124,7 @@ def setup(hass, config): return True -class RainCloudHub(object): +class RainCloudHub: """Representation of a base RainCloud device.""" def __init__(self, data): diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 22fc427ccce..9f15c8b373f 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -151,7 +151,7 @@ async def async_setup(hass, config): ('sensor', conf[CONF_SENSORS]), ('switch', conf[CONF_SWITCHES]), ]: - hass.async_add_job( + hass.async_create_task( discovery.async_load_platform(hass, component, DOMAIN, schema, config)) @@ -201,7 +201,7 @@ async def async_setup(hass, config): return True -class RainMachine(object): +class RainMachine: """Define a generic RainMachine object.""" def __init__(self, client): diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py index 41480c09a32..f43263bf4bf 100644 --- a/homeassistant/components/raspihats.py +++ b/homeassistant/components/raspihats.py @@ -63,7 +63,7 @@ class I2CHatsException(Exception): """I2C-HATs exception.""" -class I2CHatsDIScanner(object): +class I2CHatsDIScanner: """Scan Digital Inputs and fire callbacks.""" _DIGITAL_INPUTS = "di" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 43c2aa5c7b1..f3d8e269a42 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.9'] +REQUIREMENTS = ['sqlalchemy==1.2.10'] _LOGGER = logging.getLogger(__name__) @@ -284,7 +284,7 @@ class Recorder(threading.Thread): self._close_connection() self.queue.task_done() return - elif isinstance(event, PurgeTask): + if isinstance(event, PurgeTask): purge.purge_old_data(self, event.keep_days, event.repack) self.queue.task_done() continue diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 32d6291b90c..e7948446231 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -168,7 +168,7 @@ def _process_timestamp(ts): """Process a timestamp into datetime object.""" if ts is None: return None - elif ts.tzinfo is None: + if ts.tzinfo is None: return dt_util.UTC.localize(ts) return dt_util.as_utc(ts) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 98cd937de3c..a94e8e95c6f 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -138,7 +138,7 @@ def _register_new_account(hass, account_name, api_key, shared_secret, ) -class RememberTheMilkConfiguration(object): +class RememberTheMilkConfiguration: """Internal configuration data for RememberTheMilk class. This class stores the authentication token it get from the backend. diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 842dce087e8..a63b7325035 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -10,7 +10,7 @@ import time import voluptuous as vol -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.components.remote import ( ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA) diff --git a/homeassistant/components/remote/itach.py b/homeassistant/components/remote/itach.py index 78d277ca65f..829a038953c 100644 --- a/homeassistant/components/remote/itach.py +++ b/homeassistant/components/remote/itach.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.const import ( DEVICE_DEFAULT_NAME, CONF_NAME, CONF_MAC, CONF_HOST, CONF_PORT, CONF_DEVICES) diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py index 42d4ce77054..dc37eb760f7 100644 --- a/homeassistant/components/remote/kira.py +++ b/homeassistant/components/remote/kira.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/remote.kira/ import functools as ft import logging -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 59a2dc861a6..eda09e3af64 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -232,13 +232,13 @@ class XiaomiMiioRemote(RemoteDevice): @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" - _LOGGER.error("Device does not support turn_on, " + + _LOGGER.error("Device does not support turn_on, " "please use 'remote.send_command' to send commands.") @asyncio.coroutine def async_turn_off(self, **kwargs): """Turn the device off.""" - _LOGGER.error("Device does not support turn_off, " + + _LOGGER.error("Device does not support turn_off, " "please use 'remote.send_command' to send commands.") def _send_command(self, payload): diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 272a5b868ec..b8af971b3ff 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -100,7 +100,7 @@ def identify_event_type(event): """ if EVENT_KEY_COMMAND in event: return EVENT_KEY_COMMAND - elif EVENT_KEY_SENSOR in event: + if EVENT_KEY_SENSOR in event: return EVENT_KEY_SENSOR return 'unknown' diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index 5cb7bb337ce..824ec46d636 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -19,7 +19,7 @@ DOMAIN = 'rpi_gpio' def setup(hass, config): """Set up the Raspberry PI GPIO component.""" - import RPi.GPIO as GPIO + from RPi import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -36,32 +36,32 @@ def setup(hass, config): def setup_output(port): """Set up a GPIO as output.""" - import RPi.GPIO as GPIO + from RPi import GPIO GPIO.setup(port, GPIO.OUT) def setup_input(port, pull_mode): """Set up a GPIO as input.""" - import RPi.GPIO as GPIO + from RPi import GPIO GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP) def write_output(port, value): """Write a value to a GPIO.""" - import RPi.GPIO as GPIO + from RPi import GPIO GPIO.output(port, value) def read_input(port): """Read a value from a GPIO.""" - import RPi.GPIO as GPIO + from RPi import GPIO return GPIO.input(port) def edge_detect(port, event_callback, bounce): """Add detection for RISING and FALLING events.""" - import RPi.GPIO as GPIO + from RPi import GPIO GPIO.add_event_detect( port, GPIO.BOTH, diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py index a7b33b4c697..b9c75c87c1d 100644 --- a/homeassistant/components/sabnzbd.py +++ b/homeassistant/components/sabnzbd.py @@ -134,7 +134,7 @@ def async_setup_sabnzbd(hass, sab_api, config, name): if config.get(CONF_SENSORS): hass.data[DATA_SABNZBD] = sab_api_data - hass.async_add_job( + hass.async_create_task( discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) async def async_service_handler(service): diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py index 4247855da39..128377d19f7 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra.py @@ -98,10 +98,10 @@ def async_setup(hass, config): conf, conf.get(CONF_ARM_HOME_MODE)) - task_control_panel = hass.async_add_job( + task_control_panel = hass.async_create_task( async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)) - task_zones = hass.async_add_job( + task_zones = hass.async_create_task( async_load_platform(hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py index 6fe91d0acd2..a9ec1ef679c 100644 --- a/homeassistant/components/scene/lifx_cloud.py +++ b/homeassistant/components/scene/lifx_cloud.py @@ -58,7 +58,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): devices.append(LifxCloudScene(hass, headers, timeout, scene)) async_add_devices(devices) return True - elif status == 401: + if status == 401: _LOGGER.error("Unauthorized (bad token?) on %s", url) return False diff --git a/homeassistant/components/scene/litejet.py b/homeassistant/components/scene/litejet.py index 37fb58d8dc7..87539e2dded 100644 --- a/homeassistant/components/scene/litejet.py +++ b/homeassistant/components/scene/litejet.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/scene.litejet/ """ import logging -import homeassistant.components.litejet as litejet +from homeassistant.components import litejet from homeassistant.components.scene import Scene DEPENDENCIES = ['litejet'] diff --git a/homeassistant/components/scene/tuya.py b/homeassistant/components/scene/tuya.py new file mode 100644 index 00000000000..3990a7da206 --- /dev/null +++ b/homeassistant/components/scene/tuya.py @@ -0,0 +1,40 @@ +""" +Support for the Tuya scene. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/scene.tuya/ +""" +from homeassistant.components.scene import Scene, DOMAIN +from homeassistant.components.tuya import DATA_TUYA, TuyaDevice + +DEPENDENCIES = ['tuya'] + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tuya scenes.""" + if discovery_info is None: + return + tuya = hass.data[DATA_TUYA] + dev_ids = discovery_info.get('dev_ids') + devices = [] + for dev_id in dev_ids: + device = tuya.get_device_by_id(dev_id) + if device is None: + continue + devices.append(TuyaScene(device)) + add_devices(devices) + + +class TuyaScene(TuyaDevice, Scene): + """Tuya Scene.""" + + def __init__(self, tuya): + """Init Tuya scene.""" + super().__init__(tuya) + self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + + def activate(self): + """Activate the scene.""" + self.tuya.activate() diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py index a7193b40949..dcea69cbb48 100644 --- a/homeassistant/components/scsgate.py +++ b/homeassistant/components/scsgate.py @@ -60,7 +60,7 @@ def setup(hass, config): return True -class SCSGate(object): +class SCSGate: """The class for dealing with the SCSGate device via scsgate.Reactor.""" def __init__(self, device, logger): diff --git a/homeassistant/components/sensor/abode.py b/homeassistant/components/sensor/abode.py index b51ab288c1a..26247c77454 100644 --- a/homeassistant/components/sensor/abode.py +++ b/homeassistant/components/sensor/abode.py @@ -67,9 +67,9 @@ class AbodeSensor(AbodeDevice): """Return the state of the sensor.""" if self._sensor_type == 'temp': return self._device.temp - elif self._sensor_type == 'humidity': + if self._sensor_type == 'humidity': return self._device.humidity - elif self._sensor_type == 'lux': + if self._sensor_type == 'lux': return self._device.lux @property @@ -77,7 +77,7 @@ class AbodeSensor(AbodeDevice): """Return the units of measurement.""" if self._sensor_type == 'temp': return self._device.temp_unit - elif self._sensor_type == 'humidity': + if self._sensor_type == 'humidity': return self._device.humidity_unit - elif self._sensor_type == 'lux': + if self._sensor_type == 'lux': return self._device.lux_unit diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index 0002274833f..403722c7b6a 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -248,7 +248,7 @@ class AirVisualSensor(Entity): }) -class AirVisualData(object): +class AirVisualData: """Define an object to hold sensor data.""" def __init__(self, client, **kwargs): diff --git a/homeassistant/components/sensor/api_streams.py b/homeassistant/components/sensor/api_streams.py index a8ef179280b..0d193dee79b 100644 --- a/homeassistant/components/sensor/api_streams.py +++ b/homeassistant/components/sensor/api_streams.py @@ -38,9 +38,9 @@ class StreamHandler(logging.Handler): else: if not record.msg.startswith('WS'): return - elif len(record.args) < 2: + if len(record.args) < 2: return - elif record.args[1] == 'Connected': + if record.args[1] == 'Connected': self.entity.count += 1 elif record.args[1] == 'Closed connection': self.entity.count -= 1 diff --git a/homeassistant/components/sensor/arduino.py b/homeassistant/components/sensor/arduino.py index f49d8e76f6c..d4d8ea09d29 100644 --- a/homeassistant/components/sensor/arduino.py +++ b/homeassistant/components/sensor/arduino.py @@ -11,7 +11,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.components.arduino as arduino +from homeassistant.components import arduino from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index 19860ba84fd..751f0f11171 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -158,7 +158,7 @@ class ArestSensor(Entity): return self.arest.available -class ArestData(object): +class ArestData: """The Class for handling the data retrieval for variables.""" def __init__(self, resource, pin=None): diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/sensor/arlo.py index 609887e9690..6d764b1c916 100644 --- a/homeassistant/components/sensor/arlo.py +++ b/homeassistant/components/sensor/arlo.py @@ -56,9 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) else: for camera in arlo.cameras: - if sensor_type == 'temperature' or \ - sensor_type == 'humidity' or \ - sensor_type == 'air_quality': + if sensor_type in ('temperature', 'humidity', 'air_quality'): continue name = '{0} {1}'.format( @@ -66,10 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.append(ArloSensor(name, camera, sensor_type)) for base_station in arlo.base_stations: - if ((sensor_type == 'temperature' or - sensor_type == 'humidity' or - sensor_type == 'air_quality') and - base_station.model_id == 'ABC1000'): + if sensor_type in ('temperature', 'humidity', 'air_quality') \ + and base_station.model_id == 'ABC1000': name = '{0} {1}'.format( SENSOR_TYPES[sensor_type][0], base_station.name) sensors.append(ArloSensor(name, base_station, sensor_type)) @@ -127,7 +123,7 @@ class ArloSensor(Entity): """Return the device class of the sensor.""" if self._sensor_type == 'temperature': return DEVICE_CLASS_TEMPERATURE - elif self._sensor_type == 'humidity': + if self._sensor_type == 'humidity': return DEVICE_CLASS_HUMIDITY return None diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 7308cd4f791..6b0d3e569d7 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -8,7 +8,7 @@ import asyncio import json import logging -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.core import callback from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/sensor/bbox.py b/homeassistant/components/sensor/bbox.py index 3689e94b05d..d24621becc9 100644 --- a/homeassistant/components/sensor/bbox.py +++ b/homeassistant/components/sensor/bbox.py @@ -125,7 +125,7 @@ class BboxSensor(Entity): 2) -class BboxData(object): +class BboxData: """Get data from the Bbox.""" def __init__(self): diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index bd23b9850f7..f51b7dcd5bd 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -169,7 +169,7 @@ class BitcoinSensor(Entity): self._state = '{0:.2f}'.format(stats.market_price_usd) -class BitcoinData(object): +class BitcoinData: """Get the latest data and update the states.""" def __init__(self): diff --git a/homeassistant/components/sensor/bme280.py b/homeassistant/components/sensor/bme280.py index 8f3949046ca..1685d34c0ec 100644 --- a/homeassistant/components/sensor/bme280.py +++ b/homeassistant/components/sensor/bme280.py @@ -117,7 +117,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): except KeyError: pass - async_add_devices(dev) + async_add_devices(dev, True) class BME280Handler: diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 5cec528d26a..eb63e116254 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -180,7 +180,7 @@ class BOMCurrentSensor(Entity): self.bom_data.update() -class BOMCurrentData(object): +class BOMCurrentData: """Get data from BOM.""" def __init__(self, hass, station_id): diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 8806fae5974..06d7f512c9f 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -96,7 +96,7 @@ class BroadlinkSensor(Entity): self._state = self._broadlink_data.data[self._type] -class BroadlinkData(object): +class BroadlinkData: """Representation of a Broadlink data object.""" def __init__(self, interval, ip_addr, mac_addr, timeout): diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 10a96ded437..992c27bbe2e 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.buienradar/ """ import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import logging import async_timeout @@ -262,13 +262,13 @@ class BrSensor(Entity): self._entity_picture = img return True return False - else: - try: - self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) - return True - except IndexError: - _LOGGER.warning("No forecast for fcday=%s...", fcday) - return False + + try: + self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) + return True + except IndexError: + _LOGGER.warning("No forecast for fcday=%s...", fcday) + return False if self.type == SYMBOL or self.type.startswith(CONDITION): # update weather symbol & status text @@ -374,7 +374,7 @@ class BrSensor(Entity): return self._force_update -class BrData(object): +class BrData: """Get the latest data and updates the states.""" def __init__(self, hass, coordinates, timeframe, devices): @@ -481,9 +481,10 @@ class BrData(object): _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),) + if int(datetime.now().strftime('%H')) > 0: + _LOGGER.warning("Unable to parse data from Buienradar." + "(Msg: %s)", + result.get(MESSAGE),) yield from self.schedule_update(SCHEDULE_NOK) return diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index 24f8ea7e6a9..c9a69923135 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -186,19 +186,14 @@ class CityBikesNetwork: 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:]: + result = None + minimum_dist = None + for network in cls.NETWORKS_LIST: 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: + if minimum_dist is None or dist < minimum_dist: minimum_dist = dist result = network[ATTR_ID] diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index f4b666f1e5c..c4f38b1be02 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -140,7 +140,7 @@ class CoinMarketCapSensor(Entity): self._ticker = self.data.ticker.get('data') -class CoinMarketCapData(object): +class CoinMarketCapData: """Get the latest data and update the states.""" def __init__(self, currency_id, display_currency): diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index 4a26a1dc9fc..846604a9ff5 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -4,31 +4,39 @@ Allows to configure custom shell commands to turn a value for a sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.command_line/ """ -import logging -import subprocess -import shlex - +import collections from datetime import timedelta +import json +import logging +import shlex +import subprocess import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers import template -from homeassistant.exceptions import TemplateError from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_COMMAND, + CONF_COMMAND, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, STATE_UNKNOWN) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) +CONF_COMMAND_TIMEOUT = 'command_timeout' +CONF_JSON_ATTRIBUTES = 'json_attributes' + DEFAULT_NAME = 'Command Sensor' +DEFAULT_TIMEOUT = 15 SCAN_INTERVAL = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): + cv.positive_int, + vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -41,20 +49,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): command = config.get(CONF_COMMAND) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) + command_timeout = config.get(CONF_COMMAND_TIMEOUT) if value_template is not None: value_template.hass = hass - data = CommandSensorData(hass, command) + json_attributes = config.get(CONF_JSON_ATTRIBUTES) + data = CommandSensorData(hass, command, command_timeout) - add_devices([CommandSensor(hass, data, name, unit, value_template)], True) + add_devices([CommandSensor( + hass, data, name, unit, value_template, json_attributes)], True) class CommandSensor(Entity): """Representation of a sensor that is using shell commands.""" - def __init__(self, hass, data, name, unit_of_measurement, value_template): + def __init__(self, hass, data, name, unit_of_measurement, value_template, + json_attributes): """Initialize the sensor.""" self._hass = hass self.data = data + self._attributes = None + self._json_attributes = json_attributes self._name = name self._state = None self._unit_of_measurement = unit_of_measurement @@ -75,11 +89,33 @@ class CommandSensor(Entity): """Return the state of the device.""" return self._state + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + def update(self): """Get the latest data and updates the state.""" self.data.update() value = self.data.value + if self._json_attributes: + self._attributes = {} + if value: + try: + json_dict = json.loads(value) + if isinstance(json_dict, collections.Mapping): + self._attributes = {k: json_dict[k] for k in + self._json_attributes + if k in json_dict} + else: + _LOGGER.warning("JSON result was not a dictionary") + except ValueError: + _LOGGER.warning( + "Unable to parse output as JSON: %s", value) + else: + _LOGGER.warning("Empty reply found when expecting JSON data") + if value is None: value = STATE_UNKNOWN elif self._value_template is not None: @@ -89,14 +125,15 @@ class CommandSensor(Entity): self._state = value -class CommandSensorData(object): +class CommandSensorData: """The class for handling the data retrieval.""" - def __init__(self, hass, command): + def __init__(self, hass, command, command_timeout): """Initialize the data object.""" self.value = None self.hass = hass self.command = command + self.timeout = command_timeout def update(self): """Get the latest data with a shell command.""" @@ -135,7 +172,7 @@ class CommandSensorData(object): try: _LOGGER.info("Running command: %s", command) return_value = subprocess.check_output( - command, shell=shell, timeout=15) + command, shell=shell, timeout=self.timeout) self.value = return_value.strip().decode('utf-8') except subprocess.CalledProcessError: _LOGGER.error("Command failed: %s", command) diff --git a/homeassistant/components/sensor/cups.py b/homeassistant/components/sensor/cups.py index 6d55853d724..846b109afca 100644 --- a/homeassistant/components/sensor/cups.py +++ b/homeassistant/components/sensor/cups.py @@ -129,7 +129,7 @@ class CupsSensor(Entity): # pylint: disable=no-name-in-module -class CupsData(object): +class CupsData: """Get the latest data from CUPS and update the state.""" def __init__(self, host, port): diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py index f5d6f278da0..4a7face0156 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -54,8 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.append(CurrencylayerSensor(rest, base, variable)) if 'error' in response.json(): return False - else: - add_devices(sensors, True) + add_devices(sensors, True) class CurrencylayerSensor(Entity): @@ -104,7 +103,7 @@ class CurrencylayerSensor(Entity): value['{}{}'.format(self._base, self._quote)], 4) -class CurrencylayerData(object): +class CurrencylayerData: """Get data from Currencylayer.org.""" def __init__(self, resource, parameters): diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py index e045043e09c..2da5cb5cdf0 100644 --- a/homeassistant/components/sensor/daikin.py +++ b/homeassistant/components/sensor/daikin.py @@ -85,7 +85,7 @@ class DaikinClimateSensor(Entity): if value is None: _LOGGER.warning("Invalid value requested for key %s", key) else: - if value == "-" or value == "--": + if value in ("-", "--"): value = None elif cast_to_float: try: diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index e75f36d59f7..b2bb7bb4da2 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -352,12 +352,12 @@ class DarkSkySensor(Entity): # percentages if self.type in ['precip_probability', 'cloud_cover', 'humidity']: return round(state * 100, 1) - elif (self.type in ['dew_point', 'temperature', 'apparent_temperature', - 'temperature_min', 'temperature_max', - 'apparent_temperature_min', - 'apparent_temperature_max', - 'precip_accumulation', - 'pressure', 'ozone', 'uvIndex']): + if self.type in ['dew_point', 'temperature', 'apparent_temperature', + 'temperature_min', 'temperature_max', + 'apparent_temperature_min', + 'apparent_temperature_max', + 'precip_accumulation', + 'pressure', 'ozone', 'uvIndex']: return round(state, 1) return state @@ -372,7 +372,7 @@ def convert_to_camel(data): return components[0] + "".join(x.title() for x in components[1:]) -class DarkSkyData(object): +class DarkSkyData: """Get the latest data from Darksky.""" def __init__(self, api_key, latitude, longitude, units, language, diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index ec9b14883a9..0e6ab164d4f 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -85,7 +85,7 @@ class DeutscheBahnSensor(Entity): self._state += " + {}".format(self.data.connections[0]['delay']) -class SchieneData(object): +class SchieneData: """Pull data from the bahn.de web page.""" def __init__(self, start, goal, only_direct): diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index b22e4df9a50..6770594b919 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -128,7 +128,7 @@ class DHTSensor(Entity): temperature = data[SENSOR_TEMPERATURE] _LOGGER.debug("Temperature %.1f \u00b0C + offset %.1f", temperature, temperature_offset) - if (temperature >= -20) and (temperature < 80): + if -20 <= temperature < 80: self._state = round(temperature + temperature_offset, 1) if self.temp_unit == TEMP_FAHRENHEIT: self._state = round(celsius_to_fahrenheit(temperature), 1) @@ -136,11 +136,11 @@ class DHTSensor(Entity): humidity = data[SENSOR_HUMIDITY] _LOGGER.debug("Humidity %.1f%% + offset %.1f", humidity, humidity_offset) - if (humidity >= 0) and (humidity <= 100): + if 0 <= humidity <= 100: self._state = round(humidity + humidity_offset, 1) -class DHTClient(object): +class DHTClient: """Get the latest data from the DHT sensor.""" def __init__(self, adafruit_dht, sensor, pin): diff --git a/homeassistant/components/sensor/dovado.py b/homeassistant/components/sensor/dovado.py index ee2292d4122..2a78d4ad864 100644 --- a/homeassistant/components/sensor/dovado.py +++ b/homeassistant/components/sensor/dovado.py @@ -129,17 +129,16 @@ class DovadoSensor(Entity): if self._sensor == SENSOR_NETWORK: match = re.search(r"\((.+)\)", state) return match.group(1) if match else None - elif self._sensor == SENSOR_SIGNAL: + if self._sensor == SENSOR_SIGNAL: try: return int(state.split()[0]) except ValueError: return 0 - elif self._sensor == SENSOR_SMS_UNREAD: + if self._sensor == SENSOR_SMS_UNREAD: return int(state) - elif self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]: + if self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]: return round(float(state) / 1e6, 1) - else: - return state + return state def update(self): """Update sensor values.""" diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index d7982f1c9db..3a1bf1da39e 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -255,7 +255,7 @@ class DSMREntity(Entity): return ICON_POWER_FAILURE if 'Power' in self._name: return ICON_POWER - elif 'Gas' in self._name: + if 'Gas' in self._name: return ICON_GAS @property @@ -285,7 +285,7 @@ class DSMREntity(Entity): # used for normal rate. if value == '0002': return 'normal' - elif value == '0001': + if value == '0001': return 'low' return STATE_UNKNOWN diff --git a/homeassistant/components/sensor/dublin_bus_transport.py b/homeassistant/components/sensor/dublin_bus_transport.py index f6d791f9fd6..a443c78b2b1 100644 --- a/homeassistant/components/sensor/dublin_bus_transport.py +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -125,7 +125,7 @@ class DublinPublicTransportSensor(Entity): pass -class PublicTransportData(object): +class PublicTransportData: """The Class for handling the data retrieval.""" def __init__(self, stop, route): diff --git a/homeassistant/components/sensor/dwd_weather_warnings.py b/homeassistant/components/sensor/dwd_weather_warnings.py index e023dfcc49f..4f9664617a3 100644 --- a/homeassistant/components/sensor/dwd_weather_warnings.py +++ b/homeassistant/components/sensor/dwd_weather_warnings.py @@ -163,7 +163,7 @@ class DwdWeatherWarningsSensor(Entity): self._api.update() -class DwdWeatherWarningsAPI(object): +class DwdWeatherWarningsAPI: """Get the latest data and update the states.""" def __init__(self, region_name): diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index cca06bd9782..065c88d8332 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -99,7 +99,7 @@ class DweetSensor(Entity): values, STATE_UNKNOWN) -class DweetData(object): +class DweetData: """The class for handling the data retrieval.""" def __init__(self, device): diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index d7b867081a3..218968ecee8 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -129,7 +129,7 @@ class EBoxSensor(Entity): self._state = round(self.ebox_data.data[self.type], 2) -class EBoxData(object): +class EBoxData: """Get data from Ebox.""" def __init__(self, username, password, httpsession): diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index 978b8db669a..4c209d17d07 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -120,7 +120,7 @@ class EddystoneTemp(Entity): return False -class Monitor(object): +class Monitor: """Continuously scan for BLE advertisements.""" def __init__(self, hass, devices, bt_device_id): diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py index fd7c1aee3ae..5899ef267cb 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/sensor/eight_sleep.py @@ -149,7 +149,7 @@ class EightUserSensor(EightSleepUserEntity): """Return the unit the value is expressed in.""" if 'current_sleep' in self._sensor or 'last_sleep' in self._sensor: return 'Score' - elif 'bed_temp' in self._sensor: + if 'bed_temp' in self._sensor: if self._units == 'si': return '°C' return '°F' diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py index cd02137f4d5..a62eaba7df8 100644 --- a/homeassistant/components/sensor/emoncms.py +++ b/homeassistant/components/sensor/emoncms.py @@ -190,7 +190,7 @@ class EmonCmsSensor(Entity): self._state = round(float(elem["value"]), DECIMALS) -class EmonCmsData(object): +class EmonCmsData: """The class for handling the data retrieval.""" def __init__(self, hass, url, apikey, interval): diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index 265350f3e95..bf4ee55c446 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -138,7 +138,7 @@ class EnvirophatSensor(Entity): self._state = self.data.voltage_3 -class EnvirophatData(object): +class EnvirophatData: """Get the latest data and update.""" def __init__(self, envirophat, use_leds): diff --git a/homeassistant/components/sensor/fail2ban.py b/homeassistant/components/sensor/fail2ban.py index 87c301d34f5..bf868d49201 100644 --- a/homeassistant/components/sensor/fail2ban.py +++ b/homeassistant/components/sensor/fail2ban.py @@ -112,7 +112,7 @@ class BanSensor(Entity): self.last_ban = 'None' -class BanLogParser(object): +class BanLogParser: """Class to parse fail2ban logs.""" def __init__(self, interval, log_file): diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index 9143ccaf23f..65474cd4bf6 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -102,7 +102,7 @@ class SpeedtestSensor(Entity): return ICON -class SpeedtestData(object): +class SpeedtestData: """Get the latest data from fast.com.""" def __init__(self, hass, config): diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index a2ee18b3659..4f724b5b851 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -147,7 +147,7 @@ class FidoSensor(Entity): self._state = round(self._state, 2) -class FidoData(object): +class FidoData: """Get data from Fido.""" def __init__(self, username, password, httpsession): diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 261f6e2b510..15059b08a17 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -23,7 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -import homeassistant.components.history as history +from homeassistant.components import history import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -258,7 +258,7 @@ class SensorFilter(Entity): return state_attr -class FilterState(object): +class FilterState: """State abstraction for filter usage.""" def __init__(self, state): @@ -283,7 +283,7 @@ class FilterState(object): return "{} : {}".format(self.timestamp, self.state) -class Filter(object): +class Filter: """Filter skeleton. Args: diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py index 13129919139..ef064e84228 100644 --- a/homeassistant/components/sensor/fints.py +++ b/homeassistant/components/sensor/fints.py @@ -101,7 +101,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(accounts, True) -class FinTsClient(object): +class FinTsClient: """Wrapper around the FinTS3PinTanClient. Use this class as Context Manager to get the FinTS3Client object. diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index 438366ae555..5a6f8da79b2 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -102,7 +102,7 @@ class ExchangeRateSensor(Entity): self._state = round(self.data.rate['rates'][self._target], 3) -class ExchangeData(object): +class ExchangeData: """Get the latest data and update the states.""" def __init__(self, target_currency, api_key): diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index b443bd56f03..3da9c512ebd 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): phonebook = FritzBoxPhonebook( host=host, port=port, username=username, password=password, phonebook_id=phonebook_id, prefixes=prefixes) - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except phonebook = None _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box", phonebook_id) @@ -143,7 +143,7 @@ class FritzBoxCallSensor(Entity): self.phonebook.update_phonebook() -class FritzBoxCallMonitor(object): +class FritzBoxCallMonitor: """Event listener to monitor calls on the Fritz!Box.""" def __init__(self, host, port, sensor): @@ -187,7 +187,6 @@ class FritzBoxCallMonitor(object): line = response.split("\n", 1)[0] self._parse(line) time.sleep(1) - return def _parse(self, line): """Parse the call information and set the sensor states.""" @@ -225,7 +224,7 @@ class FritzBoxCallMonitor(object): self._sensor.schedule_update_ha_state() -class FritzBoxPhonebook(object): +class FritzBoxPhonebook: """This connects to a FritzBox router and downloads its phone book.""" def __init__(self, host, port, username, password, diff --git a/homeassistant/components/sensor/fritzbox_netmonitor.py b/homeassistant/components/sensor/fritzbox_netmonitor.py index 857e6cc4a07..b980323abe1 100644 --- a/homeassistant/components/sensor/fritzbox_netmonitor.py +++ b/homeassistant/components/sensor/fritzbox_netmonitor.py @@ -65,8 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if fstatus is None: _LOGGER.error("Failed to establish connection to FRITZ!Box: %s", host) return 1 - else: - _LOGGER.info("Successfully connected to FRITZ!Box") + _LOGGER.info("Successfully connected to FRITZ!Box") add_devices([FritzboxMonitorSensor(name, fstatus)], True) diff --git a/homeassistant/components/sensor/geizhals.py b/homeassistant/components/sensor/geizhals.py index 94f3f1884d1..06062b26b00 100644 --- a/homeassistant/components/sensor/geizhals.py +++ b/homeassistant/components/sensor/geizhals.py @@ -15,7 +15,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.const import (CONF_DOMAIN, CONF_NAME) -REQUIREMENTS = ['beautifulsoup4==4.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.6.1'] _LOGGER = logging.getLogger(__name__) CONF_PRODUCT_ID = 'product_id' @@ -98,7 +98,7 @@ class Geizwatch(Entity): self._state = self.data.prices[0] -class GeizParser(object): +class GeizParser: """Pull data from the geizhals website.""" def __init__(self, product_id, domain, regex): diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index c8c4db17c8d..b79e6e69adf 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -149,7 +149,7 @@ class GeoRssServiceSensor(Entity): self._state_attributes = matrix -class GeoRssServiceData(object): +class GeoRssServiceData: """Provide access to GeoRSS feed and stores the latest data.""" def __init__(self, home_latitude, home_longitude, url, radius_in_km): diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index bd6e91c7b53..a6dfd89e45a 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -176,7 +176,7 @@ class GlancesSensor(Entity): self._state = round(use / 1024**2, 1) -class GlancesData(object): +class GlancesData: """The class for handling the data retrieval.""" def __init__(self, resource): diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index e7d25872701..d14a70ecc84 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONF_MODE) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.location as location +from homeassistant.helpers import location import homeassistant.util.dt as dt_util REQUIREMENTS = ['googlemaps==2.5.1'] diff --git a/homeassistant/components/sensor/google_wifi.py b/homeassistant/components/sensor/google_wifi.py index c070a3e990f..cc5461ed548 100644 --- a/homeassistant/components/sensor/google_wifi.py +++ b/homeassistant/components/sensor/google_wifi.py @@ -10,7 +10,7 @@ from datetime import timedelta import voluptuous as vol import requests -import homeassistant.util.dt as dt +from homeassistant.util import dt import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -138,7 +138,7 @@ class GoogleWifiSensor(Entity): self._state = STATE_UNKNOWN -class GoogleWifiAPI(object): +class GoogleWifiAPI: """Get the latest data and update the states.""" def __init__(self, host, conditions): diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 1d270419933..f463d0fb8d1 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -91,7 +91,7 @@ class GpsdSensor(Entity): """Return the state of GPSD.""" if self.agps_thread.data_stream.mode == 3: return "3D Fix" - elif self.agps_thread.data_stream.mode == 2: + if self.agps_thread.data_stream.mode == 2: return "2D Fix" return STATE_UNKNOWN diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 93e15b9cd5e..120fe8fdb22 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -176,7 +176,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): gtfs = pygtfs.Schedule(joined_path) # pylint: disable=no-member - if len(gtfs.feeds) < 1: + if not gtfs.feeds: pygtfs.append_feed(gtfs, os.path.join(gtfs_dir, data)) add_devices([GTFSDepartureSensor(gtfs, name, origin, destination, offset)]) diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py index c1fe7ab4880..bc79c4d0c1d 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -122,7 +122,7 @@ class HaveIBeenPwnedSensor(Entity): self._state = len(self._data.data[self._email]) -class HaveIBeenPwnedData(object): +class HaveIBeenPwnedData: """Class for handling the data retrieval.""" def __init__(self, emails): diff --git a/homeassistant/components/sensor/hddtemp.py b/homeassistant/components/sensor/hddtemp.py index 006542a777f..f8afe9c7637 100644 --- a/homeassistant/components/sensor/hddtemp.py +++ b/homeassistant/components/sensor/hddtemp.py @@ -108,7 +108,7 @@ class HddTempSensor(Entity): self._state = None -class HddTempData(object): +class HddTempData: """Get the latest data from HDDTemp and update the states.""" def __init__(self, host, port): diff --git a/homeassistant/components/sensor/history_stats.py b/homeassistant/components/sensor/history_stats.py index 7af858b9d94..c3d0fe8f1b6 100644 --- a/homeassistant/components/sensor/history_stats.py +++ b/homeassistant/components/sensor/history_stats.py @@ -10,7 +10,7 @@ import math import voluptuous as vol -import homeassistant.components.history as history +from homeassistant.components import history import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -294,7 +294,7 @@ class HistoryStatsHelper: minutes, seconds = divmod(seconds, 60) if days > 0: return '%dd %dh %dm' % (days, hours, minutes) - elif hours > 0: + if hours > 0: return '%dh %dm' % (hours, minutes) return '%dm' % minutes diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index 8c9409ef5ff..2d609070415 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -55,7 +55,7 @@ class HiveSensorEntity(Entity): """Return the state of the sensor.""" if self.device_type == "Hub_OnlineStatus": return self.session.sensor.hub_online_status(self.node_id) - elif self.device_type == "Hive_OutsideTemperature": + if self.device_type == "Hive_OutsideTemperature": return self.session.weather.temperature() @property diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index 87021e9c7c5..7292e3b2f40 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -80,11 +80,6 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes of the access point.""" - return {} - class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" @@ -98,6 +93,8 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """Return the icon.""" from homematicip.base.enums import ValveState + if super().icon: + return super().icon if self._device.valveState != ValveState.ADAPTION_DONE: return 'mdi:alert' return 'mdi:radiator' diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py index acd10fe08af..98ee83f8958 100644 --- a/homeassistant/components/sensor/hp_ilo.py +++ b/homeassistant/components/sensor/hp_ilo.py @@ -147,7 +147,7 @@ class HpIloSensor(Entity): self._state = ilo_data -class HpIloData(object): +class HpIloData: """Gets the latest data from HP ILO.""" def __init__(self, host, port, login, password): diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 2195153ab1e..db75d51fbad 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -162,7 +162,7 @@ class HydroQuebecSensor(Entity): self._state = round(self.hydroquebec_data.data[self.type], 2) -class HydroquebecData(object): +class HydroquebecData: """Get data from HydroQuebec.""" def __init__(self, username, password, httpsession, contract=None): diff --git a/homeassistant/components/sensor/ihc.py b/homeassistant/components/sensor/ihc.py index b30a242c17c..2dcf2c3f7be 100644 --- a/homeassistant/components/sensor/ihc.py +++ b/homeassistant/components/sensor/ihc.py @@ -3,8 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ihc/ """ -from xml.etree.ElementTree import Element - import voluptuous as vol from homeassistant.components.ihc import ( @@ -62,7 +60,7 @@ class IHCSensor(IHCDevice, Entity): """Implementation of the IHC sensor.""" def __init__(self, ihc_controller, name, ihc_id: int, info: bool, - unit, product: Element = None) -> None: + unit, product=None) -> None: """Initialize the IHC sensor.""" super().__init__(ihc_controller, name, ihc_id, info, product) self._state = None diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index c0c9bf62efd..a1a604df3e4 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False -class EmailReader(object): +class EmailReader: """A class to read emails from an IMAP server.""" def __init__(self, user, password, server, port): diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index c0d492984e0..8bfbaf49837 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -155,7 +155,7 @@ class InfluxSensor(Entity): self._state = value -class InfluxSensorData(object): +class InfluxSensorData: """Class for handling the data retrieval.""" def __init__(self, influx, group, field, measurement, where): diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index 398c0b350ee..1fd556b17c0 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -86,8 +86,8 @@ class IOSSensor(Entity): battery_level = device_battery[ios.ATTR_BATTERY_LEVEL] charging = True icon_state = DEFAULT_ICON_STATE - if (battery_state == ios.ATTR_BATTERY_STATE_FULL or - battery_state == ios.ATTR_BATTERY_STATE_UNPLUGGED): + if battery_state in (ios.ATTR_BATTERY_STATE_FULL, + ios.ATTR_BATTERY_STATE_UNPLUGGED): charging = False icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_state == ios.ATTR_BATTERY_STATE_UNKNOWN: diff --git a/homeassistant/components/sensor/irish_rail_transport.py b/homeassistant/components/sensor/irish_rail_transport.py index 603d82359de..5febebeec87 100644 --- a/homeassistant/components/sensor/irish_rail_transport.py +++ b/homeassistant/components/sensor/irish_rail_transport.py @@ -132,7 +132,7 @@ class IrishRailTransportSensor(Entity): self._state = None -class IrishRailTransportData(object): +class IrishRailTransportData: """The Class for handling the data retrieval.""" def __init__(self, irish_rail, station, direction, destination, stops_at): diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 1048c04d43d..19dcfc87014 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -259,14 +259,11 @@ class ISYSensorDevice(ISYDevice): if len(self._node.uom) == 1: if self._node.uom[0] in UOM_FRIENDLY_NAME: friendly_name = UOM_FRIENDLY_NAME.get(self._node.uom[0]) - if friendly_name == TEMP_CELSIUS or \ - friendly_name == TEMP_FAHRENHEIT: + if friendly_name in (TEMP_CELSIUS, TEMP_FAHRENHEIT): friendly_name = self.hass.config.units.temperature_unit return friendly_name - else: - return self._node.uom[0] - else: - return None + return self._node.uom[0] + return None @property def state(self) -> str: diff --git a/homeassistant/components/sensor/london_air.py b/homeassistant/components/sensor/london_air.py index 2ffbb914275..bbb5993b064 100644 --- a/homeassistant/components/sensor/london_air.py +++ b/homeassistant/components/sensor/london_air.py @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors, True) -class APIData(object): +class APIData: """Get the latest data for all authorities.""" def __init__(self): diff --git a/homeassistant/components/sensor/london_underground.py b/homeassistant/components/sensor/london_underground.py index fe13c0db8a7..4619eda0611 100644 --- a/homeassistant/components/sensor/london_underground.py +++ b/homeassistant/components/sensor/london_underground.py @@ -95,7 +95,7 @@ class LondonTubeSensor(Entity): self._description = self._data.data[self.name]['Description'] -class TubeData(object): +class TubeData: """Get the latest tube data from TFL.""" def __init__(self): diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index 9952e2a1c24..c9bc7205ce6 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -137,7 +137,7 @@ class LuftdatenSensor(Entity): await self.luftdaten.async_update() -class LuftdatenData(object): +class LuftdatenData: """Class for handling the data retrieval.""" def __init__(self, data): diff --git a/homeassistant/components/sensor/lyft.py b/homeassistant/components/sensor/lyft.py index c2f6412049c..57e5f1c6b02 100644 --- a/homeassistant/components/sensor/lyft.py +++ b/homeassistant/components/sensor/lyft.py @@ -183,7 +183,7 @@ class LyftSensor(Entity): estimate.get('estimated_cost_cents_max', 0)) / 2) / 100) -class LyftEstimate(object): +class LyftEstimate: """The class for handling the time and price estimate.""" def __init__(self, session, start_latitude, start_longitude, diff --git a/homeassistant/components/sensor/magicseaweed.py b/homeassistant/components/sensor/magicseaweed.py new file mode 100644 index 00000000000..02c61024e30 --- /dev/null +++ b/homeassistant/components/sensor/magicseaweed.py @@ -0,0 +1,201 @@ +""" +Support for magicseaweed data from magicseaweed.com. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.magicseaweed/ +""" +from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['magicseaweed==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_HOURS = 'hours' +CONF_SPOT_ID = 'spot_id' +CONF_UNITS = 'units' +CONF_UPDATE_INTERVAL = 'update_interval' + +DEFAULT_UNIT = 'us' +DEFAULT_NAME = 'MSW' +DEFAULT_ATTRIBUTION = "Data provided by magicseaweed.com" + +ICON = 'mdi:waves' + +HOURS = ['12AM', '3AM', '6AM', '9AM', '12PM', '3PM', '6PM', '9PM'] + +SENSOR_TYPES = { + 'max_breaking_swell': ['Max'], + 'min_breaking_swell': ['Min'], + 'swell_forecast': ['Forecast'], +} + +UNITS = ['eu', 'uk', 'us'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SPOT_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_HOURS, default=None): + vol.All(cv.ensure_list, [vol.In(HOURS)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNITS): vol.In(UNITS), +}) + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Magicseaweed sensor.""" + name = config.get(CONF_NAME) + spot_id = config[CONF_SPOT_ID] + api_key = config[CONF_API_KEY] + hours = config.get(CONF_HOURS) + + if CONF_UNITS in config: + units = config.get(CONF_UNITS) + elif hass.config.units.is_metric: + units = UNITS[0] + else: + units = UNITS[2] + + forecast_data = MagicSeaweedData( + api_key=api_key, + spot_id=spot_id, + units=units) + forecast_data.update() + + # If connection failed don't setup platform. + if forecast_data.currently is None or forecast_data.hourly is None: + return + + sensors = [] + for variable in config[CONF_MONITORED_CONDITIONS]: + sensors.append(MagicSeaweedSensor(forecast_data, variable, name, + units)) + if 'forecast' not in variable and hours is not None: + for hour in hours: + sensors.append(MagicSeaweedSensor( + forecast_data, variable, name, units, hour)) + add_devices(sensors, True) + + +class MagicSeaweedSensor(Entity): + """Implementation of a MagicSeaweed sensor.""" + + def __init__(self, forecast_data, sensor_type, name, unit_system, + hour=None): + """Initialize the sensor.""" + self.client_name = name + self.data = forecast_data + self.hour = hour + self.type = sensor_type + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._name = SENSOR_TYPES[sensor_type][0] + self._icon = None + self._state = None + self._unit_system = unit_system + self._unit_of_measurement = None + + @property + def name(self): + """Return the name of the sensor.""" + if self.hour is None and 'forecast' in self.type: + return "{} {}".format(self.client_name, self._name) + if self.hour is None: + return "Current {} {}".format(self.client_name, self._name) + return "{} {} {}".format( + self.hour, self.client_name, self._name) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_system(self): + """Return the unit system of this entity.""" + return self._unit_system + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the entity weather icon, if any.""" + return ICON + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + def update(self): + """Get the latest data from Magicseaweed and updates the states.""" + self.data.update() + if self.hour is None: + forecast = self.data.currently + else: + forecast = self.data.hourly[self.hour] + + self._unit_of_measurement = forecast.swell_unit + if self.type == 'min_breaking_swell': + self._state = forecast.swell_minBreakingHeight + elif self.type == 'max_breaking_swell': + self._state = forecast.swell_maxBreakingHeight + elif self.type == 'swell_forecast': + summary = "{} - {}".format( + forecast.swell_minBreakingHeight, + forecast.swell_maxBreakingHeight) + self._state = summary + if self.hour is None: + for hour, data in self.data.hourly.items(): + occurs = hour + hr_summary = "{} - {} {}".format( + data.swell_minBreakingHeight, + data.swell_maxBreakingHeight, + data.swell_unit) + self._attrs[occurs] = hr_summary + + if self.type != 'swell_forecast': + self._attrs.update(forecast.attrs) + + +class MagicSeaweedData: + """Get the latest data from MagicSeaweed.""" + + def __init__(self, api_key, spot_id, units): + """Initialize the data object.""" + import magicseaweed + self._msw = magicseaweed.MSW_Forecast(api_key, spot_id, + None, units) + self.currently = None + self.hourly = {} + + # Apply throttling to methods using configured interval + self.update = Throttle(MIN_TIME_BETWEEN_UPDATES)(self._update) + + def _update(self): + """Get the latest data from MagicSeaweed.""" + try: + forecasts = self._msw.get_future() + self.currently = forecasts.data[0] + for forecast in forecasts.data[:8]: + hour = dt_util.utc_from_timestamp( + forecast.localTimestamp).strftime("%-I%p") + self.hourly[hour] = forecast + except ConnectionError: + _LOGGER.error("Unable to retrieve data from Magicseaweed") diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index b6366de6432..ec3d3f47ba7 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -174,7 +174,7 @@ class MetOfficeCurrentSensor(Entity): self.data.update() -class MetOfficeCurrentData(object): +class MetOfficeCurrentData: """Get data from Datapoint.""" def __init__(self, hass, datapoint, site): diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index ab6bd8270ce..f575768b505 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -96,7 +96,7 @@ class MfiSensor(Entity): tag = None if tag is None: return STATE_OFF - elif self._port.model == 'Input Digital': + if self._port.model == 'Input Digital': return STATE_ON if self._port.value > 0 else STATE_OFF digits = DIGITS.get(self._port.tag, 0) return round(self._port.value, digits) @@ -111,9 +111,9 @@ class MfiSensor(Entity): if tag == 'temperature': return TEMP_CELSIUS - elif tag == 'active_pwr': + if tag == 'active_pwr': return 'Watts' - elif self._port.model == 'Input Digital': + if self._port.model == 'Input Digital': return 'State' return tag diff --git a/homeassistant/components/sensor/mhz19.py b/homeassistant/components/sensor/mhz19.py index cd559d3bbd2..60f6598ab21 100644 --- a/homeassistant/components/sensor/mhz19.py +++ b/homeassistant/components/sensor/mhz19.py @@ -117,7 +117,7 @@ class MHZ19Sensor(Entity): return result -class MHZClient(object): +class MHZClient: """Get the latest data from the MH-Z sensor.""" def __init__(self, co2sensor, serial): diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index f1f8adab062..6f50a57b3ab 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MiFlora sensor.""" from miflora import miflora_poller try: - import bluepy.btle # noqa: F401 # pylint: disable=unused-variable + import bluepy.btle # noqa: F401 pylint: disable=unused-variable from btlewrap import BluepyBackend backend = BluepyBackend except ImportError: diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py index 3628765293b..249a69578db 100644 --- a/homeassistant/components/sensor/mitemp_bt.py +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MiTempBt sensor.""" from mitemp_bt import mitemp_bt_poller try: - import bluepy.btle # noqa: F401 # pylint: disable=unused-variable + import bluepy.btle # noqa: F401 pylint: disable=unused-variable from btlewrap import BluepyBackend backend = BluepyBackend except ImportError: diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index c4014fbd1dd..5f404ccd5f7 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -9,7 +9,7 @@ import struct import voluptuous as vol -import homeassistant.components.modbus as modbus +from homeassistant.components import modbus from homeassistant.const import ( CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE, CONF_STRUCTURE) diff --git a/homeassistant/components/sensor/modem_callerid.py b/homeassistant/components/sensor/modem_callerid.py index f80ea5853c8..58e8becd6bb 100644 --- a/homeassistant/components/sensor/modem_callerid.py +++ b/homeassistant/components/sensor/modem_callerid.py @@ -95,7 +95,6 @@ class ModemCalleridSensor(Entity): if self.modem: self.modem.close() self.modem = None - return def _incomingcallcallback(self, newstate): """Handle new states.""" @@ -117,4 +116,3 @@ class ModemCalleridSensor(Entity): elif newstate == self.modem.STATE_IDLE: self._state = STATE_IDLE self.schedule_update_ha_state() - return diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 2822ce01dca..319185923cd 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -10,7 +10,7 @@ import math import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.util as util +from homeassistant import util from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_state_change from homeassistant.const import ( @@ -107,11 +107,10 @@ class MoldIndicator(Entity): # convert to celsius if necessary if unit == TEMP_FAHRENHEIT: return util.temperature.fahrenheit_to_celsius(temp) - elif unit == TEMP_CELSIUS: + if unit == TEMP_CELSIUS: return temp - else: - _LOGGER.error("Temp sensor has unsupported unit: %s (allowed: %s, " - "%s)", unit, TEMP_CELSIUS, TEMP_FAHRENHEIT) + _LOGGER.error("Temp sensor has unsupported unit: %s (allowed: %s, " + "%s)", unit, TEMP_CELSIUS, TEMP_FAHRENHEIT) return None diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py index 0c57c98c0af..50f4f72078c 100644 --- a/homeassistant/components/sensor/moon.py +++ b/homeassistant/components/sensor/moon.py @@ -50,20 +50,20 @@ class MoonSensor(Entity): def state(self): """Return the state of the device.""" if self._state == 0: - return 'New moon' - elif self._state < 7: - return 'Waxing crescent' - elif self._state == 7: - return 'First quarter' - elif self._state < 14: - return 'Waxing gibbous' - elif self._state == 14: - return 'Full moon' - elif self._state < 21: - return 'Waning gibbous' - elif self._state == 21: - return 'Last quarter' - return 'Waning crescent' + return 'new_moon' + if self._state < 7: + return 'waxing_crescent' + if self._state == 7: + return 'first_quarter' + if self._state < 14: + return 'waxing_gibbous' + if self._state == 14: + return 'full_moon' + if self._state < 21: + return 'waning_gibbous' + if self._state == 21: + return 'last_quarter' + return 'waning_crescent' @property def icon(self): diff --git a/homeassistant/components/sensor/mopar.py b/homeassistant/components/sensor/mopar.py index 3e1887cfd59..81c48555cfc 100644 --- a/homeassistant/components/sensor/mopar.py +++ b/homeassistant/components/sensor/mopar.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for index, _ in enumerate(data.vehicles)], True) -class MoparData(object): +class MoparData: """Container for Mopar vehicle data. Prevents session expiry re-login race condition. diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 997fd312a6a..0e29c55d39d 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS) from homeassistant.helpers.entity import Entity -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.event import async_track_point_in_utc_time diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index 2c0f8eb4d5a..2a61c1143ee 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -11,7 +11,7 @@ from datetime import timedelta import voluptuous as vol -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv from homeassistant.components.mqtt import CONF_STATE_TOPIC from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index 81c7173e4d0..e066bb5e0b9 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor.mvglive/ import logging from datetime import timedelta +from copy import deepcopy import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -28,6 +29,7 @@ CONF_DIRECTIONS = 'directions' CONF_LINES = 'lines' CONF_PRODUCTS = 'products' CONF_TIMEOFFSET = 'timeoffset' +CONF_NUMBER = 'number' DEFAULT_PRODUCT = ['U-Bahn', 'Tram', 'Bus', 'S-Bahn'] @@ -52,6 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PRODUCTS, default=DEFAULT_PRODUCT): cv.ensure_list_csv, vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int, + vol.Optional(CONF_NUMBER, default=1): cv.positive_int, vol.Optional(CONF_NAME): cv.string}] }) @@ -68,6 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): nextdeparture.get(CONF_LINES), nextdeparture.get(CONF_PRODUCTS), nextdeparture.get(CONF_TIMEOFFSET), + nextdeparture.get(CONF_NUMBER), nextdeparture.get(CONF_NAME))) add_devices(sensors, True) @@ -76,12 +80,12 @@ class MVGLiveSensor(Entity): """Implementation of an MVG Live sensor.""" def __init__(self, station, destinations, directions, - lines, products, timeoffset, name): + lines, products, timeoffset, number, name): """Initialize the sensor.""" self._station = station self._name = name self.data = MVGLiveData(station, destinations, directions, - lines, products, timeoffset) + lines, products, timeoffset, number) self._state = STATE_UNKNOWN self._icon = ICONS['-'] @@ -98,9 +102,14 @@ class MVGLiveSensor(Entity): return self._state @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" - return self.data.departures + dep = self.data.departures + if not dep: + return None + attr = dep[0] # next depature attributes + attr['departures'] = deepcopy(dep) # all departures dictionary + return attr @property def icon(self): @@ -119,15 +128,15 @@ class MVGLiveSensor(Entity): self._state = '-' self._icon = ICONS['-'] else: - self._state = self.data.departures.get('time', '-') - self._icon = ICONS[self.data.departures.get('product', '-')] + self._state = self.data.departures[0].get('time', '-') + self._icon = ICONS[self.data.departures[0].get('product', '-')] -class MVGLiveData(object): +class MVGLiveData: """Pull data from the mvg-live.de web page.""" def __init__(self, station, destinations, directions, - lines, products, timeoffset): + lines, products, timeoffset, number): """Initialize the sensor.""" import MVGLive self._station = station @@ -136,25 +145,30 @@ class MVGLiveData(object): self._lines = lines self._products = products self._timeoffset = timeoffset + self._number = number self._include_ubahn = True if 'U-Bahn' in self._products else False self._include_tram = True if 'Tram' in self._products else False self._include_bus = True if 'Bus' in self._products else False self._include_sbahn = True if 'S-Bahn' in self._products else False self.mvg = MVGLive.MVGLive() - self.departures = {} + self.departures = [] def update(self): """Update the connection data.""" try: _departures = self.mvg.getlivedata( - station=self._station, ubahn=self._include_ubahn, - tram=self._include_tram, bus=self._include_bus, + station=self._station, + timeoffset=self._timeoffset, + ubahn=self._include_ubahn, + tram=self._include_tram, + bus=self._include_bus, sbahn=self._include_sbahn) except ValueError: - self.departures = {} + self.departures = [] _LOGGER.warning("Returned data not understood") return - for _departure in _departures: + self.departures = [] + for i, _departure in enumerate(_departures): # find the first departure meeting the criteria if ('' not in self._destinations[:1] and _departure['destination'] not in self._destinations): @@ -173,5 +187,6 @@ class MVGLiveData(object): 'product']: _nextdep[k] = _departure.get(k, '') _nextdep['time'] = int(_nextdep['time']) - self.departures = _nextdep - break + self.departures.append(_nextdep) + if i == self._number - 1: + break diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 54b095bb84b..3e3f7ce9486 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -287,7 +287,7 @@ class NetAtmoSensor(Entity): self._state = "Full" -class NetAtmoData(object): +class NetAtmoData: """Get the latest data from NetAtmo.""" def __init__(self, auth, station): @@ -340,7 +340,7 @@ class NetAtmoData(object): # Never hammer the NetAtmo API more than # twice per update interval newinterval = NETATMO_UPDATE_INTERVAL / 2 - _LOGGER.warning( + _LOGGER.info( "NetAtmo refresh interval reset to %d seconds", newinterval) else: diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py index 2d08159967c..488b1611399 100644 --- a/homeassistant/components/sensor/netdata.py +++ b/homeassistant/components/sensor/netdata.py @@ -134,7 +134,7 @@ class NetdataSensor(Entity): resource_data['dimensions'][self._element]['value'], 2) -class NetdataData(object): +class NetdataData: """The class for handling the data retrieval.""" def __init__(self, api): diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py index b4a3e2a1155..dac1f81ad23 100644 --- a/homeassistant/components/sensor/netgear_lte.py +++ b/homeassistant/components/sensor/netgear_lte.py @@ -32,11 +32,11 @@ async def async_setup_platform( modem_data = hass.data[DATA_KEY].get_modem_data(config) sensors = [] - for sensortype in config[CONF_SENSORS]: - if sensortype == SENSOR_SMS: - sensors.append(SMSSensor(modem_data)) - elif sensortype == SENSOR_USAGE: - sensors.append(UsageSensor(modem_data)) + for sensor_type in config[CONF_SENSORS]: + if sensor_type == SENSOR_SMS: + sensors.append(SMSSensor(modem_data, sensor_type)) + elif sensor_type == SENSOR_USAGE: + sensors.append(UsageSensor(modem_data, sensor_type)) async_add_devices(sensors, True) @@ -46,11 +46,17 @@ class LTESensor(Entity): """Data usage sensor entity.""" modem_data = attr.ib() + sensor_type = attr.ib() async def async_update(self): """Update state.""" await self.modem_data.async_update() + @property + def unique_id(self): + """Return a unique ID like 'usage_5TG365AB0078V'.""" + return "{}_{}".format(self.sensor_type, self.modem_data.serial_number) + class SMSSensor(LTESensor): """Unread SMS sensor entity.""" diff --git a/homeassistant/components/sensor/neurio_energy.py b/homeassistant/components/sensor/neurio_energy.py index 5e3bf55dc9d..fd8b8d2aeec 100644 --- a/homeassistant/components/sensor/neurio_energy.py +++ b/homeassistant/components/sensor/neurio_energy.py @@ -69,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([NeurioEnergy(data, DAILY_NAME, DAILY_TYPE, update_daily)]) -class NeurioData(object): +class NeurioData: """Stores data retrieved from Neurio sensor.""" def __init__(self, api_key, api_secret, sensor_id): diff --git a/homeassistant/components/sensor/nsw_fuel_station.py b/homeassistant/components/sensor/nsw_fuel_station.py index 2440dac3204..5f677d39888 100644 --- a/homeassistant/components/sensor/nsw_fuel_station.py +++ b/homeassistant/components/sensor/nsw_fuel_station.py @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -class StationPriceData(object): +class StationPriceData: """An object to store and fetch the latest data for a given station.""" def __init__(self, client, station_id: int) -> None: diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index 7c7ff3480b0..7126bd89ef9 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -236,13 +236,12 @@ class NUTSensor(Entity): """Return UPS display state.""" if self._data.status is None: return STATE_TYPES['OFF'] - else: - try: - return " ".join( - STATE_TYPES[state] - for state in self._data.status[KEY_STATUS].split()) - except KeyError: - return STATE_UNKNOWN + try: + return " ".join( + STATE_TYPES[state] + for state in self._data.status[KEY_STATUS].split()) + except KeyError: + return STATE_UNKNOWN def update(self): """Get the latest status and use it to update our sensor state.""" @@ -260,7 +259,7 @@ class NUTSensor(Entity): self._state = self._data.status[self.type] -class PyNUTData(object): +class PyNUTData: """Stores the data retrieved from NUT. For each entity to use, acts as the single point responsible for fetching diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py index 0fa6362ad05..a6fee5a69e8 100644 --- a/homeassistant/components/sensor/nzbget.py +++ b/homeassistant/components/sensor/nzbget.py @@ -138,7 +138,7 @@ class NZBGetSensor(Entity): self._state = value -class NZBGetAPI(object): +class NZBGetAPI: """Simple JSON-RPC wrapper for NZBGet's API.""" def __init__(self, api_url, username=None, password=None): @@ -173,8 +173,4 @@ class NZBGetAPI(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update cached response.""" - try: - self.status = self.post('status')['result'] - except requests.exceptions.ConnectionError: - # failed to update status - exception already logged in self.post - raise + self.status = self.post('status')['result'] diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index 20d00267dee..9e62846e4d3 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -107,13 +107,12 @@ class OctoPrintSensor(Entity): def state(self): """Return the state of the sensor.""" sensor_unit = self.unit_of_measurement - if sensor_unit == TEMP_CELSIUS or sensor_unit == "%": + if sensor_unit in (TEMP_CELSIUS, "%"): # API sometimes returns null and not 0 if self._state is None: self._state = 0 return round(self._state, 2) - else: - return self._state + return self._state @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index 741ffa2842d..5e8231bb124 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -93,7 +93,7 @@ class OpenexchangeratesSensor(Entity): self._state = round(value[str(self._quote)], 4) -class OpenexchangeratesData(object): +class OpenexchangeratesData: """Get data from Openexchangerates.org.""" def __init__(self, resource, parameters, quote): diff --git a/homeassistant/components/sensor/openhardwaremonitor.py b/homeassistant/components/sensor/openhardwaremonitor.py index 1b5867836bc..1b345c752ff 100644 --- a/homeassistant/components/sensor/openhardwaremonitor.py +++ b/homeassistant/components/sensor/openhardwaremonitor.py @@ -101,14 +101,13 @@ class OpenHardwareMonitorDevice(Entity): self.attributes = _attributes return - else: - array = array[path_number][OHM_CHILDREN] - _attributes.update({ - 'level_%s' % path_index: values[OHM_NAME] - }) + array = array[path_number][OHM_CHILDREN] + _attributes.update({ + 'level_%s' % path_index: values[OHM_NAME] + }) -class OpenHardwareMonitorData(object): +class OpenHardwareMonitorData: """Class used to pull data from OHM and create sensors.""" def __init__(self, config, hass): diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 96db4430d32..ba7fc4f9095 100644 --- 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.8.0'] +REQUIREMENTS = ['pyowm==2.9.0'] _LOGGER = logging.getLogger(__name__) @@ -179,7 +179,7 @@ class OpenWeatherMapSensor(Entity): self._state = fc_data.get_weathers()[0].get_detailed_status() -class WeatherData(object): +class WeatherData: """Get the latest data from OpenWeatherMap.""" def __init__(self, owm, forecast, latitude, longitude): diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py index 2adf5691e2e..363ada725ba 100644 --- a/homeassistant/components/sensor/pi_hole.py +++ b/homeassistant/components/sensor/pi_hole.py @@ -153,7 +153,7 @@ class PiHoleSensor(Entity): self.data = self.pi_hole.api.data -class PiHoleData(object): +class PiHoleData: """Get the latest data and update the states.""" def __init__(self, api): diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 9784cc3dc4c..c30f1575049 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_NAME, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity -import homeassistant.components.pilight as pilight +from homeassistant.components import pilight import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index c11c83ab40e..27750c9ac61 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -263,7 +263,7 @@ class PollencomSensor(Entity): self._state = average -class PollenComData(object): +class PollenComData: """Define a data object to retrieve info from Pollen.com.""" def __init__(self, client, sensor_types): @@ -307,7 +307,7 @@ class PollenComData(object): _LOGGER.error('Unable to get allergy history: %s', err) self.data[TYPE_ALLERGY_HISTORIC] = {} - if all(s in self._sensor_types + if any(s in self._sensor_types for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY]): try: diff --git a/homeassistant/components/sensor/pyload.py b/homeassistant/components/sensor/pyload.py index cc4ce1e6448..4aa121e0895 100644 --- a/homeassistant/components/sensor/pyload.py +++ b/homeassistant/components/sensor/pyload.py @@ -124,7 +124,7 @@ class PyLoadSensor(Entity): self._state = value -class PyLoadAPI(object): +class PyLoadAPI: """Simple wrapper for pyLoad's API.""" def __init__(self, api_url, username=None, password=None): @@ -162,8 +162,4 @@ class PyLoadAPI(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update cached response.""" - try: - self.status = self.post('speed') - except requests.exceptions.ConnectionError: - # Failed to update status - exception already logged in self.post - raise + self.status = self.post('speed') diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 3d9704875c9..8b25eb3de31 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -164,7 +164,7 @@ def round_nicely(number): return round(number) -class QNAPStatsAPI(object): +class QNAPStatsAPI: """Class to interface with the API.""" def __init__(self, config): @@ -192,7 +192,7 @@ class QNAPStatsAPI(object): self.data["smart_drive_health"] = self._api.get_smart_disk_health() self.data["volumes"] = self._api.get_volumes() self.data["bandwidth"] = self._api.get_bandwidth() - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except _LOGGER.exception("Failed to fetch QNAP stats from the NAS") @@ -241,7 +241,7 @@ class QNAPCPUSensor(QNAPSensor): """Return the state of the sensor.""" if self.var_id == 'cpu_temp': return self._api.data['system_stats']['cpu']['temp_c'] - elif self.var_id == 'cpu_usage': + if self.var_id == 'cpu_usage': return self._api.data['system_stats']['cpu']['usage_percent'] diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 75235bedaab..8db48719a37 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -158,7 +158,7 @@ class RestSensor(Entity): return self._attributes -class RestData(object): +class RestData: """Class for handling the data retrieval.""" def __init__(self, method, resource, auth, headers, data, verify_ssl): diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index a5a6eb5f07b..b410e7e860a 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.rfxtrx as rfxtrx +from homeassistant.components import rfxtrx from homeassistant.components.rfxtrx import ( ATTR_DATA_TYPE, ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, CONF_DATA_TYPE, CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 0065f3e0927..e7aace8ec6d 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['beautifulsoup4==4.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/season.py b/homeassistant/components/sensor/season.py index b04b7727e40..f06f6a896e7 100644 --- a/homeassistant/components/sensor/season.py +++ b/homeassistant/components/sensor/season.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_TYPE from homeassistant.helpers.entity import Entity -import homeassistant.util as util +from homeassistant import util REQUIREMENTS = ['ephem==3.7.6.0'] diff --git a/homeassistant/components/sensor/sense.py b/homeassistant/components/sensor/sense.py index 5eee9053db5..16f4ccb9b6c 100644 --- a/homeassistant/components/sensor/sense.py +++ b/homeassistant/components/sensor/sense.py @@ -27,7 +27,7 @@ CONSUMPTION_NAME = "Usage" ACTIVE_TYPE = 'active' -class SensorConfig(object): +class SensorConfig: """Data structure holding sensor config.""" def __init__(self, name, sensor_type): diff --git a/homeassistant/components/sensor/sensehat.py b/homeassistant/components/sensor/sensehat.py index a50f4cdfd2c..f0e566f718f 100644 --- a/homeassistant/components/sensor/sensehat.py +++ b/homeassistant/components/sensor/sensehat.py @@ -109,7 +109,7 @@ class SenseHatSensor(Entity): self._state = self.data.pressure -class SenseHatData(object): +class SenseHatData: """Get the latest data and update.""" def __init__(self, is_hat_attached): diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index bc3e127508b..541abea3091 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -96,7 +96,7 @@ class ShodanSensor(Entity): self._state = self.data.details['total'] -class ShodanData(object): +class ShodanData: """Get the latest data and update the states.""" def __init__(self, api, query): diff --git a/homeassistant/components/sensor/sht31.py b/homeassistant/components/sensor/sht31.py index e1a7f3c9e5f..2aeff8e73d8 100644 --- a/homeassistant/components/sensor/sht31.py +++ b/homeassistant/components/sensor/sht31.py @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devs) -class SHTClient(object): +class SHTClient: """Get the latest data from the SHT sensor.""" def __init__(self, adafruit_sht): diff --git a/homeassistant/components/sensor/sigfox.py b/homeassistant/components/sensor/sigfox.py index da8f3fcc639..408435a9667 100644 --- a/homeassistant/components/sensor/sigfox.py +++ b/homeassistant/components/sensor/sigfox.py @@ -55,7 +55,7 @@ def epoch_to_datetime(epoch_time): return datetime.datetime.fromtimestamp(epoch_time).isoformat() -class SigfoxAPI(object): +class SigfoxAPI: """Class for interacting with the SigFox API.""" def __init__(self, api_login, api_password): diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index 95bf207acf8..5600f906f34 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -83,11 +83,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if errindication and not accept_errors: _LOGGER.error("Please check the details in the configuration file") return False - else: - data = SnmpData( - host, port, community, baseoid, version, accept_errors, - default_value) - add_devices([SnmpSensor(data, name, unit, value_template)], True) + data = SnmpData( + host, port, community, baseoid, version, accept_errors, + default_value) + add_devices([SnmpSensor(data, name, unit, value_template)], True) class SnmpSensor(Entity): @@ -131,7 +130,7 @@ class SnmpSensor(Entity): self._state = value -class SnmpData(object): +class SnmpData: """Get the latest data and update the states.""" def __init__(self, host, port, community, baseoid, version, accept_errors, diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index bf2868d3b01..8c1ffc03786 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -148,7 +148,7 @@ class SpeedtestSensor(Entity): self._state = state.state -class SpeedtestData(object): +class SpeedtestData: """Get the latest data from speedtest.net.""" def __init__(self, hass, config): diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 8574a7231da..83f5478867f 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.9'] +REQUIREMENTS = ['sqlalchemy==1.2.10'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/homeassistant/components/sensor/startca.py b/homeassistant/components/sensor/startca.py index aefbc2d4626..374e14c5ac2 100644 --- a/homeassistant/components/sensor/startca.py +++ b/homeassistant/components/sensor/startca.py @@ -118,7 +118,7 @@ class StartcaSensor(Entity): self._state = round(self.startcadata.data[self.type], 2) -class StartcaData(object): +class StartcaData: """Get data from Start.ca API.""" def __init__(self, loop, websession, api_key, bandwidth_cap): diff --git a/homeassistant/components/sensor/strings.moon.json b/homeassistant/components/sensor/strings.moon.json new file mode 100644 index 00000000000..97d96623d88 --- /dev/null +++ b/homeassistant/components/sensor/strings.moon.json @@ -0,0 +1,12 @@ +{ + "state": { + "new_moon": "New moon", + "waxing_crescent": "Waxing crescent", + "first_quarter": "First quarter", + "waxing_gibbous": "Waxing gibbous", + "full_moon": "Full moon", + "waning_gibbous": "Waning gibbous", + "last_quarter": "Last quarter", + "waning_crescent": "Waning crescent" + } +} diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index 63d500e2373..b4536b48c9e 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -145,7 +145,7 @@ class SwissHydrologicalDataSensor(Entity): self._state = self.data.measurings['03']['current'] -class HydrologicalData(object): +class HydrologicalData: """The Class for handling the data retrieval.""" def __init__(self, station): diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index e3c3a0cf5ca..d431805ab19 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -129,7 +129,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) -class SynoApi(object): +class SynoApi: """Class to interface with Synology DSM API.""" def __init__(self, host, port, username, password, temp_unit, use_ssl): @@ -140,7 +140,7 @@ class SynoApi(object): try: self._api = SynologyDSM(host, port, username, password, use_https=use_ssl) - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except _LOGGER.error("Error setting up Synology DSM") # Will be updated when update() gets called. @@ -214,7 +214,7 @@ class SynoNasUtilSensor(SynoNasSensor): if self.var_id in network_sensors: return round(attr / 1024.0, 1) - elif self.var_id in memory_sensors: + if self.var_id in memory_sensors: return round(attr / 1024.0 / 1024.0, 1) else: return getattr(self._api.utilisation, self.var_id) diff --git a/homeassistant/components/sensor/sytadin.py b/homeassistant/components/sensor/sytadin.py index 9a85eb25575..ff8e7d7ddfe 100644 --- a/homeassistant/components/sensor/sytadin.py +++ b/homeassistant/components/sensor/sytadin.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['beautifulsoup4==4.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.6.1'] _LOGGER = logging.getLogger(__name__) @@ -35,9 +35,9 @@ OPTION_MEAN_VELOCITY = 'mean_velocity' OPTION_CONGESTION = 'congestion' SENSOR_TYPES = { - OPTION_TRAFFIC_JAM: ['Traffic Jam', LENGTH_KILOMETERS], - OPTION_MEAN_VELOCITY: ['Mean Velocity', LENGTH_KILOMETERS+'/h'], OPTION_CONGESTION: ['Congestion', ''], + OPTION_MEAN_VELOCITY: ['Mean Velocity', LENGTH_KILOMETERS+'/h'], + OPTION_TRAFFIC_JAM: ['Traffic Jam', LENGTH_KILOMETERS], } MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) @@ -113,7 +113,7 @@ class SytadinSensor(Entity): self._state = self.data.congestion -class SytadinData(object): +class SytadinData: """The class for handling the data retrieval.""" def __init__(self, resource): diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 737b3d08368..aa6314b8c5b 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -122,9 +122,9 @@ class TadoSensor(Entity): """Return the unit of measurement.""" if self.zone_variable == "temperature": return self.hass.config.units.temperature_unit - elif self.zone_variable == "humidity": + if self.zone_variable == "humidity": return '%' - elif self.zone_variable == "heating": + if self.zone_variable == "heating": return '%' @property @@ -132,7 +132,7 @@ class TadoSensor(Entity): """Icon for the sensor.""" if self.zone_variable == "temperature": return 'mdi:thermometer' - elif self.zone_variable == "humidity": + if self.zone_variable == "humidity": return 'mdi:water-percent' def update(self): diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index aedecfe61e5..6c6c296652a 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -46,11 +46,11 @@ class TahomaSensor(TahomaDevice, Entity): """Return the unit of measurement of this entity, if any.""" if self.tahoma_device.type == 'Temperature Sensor': return None - elif self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + if self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': return None - elif self.tahoma_device.type == 'io:LightIOSystemSensor': + if self.tahoma_device.type == 'io:LightIOSystemSensor': return 'lx' - elif self.tahoma_device.type == 'Humidity Sensor': + if self.tahoma_device.type == 'Humidity Sensor': return '%' def update(self): diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index c2ef1d4c6b9..7298181796a 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -88,7 +88,7 @@ class Ted5000Sensor(Entity): self._gateway.update() -class Ted5000Gateway(object): +class Ted5000Gateway: """The class for handling the data retrieval.""" def __init__(self, url): diff --git a/homeassistant/components/sensor/teksavvy.py b/homeassistant/components/sensor/teksavvy.py index 0bf1ef4caff..68a1cfc4fe1 100644 --- a/homeassistant/components/sensor/teksavvy.py +++ b/homeassistant/components/sensor/teksavvy.py @@ -119,7 +119,7 @@ class TekSavvySensor(Entity): self._state = round(self.teksavvydata.data[self.type], 2) -class TekSavvyData(object): +class TekSavvyData: """Get data from TekSavvy API.""" def __init__(self, loop, websession, api_key, bandwidth_cap): diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 048ca988e3d..123c11021b4 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -96,11 +96,11 @@ class TelldusLiveSensor(TelldusLiveEntity): """Return the state of the sensor.""" if not self.available: return None - elif self._type == SENSOR_TYPE_TEMPERATURE: + if self._type == SENSOR_TYPE_TEMPERATURE: return self._value_as_temperature - elif self._type == SENSOR_TYPE_HUMIDITY: + if self._type == SENSOR_TYPE_HUMIDITY: return self._value_as_humidity - elif self._type == SENSOR_TYPE_LUMINANCE: + if self._type == SENSOR_TYPE_LUMINANCE: return self._value_as_luminance return self._value diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index de929aa0942..2fc67e57162 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Tellstick sensors.""" - import tellcore.telldus as telldus + from tellcore import telldus import tellcore.constants as tellcore_constants sensor_value_descriptions = { diff --git a/homeassistant/components/sensor/thethingsnetwork.py b/homeassistant/components/sensor/thethingsnetwork.py index 28a3b48892b..0f27b656404 100644 --- a/homeassistant/components/sensor/thethingsnetwork.py +++ b/homeassistant/components/sensor/thethingsnetwork.py @@ -110,7 +110,7 @@ class TtnDataSensor(Entity): self._state = self._ttn_data_storage.data -class TtnDataStorage(object): +class TtnDataStorage: """Get the latest data from The Things Network Data Storage.""" def __init__(self, hass, app_id, device_id, access_key, values): diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/sensor/thinkingcleaner.py index 83cf799e3cd..0b936d8c8c7 100644 --- a/homeassistant/components/sensor/thinkingcleaner.py +++ b/homeassistant/components/sensor/thinkingcleaner.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/sensor.thinkingcleaner/ import logging from datetime import timedelta -import homeassistant.util as util +from homeassistant import util from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index bfdf0c3c3aa..0668b5bdbce 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -81,7 +81,7 @@ class TimeDateSensor(Entity): """Icon to use in the frontend, if any.""" if 'date' in self.type and 'time' in self.type: return 'mdi:calendar-clock' - elif 'date' in self.type: + if 'date' in self.type: return 'mdi:calendar' return 'mdi:clock' @@ -92,7 +92,7 @@ class TimeDateSensor(Entity): if self.type == 'date': now = dt_util.start_of_local_day(dt_util.as_local(now)) return now + timedelta(seconds=86400) - elif self.type == 'beat': + if self.type == 'beat': interval = 86.4 else: interval = 60 diff --git a/homeassistant/components/sensor/toon.py b/homeassistant/components/sensor/toon.py index cecce0d270f..a8875f6904c 100644 --- a/homeassistant/components/sensor/toon.py +++ b/homeassistant/components/sensor/toon.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.toon/ """ import logging -import datetime as datetime +import datetime from homeassistant.helpers.entity import Entity import homeassistant.components.toon as toon_main diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 4dac411d224..3e74b454913 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -148,7 +148,7 @@ class TransmissionSensor(Entity): self._state = self._data.torrentCount -class TransmissionData(object): +class TransmissionData: """Get the latest data and update the states.""" def __init__(self, api): diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index e80fe7d2d82..cd476a1a226 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -175,7 +175,7 @@ class UberSensor(Entity): self._state = 0 -class UberEstimate(object): +class UberEstimate: """The class for handling the time and price estimate.""" def __init__(self, session, start_latitude, start_longitude, diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 4fc92db1d90..eaef3dcf7f7 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -51,13 +51,13 @@ class VeraSensor(VeraDevice, Entity): import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units - elif self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR: + if self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR: return 'lx' - elif self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: + if self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: return 'level' - elif self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: + if self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: return '%' - elif self.vera_device.category == veraApi.CATEGORY_POWER_METER: + if self.vera_device.category == veraApi.CATEGORY_POWER_METER: return 'watts' def update(self): diff --git a/homeassistant/components/sensor/volvooncall.py b/homeassistant/components/sensor/volvooncall.py index 343bcdf2033..78e8a7e76c6 100644 --- a/homeassistant/components/sensor/volvooncall.py +++ b/homeassistant/components/sensor/volvooncall.py @@ -43,7 +43,7 @@ class VolvoSensor(VolvoEntity): if 'mil' in self.unit_of_measurement: return round(val, 2) return round(val, 1) - elif self._attribute == 'distance_to_empty': + if self._attribute == 'distance_to_empty': return int(floor(val)) return int(round(val)) diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py index 24c45ec1ff3..76c5d2f648e 100644 --- a/homeassistant/components/sensor/waterfurnace.py +++ b/homeassistant/components/sensor/waterfurnace.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -class WFSensorConfig(object): +class WFSensorConfig: """Water Furnace Sensor configuration.""" def __init__(self, friendly_name, field, icon="mdi:gauge", diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 0b059379c11..70c169a1b7c 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE) import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.location as location +from homeassistant.helpers import location from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -64,8 +64,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor = WazeTravelTime(name, origin, destination, region, incl_filter, excl_filter) - add_devices([sensor], True) + add_devices([sensor]) + # Wait until start event is sent to load this component. hass.bus.listen_once(EVENT_HOMEASSISTANT_START, sensor.update) diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py index 05d61173da0..597a971e208 100644 --- a/homeassistant/components/sensor/worldtidesinfo.py +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -85,7 +85,7 @@ class WorldTidesInfoSensor(Entity): tidetime = time.strftime('%I:%M %p', time.localtime( self.data['extremes'][0]['dt'])) return "High tide at %s" % (tidetime) - elif "Low" in str(self.data['extremes'][0]['type']): + if "Low" in str(self.data['extremes'][0]['type']): tidetime = time.strftime('%I:%M %p', time.localtime( self.data['extremes'][0]['dt'])) return "Low tide at %s" % (tidetime) diff --git a/homeassistant/components/sensor/worxlandroid.py b/homeassistant/components/sensor/worxlandroid.py index ddf506bf4eb..c49ce36bd49 100644 --- a/homeassistant/components/sensor/worxlandroid.py +++ b/homeassistant/components/sensor/worxlandroid.py @@ -152,11 +152,11 @@ class WorxLandroidSensor(Entity): if state_obj[14] == 1: return 'manual-stop' - elif state_obj[5] == 1 and state_obj[13] == 0: + if state_obj[5] == 1 and state_obj[13] == 0: return 'charging' - elif state_obj[5] == 1 and state_obj[13] == 1: + if state_obj[5] == 1 and state_obj[13] == 1: return 'charging-complete' - elif state_obj[15] == 1: + if state_obj[15] == 1: return 'going-home' return 'mowing' diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 7f2df4bcda9..24ae2d0068f 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -40,7 +40,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) # Helper classes for declaring sensor configurations -class WUSensorConfig(object): +class WUSensorConfig: """WU Sensor Configuration. defines basic HA properties of the weather sensor and @@ -764,7 +764,7 @@ class WUndergroundSensor(Entity): return self._unique_id -class WUndergroundData(object): +class WUndergroundData: """Get data from WUnderground.""" def __init__(self, hass, api_key, pws_id, lang, latitude, longitude): diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 3192d0d2f60..32139b21976 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -91,9 +91,9 @@ class XiaomiSensor(XiaomiDevice): value = max(value - 300, 0) if self._data_key == 'temperature' and (value < -50 or value > 60): return False - elif self._data_key == 'humidity' and (value <= 0 or value > 100): + if self._data_key == 'humidity' and (value <= 0 or value > 100): return False - elif self._data_key == 'pressure' and value == 0: + if self._data_key == 'pressure' and value == 0: return False self._state = round(value, 1) return True diff --git a/homeassistant/components/sensor/yahoo_finance.py b/homeassistant/components/sensor/yahoo_finance.py index 8c2cfd9923f..82cb7f845dc 100644 --- a/homeassistant/components/sensor/yahoo_finance.py +++ b/homeassistant/components/sensor/yahoo_finance.py @@ -104,7 +104,7 @@ class YahooFinanceSensor(Entity): self._state = self.data.state -class YahooFinanceData(object): +class YahooFinanceData: """Get data from Yahoo Finance.""" def __init__(self, symbol): diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index c7ff967723b..fcddf41af97 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -142,7 +142,7 @@ class YrSensor(Entity): return self._unit_of_measurement -class YrData(object): +class YrData: """Get the latest data and updates the states.""" def __init__(self, hass, coordinates, forecast, devices): diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index db66419e54a..b2279e107da 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -174,7 +174,7 @@ class YahooWeatherSensor(Entity): float(self._data.yahoo.Atmosphere['visibility'])/1.61, 2) -class YahooWeatherData(object): +class YahooWeatherData: """Handle Yahoo! API object and limit updates.""" def __init__(self, woeid, temp_unit): diff --git a/homeassistant/components/sensor/zabbix.py b/homeassistant/components/sensor/zabbix.py index baeed391557..21a3030b79b 100644 --- a/homeassistant/components/sensor/zabbix.py +++ b/homeassistant/components/sensor/zabbix.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.zabbix as zabbix +from homeassistant.components import zabbix import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py index df5ff5e8d37..e8e5fdec4d8 100644 --- a/homeassistant/components/sensor/zamg.py +++ b/homeassistant/components/sensor/zamg.py @@ -133,7 +133,7 @@ class ZamgSensor(Entity): self.probe.update() -class ZamgData(object): +class ZamgData: """The class for handling the data retrieval.""" API_URL = 'http://www.zamg.ac.at/ogd/' diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index 1189a53bb09..60b6a018fc2 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import STATE_UNKNOWN from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity -import homeassistant.components.zoneminder as zoneminder +from homeassistant.components import zoneminder import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index b2a913c2af8..c6356efe157 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.sensor import DOMAIN from homeassistant.components import zwave from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -64,7 +64,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): """Return the state of the sensor.""" if self._units in ('C', 'F'): return round(self._state, 1) - elif isinstance(self._state, float): + if isinstance(self._state, float): return round(self._state, 2) return self._state @@ -74,7 +74,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): """Return the unit the value is expressed in.""" if self._units == 'C': return TEMP_CELSIUS - elif self._units == 'F': + if self._units == 'F': return TEMP_FAHRENHEIT return self._units diff --git a/homeassistant/components/sisyphus.py b/homeassistant/components/sisyphus.py new file mode 100644 index 00000000000..dc9f9cc4c25 --- /dev/null +++ b/homeassistant/components/sisyphus.py @@ -0,0 +1,84 @@ +""" +Support for controlling Sisyphus Kinetic Art Tables. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sisyphus/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +REQUIREMENTS = ['sisyphus-control==2.1'] + +_LOGGER = logging.getLogger(__name__) + +DATA_SISYPHUS = 'sisyphus' +DOMAIN = 'sisyphus' + +AUTODETECT_SCHEMA = vol.Schema({}) + +TABLE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, +}) + +TABLES_SCHEMA = vol.Schema([TABLE_SCHEMA]) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Any(AUTODETECT_SCHEMA, TABLES_SCHEMA), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the sisyphus component.""" + from sisyphus_control import Table + tables = hass.data.setdefault(DATA_SISYPHUS, {}) + table_configs = config.get(DOMAIN) + session = async_get_clientsession(hass) + + async def add_table(host, name=None): + """Add platforms for a single table with the given hostname.""" + table = await Table.connect(host, session) + if name is None: + name = table.name + tables[name] = table + _LOGGER.debug("Connected to %s at %s", name, host) + + hass.async_add_job(async_load_platform( + hass, 'light', DOMAIN, { + CONF_NAME: name, + }, config + )) + hass.async_add_job(async_load_platform( + hass, 'media_player', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + }, config + )) + + if isinstance(table_configs, dict): # AUTODETECT_SCHEMA + for ip_address in await Table.find_table_ips(session): + await add_table(ip_address) + else: # TABLES_SCHEMA + for conf in table_configs: + await add_table(conf[CONF_HOST], conf[CONF_NAME]) + + async def close_tables(*args): + """Close all table objects.""" + tasks = [table.close() for table in tables.values()] + if tasks: + await asyncio.wait(tasks) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_tables) + + return True diff --git a/homeassistant/components/sleepiq.py b/homeassistant/components/sleepiq.py index df36eef2f9e..4d4ecf0160b 100644 --- a/homeassistant/components/sleepiq.py +++ b/homeassistant/components/sleepiq.py @@ -73,7 +73,7 @@ def setup(hass, config): return True -class SleepIQData(object): +class SleepIQData: """Get the latest data from SleepIQ.""" def __init__(self, client): diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py index b35cd8cf5a8..7904f0a6cce 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee.py @@ -16,7 +16,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['smappy==0.2.15'] +REQUIREMENTS = ['smappy==0.2.16'] _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,7 @@ def setup(hass, config): return True -class Smappee(object): +class Smappee: """Stores data retrieved from Smappee sensor.""" def __init__(self, client_id, client_secret, username, diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 4f50c6beaaa..34290819106 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.helpers import intent, config_validation as cv -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt DOMAIN = 'snips' DEPENDENCIES = ['mqtt'] diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index a5a45417de2..4c5592c02c2 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -22,7 +22,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Sonos from a config entry.""" - hass.async_add_job(hass.config_entries.async_forward_entry_setup( + hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, 'media_player')) return True @@ -31,7 +31,7 @@ async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" import soco - return await hass.async_add_job(soco.discover) + return await hass.async_add_executor_job(soco.discover) config_entry_flow.register_discovery_flow(DOMAIN, 'Sonos', _async_has_devices) diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 9742bc25c63..bf7db87f06b 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -56,14 +56,14 @@ def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) zones = yield from api.get_zones() if zones: - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'binary_sensor', DOMAIN, {ATTR_DISCOVER_DEVICES: zones}, config)) # create a separate alarm panel for each area areas = yield from api.get_areas() if areas: - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'alarm_control_panel', DOMAIN, {ATTR_DISCOVER_AREAS: areas}, config)) @@ -151,7 +151,7 @@ def _ws_process_message(message, async_callback, *args): "Unsuccessful websocket message delivered, ignoring: %s", message) try: yield from async_callback(message['data']['sia'], *args) - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except _LOGGER.exception("Exception in callback, ignoring") diff --git a/homeassistant/components/spider.py b/homeassistant/components/spider.py new file mode 100644 index 00000000000..48632be6bad --- /dev/null +++ b/homeassistant/components/spider.py @@ -0,0 +1,65 @@ +""" +Support for Spider Smart devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/spider/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['spiderpy==1.2.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'spider' + +SPIDER_COMPONENTS = [ + 'climate', + 'switch' +] + +SCAN_INTERVAL = timedelta(seconds=120) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up Spider Component.""" + from spiderpy.spiderapi import SpiderApi + from spiderpy.spiderapi import UnauthorizedException + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + refresh_rate = config[DOMAIN][CONF_SCAN_INTERVAL] + + try: + api = SpiderApi(username, password, refresh_rate.total_seconds()) + + hass.data[DOMAIN] = { + 'controller': api, + 'thermostats': api.get_thermostats(), + 'power_plugs': api.get_power_plugs() + } + + for component in SPIDER_COMPONENTS: + load_platform(hass, component, DOMAIN, {}) + + _LOGGER.debug("Connection with Spider API succeeded") + return True + except UnauthorizedException: + _LOGGER.error("Can't connect to the Spider API") + return False diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index b9ee8126ed3..cb69240ee73 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -114,7 +114,8 @@ async def async_setup(hass, config): if not switch.should_poll: continue - update_tasks.append(switch.async_update_ha_state(True)) + update_tasks.append( + switch.async_update_ha_state(True, service.context)) if update_tasks: await asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 4e62b711979..01d27b8abcd 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -107,7 +107,7 @@ class PwrCtrlSwitch(SwitchDevice): self._port.off() -class PwrCtrlDevice(object): +class PwrCtrlDevice: """Device representation for per device throttling.""" def __init__(self, device): diff --git a/homeassistant/components/switch/arduino.py b/homeassistant/components/switch/arduino.py index 1547f4f1dee..2bcb04c566e 100644 --- a/homeassistant/components/switch/arduino.py +++ b/homeassistant/components/switch/arduino.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol -import homeassistant.components.arduino as arduino +from homeassistant.components import arduino from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/switch/bbb_gpio.py b/homeassistant/components/switch/bbb_gpio.py index 5412f559b73..94952ac736b 100644 --- a/homeassistant/components/switch/bbb_gpio.py +++ b/homeassistant/components/switch/bbb_gpio.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA -import homeassistant.components.bbb_gpio as bbb_gpio +from homeassistant.components import bbb_gpio from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 46002112177..6b754effaf1 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -348,7 +348,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): self._state = self._parent_device.get_outlet_status(self._slot) -class BroadlinkMP1Switch(object): +class BroadlinkMP1Switch: """Representation of a Broadlink switch - To fetch states of all slots.""" def __init__(self, device): diff --git a/homeassistant/components/switch/digitalloggers.py b/homeassistant/components/switch/digitalloggers.py index f3af70c6222..29e6771d1d5 100644 --- a/homeassistant/components/switch/digitalloggers.py +++ b/homeassistant/components/switch/digitalloggers.py @@ -122,7 +122,7 @@ class DINRelay(SwitchDevice): self._state = outlet_status[2] == 'ON' -class DINRelayDevice(object): +class DINRelayDevice: """Device representation for per device throttling.""" def __init__(self, power_switch): diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index 1c7253c4ec3..9ce324ef6bb 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -125,7 +125,7 @@ class SmartPlugSwitch(SwitchDevice): self.data.update() -class SmartPlugData(object): +class SmartPlugData: """Get the latest data from smart plug.""" def __init__(self, smartplug): diff --git a/homeassistant/components/switch/fritzdect.py b/homeassistant/components/switch/fritzdect.py index 9968f631260..9c0f852846a 100644 --- a/homeassistant/components/switch/fritzdect.py +++ b/homeassistant/components/switch/fritzdect.py @@ -163,7 +163,7 @@ class FritzDectSwitch(SwitchDevice): self.data.is_online = False -class FritzDectSwitchData(object): +class FritzDectSwitchData: """Get the latest data from the fritz box.""" def __init__(self, fritz, ain): diff --git a/homeassistant/components/switch/ihc.py b/homeassistant/components/switch/ihc.py index 499a4ca53a7..3f461784693 100644 --- a/homeassistant/components/switch/ihc.py +++ b/homeassistant/components/switch/ihc.py @@ -3,8 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.ihc/ """ -from xml.etree.ElementTree import Element - import voluptuous as vol from homeassistant.components.ihc import ( @@ -53,7 +51,7 @@ class IHCSwitch(IHCDevice, SwitchDevice): """IHC Switch.""" def __init__(self, ihc_controller, name: str, ihc_id: int, - info: bool, product: Element = None) -> None: + info: bool, product=None) -> None: """Initialize the IHC switch.""" super().__init__(ihc_controller, name, ihc_id, product) self._state = False diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py index 4456436ea61..c4c8a854670 100644 --- a/homeassistant/components/switch/insteon_local.py +++ b/homeassistant/components/switch/insteon_local.py @@ -8,7 +8,7 @@ import logging from datetime import timedelta from homeassistant.components.switch import SwitchDevice -import homeassistant.util as util +from homeassistant import util _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/litejet.py b/homeassistant/components/switch/litejet.py index 1e7c46733ad..79ef4a5fd7f 100644 --- a/homeassistant/components/switch/litejet.py +++ b/homeassistant/components/switch/litejet.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/switch.litejet/ """ import logging -import homeassistant.components.litejet as litejet +from homeassistant.components import litejet from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['litejet'] diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index ca70c212774..94e1d7ea6d6 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/switch.modbus/ import logging import voluptuous as vol -import homeassistant.components.modbus as modbus +from homeassistant.components import modbus from homeassistant.const import ( CONF_NAME, CONF_SLAVE, CONF_COMMAND_ON, CONF_COMMAND_OFF) from homeassistant.helpers.entity import ToggleEntity diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 1075888e199..eb91f8d846a 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -18,7 +18,7 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import async_get_last_state @@ -31,12 +31,16 @@ DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False CONF_UNIQUE_ID = 'unique_id' +CONF_STATE_ON = "state_on" +CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_STATE_ON): cv.string, + vol.Optional(CONF_STATE_OFF): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -62,6 +66,8 @@ async def async_setup_platform(hass, config, async_add_devices, config.get(CONF_RETAIN), config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), + config.get(CONF_STATE_ON), + config.get(CONF_STATE_OFF), config.get(CONF_OPTIMISTIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), @@ -75,9 +81,10 @@ class MqttSwitch(MqttAvailability, SwitchDevice): def __init__(self, name, icon, state_topic, command_topic, availability_topic, - qos, retain, payload_on, payload_off, optimistic, - payload_available, payload_not_available, - unique_id: Optional[str], value_template): + qos, retain, payload_on, payload_off, state_on, + state_off, optimistic, payload_available, + payload_not_available, unique_id: Optional[str], + value_template): """Initialize the MQTT switch.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -90,6 +97,8 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._retain = retain self._payload_on = payload_on self._payload_off = payload_off + self._state_on = state_on if state_on else self._payload_on + self._state_off = state_off if state_off else self._payload_off self._optimistic = optimistic self._template = value_template self._unique_id = unique_id @@ -104,9 +113,9 @@ class MqttSwitch(MqttAvailability, SwitchDevice): if self._template is not None: payload = self._template.async_render_with_possible_json_value( payload) - if payload == self._payload_on: + if payload == self._state_on: self._state = True - elif payload == self._payload_off: + elif payload == self._state_off: self._state = False self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 57fa4b00c98..7ffce13ff6a 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -import homeassistant.components.pilight as pilight +from homeassistant.components import pilight from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE, CONF_PROTOCOL, STATE_ON) @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -class _ReceiveHandle(object): +class _ReceiveHandle: def __init__(self, config, echo): """Initialize the handle.""" self.config_items = config.items() diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index e25368f3c5c..06f2ee5f550 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -11,7 +11,7 @@ from datetime import timedelta import voluptuous as vol -import homeassistant.util as util +from homeassistant import util from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 68e91612008..17b5c8e40d5 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.rfxtrx as rfxtrx +from homeassistant.components import rfxtrx from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.components.rfxtrx import ( CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index 26de2a78e18..300af4be61d 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA -import homeassistant.components.rpi_gpio as rpi_gpio +from homeassistant.components import rpi_gpio from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/switch/rpi_pfio.py index 3031b1e0290..dad0c7c59ba 100644 --- a/homeassistant/components/switch/rpi_pfio.py +++ b/homeassistant/components/switch/rpi_pfio.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.rpi_pfio as rpi_pfio +from homeassistant.components import rpi_pfio from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index 8b2734612de..b549f351afc 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -import homeassistant.components.scsgate as scsgate +from homeassistant.components import scsgate from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_DEVICES) @@ -152,7 +152,7 @@ class SCSGateSwitch(SwitchDevice): ) -class SCSGateScenarioSwitch(object): +class SCSGateScenarioSwitch: """Provides a SCSGate scenario switch. This switch is always in an 'off" state, when toggled it's used to trigger diff --git a/homeassistant/components/switch/spider.py b/homeassistant/components/switch/spider.py new file mode 100644 index 00000000000..94b7db8f1e5 --- /dev/null +++ b/homeassistant/components/switch/spider.py @@ -0,0 +1,77 @@ +""" +Support for Spider switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.spider/ +""" + +import logging + +from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['spider'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Spider thermostat.""" + if discovery_info is None: + return + + devices = [SpiderPowerPlug(hass.data[SPIDER_DOMAIN]['controller'], device) + for device in hass.data[SPIDER_DOMAIN]['power_plugs']] + + add_devices(devices, True) + + +class SpiderPowerPlug(SwitchDevice): + """Representation of a Spider Power Plug.""" + + def __init__(self, api, power_plug): + """Initialize the Vera device.""" + self.api = api + self.power_plug = power_plug + + @property + def unique_id(self): + """Return the ID of this switch.""" + return self.power_plug.id + + @property + def name(self): + """Return the name of the switch if any.""" + return self.power_plug.name + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return round(self.power_plug.current_energy_consumption) + + @property + def today_energy_kwh(self): + """Return the current power usage in Kwh.""" + return round(self.power_plug.today_energy_consumption / 1000, 2) + + @property + def is_on(self): + """Return true if switch is on. Standby is on.""" + return self.power_plug.is_on + + @property + def available(self): + """Return true if switch is available.""" + return self.power_plug.is_available + + def turn_on(self, **kwargs): + """Turn device on.""" + self.power_plug.turn_on() + + def turn_off(self, **kwargs): + """Turn device off.""" + self.power_plug.turn_off() + + def update(self): + """Get the latest data.""" + self.power_plug = self.api.get_power_plug(self.unique_id) diff --git a/homeassistant/components/switch/thinkingcleaner.py b/homeassistant/components/switch/thinkingcleaner.py index 37c2f52e228..0753435cfba 100644 --- a/homeassistant/components/switch/thinkingcleaner.py +++ b/homeassistant/components/switch/thinkingcleaner.py @@ -8,8 +8,7 @@ import time import logging from datetime import timedelta -import homeassistant.util as util - +from homeassistant import util from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.helpers.entity import ToggleEntity diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index eb54e7982a7..0cacdfe1539 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -87,8 +87,6 @@ class SmartPlugSwitch(SwitchDevice): """Update the TP-Link switch's state.""" from pyHS100 import SmartDeviceException try: - self._available = True - self._state = self.smartplug.state == \ self.smartplug.SWITCH_STATE_ON @@ -121,6 +119,10 @@ class SmartPlugSwitch(SwitchDevice): # Device returned no daily history pass + self._available = True + except (SmartDeviceException, OSError) as ex: - _LOGGER.warning("Could not read state for %s: %s", self.name, ex) - self._available = False + if self._available: + _LOGGER.warning( + "Could not read state for %s: %s", self.name, ex) + self._available = False diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index c18ad492d40..35ea435bf48 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -35,7 +35,7 @@ WEMO_STANDBY = 8 def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up discovered WeMo switches.""" - import pywemo.discovery as discovery + from pywemo import discovery if discovery_info is not None: location = discovery_info['ssdp_description'] @@ -166,9 +166,9 @@ class WemoSwitch(SwitchDevice): standby_state = int(self.insight_params['state']) if standby_state == WEMO_ON: return STATE_ON - elif standby_state == WEMO_OFF: + if standby_state == WEMO_OFF: return STATE_OFF - elif standby_state == WEMO_STANDBY: + if standby_state == WEMO_STANDBY: return STATE_STANDBY return STATE_UNKNOWN diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py index adf3bf2d9bd..fa32843eb4b 100644 --- a/homeassistant/components/switch/zoneminder.py +++ b/homeassistant/components/switch/zoneminder.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_COMMAND_ON, CONF_COMMAND_OFF) -import homeassistant.components.zoneminder as zoneminder +from homeassistant.components import zoneminder import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 8a0a1683aa4..31f942bd3af 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -8,7 +8,7 @@ import logging import time from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components import zwave -from homeassistant.components.zwave import workaround, async_setup_platform # noqa # pylint: disable=unused-import +from homeassistant.components.zwave import workaround, async_setup_platform # noqa pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index ba91dd7c1fc..1cbc81709c4 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) TAHOMA_COMPONENTS = [ - 'scene', 'sensor', 'cover', 'switch' + 'scene', 'sensor', 'cover', 'switch', 'binary_sensor' ] TAHOMA_TYPES = { @@ -50,6 +50,7 @@ TAHOMA_TYPES = { 'io:WindowOpenerVeluxIOComponent': 'cover', 'io:LightIOSystemSensor': 'sensor', 'rts:GarageDoor4TRTSComponent': 'switch', + 'rtds:RTDSSmokeSensor': 'smoke', } diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index b9329a46b72..53695102601 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -502,7 +502,7 @@ class TelegramNotificationService: text, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, **params) - elif type_edit == SERVICE_EDIT_CAPTION: + if type_edit == SERVICE_EDIT_CAPTION: func_send = self.bot.editMessageCaption params[ATTR_CAPTION] = kwargs.get(ATTR_CAPTION) else: diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index ba8dc54b264..6ee42b32504 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -92,8 +92,7 @@ class TelegramPoll(BaseTelegramBotEntity): if resp.status == 200: _json = yield from resp.json() return _json - else: - raise WrongHttpStatus('wrong status {}'.format(resp.status)) + raise WrongHttpStatus('wrong status {}'.format(resp.status)) finally: if resp is not None: yield from resp.release() diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index dfb4b1e5fa9..c2b7ba9ba0f 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -206,7 +206,7 @@ def setup(hass, config, session=None): return True -class TelldusLiveClient(object): +class TelldusLiveClient: """Get the latest data and update the states.""" def __init__(self, hass, config, session): @@ -240,11 +240,11 @@ class TelldusLiveClient(object): from tellduslive import (DIM, UP, TURNON) if device.methods & DIM: return 'light' - elif device.methods & UP: + if device.methods & UP: return 'cover' - elif device.methods & TURNON: + if device.methods & TURNON: return 'switch' - elif device.methods == 0: + if device.methods == 0: return 'binary_sensor' _LOGGER.warning( "Unidentified device type (methods: %d)", device.methods) @@ -349,9 +349,9 @@ class TelldusLiveEntity(Entity): BATTERY_OK) if self.device.battery == BATTERY_LOW: return 1 - elif self.device.battery == BATTERY_UNKNOWN: + if self.device.battery == BATTERY_UNKNOWN: return None - elif self.device.battery == BATTERY_OK: + if self.device.battery == BATTERY_OK: return 100 return self.device.battery # Percentage diff --git a/homeassistant/components/thingspeak.py b/homeassistant/components/thingspeak.py index a21d44527a1..9a876a87683 100644 --- a/homeassistant/components/thingspeak.py +++ b/homeassistant/components/thingspeak.py @@ -11,9 +11,8 @@ import voluptuous as vol from homeassistant.const import ( CONF_API_KEY, CONF_ID, CONF_WHITELIST, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.helpers import state as state_helper +from homeassistant.helpers import event, state as state_helper import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.event as event REQUIREMENTS = ['thingspeak==0.4.1'] diff --git a/homeassistant/components/toon.py b/homeassistant/components/toon.py index ffb820e8148..cfd0d297d54 100644 --- a/homeassistant/components/toon.py +++ b/homeassistant/components/toon.py @@ -59,7 +59,7 @@ def setup(hass, config): return True -class ToonDataStore(object): +class ToonDataStore: """An object to store the Toon data.""" def __init__(self, username, password, gas=DEFAULT_GAS, diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index 9ed613abde0..b2e41902552 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -166,8 +166,8 @@ async def _setup_gateway(hass, hass_config, host, identity, key, return True gateways[gateway_id] = gateway - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'light', DOMAIN, {'gateway': gateway_id}, hass_config)) - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'sensor', DOMAIN, {'gateway': gateway_id}, hass_config)) return True diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 999b584360c..f060c9f353a 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['mutagen==1.40.0'] +REQUIREMENTS = ['mutagen==1.41.0'] _LOGGER = logging.getLogger(__name__) @@ -169,7 +169,7 @@ async def async_setup(hass, config): return True -class SpeechManager(object): +class SpeechManager: """Representation of a speech store.""" def __init__(self, hass): @@ -440,7 +440,7 @@ class SpeechManager(object): return data_bytes.getvalue() -class Provider(object): +class Provider: """Represent a single TTS provider.""" hass = None diff --git a/homeassistant/components/tuya.py b/homeassistant/components/tuya.py index c557774b5f1..490c11baad7 100644 --- a/homeassistant/components/tuya.py +++ b/homeassistant/components/tuya.py @@ -33,7 +33,11 @@ SERVICE_FORCE_UPDATE = 'force_update' SERVICE_PULL_DEVICES = 'pull_devices' TUYA_TYPE_TO_HA = { + 'climate': 'climate', + 'cover': 'cover', + 'fan': 'fan', 'light': 'light', + 'scene': 'scene', 'switch': 'switch', } @@ -140,11 +144,6 @@ class TuyaDevice(Entity): """Return Tuya device name.""" return self.tuya.name() - @property - def entity_picture(self): - """Return the entity picture to use in the frontend, if any.""" - return self.tuya.iconurl() - @property def available(self): """Return if the device is available.""" diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud.py index 9de7f6c4444..0f503dcdc39 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud.py @@ -92,7 +92,7 @@ def setup(hass, config): return True -class UpCloud(object): +class UpCloud: """Handle all communication with the UpCloud API.""" def __init__(self, manager): diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index 8aeb93fed25..b4fe9d3fce9 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -88,7 +88,7 @@ async def async_setup(hass, config): service = device.find_first_service(IP_SERVICE) if _service['serviceType'] == CIC_SERVICE: unit = config.get(CONF_UNITS) - hass.async_add_job(discovery.async_load_platform( + hass.async_create_task(discovery.async_load_platform( hass, 'sensor', DOMAIN, {'unit': unit}, config)) except UpnpSoapError as error: _LOGGER.error(error) diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps.py index 364562f1119..41aa240492b 100644 --- a/homeassistant/components/usps.py +++ b/homeassistant/components/usps.py @@ -65,7 +65,7 @@ def setup(hass, config): return True -class USPSData(object): +class USPSData: """Stores the data retrieved from USPS. For each entity to use, acts as the single point responsible for fetching diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 8c2f110257f..fd80f4cdbfb 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol -import homeassistant.components.mqtt as mqtt +from homeassistant.components import mqtt from homeassistant.components.mqtt import MqttAvailability from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 6289fed265d..224e763a097 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -12,7 +12,8 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.components.vacuum import ( VacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) + SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, + SUPPORT_LOCATE) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) @@ -24,7 +25,7 @@ SCAN_INTERVAL = timedelta(minutes=5) SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ - SUPPORT_STATUS | SUPPORT_MAP + SUPPORT_STATUS | SUPPORT_MAP | SUPPORT_LOCATE ATTR_CLEAN_START = 'clean_start' ATTR_CLEAN_STOP = 'clean_stop' @@ -211,3 +212,7 @@ class NeatoConnectedVacuum(VacuumDevice): self.robot.pause_cleaning() if self._state['state'] == 3: self.robot.resume_cleaning() + + def locate(self, **kwargs): + """Locate the robot by making it emit a sound.""" + self.robot.locate() diff --git a/homeassistant/components/velux.py b/homeassistant/components/velux.py index 47daf17f2a9..c3c6c1e2114 100644 --- a/homeassistant/components/velux.py +++ b/homeassistant/components/velux.py @@ -39,7 +39,7 @@ async def async_setup(hass, config): return False for component in SUPPORTED_DOMAINS: - hass.async_add_job( + hass.async_create_task( discovery.async_load_platform(hass, component, DOMAIN, {}, config)) return True diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 0ab5e7ce39a..5bc6260c0a7 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.43'] +REQUIREMENTS = ['pyvera==0.2.44'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index b367752c247..1f26ab639d6 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -89,7 +89,7 @@ def setup(hass, config): return True -class VerisureHub(object): +class VerisureHub: """A Verisure hub wrapper class.""" def __init__(self, domain_config, verisure): diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index 6557be2fb1b..0ce8870bedf 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -136,12 +136,11 @@ class VolvoData: if (vehicle.registration_number and vehicle.registration_number.lower()) in self.names: return self.names[vehicle.registration_number.lower()] - elif (vehicle.vin and - vehicle.vin.lower() in self.names): + if vehicle.vin and vehicle.vin.lower() in self.names: return self.names[vehicle.vin.lower()] - elif vehicle.registration_number: + if vehicle.registration_number: return vehicle.registration_number - elif vehicle.vin: + if vehicle.vin: return vehicle.vin return '' diff --git a/homeassistant/components/vultr.py b/homeassistant/components/vultr.py index 59fc707bb28..b28189444ee 100644 --- a/homeassistant/components/vultr.py +++ b/homeassistant/components/vultr.py @@ -74,7 +74,7 @@ def setup(hass, config): return True -class Vultr(object): +class Vultr: """Handle all communication with the Vultr API.""" def __init__(self, api_key): diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 7afa97fd4f6..6dac22bc941 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -149,7 +149,7 @@ class DarkSkyWeather(WeatherEntity): self._ds_daily = self._dark_sky.daily -class DarkSkyData(object): +class DarkSkyData: """Get the latest data from Dark Sky.""" def __init__(self, api_key, latitude, longitude, units): diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 65fa7c8cb0f..334948b67fb 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -18,7 +18,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.8.0'] +REQUIREMENTS = ['pyowm==2.9.0'] _LOGGER = logging.getLogger(__name__) @@ -195,7 +195,7 @@ class OpenWeatherMapWeather(WeatherEntity): self.forecast_data = self._owm.forecast_data -class WeatherData(object): +class WeatherData: """Get the latest data from OpenWeatherMap.""" def __init__(self, owm, latitude, longitude, mode): diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index f9befece5a4..3f12195d6bf 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -175,7 +175,7 @@ class YahooWeatherWeather(WeatherEntity): return -class YahooWeatherData(object): +class YahooWeatherData: """Handle the Yahoo! API object and limit updates.""" def __init__(self, woeid, temp_unit): diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 98e3057338a..ed478550c7a 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,7 +18,7 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.core import callback +from homeassistant.core import Context, callback from homeassistant.loader import bind_hass from homeassistant.remote import JSONEncoder from homeassistant.helpers import config_validation as cv @@ -262,6 +262,18 @@ class ActiveConnection: self._handle_task = None self._writer_task = None + @property + def user(self): + """Return the user associated with the connection.""" + return self.request.get('hass_user') + + def context(self, msg): + """Return a context.""" + user = self.user + if user is None: + return Context() + return Context(user_id=user.id) + def debug(self, message1, message2=''): """Print a debug message.""" _LOGGER.debug("WS %s: %s %s", id(self.wsock), message1, message2) @@ -287,7 +299,7 @@ class ActiveConnection: @callback def send_message_outside(self, message): - """Send a message to the client outside of the main task. + """Send a message to the client. Closes connection if the client is not reading the messages. @@ -508,7 +520,8 @@ def handle_call_service(hass, connection, msg): async def call_service_helper(msg): """Call a service and fire complete message.""" await hass.services.async_call( - msg['domain'], msg['service'], msg.get('service_data'), True) + msg['domain'], msg['service'], msg.get('service_data'), True, + connection.context(msg)) connection.send_message_outside(result_message(msg['id'])) hass.async_add_job(call_service_helper(msg)) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index e8c7db5efe1..27027cc9eb4 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -26,6 +26,7 @@ WEMO_MODEL_DISPATCH = { 'Insight': 'switch', 'LightSwitch': 'switch', 'Maker': 'switch', + 'Motion': 'binary_sensor', 'Sensor': 'binary_sensor', 'Socket': 'switch' } diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 7c171d74967..c996572bf51 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -173,10 +173,9 @@ def _request_app_setup(hass, config): ATTR_CLIENT_SECRET: client_secret}) setup(hass, config) return - else: - error_msg = "Your input was invalid. Please try again." - _configurator = hass.data[DOMAIN]['configuring'][DOMAIN] - configurator.notify_errors(_configurator, error_msg) + error_msg = "Your input was invalid. Please try again." + _configurator = hass.data[DOMAIN]['configuring'][DOMAIN] + configurator.notify_errors(_configurator, error_msg) start_url = "{}{}".format(hass.config.api.base_url, WINK_AUTH_CALLBACK_PATH) @@ -452,7 +451,7 @@ def setup(hass, config): _man = siren.wink.device_manufacturer() if (service.service != SERVICE_SET_AUTO_SHUTOFF and service.service != SERVICE_ENABLE_SIREN and - (_man != 'dome' and _man != 'wink')): + _man not in ('dome', 'wink')): _LOGGER.error("Service only valid for Dome or Wink sirens") return @@ -487,7 +486,7 @@ def setup(hass, config): has_dome_or_wink_siren = False for siren in pywink.get_sirens(): _man = siren.device_manufacturer() - if _man == "dome" or _man == "wink": + if _man in ("dome", "wink"): has_dome_or_wink_siren = True _id = siren.object_id() + siren.name() if _id not in hass.data[DOMAIN]['unique_ids']: diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 3a84e963841..67bdf744251 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -124,7 +124,7 @@ def frame_is_relevant(entity, frame): return True -class ZigBeeConfig(object): +class ZigBeeConfig: """Handle the fetching of configuration from the config file.""" def __init__(self, config): diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index fc2e7fc912d..f88b911a6a5 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -37,7 +37,9 @@ DISCOVERY_SCHEMAS = [ const.DISC_OPTIONAL: True, }})}, {const.DISC_COMPONENT: 'climate', - const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_THERMOSTAT], + const.DISC_GENERIC_DEVICE_CLASS: [ + const.GENERIC_TYPE_THERMOSTAT, + const.GENERIC_TYPE_SENSOR_MULTILEVEL], const.DISC_VALUES: dict(DEFAULT_VALUES_SCHEMA, **{ const.DISC_PRIMARY: { const.DISC_COMMAND_CLASS: [ diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index b62eeb67d32..312d72575a9 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -82,7 +82,7 @@ async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): if entity.unique_id: ready_callback(waited) return - elif waited >= const.NODE_READY_WAIT_SECS: + if waited >= const.NODE_READY_WAIT_SECS: # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. timeout_callback(waited) return diff --git a/homeassistant/config.py b/homeassistant/config.py index d9206d62250..6120a20fd63 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,8 +7,9 @@ import os import re import shutil # pylint: disable=unused-import -from typing import Any, Tuple, Optional # noqa: F401 - +from typing import ( # noqa: F401 + Any, Tuple, Optional, Dict, List, Union, Callable) +from types import ModuleType import voluptuous as vol from voluptuous.humanize import humanize_error @@ -21,7 +22,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_TYPE) -from homeassistant.core import callback, DOMAIN as CONF_CORE +from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component, get_platform from homeassistant.util.yaml import load_yaml, SECRET_YAML @@ -193,7 +194,7 @@ def ensure_config_exists(config_dir: str, detect_location: bool = True)\ return config_path -def create_default_config(config_dir: str, detect_location=True)\ +def create_default_config(config_dir: str, detect_location: bool = True)\ -> Optional[str]: """Create a default configuration file in given configuration directory. @@ -276,7 +277,7 @@ def create_default_config(config_dir: str, detect_location=True)\ return None -async def async_hass_config_yaml(hass): +async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: """Load YAML from a Home Assistant configuration file. This function allow a component inside the asyncio loop to reload its @@ -284,23 +285,26 @@ async def async_hass_config_yaml(hass): This method is a coroutine. """ - def _load_hass_yaml_config(): + def _load_hass_yaml_config() -> Dict: path = find_config_file(hass.config.config_dir) - conf = load_yaml_config_file(path) - return conf + if path is None: + raise HomeAssistantError( + "Config file not found in: {}".format(hass.config.config_dir)) + return load_yaml_config_file(path) - conf = await hass.async_add_job(_load_hass_yaml_config) - return conf + return await hass.async_add_executor_job(_load_hass_yaml_config) -def find_config_file(config_dir: str) -> Optional[str]: +def find_config_file(config_dir: Optional[str]) -> Optional[str]: """Look in given directory for supported configuration files.""" + if config_dir is None: + return None config_path = os.path.join(config_dir, YAML_CONFIG_FILE) return config_path if os.path.isfile(config_path) else None -def load_yaml_config_file(config_path): +def load_yaml_config_file(config_path: str) -> Dict[Any, Any]: """Parse a YAML configuration file. This method needs to run in an executor. @@ -323,7 +327,7 @@ def load_yaml_config_file(config_path): return conf_dict -def process_ha_config_upgrade(hass): +def process_ha_config_upgrade(hass: HomeAssistant) -> None: """Upgrade configuration if necessary. This method needs to run in an executor. @@ -360,7 +364,8 @@ def process_ha_config_upgrade(hass): @callback -def async_log_exception(ex, domain, config, hass): +def async_log_exception(ex: vol.Invalid, domain: str, config: Dict, + hass: HomeAssistant) -> None: """Log an error for configuration validation. This method must be run in the event loop. @@ -371,7 +376,7 @@ def async_log_exception(ex, domain, config, hass): @callback -def _format_config_error(ex, domain, config): +def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. @@ -396,7 +401,8 @@ def _format_config_error(ex, domain, config): return message -async def async_process_ha_core_config(hass, config): +async def async_process_ha_core_config( + hass: HomeAssistant, config: Dict) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -405,12 +411,12 @@ async def async_process_ha_core_config(hass, config): # Only load auth during startup. if not hasattr(hass, 'auth'): - hass.auth = await auth.auth_manager_from_config( - hass, config.get(CONF_AUTH_PROVIDERS, [])) + setattr(hass, 'auth', await auth.auth_manager_from_config( + hass, config.get(CONF_AUTH_PROVIDERS, []))) hac = hass.config - def set_time_zone(time_zone_str): + def set_time_zone(time_zone_str: Optional[str]) -> None: """Help to set the time zone.""" if time_zone_str is None: return @@ -430,11 +436,10 @@ async def async_process_ha_core_config(hass, config): if key in config: setattr(hac, attr, config[key]) - if CONF_TIME_ZONE in config: - set_time_zone(config.get(CONF_TIME_ZONE)) + set_time_zone(config.get(CONF_TIME_ZONE)) # Init whitelist external dir - hac.whitelist_external_dirs = set((hass.config.path('www'),)) + hac.whitelist_external_dirs = {hass.config.path('www')} if CONF_WHITELIST_EXTERNAL_DIRS in config: hac.whitelist_external_dirs.update( set(config[CONF_WHITELIST_EXTERNAL_DIRS])) @@ -484,12 +489,12 @@ async def async_process_ha_core_config(hass, config): hac.time_zone, hac.elevation): return - discovered = [] + discovered = [] # type: List[Tuple[str, Any]] # If we miss some of the needed values, auto detect them if None in (hac.latitude, hac.longitude, hac.units, hac.time_zone): - info = await hass.async_add_job( + info = await hass.async_add_executor_job( loc_util.detect_location_info) if info is None: @@ -515,7 +520,7 @@ async def async_process_ha_core_config(hass, config): if hac.elevation is None and hac.latitude is not None and \ hac.longitude is not None: - elevation = await hass.async_add_job( + elevation = await hass.async_add_executor_job( loc_util.elevation, hac.latitude, hac.longitude) hac.elevation = elevation discovered.append(('elevation', elevation)) @@ -526,7 +531,8 @@ async def async_process_ha_core_config(hass, config): ", ".join('{}: {}'.format(key, val) for key, val in discovered)) -def _log_pkg_error(package, component, config, message): +def _log_pkg_error( + package: str, component: str, config: Dict, message: str) -> None: """Log an error while merging packages.""" message = "Package {} setup failed. Component {} {}".format( package, component, message) @@ -539,12 +545,13 @@ def _log_pkg_error(package, component, config, message): _LOGGER.error(message) -def _identify_config_schema(module): +def _identify_config_schema(module: ModuleType) -> \ + Tuple[Optional[str], Optional[Dict]]: """Extract the schema and identify list or dict based.""" try: - schema = module.CONFIG_SCHEMA.schema[module.DOMAIN] + schema = module.CONFIG_SCHEMA.schema[module.DOMAIN] # type: ignore except (AttributeError, KeyError): - return (None, None) + return None, None t_schema = str(schema) if t_schema.startswith('{'): return ('dict', schema) @@ -553,9 +560,10 @@ def _identify_config_schema(module): return '', schema -def _recursive_merge(conf, package): +def _recursive_merge( + conf: Dict[str, Any], package: Dict[str, Any]) -> Union[bool, str]: """Merge package into conf, recursively.""" - error = False + error = False # type: Union[bool, str] for key, pack_conf in package.items(): if isinstance(pack_conf, dict): if not pack_conf: @@ -572,13 +580,12 @@ def _recursive_merge(conf, package): else: if conf.get(key) is not None: return key - else: - conf[key] = pack_conf + conf[key] = pack_conf return error -def merge_packages_config(hass, config, packages, - _log_pkg_error=_log_pkg_error): +def merge_packages_config(hass: HomeAssistant, config: Dict, packages: Dict, + _log_pkg_error: Callable = _log_pkg_error) -> Dict: """Merge packages into the top-level configuration. Mutate config.""" # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) @@ -642,7 +649,8 @@ def merge_packages_config(hass, config, packages, @callback -def async_process_component_config(hass, config, domain): +def async_process_component_config( + hass: HomeAssistant, config: Dict, domain: str) -> Optional[Dict]: """Check component configuration and return processed configuration. Returns None on error. @@ -653,7 +661,7 @@ def async_process_component_config(hass, config, domain): if hasattr(component, 'CONFIG_SCHEMA'): try: - config = component.CONFIG_SCHEMA(config) + config = component.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as ex: async_log_exception(ex, domain, config, hass) return None @@ -663,7 +671,8 @@ def async_process_component_config(hass, config, domain): for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema try: - p_validated = component.PLATFORM_SCHEMA(p_config) + p_validated = component.PLATFORM_SCHEMA( # type: ignore + p_config) except vol.Invalid as ex: async_log_exception(ex, domain, config, hass) continue @@ -684,7 +693,8 @@ def async_process_component_config(hass, config, domain): if hasattr(platform, 'PLATFORM_SCHEMA'): # pylint: disable=no-member try: - p_validated = platform.PLATFORM_SCHEMA(p_validated) + p_validated = platform.PLATFORM_SCHEMA( # type: ignore + p_validated) except vol.Invalid as ex: async_log_exception(ex, '{}.{}'.format(domain, p_name), p_validated, hass) @@ -702,14 +712,14 @@ def async_process_component_config(hass, config, domain): return config -async def async_check_ha_config_file(hass): +async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: """Check if Home Assistant configuration file is valid. This method is a coroutine. """ from homeassistant.scripts.check_config import check_ha_config_file - res = await hass.async_add_job( + res = await hass.async_add_executor_job( check_ha_config_file, hass) if not res.errors: @@ -718,7 +728,9 @@ async def async_check_ha_config_file(hass): @callback -def async_notify_setup_error(hass, component, display_link=False): +def async_notify_setup_error( + hass: HomeAssistant, component: str, + display_link: bool = False) -> None: """Print a persistent notification. This method must be run in the event loop. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2e5613057f1..8e2bb3fa5df 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -113,9 +113,10 @@ the flow from the config panel. import logging import uuid +from typing import Set, Optional # noqa pylint: disable=unused-import from homeassistant import data_entry_flow -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry @@ -163,8 +164,9 @@ class ConfigEntry: __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source', 'state') - def __init__(self, version, domain, title, data, source, entry_id=None, - state=ENTRY_STATE_NOT_LOADED): + def __init__(self, version: str, domain: str, title: str, data: dict, + source: str, entry_id: Optional[str] = None, + state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" # Unique id of the config entry self.entry_id = entry_id or uuid.uuid4().hex @@ -187,7 +189,8 @@ class ConfigEntry: # State of the entry (LOADED, NOT_LOADED) self.state = state - async def async_setup(self, hass, *, component=None): + async def async_setup( + self, hass: HomeAssistant, *, component=None) -> None: """Set up an entry.""" if component is None: component = getattr(hass.components, self.domain) @@ -279,7 +282,7 @@ class ConfigEntries: @callback def async_domains(self): """Return domains for which we have entries.""" - seen = set() + seen = set() # type: Set[ConfigEntry] result = [] for entry in self._entries: diff --git a/homeassistant/const.py b/homeassistant/const.py index 34cc3329a56..33a00b65533 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 = 74 -PATCH_VERSION = '2' +MINOR_VERSION = 75 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) @@ -224,9 +224,6 @@ ATTR_ID = 'id' # Name ATTR_NAME = 'name' -# Data for a SERVICE_EXECUTED event -ATTR_SERVICE_CALL_ID = 'service_call_id' - # Contains one string or a list of strings, each being an entity id ATTR_ENTITY_ID = 'entity_id' diff --git a/homeassistant/core.py b/homeassistant/core.py index 8b534bf1731..b17df2c11fe 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -4,9 +4,9 @@ Core components of Home Assistant. Home Assistant is a Home Automation framework for observing the state of entities and react to changes. """ -# pylint: disable=unused-import import asyncio from concurrent.futures import ThreadPoolExecutor +import datetime import enum import logging import os @@ -15,18 +15,22 @@ import re import sys import threading from time import monotonic +import uuid from types import MappingProxyType +# pylint: disable=unused-import from typing import ( # NOQA - Optional, Any, Callable, List, TypeVar, Dict, Coroutine) + Optional, Any, Callable, List, TypeVar, Dict, Coroutine, Set, + TYPE_CHECKING, Awaitable, Iterator) from async_timeout import timeout +import attr import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE, - ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, EVENT_HOMEASSISTANT_CLOSE, @@ -37,12 +41,21 @@ from homeassistant.exceptions import ( from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe, fire_coroutine_threadsafe) -import homeassistant.util as util +from homeassistant import util import homeassistant.util.dt as dt_util -import homeassistant.util.location as location +from homeassistant.util import location from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA +# Typing imports that create a circular dependency +# pylint: disable=using-constant-test +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntries # noqa + +# pylint: disable=invalid-name T = TypeVar('T') +CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable) +CALLBACK_TYPE = Callable[[], None] +# pylint: enable=invalid-name DOMAIN = 'homeassistant' @@ -73,7 +86,7 @@ def valid_state(state: str) -> bool: return len(state) < 256 -def callback(func: Callable[..., T]) -> Callable[..., T]: +def callback(func: CALLABLE_T) -> CALLABLE_T: """Annotation to mark method as safe to call from within the event loop.""" setattr(func, '_hass_callback', True) return func @@ -85,7 +98,7 @@ def is_callback(func: Callable[..., Any]) -> bool: @callback -def async_loop_exception_handler(loop, context): +def async_loop_exception_handler(_: Any, context: Dict) -> None: """Handle all exception inside the core loop.""" kwargs = {} exception = context.get('exception') @@ -93,7 +106,8 @@ def async_loop_exception_handler(loop, context): kwargs['exc_info'] = (type(exception), exception, exception.__traceback__) - _LOGGER.error("Error doing job: %s", context['message'], **kwargs) + _LOGGER.error( # type: ignore + "Error doing job: %s", context['message'], **kwargs) class CoreState(enum.Enum): @@ -109,24 +123,26 @@ class CoreState(enum.Enum): return self.value # type: ignore -class HomeAssistant(object): +class HomeAssistant: """Root object of the Home Assistant home automation.""" - def __init__(self, loop=None): + def __init__( + self, + loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None: """Initialize new Home Assistant object.""" if sys.platform == 'win32': self.loop = loop or asyncio.ProactorEventLoop() else: self.loop = loop or asyncio.get_event_loop() - executor_opts = {'max_workers': None} + executor_opts = {'max_workers': None} # type: Dict[str, Any] if sys.version_info[:2] >= (3, 6): executor_opts['thread_name_prefix'] = 'SyncWorker' self.executor = ThreadPoolExecutor(**executor_opts) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(async_loop_exception_handler) - self._pending_tasks = [] + self._pending_tasks = [] # type: list self._track_task = True self.bus = EventBus(self) self.services = ServiceRegistry(self) @@ -135,10 +151,10 @@ class HomeAssistant(object): self.components = loader.Components(self) self.helpers = loader.Helpers(self) # This is a dictionary that any component can store any data on. - self.data = {} + self.data = {} # type: dict self.state = CoreState.not_running self.exit_code = 0 # type: int - self.config_entries = None + self.config_entries = None # type: Optional[ConfigEntries] @property def is_running(self) -> bool: @@ -163,7 +179,7 @@ class HomeAssistant(object): self.loop.close() return self.exit_code - async def async_start(self): + async def async_start(self) -> None: """Finalize startup from inside the event loop. This method is a coroutine. @@ -171,14 +187,13 @@ class HomeAssistant(object): _LOGGER.info("Starting Home Assistant") self.state = CoreState.starting - # pylint: disable=protected-access - self.loop._thread_ident = threading.get_ident() + setattr(self.loop, '_thread_ident', threading.get_ident()) self.bus.async_fire(EVENT_HOMEASSISTANT_START) try: # Only block for EVENT_HOMEASSISTANT_START listener self.async_stop_track_tasks() - with timeout(TIMEOUT_EVENT_START, loop=self.loop): + with timeout(TIMEOUT_EVENT_START): await self.async_block_till_done() except asyncio.TimeoutError: _LOGGER.warning( @@ -188,7 +203,7 @@ class HomeAssistant(object): ', '.join(self.config.components)) # Allow automations to set up the start triggers before changing state - await asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0) self.state = CoreState.running _async_create_timer(self) @@ -217,13 +232,14 @@ class HomeAssistant(object): task = None if asyncio.iscoroutine(target): - task = self.loop.create_task(target) + task = self.loop.create_task(target) # type: ignore elif is_callback(target): self.loop.call_soon(target, *args) elif asyncio.iscoroutinefunction(target): task = self.loop.create_task(target(*args)) else: - task = self.loop.run_in_executor(None, target, *args) + task = self.loop.run_in_executor( # type: ignore + None, target, *args) # If a task is scheduled if self._track_task and task is not None: @@ -249,11 +265,11 @@ class HomeAssistant(object): @callback def async_add_executor_job( self, - target: Callable[..., Any], - *args: Any) -> asyncio.Future: + target: Callable[..., T], + *args: Any) -> Awaitable[T]: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor( - None, target, *args) # type: asyncio.Future + None, target, *args) # If a task is scheduled if self._track_task: @@ -262,12 +278,12 @@ class HomeAssistant(object): return task @callback - def async_track_tasks(self): + def async_track_tasks(self) -> None: """Track tasks so you can wait for all tasks to be done.""" self._track_task = True @callback - def async_stop_track_tasks(self): + def async_stop_track_tasks(self) -> None: """Stop track tasks so you can't wait for all tasks to be done.""" self._track_task = False @@ -290,19 +306,19 @@ class HomeAssistant(object): run_coroutine_threadsafe( self.async_block_till_done(), loop=self.loop).result() - async def async_block_till_done(self): + async def async_block_till_done(self) -> None: """Block till all pending work is done.""" # To flush out any call_soon_threadsafe - await asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0) while self._pending_tasks: pending = [task for task in self._pending_tasks if not task.done()] self._pending_tasks.clear() if pending: - await asyncio.wait(pending, loop=self.loop) + await asyncio.wait(pending) else: - await asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0) def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" @@ -329,31 +345,55 @@ class HomeAssistant(object): self.loop.stop() +@attr.s(slots=True, frozen=True) +class Context: + """The context that triggered something.""" + + user_id = attr.ib( + type=str, + default=None, + ) + id = attr.ib( + type=str, + default=attr.Factory(lambda: uuid.uuid4().hex), + ) + + def as_dict(self) -> dict: + """Return a dictionary representation of the context.""" + return { + 'id': self.id, + 'user_id': self.user_id, + } + + class EventOrigin(enum.Enum): """Represent the origin of an event.""" local = 'LOCAL' remote = 'REMOTE' - def __str__(self): + def __str__(self) -> str: """Return the event.""" - return self.value + return self.value # type: ignore -class Event(object): +class Event: """Representation of an event within the bus.""" - __slots__ = ['event_type', 'data', 'origin', 'time_fired'] + __slots__ = ['event_type', 'data', 'origin', 'time_fired', 'context'] - def __init__(self, event_type, data=None, origin=EventOrigin.local, - time_fired=None): + def __init__(self, event_type: str, data: Optional[Dict] = None, + origin: EventOrigin = EventOrigin.local, + time_fired: Optional[int] = None, + context: Optional[Context] = None) -> None: """Initialize a new event.""" self.event_type = event_type self.data = data or {} self.origin = origin self.time_fired = time_fired or dt_util.utcnow() + self.context = context or Context() - def as_dict(self): + def as_dict(self) -> Dict: """Create a dict representation of this Event. Async friendly. @@ -363,9 +403,10 @@ class Event(object): 'data': dict(self.data), 'origin': str(self.origin), 'time_fired': self.time_fired, + 'context': self.context.as_dict() } - def __repr__(self): + def __repr__(self) -> str: """Return the representation.""" # pylint: disable=maybe-no-member if self.data: @@ -376,16 +417,16 @@ class Event(object): return "".format(self.event_type, str(self.origin)[0]) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Return the comparison.""" - return (self.__class__ == other.__class__ and + return (self.__class__ == other.__class__ and # type: ignore self.event_type == other.event_type and self.data == other.data and self.origin == other.origin and self.time_fired == other.time_fired) -class EventBus(object): +class EventBus: """Allow the firing of and listening for events.""" def __init__(self, hass: HomeAssistant) -> None: @@ -394,7 +435,7 @@ class EventBus(object): self._hass = hass @callback - def async_listeners(self): + def async_listeners(self) -> Dict[str, int]: """Return dictionary with events and the number of listeners. This method must be run in the event loop. @@ -403,20 +444,23 @@ class EventBus(object): for key in self._listeners} @property - def listeners(self): + def listeners(self) -> Dict[str, int]: """Return dictionary with events and the number of listeners.""" - return run_callback_threadsafe( + return run_callback_threadsafe( # type: ignore self._hass.loop, self.async_listeners ).result() - def fire(self, event_type: str, event_data=None, origin=EventOrigin.local): + def fire(self, event_type: str, event_data: Optional[Dict] = None, + origin: EventOrigin = EventOrigin.local, + context: Optional[Context] = None) -> None: """Fire an event.""" self._hass.loop.call_soon_threadsafe( - self.async_fire, event_type, event_data, origin) + self.async_fire, event_type, event_data, origin, context) @callback - def async_fire(self, event_type: str, event_data=None, - origin=EventOrigin.local): + def async_fire(self, event_type: str, event_data: Optional[Dict] = None, + origin: EventOrigin = EventOrigin.local, + context: Optional[Context] = None) -> None: """Fire an event. This method must be run in the event loop. @@ -429,7 +473,7 @@ class EventBus(object): event_type != EVENT_HOMEASSISTANT_CLOSE): listeners = match_all_listeners + listeners - event = Event(event_type, event_data, origin) + event = Event(event_type, event_data, origin, None, context) if event_type != EVENT_TIME_CHANGED: _LOGGER.info("Bus:Handling %s", event) @@ -440,7 +484,8 @@ class EventBus(object): for func in listeners: self._hass.async_add_job(func, event) - def listen(self, event_type, listener): + def listen( + self, event_type: str, listener: Callable) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` @@ -449,7 +494,7 @@ class EventBus(object): async_remove_listener = run_callback_threadsafe( self._hass.loop, self.async_listen, event_type, listener).result() - def remove_listener(): + def remove_listener() -> None: """Remove the listener.""" run_callback_threadsafe( self._hass.loop, async_remove_listener).result() @@ -457,7 +502,8 @@ class EventBus(object): return remove_listener @callback - def async_listen(self, event_type, listener): + def async_listen( + self, event_type: str, listener: Callable) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` @@ -470,13 +516,14 @@ class EventBus(object): else: self._listeners[event_type] = [listener] - def remove_listener(): + def remove_listener() -> None: """Remove the listener.""" self._async_remove_listener(event_type, listener) return remove_listener - def listen_once(self, event_type, listener): + def listen_once( + self, event_type: str, listener: Callable) -> CALLBACK_TYPE: """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` @@ -488,7 +535,7 @@ class EventBus(object): self._hass.loop, self.async_listen_once, event_type, listener, ).result() - def remove_listener(): + def remove_listener() -> None: """Remove the listener.""" run_callback_threadsafe( self._hass.loop, async_remove_listener).result() @@ -496,7 +543,8 @@ class EventBus(object): return remove_listener @callback - def async_listen_once(self, event_type, listener): + def async_listen_once( + self, event_type: str, listener: Callable) -> CALLBACK_TYPE: """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` @@ -507,8 +555,8 @@ class EventBus(object): This method must be run in the event loop. """ @callback - def onetime_listener(event): - """Remove listener from eventbus and then fire listener.""" + def onetime_listener(event: Event) -> None: + """Remove listener from event bus and then fire listener.""" if hasattr(onetime_listener, 'run'): return # Set variable so that we will never run twice. @@ -523,7 +571,8 @@ class EventBus(object): return self.async_listen(event_type, onetime_listener) @callback - def _async_remove_listener(self, event_type, listener): + def _async_remove_listener( + self, event_type: str, listener: Callable) -> None: """Remove a listener of a specific event_type. This method must be run in the event loop. @@ -540,7 +589,7 @@ class EventBus(object): _LOGGER.warning("Unable to remove unknown listener %s", listener) -class State(object): +class State: """Object to represent a state within the state machine. entity_id: the entity that is represented. @@ -548,13 +597,17 @@ class State(object): attributes: extra information on entity and state last_changed: last time the state was changed, not the attributes. last_updated: last time this object was updated. + context: Context in which it was created """ __slots__ = ['entity_id', 'state', 'attributes', - 'last_changed', 'last_updated'] + 'last_changed', 'last_updated', 'context'] - def __init__(self, entity_id, state, attributes=None, last_changed=None, - last_updated=None): + def __init__(self, entity_id: str, state: Any, + attributes: Optional[Dict] = None, + last_changed: Optional[datetime.datetime] = None, + last_updated: Optional[datetime.datetime] = None, + context: Optional[Context] = None) -> None: """Initialize a new state.""" state = str(state) @@ -573,25 +626,26 @@ class State(object): self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated + self.context = context or Context() @property - def domain(self): + def domain(self) -> str: """Domain of this state.""" return split_entity_id(self.entity_id)[0] @property - def object_id(self): + def object_id(self) -> str: """Object id of this state.""" return split_entity_id(self.entity_id)[1] @property - def name(self): + def name(self) -> str: """Name of this state.""" return ( self.attributes.get(ATTR_FRIENDLY_NAME) or self.object_id.replace('_', ' ')) - def as_dict(self): + def as_dict(self) -> Dict: """Return a dict representation of the State. Async friendly. @@ -603,10 +657,11 @@ class State(object): 'state': self.state, 'attributes': dict(self.attributes), 'last_changed': self.last_changed, - 'last_updated': self.last_updated} + 'last_updated': self.last_updated, + 'context': self.context.as_dict()} @classmethod - def from_dict(cls, json_dict): + def from_dict(cls, json_dict: Dict) -> Any: """Initialize a state from a dict. Async friendly. @@ -627,44 +682,51 @@ class State(object): if isinstance(last_updated, str): last_updated = dt_util.parse_datetime(last_updated) - return cls(json_dict['entity_id'], json_dict['state'], - json_dict.get('attributes'), last_changed, last_updated) + context = json_dict.get('context') + if context: + context = Context(**context) - def __eq__(self, other): + return cls(json_dict['entity_id'], json_dict['state'], + json_dict.get('attributes'), last_changed, last_updated, + context) + + def __eq__(self, other: Any) -> bool: """Return the comparison of the state.""" - return (self.__class__ == other.__class__ and + return (self.__class__ == other.__class__ and # type: ignore self.entity_id == other.entity_id and self.state == other.state and self.attributes == other.attributes) - def __repr__(self): + def __repr__(self) -> str: """Return the representation of the states.""" - attr = "; {}".format(util.repr_helper(self.attributes)) \ - if self.attributes else "" + attrs = "; {}".format(util.repr_helper(self.attributes)) \ + if self.attributes else "" return "".format( - self.entity_id, self.state, attr, + self.entity_id, self.state, attrs, dt_util.as_local(self.last_changed).isoformat()) -class StateMachine(object): +class StateMachine: """Helper class that tracks the state of different entities.""" - def __init__(self, bus, loop): + def __init__(self, bus: EventBus, + loop: asyncio.events.AbstractEventLoop) -> None: """Initialize state machine.""" - self._states = {} + self._states = {} # type: Dict[str, State] self._bus = bus self._loop = loop - def entity_ids(self, domain_filter=None): + def entity_ids(self, domain_filter: Optional[str] = None)-> List[str]: """List of entity ids that are being tracked.""" future = run_callback_threadsafe( self._loop, self.async_entity_ids, domain_filter ) - return future.result() + return future.result() # type: ignore @callback - def async_entity_ids(self, domain_filter=None): + def async_entity_ids( + self, domain_filter: Optional[str] = None) -> List[str]: """List of entity ids that are being tracked. This method must be run in the event loop. @@ -677,26 +739,27 @@ class StateMachine(object): return [state.entity_id for state in self._states.values() if state.domain == domain_filter] - def all(self): + def all(self)-> List[State]: """Create a list of all states.""" - return run_callback_threadsafe(self._loop, self.async_all).result() + return run_callback_threadsafe( # type: ignore + self._loop, self.async_all).result() @callback - def async_all(self): + def async_all(self)-> List[State]: """Create a list of all states. This method must be run in the event loop. """ return list(self._states.values()) - def get(self, entity_id): + def get(self, entity_id: str) -> Optional[State]: """Retrieve state of entity_id or None if not found. Async friendly. """ return self._states.get(entity_id.lower()) - def is_state(self, entity_id, state): + def is_state(self, entity_id: str, state: State) -> bool: """Test if entity exists and is specified state. Async friendly. @@ -704,16 +767,16 @@ class StateMachine(object): state_obj = self.get(entity_id) return state_obj is not None and state_obj.state == state - def remove(self, entity_id): + def remove(self, entity_id: str) -> bool: """Remove the state of an entity. Returns boolean to indicate if an entity was removed. """ - return run_callback_threadsafe( + return run_callback_threadsafe( # type: ignore self._loop, self.async_remove, entity_id).result() @callback - def async_remove(self, entity_id): + def async_remove(self, entity_id: str) -> bool: """Remove the state of an entity. Returns boolean to indicate if an entity was removed. @@ -733,7 +796,10 @@ class StateMachine(object): }) return True - def set(self, entity_id, new_state, attributes=None, force_update=False): + def set(self, entity_id: str, new_state: Any, + attributes: Optional[Dict] = None, + force_update: bool = False, + context: Optional[Context] = None) -> None: """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -744,11 +810,14 @@ class StateMachine(object): run_callback_threadsafe( self._loop, self.async_set, entity_id, new_state, attributes, force_update, + context, ).result() @callback - def async_set(self, entity_id, new_state, attributes=None, - force_update=False): + def async_set(self, entity_id: str, new_state: Any, + attributes: Optional[Dict] = None, + force_update: bool = False, + context: Optional[Context] = None) -> None: """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -762,30 +831,39 @@ class StateMachine(object): new_state = str(new_state) attributes = attributes or {} old_state = self._states.get(entity_id) - is_existing = old_state is not None - same_state = (is_existing and old_state.state == new_state and - not force_update) - same_attr = is_existing and old_state.attributes == attributes + if old_state is None: + same_state = False + same_attr = False + last_changed = None + else: + same_state = (old_state.state == new_state and + not force_update) + same_attr = old_state.attributes == attributes + last_changed = old_state.last_changed if same_state else None if same_state and same_attr: return - last_changed = old_state.last_changed if same_state else None - state = State(entity_id, new_state, attributes, last_changed) + if context is None: + context = Context() + + state = State(entity_id, new_state, attributes, last_changed, None, + context) self._states[entity_id] = state self._bus.async_fire(EVENT_STATE_CHANGED, { 'entity_id': entity_id, 'old_state': old_state, 'new_state': state, - }) + }, EventOrigin.local, context) -class Service(object): +class Service: """Representation of a callable service.""" __slots__ = ['func', 'schema', 'is_callback', 'is_coroutinefunction'] - def __init__(self, func, schema): + def __init__(self, func: Callable, schema: Optional[vol.Schema], + context: Optional[Context] = None) -> None: """Initialize a service.""" self.func = func self.schema = schema @@ -793,54 +871,48 @@ class Service(object): self.is_coroutinefunction = asyncio.iscoroutinefunction(func) -class ServiceCall(object): +class ServiceCall: """Representation of a call to a service.""" - __slots__ = ['domain', 'service', 'data', 'call_id'] + __slots__ = ['domain', 'service', 'data', 'context'] - def __init__(self, domain, service, data=None, call_id=None): + def __init__(self, domain: str, service: str, data: Optional[Dict] = None, + context: Optional[Context] = None) -> None: """Initialize a service call.""" self.domain = domain.lower() self.service = service.lower() self.data = MappingProxyType(data or {}) - self.call_id = call_id + self.context = context or Context() - def __repr__(self): + def __repr__(self) -> str: """Return the representation of the service.""" if self.data: - return "".format( - self.domain, self.service, util.repr_helper(self.data)) + return "".format( + self.domain, self.service, self.context.id, + util.repr_helper(self.data)) - return "".format(self.domain, self.service) + return "".format( + self.domain, self.service, self.context.id) -class ServiceRegistry(object): +class ServiceRegistry: """Offer the services over the eventbus.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize a service registry.""" - self._services = {} + self._services = {} # type: Dict[str, Dict[str, Service]] self._hass = hass - self._async_unsub_call_event = None - - def _gen_unique_id(): - cur_id = 1 - while True: - yield '{}-{}'.format(id(self), cur_id) - cur_id += 1 - - gen = _gen_unique_id() - self._generate_unique_id = lambda: next(gen) + self._async_unsub_call_event = None # type: Optional[CALLBACK_TYPE] @property - def services(self): + def services(self) -> Dict[str, Dict[str, Service]]: """Return dictionary with per domain a list of available services.""" - return run_callback_threadsafe( + return run_callback_threadsafe( # type: ignore self._hass.loop, self.async_services, ).result() @callback - def async_services(self): + def async_services(self) -> Dict[str, Dict[str, Service]]: """Return dictionary with per domain a list of available services. This method must be run in the event loop. @@ -848,14 +920,15 @@ class ServiceRegistry(object): return {domain: self._services[domain].copy() for domain in self._services} - def has_service(self, domain, service): + def has_service(self, domain: str, service: str) -> bool: """Test if specified service exists. Async friendly. """ return service.lower() in self._services.get(domain.lower(), []) - def register(self, domain, service, service_func, schema=None): + def register(self, domain: str, service: str, service_func: Callable, + schema: Optional[vol.Schema] = None) -> None: """ Register a service. @@ -867,7 +940,8 @@ class ServiceRegistry(object): ).result() @callback - def async_register(self, domain, service, service_func, schema=None): + def async_register(self, domain: str, service: str, service_func: Callable, + schema: Optional[vol.Schema] = None) -> None: """ Register a service. @@ -893,13 +967,13 @@ class ServiceRegistry(object): {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) - def remove(self, domain, service): + def remove(self, domain: str, service: str) -> None: """Remove a registered service from service handler.""" run_callback_threadsafe( self._hass.loop, self.async_remove, domain, service).result() @callback - def async_remove(self, domain, service): + def async_remove(self, domain: str, service: str) -> None: """Remove a registered service from service handler. This method must be run in the event loop. @@ -919,7 +993,10 @@ class ServiceRegistry(object): {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) - def call(self, domain, service, service_data=None, blocking=False): + def call(self, domain: str, service: str, + service_data: Optional[Dict] = None, + blocking: bool = False, + context: Optional[Context] = None) -> Optional[bool]: """ Call a service. @@ -936,13 +1013,15 @@ class ServiceRegistry(object): Because the service is sent as an event you are not allowed to use the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. """ - return run_coroutine_threadsafe( - self.async_call(domain, service, service_data, blocking), + return run_coroutine_threadsafe( # type: ignore + self.async_call(domain, service, service_data, blocking, context), self._hass.loop ).result() - async def async_call(self, domain, service, service_data=None, - blocking=False): + async def async_call(self, domain: str, service: str, + service_data: Optional[Dict] = None, + blocking: bool = False, + context: Optional[Context] = None) -> Optional[bool]: """ Call a service. @@ -961,42 +1040,42 @@ class ServiceRegistry(object): This method is a coroutine. """ - call_id = self._generate_unique_id() - + context = context or Context() event_data = { ATTR_DOMAIN: domain.lower(), ATTR_SERVICE: service.lower(), ATTR_SERVICE_DATA: service_data, - ATTR_SERVICE_CALL_ID: call_id, } - if blocking: - fut = asyncio.Future(loop=self._hass.loop) + if not blocking: + self._hass.bus.async_fire( + EVENT_CALL_SERVICE, event_data, EventOrigin.local, context) + return None - @callback - def service_executed(event): - """Handle an executed service.""" - if event.data[ATTR_SERVICE_CALL_ID] == call_id: - fut.set_result(True) + fut = asyncio.Future() # type: asyncio.Future - unsub = self._hass.bus.async_listen( - EVENT_SERVICE_EXECUTED, service_executed) + @callback + def service_executed(event: Event) -> None: + """Handle an executed service.""" + if event.context == context: + fut.set_result(True) - self._hass.bus.async_fire(EVENT_CALL_SERVICE, event_data) + unsub = self._hass.bus.async_listen( + EVENT_SERVICE_EXECUTED, service_executed) - if blocking: - done, _ = await asyncio.wait( - [fut], loop=self._hass.loop, timeout=SERVICE_CALL_LIMIT) - success = bool(done) - unsub() - return success + self._hass.bus.async_fire(EVENT_CALL_SERVICE, event_data, + EventOrigin.local, context) - async def _event_to_service_call(self, event): + done, _ = await asyncio.wait([fut], timeout=SERVICE_CALL_LIMIT) + success = bool(done) + unsub() + return success + + async def _event_to_service_call(self, event: Event) -> None: """Handle the SERVICE_CALLED events from the EventBus.""" service_data = event.data.get(ATTR_SERVICE_DATA) or {} - domain = event.data.get(ATTR_DOMAIN).lower() - service = event.data.get(ATTR_SERVICE).lower() - call_id = event.data.get(ATTR_SERVICE_CALL_ID) + domain = event.data.get(ATTR_DOMAIN).lower() # type: ignore + service = event.data.get(ATTR_SERVICE).lower() # type: ignore if not self.has_service(domain, service): if event.origin == EventOrigin.local: @@ -1006,18 +1085,15 @@ class ServiceRegistry(object): service_handler = self._services[domain][service] - def fire_service_executed(): + def fire_service_executed() -> None: """Fire service executed event.""" - if not call_id: - return - - data = {ATTR_SERVICE_CALL_ID: call_id} - if (service_handler.is_coroutinefunction or service_handler.is_callback): - self._hass.bus.async_fire(EVENT_SERVICE_EXECUTED, data) + self._hass.bus.async_fire(EVENT_SERVICE_EXECUTED, {}, + EventOrigin.local, event.context) else: - self._hass.bus.fire(EVENT_SERVICE_EXECUTED, data) + self._hass.bus.fire(EVENT_SERVICE_EXECUTED, {}, + EventOrigin.local, event.context) try: if service_handler.schema: @@ -1028,7 +1104,8 @@ class ServiceRegistry(object): fire_service_executed() return - service_call = ServiceCall(domain, service, service_data, call_id) + service_call = ServiceCall( + domain, service, service_data, event.context) try: if service_handler.is_callback: @@ -1038,44 +1115,44 @@ class ServiceRegistry(object): await service_handler.func(service_call) fire_service_executed() else: - def execute_service(): + def execute_service() -> None: """Execute a service and fires a SERVICE_EXECUTED event.""" service_handler.func(service_call) fire_service_executed() - await self._hass.async_add_job(execute_service) + await self._hass.async_add_executor_job(execute_service) except Exception: # pylint: disable=broad-except _LOGGER.exception('Error executing service %s', service_call) -class Config(object): +class Config: """Configuration settings for Home Assistant.""" - def __init__(self): + def __init__(self) -> None: """Initialize a new config object.""" self.latitude = None # type: Optional[float] self.longitude = None # type: Optional[float] self.elevation = None # type: Optional[int] self.location_name = None # type: Optional[str] - self.time_zone = None # type: Optional[str] + self.time_zone = None # type: Optional[datetime.tzinfo] self.units = METRIC_SYSTEM # type: UnitSystem # If True, pip install is skipped for requirements on startup self.skip_pip = False # type: bool # List of loaded components - self.components = set() + self.components = set() # type: set # Remote.API object pointing at local API self.api = None # Directory that holds the configuration - self.config_dir = None + self.config_dir = None # type: Optional[str] # List of allowed external dirs to access - self.whitelist_external_dirs = set() + self.whitelist_external_dirs = set() # type: Set[str] - def distance(self, lat: float, lon: float) -> float: + def distance(self, lat: float, lon: float) -> Optional[float]: """Calculate distance from Home Assistant. Async friendly. @@ -1083,7 +1160,7 @@ class Config(object): return self.units.length( location.distance(self.latitude, self.longitude, lat, lon), 'm') - def path(self, *path): + def path(self, *path: str) -> str: """Generate path to the file within the configuration directory. Async friendly. @@ -1115,12 +1192,14 @@ class Config(object): return False - def as_dict(self): + def as_dict(self) -> Dict: """Create a dictionary representation of this dict. Async friendly. """ - time_zone = self.time_zone or dt_util.UTC + time_zone = dt_util.UTC.zone + if self.time_zone and getattr(self.time_zone, 'zone'): + time_zone = getattr(self.time_zone, 'zone') return { 'latitude': self.latitude, @@ -1128,7 +1207,7 @@ class Config(object): 'elevation': self.elevation, 'unit_system': self.units.as_dict(), 'location_name': self.location_name, - 'time_zone': time_zone.zone, + 'time_zone': time_zone, 'components': self.components, 'config_dir': self.config_dir, 'whitelist_external_dirs': self.whitelist_external_dirs, @@ -1136,12 +1215,12 @@ class Config(object): } -def _async_create_timer(hass): +def _async_create_timer(hass: HomeAssistant) -> None: """Create a timer that will start on HOMEASSISTANT_START.""" handle = None @callback - def fire_time_event(nxt): + def fire_time_event(nxt: float) -> None: """Fire next time event.""" nonlocal handle @@ -1158,7 +1237,7 @@ def _async_create_timer(hass): handle = hass.loop.call_later(slp_seconds, fire_time_event, nxt) @callback - def stop_timer(event): + def stop_timer(_: Event) -> None: """Stop the timer.""" if handle is not None: handle.cancel() diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e51ba4d9718..f010ada02f3 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,8 +1,9 @@ """Classes to help gather user submissions.""" import logging import uuid - -from .core import callback +import voluptuous as vol +from typing import Dict, Any, Callable, List, Optional # noqa pylint: disable=unused-import +from .core import callback, HomeAssistant from .exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -35,15 +36,16 @@ class UnknownStep(FlowError): class FlowManager: """Manage all the flows that are in progress.""" - def __init__(self, hass, async_create_flow, async_finish_flow): + def __init__(self, hass: HomeAssistant, async_create_flow: Callable, + async_finish_flow: Callable) -> None: """Initialize the flow manager.""" self.hass = hass - self._progress = {} + self._progress = {} # type: Dict[str, Any] self._async_create_flow = async_create_flow self._async_finish_flow = async_finish_flow @callback - def async_progress(self): + def async_progress(self) -> List[Dict]: """Return the flows in progress.""" return [{ 'flow_id': flow.flow_id, @@ -51,7 +53,8 @@ class FlowManager: 'source': flow.source, } for flow in self._progress.values()] - async def async_init(self, handler, *, source=SOURCE_USER, data=None): + async def async_init(self, handler: Callable, *, source: str = SOURCE_USER, + data: str = None) -> Any: """Start a configuration flow.""" flow = await self._async_create_flow(handler, source=source, data=data) flow.hass = self.hass @@ -67,7 +70,8 @@ class FlowManager: return await self._async_handle_step(flow, step, data) - async def async_configure(self, flow_id, user_input=None): + async def async_configure( + self, flow_id: str, user_input: str = None) -> Any: """Continue a configuration flow.""" flow = self._progress.get(flow_id) @@ -83,12 +87,13 @@ class FlowManager: flow, step_id, user_input) @callback - def async_abort(self, flow_id): + def async_abort(self, flow_id: str) -> None: """Abort a flow.""" if self._progress.pop(flow_id, None) is None: raise UnknownFlow - async def _async_handle_step(self, flow, step_id, user_input): + async def _async_handle_step(self, flow: Any, step_id: str, + user_input: Optional[str]) -> Dict: """Handle a step of a flow.""" method = "async_step_{}".format(step_id) @@ -97,7 +102,7 @@ class FlowManager: raise UnknownStep("Handler {} doesn't support step {}".format( flow.__class__.__name__, step_id)) - result = await getattr(flow, method)(user_input) + result = await getattr(flow, method)(user_input) # type: Dict if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ABORT): @@ -133,8 +138,9 @@ class FlowHandler: VERSION = 1 @callback - def async_show_form(self, *, step_id, data_schema=None, errors=None, - description_placeholders=None): + def async_show_form(self, *, step_id: str, data_schema: vol.Schema = None, + errors: Dict = None, + description_placeholders: Dict = None) -> Dict: """Return the definition of a form to gather user input.""" return { 'type': RESULT_TYPE_FORM, @@ -147,7 +153,7 @@ class FlowHandler: } @callback - def async_create_entry(self, *, title, data): + def async_create_entry(self, *, title: str, data: Dict) -> Dict: """Finish config flow and create a config entry.""" return { 'version': self.VERSION, @@ -160,7 +166,7 @@ class FlowHandler: } @callback - def async_abort(self, *, reason): + def async_abort(self, *, reason: str) -> Dict: """Abort the config flow.""" return { 'type': RESULT_TYPE_ABORT, diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 54cd569aceb..ed489ed858b 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -5,11 +5,6 @@ from typing import Any, Iterable, Tuple, Sequence, Dict from homeassistant.const import CONF_PLATFORM -# Typing Imports and TypeAlias -# pylint: disable=using-constant-test,unused-import -if False: - from logging import Logger # NOQA - # pylint: disable=invalid-name ConfigType = Dict[str, Any] diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 71f3374f0c0..53b246c700d 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -66,14 +66,13 @@ def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, return clientsession -@asyncio.coroutine @bind_hass -def async_aiohttp_proxy_web(hass, request, web_coro, buffer_size=102400, - timeout=10): +async def async_aiohttp_proxy_web(hass, request, web_coro, + buffer_size=102400, timeout=10): """Stream websession request to aiohttp web response.""" try: with async_timeout.timeout(timeout, loop=hass.loop): - req = yield from web_coro + req = await web_coro except asyncio.CancelledError: # The user cancelled the request @@ -88,7 +87,7 @@ def async_aiohttp_proxy_web(hass, request, web_coro, buffer_size=102400, raise HTTPBadGateway() from err try: - yield from async_aiohttp_proxy_stream( + return await async_aiohttp_proxy_stream( hass, request, req.content, @@ -112,19 +111,15 @@ async def async_aiohttp_proxy_stream(hass, request, stream, content_type, data = await stream.read(buffer_size) if not data: - await response.write_eof() break - await response.write(data) except (asyncio.TimeoutError, aiohttp.ClientError): - # Something went wrong fetching data, close connection gracefully - await response.write_eof() - - except asyncio.CancelledError: - # The user closed the connection + # Something went wrong fetching data, closed connection pass + return response + @callback def _async_register_clientsession_shutdown(hass, clientsession): diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 921b3bcf06b..930f68c3da4 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -246,26 +246,24 @@ def sun(hass, before=None, after=None, before_offset=None, after_offset=None): sunrise = get_astral_event_date(hass, 'sunrise', today) sunset = get_astral_event_date(hass, 'sunset', today) - if sunrise is None and (before == SUN_EVENT_SUNRISE or - after == SUN_EVENT_SUNRISE): + if sunrise is None and SUN_EVENT_SUNRISE in (before, after): # There is no sunrise today return False - if sunset is None and (before == SUN_EVENT_SUNSET or - after == SUN_EVENT_SUNSET): + if sunset is None and SUN_EVENT_SUNSET in (before, after): # There is no sunset today return False if before == SUN_EVENT_SUNRISE and utcnow > sunrise + before_offset: return False - elif before == SUN_EVENT_SUNSET and utcnow > sunset + before_offset: + if before == SUN_EVENT_SUNSET and utcnow > sunset + before_offset: return False if after == SUN_EVENT_SUNRISE and utcnow < sunrise + after_offset: return False - elif after == SUN_EVENT_SUNSET and utcnow < sunset + after_offset: + if after == SUN_EVENT_SUNSET and utcnow < sunset + after_offset: return False return True diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0bd490940a9..056d45ad656 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -361,7 +361,7 @@ def temperature_unit(value) -> str: value = str(value).upper() if value == 'C': return TEMP_CELSIUS - elif value == 'F': + if value == 'F': return TEMP_FAHRENHEIT raise vol.Invalid('invalid temperature unit (expected C or F)') @@ -435,15 +435,14 @@ def socket_timeout(value): """ if value is None: return _GLOBAL_DEFAULT_TIMEOUT - else: - try: - float_value = float(value) - if float_value > 0.0: - return float_value - raise vol.Invalid('Invalid socket timeout value.' - ' float > 0.0 required.') - except Exception as _: - raise vol.Invalid('Invalid socket timeout: {err}'.format(err=_)) + try: + float_value = float(value) + if float_value > 0.0: + return float_value + raise vol.Invalid('Invalid socket timeout value.' + ' float > 0.0 required.') + except Exception as _: + raise vol.Invalid('Invalid socket timeout: {err}'.format(err=_)) # pylint: disable=no-value-for-parameter diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 5a0b2ca56ea..4f412eb58e7 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -23,7 +23,7 @@ class _BaseFlowManagerView(HomeAssistantView): data.pop('data') return data - elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: + if result['type'] != data_entry_flow.RESULT_TYPE_FORM: return result import voluptuous_serialize diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 73a09464439..8b621b2f01c 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -33,8 +33,7 @@ def deprecated_substitute(substitute_name): # Return the old property return getattr(self, substitute_name) - else: - return func(self) + return func(self) return func_wrapper return decorator diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index cb587c432c1..7d0730a969c 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -145,7 +145,7 @@ async def async_load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. Warning: Do not await this inside a setup method to avoid a dead lock. - Use `hass.async_add_job(async_load_platform(..))` instead. + Use `hass.async_create_task(async_load_platform(..))` instead. This method is a coroutine. """ diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7dc5d2524ec..c356c266db6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -56,7 +56,7 @@ def async_generate_entity_id(entity_id_format: str, name: Optional[str], entity_id_format.format(slugify(name)), current_ids) -class Entity(object): +class Entity: """An abstract class for Home Assistant entities.""" # SAFE TO OVERWRITE @@ -82,6 +82,9 @@ class Entity(object): # Name in the entity registry registry_name = None + # Hold list for functions to call on remove. + _on_remove = None + @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -176,7 +179,7 @@ class Entity(object): # produce undesirable effects in the entity's operation. @asyncio.coroutine - def async_update_ha_state(self, force_refresh=False): + def async_update_ha_state(self, force_refresh=False, context=None): """Update Home Assistant with current state of entity. If force_refresh == True will update entity before setting state. @@ -276,7 +279,7 @@ class Entity(object): pass self.hass.states.async_set( - self.entity_id, state, attr, self.force_update) + self.entity_id, state, attr, self.force_update, context) def schedule_update_ha_state(self, force_refresh=False): """Schedule an update ha state change task. @@ -324,8 +327,19 @@ class Entity(object): if self.parallel_updates: self.parallel_updates.release() + @callback + def async_on_remove(self, func): + """Add a function to call when entity removed.""" + if self._on_remove is None: + self._on_remove = [] + self._on_remove.append(func) + async def async_remove(self): """Remove entity from Home Assistant.""" + if self._on_remove is not None: + while self._on_remove: + self._on_remove.pop()() + if self.platform is not None: await self.platform.async_remove_entity(self.entity_id) else: @@ -335,7 +349,17 @@ class Entity(object): def async_registry_updated(self, old, new): """Called when the entity registry has been updated.""" self.registry_name = new.name - self.async_schedule_update_ha_state() + + if new.entity_id == self.entity_id: + self.async_schedule_update_ha_state() + return + + async def readd(): + """Remove and add entity again.""" + await self.async_remove() + await self.platform.async_add_entities([self]) + + self.hass.async_create_task(readd()) def __eq__(self, other): """Return the comparison.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4ac3a147296..72b6ceecbfd 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -17,7 +17,7 @@ from .entity_platform import EntityPlatform DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) -class EntityComponent(object): +class EntityComponent: """The EntityComponent manages platforms that manages entities. This class has the following responsibilities: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 472a88888d8..dc1e376f471 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -15,7 +15,7 @@ SLOW_SETUP_MAX_WAIT = 60 PLATFORM_NOT_READY_RETRIES = 10 -class EntityPlatform(object): +class EntityPlatform: """Manage the entities for a single platform.""" def __init__(self, *, hass, logger, domain, platform_name, platform, @@ -283,7 +283,7 @@ class EntityPlatform(object): entity.entity_id = entry.entity_id entity.registry_name = entry.name - entry.add_update_listener(entity) + entity.async_on_remove(entry.add_update_listener(entity)) # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 04d9cc450ba..2fa64ff8680 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -19,10 +19,10 @@ import weakref import attr -from ..core import callback, split_entity_id -from ..loader import bind_hass -from ..util import ensure_unique_string, slugify -from ..util.yaml import load_yaml, save_yaml +from homeassistant.core import callback, split_entity_id, valid_entity_id +from homeassistant.loader import bind_hass +from homeassistant.util import ensure_unique_string, slugify +from homeassistant.util.yaml import load_yaml, save_yaml PATH_REGISTRY = 'entity_registry.yaml' DATA_REGISTRY = 'entity_registry' @@ -63,8 +63,13 @@ class RegistryEntry: """Listen for when entry is updated. Listener: Callback function(old_entry, new_entry) + + Returns function to unlisten. """ - self.update_listeners.append(weakref.ref(listener)) + weak_listener = weakref.ref(listener) + self.update_listeners.append(weak_listener) + + return lambda: self.update_listeners.remove(weak_listener) class EntityRegistry: @@ -109,6 +114,12 @@ class EntityRegistry: """Get entity. Create if it doesn't exist.""" entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: + entry = self.entities[entity_id] + if entry.config_entry_id == config_entry_id: + return entry + + self._async_update_entity( + entity_id, config_entry_id=config_entry_id) return self.entities[entity_id] entity_id = self.async_generate_entity_id( @@ -127,8 +138,19 @@ class EntityRegistry: return entity @callback - def async_update_entity(self, entity_id, *, name=_UNDEF): + def async_update_entity(self, entity_id, *, name=_UNDEF, + new_entity_id=_UNDEF): """Update properties of an entity.""" + return self._async_update_entity( + entity_id, + name=name, + new_entity_id=new_entity_id + ) + + @callback + def _async_update_entity(self, entity_id, *, name=_UNDEF, + config_entry_id=_UNDEF, new_entity_id=_UNDEF): + """Private facing update properties method.""" old = self.entities[entity_id] changes = {} @@ -136,6 +158,24 @@ class EntityRegistry: if name is not _UNDEF and name != old.name: changes['name'] = name + if (config_entry_id is not _UNDEF and + config_entry_id != old.config_entry_id): + changes['config_entry_id'] = config_entry_id + + if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: + if self.async_is_registered(new_entity_id): + raise ValueError('Entity is already registered') + + if not valid_entity_id(new_entity_id): + raise ValueError('Invalid entity ID') + + if (split_entity_id(new_entity_id)[0] != + split_entity_id(entity_id)[0]): + raise ValueError('New entity ID should be same domain') + + self.entities.pop(entity_id) + entity_id = changes['entity_id'] = new_entity_id + if not changes: return old diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index 19980394d26..5caa6b93131 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -6,7 +6,7 @@ import re from homeassistant.core import split_entity_id -class EntityValues(object): +class EntityValues: """Class to store entity id based values.""" def __init__(self, exact=None, domain=None, glob=None): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 712b48da0d7..c8488fa3334 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -374,7 +374,7 @@ def _process_state_match(parameter): if parameter is None or parameter == MATCH_ALL: return lambda _: True - elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): + if isinstance(parameter, str) or not hasattr(parameter, '__iter__'): return lambda state: state == parameter parameter = tuple(parameter) @@ -386,11 +386,11 @@ def _process_time_match(parameter): if parameter is None or parameter == MATCH_ALL: return lambda _: True - elif isinstance(parameter, str) and parameter.startswith('/'): + if isinstance(parameter, str) and parameter.startswith('/'): parameter = float(parameter[1:]) return lambda time: time % parameter == 0 - elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): + if isinstance(parameter, str) or not hasattr(parameter, '__iter__'): return lambda time: time == parameter parameter = tuple(parameter) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 4357c4109eb..8f26d4fe0ee 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -63,7 +63,8 @@ async def async_handle(hass, platform, intent_type, slots=None, intent_type, err) raise InvalidSlotInfo( 'Received invalid slot info for {}'.format(intent_type)) from err - except IntentHandleError: + # https://github.com/PyCQA/pylint/issues/2284 + except IntentHandleError: # pylint: disable=try-except-raise raise except Exception as err: raise IntentUnexpectedError( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index f2ae36e7fd0..a139be4b260 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -119,7 +119,7 @@ class Script(): self.hass.async_add_job(self._change_listener) return - elif CONF_WAIT_TEMPLATE in action: + if CONF_WAIT_TEMPLATE in action: # Call ourselves in the future to continue work wait_template = action[CONF_WAIT_TEMPLATE] wait_template.hass = self.hass @@ -147,7 +147,7 @@ class Script(): return - elif CONF_CONDITION in action: + if CONF_CONDITION in action: if not self._async_check_condition(action, variables): break diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7ab90b7a048..8aa3b553f3a 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -103,12 +103,10 @@ def extract_entity_ids(hass, service_call, expand_group=True): return [ent_id for ent_id in group.expand_entity_ids(service_ent_id)] - else: + if isinstance(service_ent_id, str): + return [service_ent_id] - if isinstance(service_ent_id, str): - return [service_ent_id] - - return service_ent_id + return service_ent_id @bind_hass @@ -123,7 +121,7 @@ async def async_get_all_descriptions(hass): def domain_yaml_file(domain): """Return the services.yaml location for a domain.""" if domain == ha.DOMAIN: - import homeassistant.components as components + from homeassistant import components component_path = path.dirname(components.__file__) else: component_path = path.dirname(get_component(hass, domain).__file__) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 72deabaae28..4a3f915e810 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -92,7 +92,7 @@ SERVICE_TO_STATE = { } -class AsyncTrackStates(object): +class AsyncTrackStates: """ Record the time when the with-block is entered. @@ -214,9 +214,9 @@ def state_as_number(state): if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON, STATE_OPEN, STATE_HOME, STATE_HEAT, STATE_COOL): return 1 - elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN, - STATE_BELOW_HORIZON, STATE_CLOSED, STATE_NOT_HOME, - STATE_IDLE): + if state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN, + STATE_BELOW_HORIZON, STATE_CLOSED, STATE_NOT_HOME, + STATE_IDLE): return 0 return float(state.state) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f523726c388..ea620c9bccd 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -51,7 +51,7 @@ def render_complex(value, variables=None): if isinstance(value, list): return [render_complex(item, variables) for item in value] - elif isinstance(value, dict): + if isinstance(value, dict): return {key: render_complex(item, variables) for key, item in value.items()} return value.async_render(variables) @@ -82,7 +82,7 @@ def extract_entities(template, variables=None): return MATCH_ALL -class Template(object): +class Template: """Class to hold a template and manage caching and rendering.""" def __init__(self, template, hass=None): @@ -198,7 +198,7 @@ class Template(object): self.hass == other.hass) -class AllStates(object): +class AllStates: """Class to expose all HA states as attributes.""" def __init__(self, hass): @@ -226,7 +226,7 @@ class AllStates(object): return STATE_UNKNOWN if state is None else state.state -class DomainStates(object): +class DomainStates: """Class to expose a specific HA domain as attributes.""" def __init__(self, hass, domain): @@ -286,7 +286,7 @@ def _wrap_state(state): return None if state is None else TemplateState(state) -class TemplateMethods(object): +class TemplateMethods: """Class to expose helpers to templates.""" def __init__(self, hass): @@ -318,7 +318,7 @@ class TemplateMethods(object): if point_state is None: _LOGGER.warning("Closest:Unable to find state %s", args[0]) return None - elif not loc_helper.has_location(point_state): + if not loc_helper.has_location(point_state): _LOGGER.warning( "Closest:State does not contain valid location: %s", point_state) @@ -420,7 +420,7 @@ class TemplateMethods(object): """Return state or entity_id if given.""" if isinstance(entity_id_or_state, State): return entity_id_or_state - elif isinstance(entity_id_or_state, str): + if isinstance(entity_id_or_state, str): return self._hass.states.get(entity_id_or_state) return None diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 52e6b1e7703..3ac49e354b5 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -17,7 +17,7 @@ import sys from types import ModuleType # pylint: disable=unused-import -from typing import Dict, List, Optional, Sequence, Set, TYPE_CHECKING # NOQA +from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # NOQA from homeassistant.const import PLATFORM_FORMAT from homeassistant.util import OrderedSet @@ -27,6 +27,8 @@ from homeassistant.util import OrderedSet if TYPE_CHECKING: from homeassistant.core import HomeAssistant # NOQA +CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable) # noqa pylint: disable=invalid-name + PREPARED = False DEPENDENCY_BLACKLIST = {'config'} @@ -51,7 +53,8 @@ def set_component(hass, # type: HomeAssistant cache[comp_name] = component -def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: +def get_platform(hass, # type: HomeAssistant + domain: str, platform: str) -> Optional[ModuleType]: """Try to load specified platform. Async friendly. @@ -59,7 +62,8 @@ def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(hass, comp_or_platform) -> Optional[ModuleType]: +def get_component(hass, # type: HomeAssistant + comp_or_platform: str) -> Optional[ModuleType]: """Try to load specified component. Looks in config dir first, then built-in components. @@ -73,6 +77,9 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: cache = hass.data.get(DATA_KEY) if cache is None: + if hass.config.config_dir is None: + _LOGGER.error("Can't load components - config dir is not set") + return None # Only insert if it's not there (happens during tests) if sys.path[0] != hass.config.config_dir: sys.path.insert(0, hass.config.config_dir) @@ -134,14 +141,38 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: return None +class ModuleWrapper: + """Class to wrap a Python module and auto fill in hass argument.""" + + def __init__(self, + hass, # type: HomeAssistant + module: ModuleType) -> None: + """Initialize the module wrapper.""" + self._hass = hass + self._module = module + + def __getattr__(self, attr: str) -> Any: + """Fetch an attribute.""" + value = getattr(self._module, attr) + + if hasattr(value, '__bind_hass'): + value = ft.partial(value, self._hass) + + setattr(self, attr, value) + return value + + class Components: """Helper to load components.""" - def __init__(self, hass): + def __init__( + self, + hass # type: HomeAssistant + ) -> None: """Initialize the Components class.""" self._hass = hass - def __getattr__(self, comp_name): + def __getattr__(self, comp_name: str) -> ModuleWrapper: """Fetch a component.""" component = get_component(self._hass, comp_name) if component is None: @@ -154,11 +185,14 @@ class Components: class Helpers: """Helper to load helpers.""" - def __init__(self, hass): + def __init__( + self, + hass # type: HomeAssistant + ) -> None: """Initialize the Helpers class.""" self._hass = hass - def __getattr__(self, helper_name): + def __getattr__(self, helper_name: str) -> ModuleWrapper: """Fetch a helper.""" helper = importlib.import_module( 'homeassistant.helpers.{}'.format(helper_name)) @@ -167,33 +201,14 @@ class Helpers: return wrapped -class ModuleWrapper: - """Class to wrap a Python module and auto fill in hass argument.""" - - def __init__(self, hass, module): - """Initialize the module wrapper.""" - self._hass = hass - self._module = module - - def __getattr__(self, attr): - """Fetch an attribute.""" - value = getattr(self._module, attr) - - if hasattr(value, '__bind_hass'): - value = ft.partial(value, self._hass) - - setattr(self, attr, value) - return value - - -def bind_hass(func): +def bind_hass(func: CALLABLE_T) -> CALLABLE_T: """Decorate function to indicate that first argument is hass.""" - # pylint: disable=protected-access - func.__bind_hass = True + setattr(func, '__bind_hass', True) return func -def load_order_component(hass, comp_name: str) -> OrderedSet: +def load_order_component(hass, # type: HomeAssistant + comp_name: str) -> OrderedSet: """Return an OrderedSet of components in the correct order of loading. Raises HomeAssistantError if a circular dependency is detected. @@ -204,7 +219,8 @@ def load_order_component(hass, comp_name: str) -> OrderedSet: return _load_order_component(hass, comp_name, OrderedSet(), set()) -def _load_order_component(hass, comp_name: str, load_order: OrderedSet, +def _load_order_component(hass, # type: HomeAssistant + comp_name: str, load_order: OrderedSet, loading: Set) -> OrderedSet: """Recursive function to get load order of components. diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index d5c629c9d34..edd25817f5a 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -20,9 +20,10 @@ Related Python bugs: - https://bugs.python.org/issue26617 """ import sys +from typing import Any -def patch_weakref_tasks(): +def patch_weakref_tasks() -> None: """Replace weakref.WeakSet to address Python 3 bug.""" # pylint: disable=no-self-use, protected-access, bare-except import asyncio.tasks @@ -30,18 +31,18 @@ def patch_weakref_tasks(): class IgnoreCalls: """Ignore add calls.""" - def add(self, other): + def add(self, other: Any) -> None: """No-op add.""" return - asyncio.tasks.Task._all_tasks = IgnoreCalls() + asyncio.tasks.Task._all_tasks = IgnoreCalls() # type: ignore try: del asyncio.tasks.Task.__del__ except: # noqa: E722 pass -def disable_c_asyncio(): +def disable_c_asyncio() -> None: """Disable using C implementation of asyncio. Required to be able to apply the weakref monkey patch. @@ -53,18 +54,16 @@ def disable_c_asyncio(): PATH_TRIGGER = '_asyncio' - def __init__(self, path_entry): + def __init__(self, path_entry: str) -> None: if path_entry != self.PATH_TRIGGER: raise ImportError() - return - def find_module(self, fullname, path=None): + def find_module(self, fullname: str, path: Any = None) -> None: """Find a module.""" if fullname == self.PATH_TRIGGER: # We lint in Py35, exception is introduced in Py36 # pylint: disable=undefined-variable - raise ModuleNotFoundError() # noqa - return None + raise ModuleNotFoundError() # type: ignore # noqa sys.path_hooks.append(AsyncioImportFinder) sys.path.insert(0, AsyncioImportFinder.PATH_TRIGGER) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 66b17cf9bd9..e832314cf17 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 requests==2.19.1 -voluptuous==0.11.1 +voluptuous==0.11.3 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/homeassistant/remote.py b/homeassistant/remote.py index ae932b7d955..313f98a890c 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -13,7 +13,7 @@ import json import logging import urllib.parse -from typing import Optional +from typing import Optional, Dict, Any, List from aiohttp.hdrs import METH_GET, METH_POST, METH_DELETE, CONTENT_TYPE import requests @@ -41,7 +41,7 @@ class APIStatus(enum.Enum): return self.value # type: ignore -class API(object): +class API: """Object to pass around Home Assistant API location and credentials.""" def __init__(self, host: str, api_password: Optional[str] = None, @@ -62,7 +62,7 @@ class API(object): if port is not None: self.base_url += ':{}'.format(port) - self.status = None + self.status = None # type: Optional[APIStatus] self._headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} if api_password is not None: @@ -75,20 +75,24 @@ class API(object): return self.status == APIStatus.OK - def __call__(self, method, path, data=None, timeout=5): + def __call__(self, method: str, path: str, data: Dict = None, + timeout: int = 5) -> requests.Response: """Make a call to the Home Assistant API.""" - if data is not None: - data = json.dumps(data, cls=JSONEncoder) + if data is None: + data_str = None + else: + data_str = json.dumps(data, cls=JSONEncoder) url = urllib.parse.urljoin(self.base_url, path) try: if method == METH_GET: return requests.get( - url, params=data, timeout=timeout, headers=self._headers) + url, params=data_str, timeout=timeout, + headers=self._headers) return requests.request( - method, url, data=data, timeout=timeout, + method, url, data=data_str, timeout=timeout, headers=self._headers) except requests.exceptions.ConnectionError: @@ -110,22 +114,22 @@ class JSONEncoder(json.JSONEncoder): """JSONEncoder that supports Home Assistant objects.""" # pylint: disable=method-hidden - def default(self, o): + def default(self, o: Any) -> Any: """Convert Home Assistant objects. Hand other objects to the original method. """ if isinstance(o, datetime): return o.isoformat() - elif isinstance(o, set): + if isinstance(o, set): return list(o) - elif hasattr(o, 'as_dict'): + if hasattr(o, 'as_dict'): return o.as_dict() return json.JSONEncoder.default(self, o) -def validate_api(api): +def validate_api(api: API) -> APIStatus: """Make a call to validate API.""" try: req = api(METH_GET, URL_API) @@ -133,7 +137,7 @@ def validate_api(api): if req.status_code == 200: return APIStatus.OK - elif req.status_code == 401: + if req.status_code == 401: return APIStatus.INVALID_PASSWORD return APIStatus.UNKNOWN @@ -142,12 +146,12 @@ def validate_api(api): return APIStatus.CANNOT_CONNECT -def get_event_listeners(api): +def get_event_listeners(api: API) -> Dict: """List of events that is being listened for.""" try: req = api(METH_GET, URL_API_EVENTS) - return req.json() if req.status_code == 200 else {} + return req.json() if req.status_code == 200 else {} # type: ignore except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json @@ -156,7 +160,7 @@ def get_event_listeners(api): return {} -def fire_event(api, event_type, data=None): +def fire_event(api: API, event_type: str, data: Dict = None) -> None: """Fire an event at remote API.""" try: req = api(METH_POST, URL_API_EVENTS_EVENT.format(event_type), data) @@ -169,7 +173,7 @@ def fire_event(api, event_type, data=None): _LOGGER.exception("Error firing event") -def get_state(api, entity_id): +def get_state(api: API, entity_id: str) -> Optional[ha.State]: """Query given API for state of entity_id.""" try: req = api(METH_GET, URL_API_STATES_ENTITY.format(entity_id)) @@ -186,7 +190,7 @@ def get_state(api, entity_id): return None -def get_states(api): +def get_states(api: API) -> List[ha.State]: """Query given API for all states.""" try: req = api(METH_GET, @@ -202,7 +206,7 @@ def get_states(api): return [] -def remove_state(api, entity_id): +def remove_state(api: API, entity_id: str) -> bool: """Call API to remove state for entity_id. Return True if entity is gone (removed/never existed). @@ -222,7 +226,8 @@ def remove_state(api, entity_id): return False -def set_state(api, entity_id, new_state, attributes=None, force_update=False): +def set_state(api: API, entity_id: str, new_state: str, + attributes: Dict = None, force_update: bool = False) -> bool: """Tell API to update state for entity_id. Return True if success. @@ -249,14 +254,14 @@ def set_state(api, entity_id, new_state, attributes=None, force_update=False): return False -def is_state(api, entity_id, state): +def is_state(api: API, entity_id: str, state: str) -> bool: """Query API to see if entity_id is specified state.""" cur_state = get_state(api, entity_id) - return cur_state and cur_state.state == state + return bool(cur_state and cur_state.state == state) -def get_services(api): +def get_services(api: API) -> Dict: """Return a list of dicts. Each dict has a string "domain" and a list of strings "services". @@ -264,7 +269,7 @@ def get_services(api): try: req = api(METH_GET, URL_API_SERVICES) - return req.json() if req.status_code == 200 else {} + return req.json() if req.status_code == 200 else {} # type: ignore except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json @@ -273,7 +278,9 @@ def get_services(api): return {} -def call_service(api, domain, service, service_data=None, timeout=5): +def call_service(api: API, domain: str, service: str, + service_data: Dict = None, + timeout: int = 5) -> None: """Call a service at the remote API.""" try: req = api(METH_POST, @@ -288,7 +295,7 @@ def call_service(api, domain, service, service_data=None, timeout=5): _LOGGER.exception("Error calling service") -def get_config(api): +def get_config(api: API) -> Dict: """Return configuration.""" try: req = api(METH_GET, URL_API_CONFIG) @@ -299,7 +306,7 @@ def get_config(api): result = req.json() if 'components' in result: result['components'] = set(result['components']) - return result + return result # type: ignore except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the JSON diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 753947a2c12..b73ec4e184e 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,15 +3,18 @@ import asyncio from functools import partial import logging import os +from typing import List, Dict, Optional import homeassistant.util.package as pkg_util +from homeassistant.core import HomeAssistant DATA_PIP_LOCK = 'pip_lock' CONSTRAINT_FILE = 'package_constraints.txt' _LOGGER = logging.getLogger(__name__) -async def async_process_requirements(hass, name, requirements): +async def async_process_requirements(hass: HomeAssistant, name: str, + requirements: List[str]) -> bool: """Install the requirements for a component or platform. This method is a coroutine. @@ -25,7 +28,7 @@ async def async_process_requirements(hass, name, requirements): async with pip_lock: for req in requirements: - ret = await hass.async_add_job(pip_install, req) + ret = await hass.async_add_executor_job(pip_install, req) if not ret: _LOGGER.error("Not initializing %s because could not install " "requirement %s", name, req) @@ -34,11 +37,11 @@ async def async_process_requirements(hass, name, requirements): return True -def pip_kwargs(config_dir): +def pip_kwargs(config_dir: Optional[str]) -> Dict[str, str]: """Return keyword arguments for PIP install.""" kwargs = { 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) } - if not pkg_util.is_virtual_env(): + if not (config_dir is None or pkg_util.is_virtual_env()): kwargs['target'] = os.path.join(config_dir, 'deps') return kwargs diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 69b1bf21c08..d7be5b1a91c 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -18,7 +18,7 @@ from homeassistant.config import ( CONF_PACKAGES, merge_packages_config, _format_config_error, find_config_file, load_yaml_config_file, extract_domain_configs, config_per_platform) -import homeassistant.util.yaml as yaml +from homeassistant.util import yaml from homeassistant.exceptions import HomeAssistantError REQUIREMENTS = ('colorlog==3.1.4',) @@ -163,13 +163,13 @@ def check(config_dir, secrets=False): 'secret_cache': None, } - # pylint: disable=unused-variable + # pylint: disable=possibly-unused-variable def mock_load(filename): """Mock hass.util.load_yaml to save config file names.""" res['yaml_files'][filename] = True return MOCKS['load'][1](filename) - # pylint: disable=unused-variable + # pylint: disable=possibly-unused-variable def mock_secrets(ldr, node): """Mock _get_secrets.""" try: diff --git a/homeassistant/scripts/influxdb_import.py b/homeassistant/scripts/influxdb_import.py index 421e84d503a..031df1d3a72 100644 --- a/homeassistant/scripts/influxdb_import.py +++ b/homeassistant/scripts/influxdb_import.py @@ -137,6 +137,7 @@ def run(script_args: List) -> int: override_measurement = args.override_measurement default_measurement = args.default_measurement + # pylint: disable=assignment-from-no-return query = session.query(func.count(models.Events.event_type)).filter( models.Events.event_type == 'state_changed') diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index 275a33627a9..6c6557897ee 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -52,10 +52,10 @@ def run(args): if args[0] == 'install': install_osx() return 0 - elif args[0] == 'uninstall': + if args[0] == 'uninstall': uninstall_osx() return 0 - elif args[0] == 'restart': + if args[0] == 'restart': uninstall_osx() # A small delay is needed on some systems to let the unload finish. time.sleep(0.5) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 478320dca27..31404b978eb 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -4,7 +4,7 @@ import logging.handlers from timeit import default_timer as timer from types import ModuleType -from typing import Optional, Dict +from typing import Optional, Dict, List from homeassistant import requirements, core, loader, config as conf_util from homeassistant.config import async_notify_setup_error @@ -56,7 +56,9 @@ async def async_setup_component(hass: core.HomeAssistant, domain: str, return await task # type: ignore -async def _async_process_dependencies(hass, config, name, dependencies): +async def _async_process_dependencies( + hass: core.HomeAssistant, config: Dict, name: str, + dependencies: List[str]) -> bool: """Ensure all dependencies are set up.""" blacklisted = [dep for dep in dependencies if dep in loader.DEPENDENCY_BLACKLIST] @@ -88,12 +90,12 @@ async def _async_process_dependencies(hass, config, name, dependencies): async def _async_setup_component(hass: core.HomeAssistant, - domain: str, config) -> bool: + domain: str, config: Dict) -> bool: """Set up a component for Home Assistant. This method is a coroutine. """ - def log_error(msg, link=True): + def log_error(msg: str, link: bool = True) -> None: """Log helper.""" _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) @@ -157,14 +159,15 @@ async def _async_setup_component(hass: core.HomeAssistant, if result is False: log_error("Component failed to initialize.") return False - elif result is not True: + if result is not True: log_error("Component did not return boolean if setup was successful. " "Disabling component.") loader.set_component(hass, domain, None) return False - for entry in hass.config_entries.async_entries(domain): - await entry.async_setup(hass, component=component) + if hass.config_entries: + for entry in hass.config_entries.async_entries(domain): + await entry.async_setup(hass, component=component) hass.config.components.add(component.DOMAIN) # type: ignore @@ -180,7 +183,7 @@ async def _async_setup_component(hass: core.HomeAssistant, return True -async def async_prepare_setup_platform(hass: core.HomeAssistant, config, +async def async_prepare_setup_platform(hass: core.HomeAssistant, config: Dict, domain: str, platform_name: str) \ -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup. @@ -189,7 +192,7 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config, """ platform_path = PLATFORM_FORMAT.format(domain, platform_name) - def log_error(msg): + def log_error(msg: str) -> None: """Log helper.""" _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) @@ -203,7 +206,7 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config, return None # Already loaded - elif platform_path in hass.config.components: + if platform_path in hass.config.components: return platform try: @@ -216,7 +219,9 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config, return platform -async def async_process_deps_reqs(hass, config, name, module): +async def async_process_deps_reqs( + hass: core.HomeAssistant, config: Dict, name: str, + module: ModuleType) -> None: """Process all dependencies and requirements for a module. Module is a Python module of either a component or platform. @@ -230,14 +235,14 @@ async def async_process_deps_reqs(hass, config, name, module): if hasattr(module, 'DEPENDENCIES'): dep_success = await _async_process_dependencies( - hass, config, name, module.DEPENDENCIES) + hass, config, name, module.DEPENDENCIES) # type: ignore if not dep_success: raise HomeAssistantError("Could not setup all dependencies.") if not hass.config.skip_pip and hasattr(module, 'REQUIREMENTS'): req_success = await requirements.async_process_requirements( - hass, name, module.REQUIREMENTS) + hass, name, module.REQUIREMENTS) # type: ignore if not req_success: raise HomeAssistantError("Could not install all requirements.") diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index bbf0f7e11e2..64c9f4f02c9 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,9 +1,8 @@ """Helper methods for various modules.""" import asyncio -from collections.abc import MutableSet +from datetime import datetime, timedelta from itertools import chain import threading -from datetime import datetime import re import enum import socket @@ -13,12 +12,16 @@ from functools import wraps from types import MappingProxyType from unicodedata import normalize -from typing import Any, Optional, TypeVar, Callable, KeysView, Union, Iterable +from typing import (Any, Optional, TypeVar, Callable, KeysView, Union, # noqa + Iterable, List, Dict, Iterator, Coroutine, MutableSet) from .dt import as_local, utcnow +# pylint: disable=invalid-name T = TypeVar('T') U = TypeVar('U') +ENUM_T = TypeVar('ENUM_T', bound=enum.Enum) +# pylint: enable=invalid-name RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') @@ -55,7 +58,7 @@ def repr_helper(inp: Any) -> str: return ", ".join( repr_helper(key)+"="+repr_helper(item) for key, item in inp.items()) - elif isinstance(inp, datetime): + if isinstance(inp, datetime): return as_local(inp).isoformat() return str(inp) @@ -90,7 +93,7 @@ def ensure_unique_string(preferred_string: str, current_strings: # Taken from: http://stackoverflow.com/a/11735897 -def get_local_ip(): +def get_local_ip() -> str: """Try to determine the local IP address of the machine.""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -98,7 +101,7 @@ def get_local_ip(): # Use Google Public DNS server to determine own IP sock.connect(('8.8.8.8', 80)) - return sock.getsockname()[0] + return sock.getsockname()[0] # type: ignore except socket.error: try: return socket.gethostbyname(socket.gethostname()) @@ -109,7 +112,7 @@ def get_local_ip(): # Taken from http://stackoverflow.com/a/23728630 -def get_random_string(length=10): +def get_random_string(length: int = 10) -> str: """Return a random string with letters and digits.""" generator = random.SystemRandom() source_chars = string.ascii_letters + string.digits @@ -120,59 +123,62 @@ def get_random_string(length=10): class OrderedEnum(enum.Enum): """Taken from Python 3.4.0 docs.""" - def __ge__(self, other): + # https://github.com/PyCQA/pylint/issues/2306 + # pylint: disable=comparison-with-callable + + def __ge__(self, other: ENUM_T) -> bool: """Return the greater than element.""" if self.__class__ is other.__class__: - return self.value >= other.value + return bool(self.value >= other.value) return NotImplemented - def __gt__(self, other): + def __gt__(self, other: ENUM_T) -> bool: """Return the greater element.""" if self.__class__ is other.__class__: - return self.value > other.value + return bool(self.value > other.value) return NotImplemented - def __le__(self, other): + def __le__(self, other: ENUM_T) -> bool: """Return the lower than element.""" if self.__class__ is other.__class__: - return self.value <= other.value + return bool(self.value <= other.value) return NotImplemented - def __lt__(self, other): + def __lt__(self, other: ENUM_T) -> bool: """Return the lower element.""" if self.__class__ is other.__class__: - return self.value < other.value + return bool(self.value < other.value) return NotImplemented -class OrderedSet(MutableSet): +class OrderedSet(MutableSet[T]): """Ordered set taken from http://code.activestate.com/recipes/576694/.""" - def __init__(self, iterable=None): + def __init__(self, iterable: Iterable[T] = None) -> None: """Initialize the set.""" - self.end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.map = {} # key --> [key, prev, next] + self.end = end = [] # type: List[Any] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # type: Dict[T, List] # key --> [key, prev, next] if iterable is not None: - self |= iterable + self |= iterable # type: ignore - def __len__(self): + def __len__(self) -> int: """Return the length of the set.""" return len(self.map) - def __contains__(self, key): + def __contains__(self, key: T) -> bool: # type: ignore """Check if key is in set.""" return key in self.map # pylint: disable=arguments-differ - def add(self, key): + def add(self, key: T) -> None: """Add an element to the end of the set.""" if key not in self.map: end = self.end curr = end[1] curr[2] = end[1] = self.map[key] = [key, curr, end] - def promote(self, key): + def promote(self, key: T) -> None: """Promote element to beginning of the set, add if not there.""" if key in self.map: self.discard(key) @@ -182,14 +188,14 @@ class OrderedSet(MutableSet): curr[2] = begin[1] = self.map[key] = [key, curr, begin] # pylint: disable=arguments-differ - def discard(self, key): + def discard(self, key: T) -> None: """Discard an element from the set.""" if key in self.map: key, prev_item, next_item = self.map.pop(key) prev_item[2] = next_item next_item[1] = prev_item - def __iter__(self): + def __iter__(self) -> Iterator[T]: """Iterate of the set.""" end = self.end curr = end[2] @@ -197,7 +203,7 @@ class OrderedSet(MutableSet): yield curr[0] curr = curr[2] - def __reversed__(self): + def __reversed__(self) -> Iterator[T]: """Reverse the ordering.""" end = self.end curr = end[1] @@ -206,7 +212,7 @@ class OrderedSet(MutableSet): curr = curr[1] # pylint: disable=arguments-differ - def pop(self, last=True): + def pop(self, last: bool = True) -> T: """Pop element of the end of the set. Set last=False to pop from the beginning. @@ -215,27 +221,27 @@ class OrderedSet(MutableSet): raise KeyError('set is empty') key = self.end[1][0] if last else self.end[2][0] self.discard(key) - return key + return key # type: ignore - def update(self, *args): + def update(self, *args: Any) -> None: """Add elements from args to the set.""" for item in chain(*args): self.add(item) - def __repr__(self): + def __repr__(self) -> str: """Return the representation.""" if not self: return '%s()' % (self.__class__.__name__,) return '%s(%r)' % (self.__class__.__name__, list(self)) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Return the comparison.""" if isinstance(other, OrderedSet): return len(self) == len(other) and list(self) == list(other) return set(self) == set(other) -class Throttle(object): +class Throttle: """A class for throttling the execution of tasks. This method decorator adds a cooldown to a method to prevent it from being @@ -253,20 +259,21 @@ class Throttle(object): Adds a datetime attribute `last_call` to the method. """ - def __init__(self, min_time, limit_no_throttle=None): + def __init__(self, min_time: timedelta, + limit_no_throttle: timedelta = None) -> None: """Initialize the throttle.""" self.min_time = min_time self.limit_no_throttle = limit_no_throttle - def __call__(self, method): + def __call__(self, method: Callable) -> Callable: """Caller for the throttle.""" # Make sure we return a coroutine if the method is async. if asyncio.iscoroutinefunction(method): - async def throttled_value(): + async def throttled_value() -> None: """Stand-in function for when real func is being throttled.""" return None else: - def throttled_value(): + def throttled_value() -> None: # type: ignore """Stand-in function for when real func is being throttled.""" return None @@ -287,14 +294,14 @@ class Throttle(object): '.' not in method.__qualname__.split('..')[-1]) @wraps(method) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Union[Callable, Coroutine]: """Wrap that allows wrapped to be called only once per min_time. If we cannot acquire the lock, it is running so return None. """ # pylint: disable=protected-access if hasattr(method, '__self__'): - host = method.__self__ + host = getattr(method, '__self__') elif is_func: host = wrapper else: @@ -317,7 +324,7 @@ class Throttle(object): if force or utcnow() - throttle[1] > self.min_time: result = method(*args, **kwargs) throttle[1] = utcnow() - return result + return result # type: ignore return throttled_value() finally: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index b3aa370da2e..aa030bf13c7 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -3,22 +3,25 @@ import concurrent.futures import threading import logging from asyncio import coroutines +from asyncio.events import AbstractEventLoop from asyncio.futures import Future from asyncio import ensure_future - +from typing import Any, Union, Coroutine, Callable, Generator _LOGGER = logging.getLogger(__name__) -def _set_result_unless_cancelled(fut, result): +def _set_result_unless_cancelled(fut: Future, result: Any) -> None: """Set the result only if the Future was not cancelled.""" if fut.cancelled(): return fut.set_result(result) -def _set_concurrent_future_state(concurr, source): +def _set_concurrent_future_state( + concurr: concurrent.futures.Future, + source: Union[concurrent.futures.Future, Future]) -> None: """Copy state from a future to a concurrent.futures.Future.""" assert source.done() if source.cancelled(): @@ -33,7 +36,8 @@ def _set_concurrent_future_state(concurr, source): concurr.set_result(result) -def _copy_future_state(source, dest): +def _copy_future_state(source: Union[concurrent.futures.Future, Future], + dest: Union[concurrent.futures.Future, Future]) -> None: """Copy state from another Future. The other Future may be a concurrent.futures.Future. @@ -53,7 +57,9 @@ def _copy_future_state(source, dest): dest.set_result(result) -def _chain_future(source, destination): +def _chain_future( + source: Union[concurrent.futures.Future, Future], + destination: Union[concurrent.futures.Future, Future]) -> None: """Chain two futures so that when one completes, so does the other. The result (or exception) of source will be copied to destination. @@ -65,23 +71,32 @@ def _chain_future(source, destination): if not isinstance(destination, (Future, concurrent.futures.Future)): raise TypeError('A future is required for destination argument') # pylint: disable=protected-access - source_loop = source._loop if isinstance(source, Future) else None - dest_loop = destination._loop if isinstance(destination, Future) else None + if isinstance(source, Future): + source_loop = source._loop # type: ignore + else: + source_loop = None + if isinstance(destination, Future): + dest_loop = destination._loop # type: ignore + else: + dest_loop = None - def _set_state(future, other): + def _set_state(future: Union[concurrent.futures.Future, Future], + other: Union[concurrent.futures.Future, Future]) -> None: if isinstance(future, Future): _copy_future_state(other, future) else: _set_concurrent_future_state(future, other) - def _call_check_cancel(destination): + def _call_check_cancel( + destination: Union[concurrent.futures.Future, Future]) -> None: if destination.cancelled(): if source_loop is None or source_loop is dest_loop: source.cancel() else: source_loop.call_soon_threadsafe(source.cancel) - def _call_set_state(source): + def _call_set_state( + source: Union[concurrent.futures.Future, Future]) -> None: if dest_loop is None or dest_loop is source_loop: _set_state(destination, source) else: @@ -91,7 +106,9 @@ def _chain_future(source, destination): source.add_done_callback(_call_set_state) -def run_coroutine_threadsafe(coro, loop): +def run_coroutine_threadsafe( + coro: Union[Coroutine, Generator], + loop: AbstractEventLoop) -> concurrent.futures.Future: """Submit a coroutine object to a given event loop. Return a concurrent.futures.Future to access the result. @@ -102,9 +119,9 @@ def run_coroutine_threadsafe(coro, loop): if not coroutines.iscoroutine(coro): raise TypeError('A coroutine object is required') - future = concurrent.futures.Future() + future = concurrent.futures.Future() # type: concurrent.futures.Future - def callback(): + def callback() -> None: """Handle the call to the coroutine.""" try: _chain_future(ensure_future(coro, loop=loop), future) @@ -119,7 +136,8 @@ def run_coroutine_threadsafe(coro, loop): return future -def fire_coroutine_threadsafe(coro, loop): +def fire_coroutine_threadsafe(coro: Coroutine, + loop: AbstractEventLoop) -> None: """Submit a coroutine object to a given event loop. This method does not provide a way to retrieve the result and @@ -133,15 +151,15 @@ def fire_coroutine_threadsafe(coro, loop): if not coroutines.iscoroutine(coro): raise TypeError('A coroutine object is required: %s' % coro) - def callback(): + def callback() -> None: """Handle the firing of a coroutine.""" ensure_future(coro, loop=loop) loop.call_soon_threadsafe(callback) - return -def run_callback_threadsafe(loop, callback, *args): +def run_callback_threadsafe(loop: AbstractEventLoop, callback: Callable, + *args: Any) -> concurrent.futures.Future: """Submit a callback object to a given event loop. Return a concurrent.futures.Future to access the result. @@ -150,9 +168,9 @@ def run_callback_threadsafe(loop, callback, *args): if ident is not None and ident == threading.get_ident(): raise RuntimeError('Cannot be called from within the event loop') - future = concurrent.futures.Future() + future = concurrent.futures.Future() # type: concurrent.futures.Future - def run_callback(): + def run_callback() -> None: """Run callback and store result.""" try: future.set_result(callback(*args)) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index a26f7014444..0538bfbf369 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -2,7 +2,7 @@ import math import colorsys -from typing import Tuple +from typing import Tuple, List # Official CSS3 colors from w3.org: # https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 @@ -162,7 +162,7 @@ COLORS = { } -def color_name_to_rgb(color_name): +def color_name_to_rgb(color_name: str) -> Tuple[int, int, int]: """Convert color name to RGB hex value.""" # COLORS map has no spaces in it, so make the color_name have no # spaces in it as well for matching purposes @@ -305,7 +305,8 @@ def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: return (r, g, b) -def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: +def color_RGB_to_hsv( + iR: float, iG: float, iB: float) -> Tuple[float, float, float]: """Convert an rgb color to its hsv representation. Hue is scaled 0-360 @@ -316,7 +317,7 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) -def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: +def color_RGB_to_hs(iR: float, iG: float, iB: float) -> Tuple[float, float]: """Convert an rgb color to its hs representation.""" return color_RGB_to_hsv(iR, iG, iB)[:2] @@ -340,7 +341,7 @@ def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) - return (h, s) + return h, s def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: @@ -348,8 +349,7 @@ def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) -def _match_max_scale(input_colors: Tuple[int, ...], - output_colors: Tuple[int, ...]) -> Tuple[int, ...]: +def _match_max_scale(input_colors: Tuple, output_colors: Tuple) -> Tuple: """Match the maximum value of the output to the input.""" max_in = max(input_colors) max_out = max(output_colors) @@ -360,7 +360,7 @@ def _match_max_scale(input_colors: Tuple[int, ...], return tuple(int(round(i * factor)) for i in output_colors) -def color_rgb_to_rgbw(r, g, b): +def color_rgb_to_rgbw(r: int, g: int, b: int) -> Tuple[int, int, int, int]: """Convert an rgb color to an rgbw representation.""" # Calculate the white channel as the minimum of input rgb channels. # Subtract the white portion from the remaining rgb channels. @@ -369,25 +369,25 @@ def color_rgb_to_rgbw(r, g, b): # Match the output maximum value to the input. This ensures the full # channel range is used. - return _match_max_scale((r, g, b), rgbw) + return _match_max_scale((r, g, b), rgbw) # type: ignore -def color_rgbw_to_rgb(r, g, b, w): +def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> Tuple[int, int, int]: """Convert an rgbw color to an rgb representation.""" # Add the white channel back into the rgb channels. rgb = (r + w, g + w, b + w) # Match the output maximum value to the input. This ensures the # output doesn't overflow. - return _match_max_scale((r, g, b, w), rgb) + return _match_max_scale((r, g, b, w), rgb) # type: ignore -def color_rgb_to_hex(r, g, b): +def color_rgb_to_hex(r: int, g: int, b: int) -> str: """Return a RGB color from a hex color string.""" return '{0:02x}{1:02x}{2:02x}'.format(round(r), round(g), round(b)) -def rgb_hex_to_rgb_list(hex_string): +def rgb_hex_to_rgb_list(hex_string: str) -> List[int]: """Return an RGB color value list from a hex color string.""" return [int(hex_string[i:i + len(hex_string) // 3], 16) for i in range(0, @@ -395,12 +395,14 @@ def rgb_hex_to_rgb_list(hex_string): len(hex_string) // 3)] -def color_temperature_to_hs(color_temperature_kelvin): +def color_temperature_to_hs( + color_temperature_kelvin: float) -> Tuple[float, float]: """Return an hs color from a color temperature in Kelvin.""" return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin)) -def color_temperature_to_rgb(color_temperature_kelvin): +def color_temperature_to_rgb( + color_temperature_kelvin: float) -> Tuple[float, float, float]: """ Return an RGB color from a color temperature in Kelvin. @@ -421,7 +423,7 @@ def color_temperature_to_rgb(color_temperature_kelvin): blue = _get_blue(tmp_internal) - return (red, green, blue) + return red, green, blue def _bound(color_component: float, minimum: float = 0, @@ -464,11 +466,11 @@ def _get_blue(temperature: float) -> float: return _bound(blue) -def color_temperature_mired_to_kelvin(mired_temperature): +def color_temperature_mired_to_kelvin(mired_temperature: float) -> float: """Convert absolute mired shift to degrees kelvin.""" return math.floor(1000000 / mired_temperature) -def color_temperature_kelvin_to_mired(kelvin_temperature): +def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> float: """Convert degrees kelvin to mired shift.""" return math.floor(1000000 / kelvin_temperature) diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py index c26606d52cf..22ed1a4dae6 100644 --- a/homeassistant/util/decorator.py +++ b/homeassistant/util/decorator.py @@ -1,12 +1,15 @@ """Decorator utility functions.""" +from typing import Callable, TypeVar + +CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable) # noqa pylint: disable=invalid-name class Registry(dict): """Registry of items.""" - def register(self, name): + def register(self, name: str) -> Callable[[CALLABLE_T], CALLABLE_T]: """Return decorator to register item with a specific name.""" - def decorator(func): + def decorator(func: CALLABLE_T) -> CALLABLE_T: """Register decorated function.""" self[name] = func return func diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 0f07a90e9bb..ce6775b9ea7 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -65,20 +65,20 @@ def as_utc(dattim: dt.datetime) -> dt.datetime: """ if dattim.tzinfo == UTC: return dattim - elif dattim.tzinfo is None: + if dattim.tzinfo is None: dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore return dattim.astimezone(UTC) -def as_timestamp(dt_value): +def as_timestamp(dt_value: dt.datetime) -> float: """Convert a date/time into a unix time (seconds since 1970).""" if hasattr(dt_value, "timestamp"): - parsed_dt = dt_value + parsed_dt = dt_value # type: Optional[dt.datetime] else: parsed_dt = parse_datetime(str(dt_value)) - if not parsed_dt: - raise ValueError("not a valid date/time.") + if parsed_dt is None: + raise ValueError("not a valid date/time.") return parsed_dt.timestamp() @@ -86,7 +86,7 @@ def as_local(dattim: dt.datetime) -> dt.datetime: """Convert a UTC datetime object to local time zone.""" if dattim.tzinfo == DEFAULT_TIME_ZONE: return dattim - elif dattim.tzinfo is None: + if dattim.tzinfo is None: dattim = UTC.localize(dattim) return dattim.astimezone(DEFAULT_TIME_ZONE) @@ -98,7 +98,7 @@ def utc_from_timestamp(timestamp: float) -> dt.datetime: def start_of_local_day(dt_or_d: - Union[dt.date, dt.datetime]=None) -> dt.datetime: + Union[dt.date, dt.datetime] = None) -> dt.datetime: """Return local datetime object of start of day from date or datetime.""" if dt_or_d is None: date = now().date() # type: dt.date @@ -150,7 +150,7 @@ def parse_date(dt_str: str) -> Optional[dt.date]: return None -def parse_time(time_str): +def parse_time(time_str: str) -> Optional[dt.time]: """Parse a time string (00:20:00) into Time object. Return None if invalid. diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 1029e58c118..8ecfebd5b33 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -38,7 +38,7 @@ def load_json(filename: str, default: Union[List, Dict, None] = None) \ return {} if default is None else default -def save_json(filename: str, data: Union[List, Dict]): +def save_json(filename: str, data: Union[List, Dict]) -> None: """Save JSON data to a file. Returns True on success. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index e390b537d34..16aec2ec617 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -33,7 +33,7 @@ LocationInfo = collections.namedtuple( 'use_metric']) -def detect_location_info(): +def detect_location_info() -> Optional[LocationInfo]: """Detect location information.""" data = _get_freegeoip() @@ -49,15 +49,21 @@ def detect_location_info(): return LocationInfo(**data) -def distance(lat1, lon1, lat2, lon2): +def distance(lat1: Optional[float], lon1: Optional[float], + lat2: float, lon2: float) -> Optional[float]: """Calculate the distance in meters between two points. Async friendly. """ - return vincenty((lat1, lon1), (lat2, lon2)) * 1000 + if lat1 is None or lon1 is None: + return None + result = vincenty((lat1, lon1), (lat2, lon2)) + if result is None: + return None + return result * 1000 -def elevation(latitude, longitude): +def elevation(latitude: float, longitude: float) -> int: """Return elevation for given latitude and longitude.""" try: req = requests.get( diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 10b43445184..f2bf15d8a03 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -1,7 +1,9 @@ """Logging utilities.""" import asyncio +from asyncio.events import AbstractEventLoop import logging import threading +from typing import Optional from .async_ import run_coroutine_threadsafe @@ -9,12 +11,12 @@ from .async_ import run_coroutine_threadsafe class HideSensitiveDataFilter(logging.Filter): """Filter API password calls.""" - def __init__(self, text): + def __init__(self, text: str) -> None: """Initialize sensitive data filter.""" super().__init__() self.text = text - def filter(self, record): + def filter(self, record: logging.LogRecord) -> bool: """Hide sensitive data in messages.""" record.msg = record.msg.replace(self.text, '*******') @@ -22,14 +24,15 @@ class HideSensitiveDataFilter(logging.Filter): # pylint: disable=invalid-name -class AsyncHandler(object): +class AsyncHandler: """Logging handler wrapper to add an async layer.""" - def __init__(self, loop, handler): + def __init__( + self, loop: AbstractEventLoop, handler: logging.Handler) -> None: """Initialize async logging handler wrapper.""" self.handler = handler self.loop = loop - self._queue = asyncio.Queue(loop=loop) + self._queue = asyncio.Queue(loop=loop) # type: asyncio.Queue self._thread = threading.Thread(target=self._process) # Delegate from handler @@ -45,11 +48,11 @@ class AsyncHandler(object): self._thread.start() - def close(self): + def close(self) -> None: """Wrap close to handler.""" self.emit(None) - async def async_close(self, blocking=False): + async def async_close(self, blocking: bool = False) -> None: """Close the handler. When blocking=True, will wait till closed. @@ -60,7 +63,7 @@ class AsyncHandler(object): while self._thread.is_alive(): await asyncio.sleep(0, loop=self.loop) - def emit(self, record): + def emit(self, record: Optional[logging.LogRecord]) -> None: """Process a record.""" ident = self.loop.__dict__.get("_thread_ident") @@ -71,11 +74,11 @@ class AsyncHandler(object): else: self.loop.call_soon_threadsafe(self._queue.put_nowait, record) - def __repr__(self): + def __repr__(self) -> str: """Return the string names.""" return str(self.handler) - def _process(self): + def _process(self) -> None: """Process log in a thread.""" while True: record = run_coroutine_threadsafe( @@ -87,34 +90,34 @@ class AsyncHandler(object): self.handler.emit(record) - def createLock(self): + def createLock(self) -> None: """Ignore lock stuff.""" pass - def acquire(self): + def acquire(self) -> None: """Ignore lock stuff.""" pass - def release(self): + def release(self) -> None: """Ignore lock stuff.""" pass @property - def level(self): + def level(self) -> int: """Wrap property level to handler.""" return self.handler.level @property - def formatter(self): + def formatter(self) -> Optional[logging.Formatter]: """Wrap property formatter to handler.""" return self.handler.formatter @property - def name(self): + def name(self) -> str: """Wrap property set_name to handler.""" - return self.handler.get_name() + return self.handler.get_name() # type: ignore @name.setter - def name(self, name): + def name(self, name: str) -> None: """Wrap property get_name to handler.""" - self.handler.name = name + self.handler.set_name(name) # type: ignore diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index d1d398020de..9433046e688 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) INSTALL_LOCK = threading.Lock() -def is_virtual_env(): +def is_virtual_env() -> bool: """Return if we run in a virtual environtment.""" # Check supports venv && virtualenv return (getattr(sys, 'base_prefix', sys.prefix) != sys.prefix or diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 4f528cfcb51..392c5986c89 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -4,7 +4,7 @@ import ssl import certifi -def client_context(): +def client_context() -> ssl.SSLContext: """Return an SSL context for making requests.""" context = ssl.create_default_context( purpose=ssl.Purpose.SERVER_AUTH, @@ -13,7 +13,7 @@ def client_context(): return context -def server_context(): +def server_context() -> ssl.SSLContext: """Return an SSL context following the Mozilla recommendations. TLS configuration follows the best-practice guidelines specified here: diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 913d6456906..6e2b378b218 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -29,6 +29,6 @@ def convert(temperature: float, from_unit: str, to_unit: str, if from_unit == to_unit: return temperature - elif from_unit == TEMP_CELSIUS: + if from_unit == TEMP_CELSIUS: return celsius_to_fahrenheit(temperature, interval) return fahrenheit_to_celsius(temperature, interval) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 4cc0fff96b9..5a8f515c3ad 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -1,6 +1,7 @@ """Unit system helper class and methods.""" import logging +from typing import Optional from numbers import Number from homeassistant.const import ( @@ -61,10 +62,10 @@ def is_valid_unit(unit: str, unit_type: str) -> bool: return unit in units -class UnitSystem(object): +class UnitSystem: """A container for units of measure.""" - def __init__(self: object, name: str, temperature: str, length: str, + def __init__(self, name: str, temperature: str, length: str, volume: str, mass: str) -> None: """Initialize the unit system object.""" errors = \ @@ -99,7 +100,7 @@ class UnitSystem(object): return temperature_util.convert(temperature, from_unit, self.temperature_unit) - def length(self, length: float, from_unit: str) -> float: + def length(self, length: Optional[float], from_unit: str) -> float: """Convert the given length to this unit system.""" if not isinstance(length, Number): raise TypeError('{} is not a numeric value.'.format(str(length))) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 298d52722a5..69f83aefad7 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -4,7 +4,7 @@ import os import sys import fnmatch from collections import OrderedDict -from typing import Union, List, Dict +from typing import Union, List, Dict, Iterator, overload, TypeVar import yaml try: @@ -22,7 +22,10 @@ from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) _SECRET_NAMESPACE = 'homeassistant' SECRET_YAML = 'secrets.yaml' -__SECRET_CACHE = {} # type: Dict +__SECRET_CACHE = {} # type: Dict[str, JSON_TYPE] + +JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +DICT_T = TypeVar('DICT_T', bound=Dict) # pylint: disable=invalid-name class NodeListClass(list): @@ -37,7 +40,42 @@ class NodeStrClass(str): pass -def _add_reference(obj, loader, node): +# pylint: disable=too-many-ancestors +class SafeLineLoader(yaml.SafeLoader): + """Loader class that keeps track of line numbers.""" + + def compose_node(self, parent: yaml.nodes.Node, + index: int) -> yaml.nodes.Node: + """Annotate a node with the first line it was seen.""" + last_line = self.line # type: int + node = super(SafeLineLoader, + self).compose_node(parent, index) # type: yaml.nodes.Node + node.__line__ = last_line + 1 # type: ignore + return node + + +# pylint: disable=pointless-statement +@overload +def _add_reference(obj: Union[list, NodeListClass], + loader: yaml.SafeLoader, + node: yaml.nodes.Node) -> NodeListClass: ... + + +@overload # noqa: F811 +def _add_reference(obj: Union[str, NodeStrClass], + loader: yaml.SafeLoader, + node: yaml.nodes.Node) -> NodeStrClass: ... + + +@overload # noqa: F811 +def _add_reference(obj: DICT_T, + loader: yaml.SafeLoader, + node: yaml.nodes.Node) -> DICT_T: ... +# pylint: enable=pointless-statement + + +def _add_reference(obj, loader: SafeLineLoader, # type: ignore # noqa: F811 + node: yaml.nodes.Node): """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) @@ -48,20 +86,7 @@ def _add_reference(obj, loader, node): return obj -# pylint: disable=too-many-ancestors -class SafeLineLoader(yaml.SafeLoader): - """Loader class that keeps track of line numbers.""" - - def compose_node(self, parent: yaml.nodes.Node, index) -> yaml.nodes.Node: - """Annotate a node with the first line it was seen.""" - last_line = self.line # type: int - node = super(SafeLineLoader, - self).compose_node(parent, index) # type: yaml.nodes.Node - node.__line__ = last_line + 1 # type: ignore - return node - - -def load_yaml(fname: str) -> Union[List, Dict]: +def load_yaml(fname: str) -> JSON_TYPE: """Load a YAML file.""" try: with open(fname, encoding='utf-8') as conf_file: @@ -83,12 +108,12 @@ def dump(_dict: dict) -> str: .replace(': null\n', ':\n') -def save_yaml(path, data): +def save_yaml(path: str, data: dict) -> None: """Save YAML to a file.""" # Dump before writing to not truncate the file if dumping fails - data = dump(data) + str_data = dump(data) with open(path, 'w', encoding='utf-8') as outfile: - outfile.write(data) + outfile.write(str_data) def clear_secret_cache() -> None: @@ -100,7 +125,7 @@ def clear_secret_cache() -> None: def _include_yaml(loader: SafeLineLoader, - node: yaml.nodes.Node) -> Union[List, Dict]: + node: yaml.nodes.Node) -> JSON_TYPE: """Load another YAML file and embeds it using the !include tag. Example: @@ -115,7 +140,7 @@ def _is_file_valid(name: str) -> bool: return not name.startswith('.') -def _find_files(directory: str, pattern: str): +def _find_files(directory: str, pattern: str) -> Iterator[str]: """Recursively load files in a directory.""" for root, dirs, files in os.walk(directory, topdown=True): dirs[:] = [d for d in dirs if _is_file_valid(d)] @@ -151,7 +176,7 @@ def _include_dir_merge_named_yaml(loader: SafeLineLoader, def _include_dir_list_yaml(loader: SafeLineLoader, - node: yaml.nodes.Node): + node: yaml.nodes.Node) -> List[JSON_TYPE]: """Load multiple files from directory as a list.""" loc = os.path.join(os.path.dirname(loader.name), node.value) return [load_yaml(f) for f in _find_files(loc, '*.yaml') @@ -159,11 +184,11 @@ def _include_dir_list_yaml(loader: SafeLineLoader, def _include_dir_merge_list_yaml(loader: SafeLineLoader, - node: yaml.nodes.Node): + node: yaml.nodes.Node) -> JSON_TYPE: """Load multiple files from directory as a merged list.""" loc = os.path.join(os.path.dirname(loader.name), node.value) # type: str - merged_list = [] # type: List + merged_list = [] # type: List[JSON_TYPE] for fname in _find_files(loc, '*.yaml'): if os.path.basename(fname) == SECRET_YAML: continue @@ -202,28 +227,27 @@ def _ordered_dict(loader: SafeLineLoader, return _add_reference(OrderedDict(nodes), loader, node) -def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node): +def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """Add line number and file name to Load YAML sequence.""" obj, = loader.construct_yaml_seq(node) return _add_reference(obj, loader, node) def _env_var_yaml(loader: SafeLineLoader, - node: yaml.nodes.Node): + node: yaml.nodes.Node) -> str: """Load environment variables and embed it into the configuration YAML.""" args = node.value.split() # Check for a default value if len(args) > 1: return os.getenv(args[0], ' '.join(args[1:])) - elif args[0] in os.environ: + if args[0] in os.environ: return os.environ[args[0]] - else: - _LOGGER.error("Environment variable %s not defined.", node.value) - raise HomeAssistantError(node.value) + _LOGGER.error("Environment variable %s not defined.", node.value) + raise HomeAssistantError(node.value) -def _load_secret_yaml(secret_path: str) -> Dict: +def _load_secret_yaml(secret_path: str) -> JSON_TYPE: """Load the secrets yaml from path.""" secret_path = os.path.join(secret_path, SECRET_YAML) if secret_path in __SECRET_CACHE: @@ -249,7 +273,7 @@ def _load_secret_yaml(secret_path: str) -> Dict: def _secret_yaml(loader: SafeLineLoader, - node: yaml.nodes.Node): + node: yaml.nodes.Node) -> JSON_TYPE: """Load secrets and embed it into the configuration YAML.""" secret_path = os.path.dirname(loader.name) while True: @@ -309,9 +333,10 @@ yaml.SafeLoader.add_constructor('!include_dir_merge_named', # From: https://gist.github.com/miracle2k/3184458 # pylint: disable=redefined-outer-name -def represent_odict(dump, tag, mapping, flow_style=None): +def represent_odict(dump, tag, mapping, # type: ignore + flow_style=None) -> yaml.MappingNode: """Like BaseRepresenter.represent_mapping but does not issue the sort().""" - value = [] + value = [] # type: list node = yaml.MappingNode(tag, value, flow_style=flow_style) if dump.alias_key is not None: dump.represented_objects[dump.alias_key] = node diff --git a/mypy.ini b/mypy.ini index 3970ea72d47..c92786e643f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,10 +1,18 @@ [mypy] -warn_redundant_casts = true -warn_unused_configs = true -ignore_missing_imports = true +check_untyped_defs = true follow_imports = silent -warn_unused_ignores = true +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true warn_return_any = true +warn_unused_configs = true +warn_unused_ignores = true + +[mypy-homeassistant.*] +disallow_untyped_defs = true + +[mypy-homeassistant.config_entries] +disallow_untyped_defs = false [mypy-homeassistant.util.yaml] warn_return_any = false diff --git a/pylintrc b/pylintrc index d47437cb121..00bc6582f3a 100644 --- a/pylintrc +++ b/pylintrc @@ -4,7 +4,6 @@ # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation -# abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation # redefined-variable-type - this is Python, we're duck typing! @@ -12,15 +11,17 @@ # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise +# useless-return - https://github.com/PyCQA/pylint/issues/2300 +# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 disable= abstract-class-little-used, - abstract-class-not-used, abstract-method, cyclic-import, duplicate-code, global-statement, inconsistent-return-statements, locally-disabled, + not-an-iterable, not-context-manager, redefined-variable-type, too-few-public-methods, @@ -32,7 +33,8 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - unused-argument + unused-argument, + useless-return [REPORTS] reports=no diff --git a/requirements_all.txt b/requirements_all.txt index 72dc74b0f66..9be2d66f327 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 requests==2.19.1 -voluptuous==0.11.1 +voluptuous==0.11.3 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 @@ -108,6 +108,9 @@ aiolifx_effects==0.1.2 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.5.4 +# homeassistant.components.cover.aladdin_connect +aladdin_connect==0.1 + # homeassistant.components.alarmdecoder alarmdecoder==1.13.2 @@ -154,7 +157,7 @@ batinfo==0.4.2 # homeassistant.components.sensor.geizhals # homeassistant.components.sensor.scrape # homeassistant.components.sensor.sytadin -beautifulsoup4==4.6.0 +beautifulsoup4==4.6.1 # homeassistant.components.zha bellows==0.6.0 @@ -196,6 +199,9 @@ braviarc-homeassistant==0.3.7.dev0 # homeassistant.components.switch.broadlink broadlink==0.9.0 +# homeassistant.components.cover.brunt +brunt==0.1.2 + # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 @@ -260,7 +266,7 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.7.4 +denonavr==0.7.5 # homeassistant.components.media_player.directv directpy==0.5 @@ -309,7 +315,7 @@ ephem==3.7.6.0 epson-projector==0.1.3 # homeassistant.components.netgear_lte -eternalegypt==0.0.2 +eternalegypt==0.0.3 # homeassistant.components.keyboard_remote # evdev==0.6.1 @@ -415,7 +421,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180720.0 +home-assistant-frontend==20180726.0 # homeassistant.components.homekit_controller # homekit==0.10 @@ -452,7 +458,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.11.3 +insteonplm==0.11.7 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 @@ -529,6 +535,9 @@ lw12==0.9.2 # homeassistant.components.sensor.lyft lyft_rides==0.2 +# homeassistant.components.sensor.magicseaweed +magicseaweed==1.0.0 + # homeassistant.components.matrix matrix-client==0.2.0 @@ -552,7 +561,7 @@ mitemp_bt==0.0.1 motorparts==1.0.2 # homeassistant.components.tts -mutagen==1.40.0 +mutagen==1.41.0 # homeassistant.components.mychevy mychevy==0.4.0 @@ -590,7 +599,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.5 +numpy==1.15.0 # homeassistant.components.google oauth2client==4.0.0 @@ -643,7 +652,7 @@ piglow==1.2.4 pilight==0.1.1 # homeassistant.components.camera.proxy -pillow==5.0.0 +pillow==5.2.0 # homeassistant.components.dominos pizzapi==0.0.3 @@ -728,7 +737,7 @@ pyairvisual==2.0.1 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.1.9 +pyarlo==0.2.0 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 @@ -737,7 +746,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.netatmo -pyatmo==1.0.0 +pyatmo==1.1.1 # homeassistant.components.apple_tv pyatv==0.3.10 @@ -753,7 +762,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.7 +pybotvac==0.0.9 # homeassistant.components.cloudflare pycfdns==0.0.1 @@ -822,6 +831,9 @@ pyflexit==0.3 # homeassistant.components.binary_sensor.flic pyflic-homeassistant==0.4.dev0 +# homeassistant.components.light.futurenow +pyfnip==0.2 + # homeassistant.components.fritzbox pyfritzhome==0.3.7 @@ -847,7 +859,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.45 +pyhomematic==0.1.46 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 @@ -955,7 +967,7 @@ pyotp==2.2.6 # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap -pyowm==2.8.0 +pyowm==2.9.0 # homeassistant.components.sensor.pollen pypollencom==2.1.0 @@ -1141,7 +1153,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.43 +pyvera==0.2.44 # homeassistant.components.switch.vesync pyvesync==0.1.1 @@ -1241,7 +1253,10 @@ shodan==1.8.1 simplepush==1.1.4 # homeassistant.components.alarm_control_panel.simplisafe -simplisafe-python==1.0.5 +simplisafe-python==2.0.2 + +# homeassistant.components.sisyphus +sisyphus-control==2.1 # homeassistant.components.skybell skybellpy==0.1.2 @@ -1256,7 +1271,7 @@ sleekxmpp==1.3.2 sleepyq==0.6 # homeassistant.components.smappee -smappy==0.2.15 +smappy==0.2.16 # homeassistant.components.raspihats # homeassistant.components.sensor.bh1750 @@ -1278,6 +1293,9 @@ somecomfort==0.5.2 # homeassistant.components.sensor.speedtest speedtest-cli==2.0.2 +# homeassistant.components.spider +spiderpy==1.2.0 + # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 @@ -1287,7 +1305,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.9 +sqlalchemy==1.2.10 # homeassistant.components.statsd statsd==3.2.1 @@ -1340,6 +1358,9 @@ toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.18 +# homeassistant.components.device_tracker.tplink +tplink==0.2.1 + # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission transmissionrpc==0.11 @@ -1442,7 +1463,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.07.04 +youtube_dl==2018.07.29 # homeassistant.components.light.zengge zengge==0.2 diff --git a/requirements_test.txt b/requirements_test.txt index db53699379c..225958a722c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,9 +6,9 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.610 +mypy==0.620 pydocstyle==1.1.1 -pylint==1.9.2 +pylint==2.0.1 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3de2285eae9..ff95cd3be25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,9 +7,9 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.610 +mypy==0.620 pydocstyle==1.1.1 -pylint==1.9.2 +pylint==2.0.1 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180720.0 +home-assistant-frontend==20180726.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 @@ -102,7 +102,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.5 +numpy==1.15.0 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -197,7 +197,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.9 +sqlalchemy==1.2.10 # homeassistant.components.statsd statsd==3.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9a5b4dd1a43..d92502de078 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -182,6 +182,10 @@ def gather_modules(): for req in module.REQUIREMENTS: if req in IGNORE_REQ: continue + if '://' in req: + errors.append( + "{}[Only pypi dependencies are allowed: {}]".format( + package, req)) if req.partition('==')[1] == '' and req not in IGNORE_PIN: errors.append( "{}[Please pin requirement {}, see {}]".format( @@ -257,7 +261,7 @@ def write_requirements_file(data): def write_test_requirements_file(data): - """Write the modules to the requirements_all.txt.""" + """Write the modules to the requirements_test_all.txt.""" with open('requirements_test_all.txt', 'w+', newline="\n") as req_file: req_file.write(data) @@ -275,7 +279,7 @@ def validate_requirements_file(data): def validate_requirements_test_file(data): - """Validate if requirements_all.txt is up to date.""" + """Validate if requirements_test_all.txt is up to date.""" with open('requirements_test_all.txt', 'r') as req_file: return data == req_file.read() diff --git a/setup.py b/setup.py index bbf10dd309d..7519fc6a873 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ 'pytz>=2018.04', 'pyyaml>=3.13,<4', 'requests==2.19.1', - 'voluptuous==0.11.1', + 'voluptuous==0.11.3', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/common.py b/tests/common.py index b03d473e6f3..5567a431e58 100644 --- a/tests/common.py +++ b/tests/common.py @@ -187,7 +187,7 @@ def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] - @asyncio.coroutine + @ha.callback def mock_service_log(call): # pylint: disable=unnecessary-lambda """Mock service call.""" calls.append(call) @@ -355,7 +355,7 @@ def ensure_auth_manager_loaded(auth_mgr): store._users = OrderedDict() -class MockModule(object): +class MockModule: """Representation of a fake module.""" # pylint: disable=invalid-name @@ -391,7 +391,7 @@ class MockModule(object): self.async_unload_entry = async_unload_entry -class MockPlatform(object): +class MockPlatform: """Provide a fake platform.""" # pylint: disable=invalid-name @@ -496,14 +496,13 @@ class MockToggleDevice(entity.ToggleEntity): """Return the last call.""" if not self.calls: return None - elif method is None: + if method is None: return self.calls[-1] - else: - try: - return next(call for call in reversed(self.calls) - if call[0] == method) - except StopIteration: - return None + try: + return next(call for call in reversed(self.calls) + if call[0] == method) + except StopIteration: + return None class MockConfigEntry(config_entries.ConfigEntry): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index afa4d19b5d9..cf8535653a9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -124,7 +124,7 @@ def discovery_test(device, hass, expected_endpoints=1): if expected_endpoints == 1: return endpoints[0] - elif expected_endpoints > 1: + if expected_endpoints > 1: return endpoints return None @@ -1225,7 +1225,7 @@ def reported_properties(hass, endpoint): return _ReportedProperties(msg['context']['properties']) -class _ReportedProperties(object): +class _ReportedProperties: def __init__(self, properties): self.properties = properties diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 807bf15854b..eea768c96a0 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import patch +from homeassistant.auth.models import Credentials from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from homeassistant.components import auth @@ -90,12 +91,20 @@ def test_credential_store_expiration(): async def test_ws_current_user(hass, hass_ws_client, hass_access_token): - """Test the current user command.""" + """Test the current user command with homeassistant creds.""" assert await async_setup_component(hass, 'auth', { 'http': { 'api_password': 'bla' } }) + + user = hass_access_token.refresh_token.user + credential = Credentials(auth_provider_type='homeassistant', + auth_provider_id=None, + data={}, id='test-id') + user.credentials.append(credential) + assert len(user.credentials) == 1 + with patch('homeassistant.auth.AuthManager.active', return_value=True): client = await hass_ws_client(hass, hass_access_token) @@ -107,12 +116,17 @@ async def test_ws_current_user(hass, hass_ws_client, hass_access_token): result = await client.receive_json() assert result['success'], result - user = hass_access_token.refresh_token.user user_dict = result['result'] assert user_dict['name'] == user.name assert user_dict['id'] == user.id assert user_dict['is_owner'] == user.is_owner + assert len(user_dict['credentials']) == 1 + + hass_cred = user_dict['credentials'][0] + assert hass_cred['auth_provider_type'] == 'homeassistant' + assert hass_cred['auth_provider_id'] is None + assert 'data' not in hass_cred async def test_cors_on_token(hass, aiohttp_client): @@ -130,3 +144,68 @@ async def test_cors_on_token(hass, aiohttp_client): 'origin': 'http://example.com' }) assert resp.headers['Access-Control-Allow-Origin'] == 'http://example.com' + + +async def test_refresh_token_system_generated(hass, aiohttp_client): + """Test that we can get access tokens for system generated user.""" + client = await async_setup_auth(hass, aiohttp_client) + user = await hass.auth.async_create_system_user('Test System') + refresh_token = await hass.auth.async_create_refresh_token(user, None) + + resp = await client.post('/auth/token', data={ + 'client_id': 'https://this-is-not-allowed-for-system-users.com/', + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token.token, + }) + + assert resp.status == 400 + result = await resp.json() + assert result['error'] == 'invalid_request' + + resp = await client.post('/auth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token.token, + }) + + assert resp.status == 200 + tokens = await resp.json() + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + +async def test_refresh_token_different_client_id(hass, aiohttp_client): + """Test that we verify client ID.""" + client = await async_setup_auth(hass, aiohttp_client) + user = await hass.auth.async_create_user('Test User') + refresh_token = await hass.auth.async_create_refresh_token(user, CLIENT_ID) + + # No client ID + resp = await client.post('/auth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token.token, + }) + + assert resp.status == 400 + result = await resp.json() + assert result['error'] == 'invalid_request' + + # Different client ID + resp = await client.post('/auth/token', data={ + 'client_id': 'http://example-different.com', + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token.token, + }) + + assert resp.status == 400 + result = await resp.json() + assert result['error'] == 'invalid_request' + + # Correct + resp = await client.post('/auth/token', data={ + 'client_id': CLIENT_ID, + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token.token, + }) + + assert resp.status == 200 + tokens = await resp.json() + assert hass.auth.async_get_access_token(tokens['access_token']) is not None diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_login_flow.py similarity index 66% rename from tests/components/auth/test_init_login_flow.py rename to tests/components/auth/test_login_flow.py index 50bd03d6ced..8b6108067c5 100644 --- a/tests/components/auth/test_init_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -8,6 +8,7 @@ async def test_fetch_auth_providers(hass, aiohttp_client): """Test fetching auth providers.""" client = await async_setup_auth(hass, aiohttp_client) resp = await client.get('/auth/providers') + assert resp.status == 200 assert await resp.json() == [{ 'name': 'Example', 'type': 'insecure_example', @@ -60,3 +61,31 @@ async def test_invalid_username_password(hass, aiohttp_client): assert step['step_id'] == 'init' assert step['errors']['base'] == 'invalid_auth' + + +async def test_login_exist_user(hass, aiohttp_client): + """Test logging in with exist user.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + cred = await hass.auth.auth_providers[0].async_get_or_create_credentials( + {'username': 'test-user'}) + await hass.auth.async_get_or_create_user(cred) + + resp = await client.post('/auth/login_flow', json={ + 'client_id': CLIENT_ID, + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, + }) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'client_id': CLIENT_ID, + 'username': 'test-user', + 'password': 'test-pass', + }) + + assert resp.status == 200 + step = await resp.json() + assert step['type'] == 'create_entry' + assert len(step['result']) > 1 diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 33f1a7aa704..b1990fb80aa 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -437,10 +437,12 @@ class TestAutomation(unittest.TestCase): } } }}): - automation.reload(self.hass) - self.hass.block_till_done() - # De-flake ?! - self.hass.block_till_done() + with patch('homeassistant.config.find_config_file', + return_value=''): + automation.reload(self.hass) + self.hass.block_till_done() + # De-flake ?! + self.hass.block_till_done() assert self.hass.states.get('automation.hello') is None assert self.hass.states.get('automation.bye') is not None @@ -485,8 +487,10 @@ class TestAutomation(unittest.TestCase): with patch('homeassistant.config.load_yaml_config_file', autospec=True, return_value={automation.DOMAIN: 'not valid'}): - automation.reload(self.hass) - self.hass.block_till_done() + with patch('homeassistant.config.find_config_file', + return_value=''): + automation.reload(self.hass) + self.hass.block_till_done() assert self.hass.states.get('automation.hello') is None @@ -521,8 +525,10 @@ class TestAutomation(unittest.TestCase): with patch('homeassistant.config.load_yaml_config_file', side_effect=HomeAssistantError('bla')): - automation.reload(self.hass) - self.hass.block_till_done() + with patch('homeassistant.config.find_config_file', + return_value=''): + automation.reload(self.hass) + self.hass.block_till_done() assert self.hass.states.get('automation.hello') is not None diff --git a/tests/components/binary_sensor/test_command_line.py b/tests/components/binary_sensor/test_command_line.py index d01b62e4c12..07389c7c8a9 100644 --- a/tests/components/binary_sensor/test_command_line.py +++ b/tests/components/binary_sensor/test_command_line.py @@ -24,7 +24,9 @@ class TestCommandSensorBinarySensor(unittest.TestCase): config = {'name': 'Test', 'command': 'echo 1', 'payload_on': '1', - 'payload_off': '0'} + 'payload_off': '0', + 'command_timeout': 15 + } devices = [] @@ -43,7 +45,7 @@ class TestCommandSensorBinarySensor(unittest.TestCase): def test_template(self): """Test setting the state with a template.""" - data = command_line.CommandSensorData(self.hass, 'echo 10') + data = command_line.CommandSensorData(self.hass, 'echo 10', 15) entity = command_line.CommandBinarySensor( self.hass, data, 'test', None, '1.0', '0', @@ -53,7 +55,7 @@ class TestCommandSensorBinarySensor(unittest.TestCase): def test_sensor_off(self): """Test setting the state with a template.""" - data = command_line.CommandSensorData(self.hass, 'echo 0') + data = command_line.CommandSensorData(self.hass, 'echo 0', 15) entity = command_line.CommandBinarySensor( self.hass, data, 'test', None, '1', '0', None) diff --git a/tests/components/binary_sensor/test_ffmpeg.py b/tests/components/binary_sensor/test_ffmpeg.py index aadafadd4a6..da9350008d8 100644 --- a/tests/components/binary_sensor/test_ffmpeg.py +++ b/tests/components/binary_sensor/test_ffmpeg.py @@ -7,7 +7,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, mock_coro) -class TestFFmpegNoiseSetup(object): +class TestFFmpegNoiseSetup: """Test class for ffmpeg.""" def setup_method(self): @@ -72,7 +72,7 @@ class TestFFmpegNoiseSetup(object): assert entity.state == 'on' -class TestFFmpegMotionSetup(object): +class TestFFmpegMotionSetup: """Test class for ffmpeg.""" def setup_method(self): diff --git a/tests/components/binary_sensor/test_workday.py b/tests/components/binary_sensor/test_workday.py index af7e856e417..893745ce3de 100644 --- a/tests/components/binary_sensor/test_workday.py +++ b/tests/components/binary_sensor/test_workday.py @@ -12,7 +12,7 @@ from tests.common import ( FUNCTION_PATH = 'homeassistant.components.binary_sensor.workday.get_date' -class TestWorkdaySetup(object): +class TestWorkdaySetup: """Test class for workday sensor.""" def setup_method(self): diff --git a/tests/components/camera/test_demo.py b/tests/components/camera/test_demo.py index 51e04fca351..b901b723c0b 100644 --- a/tests/components/camera/test_demo.py +++ b/tests/components/camera/test_demo.py @@ -1,14 +1,89 @@ """The tests for local file camera component.""" -import asyncio +from unittest.mock import mock_open, patch, PropertyMock + +import pytest + from homeassistant.components import camera +from homeassistant.components.camera import STATE_STREAMING, STATE_IDLE +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -@asyncio.coroutine -def test_motion_detection(hass): +@pytest.fixture +def demo_camera(hass): + """Initialize a demo camera platform.""" + hass.loop.run_until_complete(async_setup_component(hass, 'camera', { + camera.DOMAIN: { + 'platform': 'demo' + } + })) + return hass.data['camera'].get_entity('camera.demo_camera') + + +async def test_init_state_is_streaming(hass, demo_camera): + """Demo camera initialize as streaming.""" + assert demo_camera.state == STATE_STREAMING + + mock_on_img = mock_open(read_data=b'ON') + with patch('homeassistant.components.camera.demo.open', mock_on_img, + create=True): + image = await camera.async_get_image(hass, demo_camera.entity_id) + assert mock_on_img.called + assert mock_on_img.call_args_list[0][0][0][-6:] \ + in ['_0.jpg', '_1.jpg', '_2.jpg', '_3.jpg'] + assert image.content == b'ON' + + +async def test_turn_on_state_back_to_streaming(hass, demo_camera): + """After turn on state back to streaming.""" + assert demo_camera.state == STATE_STREAMING + await camera.async_turn_off(hass, demo_camera.entity_id) + await hass.async_block_till_done() + + assert demo_camera.state == STATE_IDLE + + await camera.async_turn_on(hass, demo_camera.entity_id) + await hass.async_block_till_done() + + assert demo_camera.state == STATE_STREAMING + + +async def test_turn_off_image(hass, demo_camera): + """After turn off, Demo camera raise error.""" + await camera.async_turn_off(hass, demo_camera.entity_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError) as error: + await camera.async_get_image(hass, demo_camera.entity_id) + assert error.args[0] == 'Camera is off' + + +async def test_turn_off_invalid_camera(hass, demo_camera): + """Turn off non-exist camera should quietly fail.""" + assert demo_camera.state == STATE_STREAMING + await camera.async_turn_off(hass, 'camera.invalid_camera') + await hass.async_block_till_done() + + assert demo_camera.state == STATE_STREAMING + + +async def test_turn_off_unsupport_camera(hass, demo_camera): + """Turn off unsupported camera should quietly fail.""" + assert demo_camera.state == STATE_STREAMING + with patch('homeassistant.components.camera.demo.DemoCamera' + '.supported_features', new_callable=PropertyMock) as m: + m.return_value = 0 + + await camera.async_turn_off(hass, demo_camera.entity_id) + await hass.async_block_till_done() + + assert demo_camera.state == STATE_STREAMING + + +async def test_motion_detection(hass): """Test motion detection services.""" # Setup platform - yield from async_setup_component(hass, 'camera', { + await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'demo' } @@ -20,7 +95,7 @@ def test_motion_detection(hass): # Call service to turn on motion detection camera.enable_motion_detection(hass, 'camera.demo_camera') - yield from hass.async_block_till_done() + await hass.async_block_till_done() # Check if state has been updated. state = hass.states.get('camera.demo_camera') diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index d0f1425a595..cf902ca1779 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -30,7 +30,7 @@ def mock_camera(hass): yield -class TestSetupCamera(object): +class TestSetupCamera: """Test class for setup camera.""" def setup_method(self): @@ -53,7 +53,7 @@ class TestSetupCamera(object): setup_component(self.hass, camera.DOMAIN, config) -class TestGetImage(object): +class TestGetImage: """Test class for camera.""" def setup_method(self): diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 2de0782fd91..18292d32a02 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -45,8 +45,7 @@ class TestUVCSetup(unittest.TestCase): """Create a mock camera.""" if uuid == 'id3': return {'model': 'airCam'} - else: - return {'model': 'UVC'} + return {'model': 'UVC'} mock_remote.return_value.index.return_value = mock_cameras mock_remote.return_value.get_camera.side_effect = mock_get_camera diff --git a/tests/components/climate/test_honeywell.py b/tests/components/climate/test_honeywell.py index b12c0c38f3a..69df11715e9 100644 --- a/tests/components/climate/test_honeywell.py +++ b/tests/components/climate/test_honeywell.py @@ -320,7 +320,7 @@ class TestHoneywellRound(unittest.TestCase): self.device.set_temperature.call_args, mock.call('House', 25) ) - def test_set_operation_mode(self: unittest.TestCase) -> None: + def test_set_operation_mode(self) -> None: """Test setting the system operation.""" self.round1.set_operation_mode('cool') self.assertEqual('cool', self.round1.current_operation) @@ -384,7 +384,7 @@ class TestHoneywellUS(unittest.TestCase): self.assertEqual(74, self.device.setpoint_cool) self.assertEqual(74, self.honeywell.target_temperature) - def test_set_operation_mode(self: unittest.TestCase) -> None: + def test_set_operation_mode(self) -> None: """Test setting the operation mode.""" self.honeywell.set_operation_mode('cool') self.assertEqual('cool', self.device.system_mode) diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 1591b8da1d2..559f29372de 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -54,8 +54,8 @@ async def test_get_entity(hass, client): } -async def test_update_entity(hass, client): - """Test get entry.""" +async def test_update_entity_name(hass, client): + """Test updating entity name.""" mock_registry(hass, { 'test_domain.world': RegistryEntry( entity_id='test_domain.world', @@ -92,7 +92,7 @@ async def test_update_entity(hass, client): async def test_update_entity_no_changes(hass, client): - """Test get entry.""" + """Test update entity with no changes.""" mock_registry(hass, { 'test_domain.world': RegistryEntry( entity_id='test_domain.world', @@ -129,7 +129,7 @@ async def test_update_entity_no_changes(hass, client): async def test_get_nonexisting_entity(client): - """Test get entry.""" + """Test get entry with nonexisting entity.""" await client.send_json({ 'id': 6, 'type': 'config/entity_registry/get', @@ -141,7 +141,7 @@ async def test_get_nonexisting_entity(client): async def test_update_nonexisting_entity(client): - """Test get entry.""" + """Test update a nonexisting entity.""" await client.send_json({ 'id': 6, 'type': 'config/entity_registry/update', @@ -151,3 +151,37 @@ async def test_update_nonexisting_entity(client): msg = await client.receive_json() assert not msg['success'] + + +async def test_update_entity_id(hass, client): + """Test update entity id.""" + mock_registry(hass, { + 'test_domain.world': RegistryEntry( + entity_id='test_domain.world', + unique_id='1234', + # Using component.async_add_entities is equal to platform "domain" + platform='test_platform', + ) + }) + platform = MockEntityPlatform(hass) + entity = MockEntity(unique_id='1234') + await platform.async_add_entities([entity]) + + assert hass.states.get('test_domain.world') is not None + + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/update', + 'entity_id': 'test_domain.world', + 'new_entity_id': 'test_domain.planet', + }) + + msg = await client.receive_json() + + assert msg['result'] == { + 'entity_id': 'test_domain.planet', + 'name': None + } + + assert hass.states.get('test_domain.world') is None + assert hass.states.get('test_domain.planet') is not None diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 672bafeaf28..8aae5c0a28b 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -367,3 +367,192 @@ def test_save_config(hass, client): result = yield from resp.json() assert network.write_config.called assert result == {'message': 'Z-Wave configuration saved to file.'} + + +async def test_get_protection_values(hass, client): + """Test getting protection values on node.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=18, + command_classes=[const.COMMAND_CLASS_PROTECTION]) + value = MockValue( + value_id=123456, + index=0, + instance=1, + command_class=const.COMMAND_CLASS_PROTECTION) + value.label = 'Protection Test' + value.data_items = ['Unprotected', 'Protection by Sequence', + 'No Operation Possible'] + value.data = 'Unprotected' + network.nodes = {18: node} + node.value = value + + node.get_protection_item.return_value = "Unprotected" + node.get_protection_items.return_value = value.data_items + node.get_protections.return_value = {value.value_id: 'Object'} + + resp = await client.get('/api/zwave/protection/18') + + assert resp.status == 200 + result = await resp.json() + assert node.get_protections.called + assert node.get_protection_item.called + assert node.get_protection_items.called + assert result == { + 'value_id': '123456', + 'selected': 'Unprotected', + 'options': ['Unprotected', 'Protection by Sequence', + 'No Operation Possible'] + } + + +async def test_get_protection_values_nonexisting_node(hass, client): + """Test getting protection values on node with wrong nodeid.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=18, + command_classes=[const.COMMAND_CLASS_PROTECTION]) + value = MockValue( + value_id=123456, + index=0, + instance=1, + command_class=const.COMMAND_CLASS_PROTECTION) + value.label = 'Protection Test' + value.data_items = ['Unprotected', 'Protection by Sequence', + 'No Operation Possible'] + value.data = 'Unprotected' + network.nodes = {17: node} + node.value = value + + resp = await client.get('/api/zwave/protection/18') + + assert resp.status == 404 + result = await resp.json() + assert not node.get_protections.called + assert not node.get_protection_item.called + assert not node.get_protection_items.called + assert result == {'message': 'Node not found'} + + +async def test_get_protection_values_without_protectionclass(hass, client): + """Test getting protection values on node without protectionclass.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=18) + value = MockValue( + value_id=123456, + index=0, + instance=1) + network.nodes = {18: node} + node.value = value + + resp = await client.get('/api/zwave/protection/18') + + assert resp.status == 200 + result = await resp.json() + assert not node.get_protections.called + assert not node.get_protection_item.called + assert not node.get_protection_items.called + assert result == {} + + +async def test_set_protection_value(hass, client): + """Test setting protection value on node.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=18, + command_classes=[const.COMMAND_CLASS_PROTECTION]) + value = MockValue( + value_id=123456, + index=0, + instance=1, + command_class=const.COMMAND_CLASS_PROTECTION) + value.label = 'Protection Test' + value.data_items = ['Unprotected', 'Protection by Sequence', + 'No Operation Possible'] + value.data = 'Unprotected' + network.nodes = {18: node} + node.value = value + + resp = await client.post( + '/api/zwave/protection/18', data=json.dumps({ + 'value_id': '123456', 'selection': 'Protection by Sequence'})) + + assert resp.status == 200 + result = await resp.json() + assert node.set_protection.called + assert result == {'message': 'Protection setting succsessfully set'} + + +async def test_set_protection_value_failed(hass, client): + """Test setting protection value failed on node.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=18, + command_classes=[const.COMMAND_CLASS_PROTECTION]) + value = MockValue( + value_id=123456, + index=0, + instance=1, + command_class=const.COMMAND_CLASS_PROTECTION) + value.label = 'Protection Test' + value.data_items = ['Unprotected', 'Protection by Sequence', + 'No Operation Possible'] + value.data = 'Unprotected' + network.nodes = {18: node} + node.value = value + node.set_protection.return_value = False + + resp = await client.post( + '/api/zwave/protection/18', data=json.dumps({ + 'value_id': '123456', 'selection': 'Protecton by Seuence'})) + + assert resp.status == 202 + result = await resp.json() + assert node.set_protection.called + assert result == {'message': 'Protection setting did not complete'} + + +async def test_set_protection_value_nonexisting_node(hass, client): + """Test setting protection value on nonexisting node.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=17, + command_classes=[const.COMMAND_CLASS_PROTECTION]) + value = MockValue( + value_id=123456, + index=0, + instance=1, + command_class=const.COMMAND_CLASS_PROTECTION) + value.label = 'Protection Test' + value.data_items = ['Unprotected', 'Protection by Sequence', + 'No Operation Possible'] + value.data = 'Unprotected' + network.nodes = {17: node} + node.value = value + node.set_protection.return_value = False + + resp = await client.post( + '/api/zwave/protection/18', data=json.dumps({ + 'value_id': '123456', 'selection': 'Protecton by Seuence'})) + + assert resp.status == 404 + result = await resp.json() + assert not node.set_protection.called + assert result == {'message': 'Node not found'} + + +async def test_set_protection_value_missing_class(hass, client): + """Test setting protection value on node without protectionclass.""" + network = hass.data[DATA_NETWORK] = MagicMock() + node = MockNode(node_id=17) + value = MockValue( + value_id=123456, + index=0, + instance=1) + network.nodes = {17: node} + node.value = value + node.set_protection.return_value = False + + resp = await client.post( + '/api/zwave/protection/17', data=json.dumps({ + 'value_id': '123456', 'selection': 'Protecton by Seuence'})) + + assert resp.status == 404 + result = await resp.json() + assert not node.set_protection.called + assert result == {'message': 'No protection commandclass on this node'} diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 1cee08feb0a..8f5342de1e3 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -91,7 +91,7 @@ async def test_setup_entry_successful(hass): """Test setup entry is successful.""" entry = Mock() entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - with patch.object(hass, 'async_add_job') as mock_add_job, \ + with patch.object(hass, 'async_create_task') as mock_add_job, \ patch.object(hass, 'config_entries') as mock_config_entries, \ patch('pydeconz.DeconzSession.async_load_parameters', return_value=mock_coro(True)): diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index cce39ce43a7..0c20350a845 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -22,9 +22,9 @@ def mock_session_response(*args, **kwargs): # Password: bar if args[0].headers['Authorization'] != 'Basic Zm9vOmJhcg==': return MockSessionResponse(None, 401) - elif "gimmie_bad_data" in args[0].body: + if "gimmie_bad_data" in args[0].body: return MockSessionResponse('This shouldn\'t (wldev = be here.;', 200) - elif "gimmie_good_data" in args[0].body: + if "gimmie_good_data" in args[0].body: return MockSessionResponse( "wldev = [ ['eth1','F4:F5:D8:AA:AA:AA'," "-42,5500,1000,7043,0],['eth1','58:EF:68:00:00:00'," diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index e45d70bc172..6294ba3467a 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -33,7 +33,7 @@ def mock_load_config(): yield -class TestUPCConnect(object): +class TestUPCConnect: """Tests for the Ddwrt device tracker platform.""" def setup_method(self): diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index bdd921f395f..0705fb2c399 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -55,21 +55,21 @@ def mocked_requests(*args, **kwargs): "code": "401", "msg": "Invalid token" }, 200) - elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: + if data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: # deliver an expired token return MockResponse({ "url": "/cgi-bin/luci/;stok=ef5860/web/home", "token": "timedOut", "code": "0" }, 200) - elif str(args[0]).startswith(URL_AUTHORIZE): + if str(args[0]).startswith(URL_AUTHORIZE): # deliver an authorized token return MockResponse({ "url": "/cgi-bin/luci/;stok=ef5860/web/home", "token": "ef5860", "code": "0" }, 200) - elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \ + if str(args[0]).endswith("timedOut/" + URL_LIST_END) \ and FIRST_CALL is True: FIRST_CALL = False # deliver an error when called with expired token @@ -77,7 +77,7 @@ def mocked_requests(*args, **kwargs): "code": "401", "msg": "Invalid token" }, 200) - elif str(args[0]).endswith(URL_LIST_END): + if str(args[0]).endswith(URL_LIST_END): # deliver the device list return MockResponse({ "mac": "1C:98:EC:0E:D5:A4", @@ -149,8 +149,7 @@ def mocked_requests(*args, **kwargs): ], "code": 0 }, 200) - else: - _LOGGER.debug('UNKNOWN ROUTE') + _LOGGER.debug('UNKNOWN ROUTE') class TestXiaomiDeviceScanner(unittest.TestCase): diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 555802f9a2c..8315de34e06 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -89,7 +89,7 @@ class TestEmulatedHue(unittest.TestCase): # Make sure the XML is parsable try: ET.fromstring(result.text) - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except self.fail('description.xml is not valid XML!') def test_create_username(self): diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 4a950910809..dfa67f48614 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -71,7 +71,7 @@ def test_frontend_and_static(mock_http_client): # Test we can retrieve frontend.js frontendjs = re.search( - r'(?P\/frontend_es5\/app-[A-Za-z0-9]{32}.js)', text) + r'(?P\/frontend_es5\/app-[A-Za-z0-9]{8}.js)', text) assert frontendjs is not None resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) @@ -226,7 +226,7 @@ def test_extra_urls(mock_http_client_with_urls): resp = yield from mock_http_client_with_urls.get('/states?latest') assert resp.status == 200 text = yield from resp.text() - assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 + assert text.find("href='https://domain.com/my_extra_url.html'") >= 0 @asyncio.coroutine @@ -235,7 +235,7 @@ def test_extra_urls_es5(mock_http_client_with_urls): resp = yield from mock_http_client_with_urls.get('/states?es5') assert resp.status == 200 text = yield from resp.text() - assert text.find('href="https://domain.com/my_extra_url_es5.html"') >= 0 + assert text.find("href='https://domain.com/my_extra_url_es5.html'") >= 0 async def test_get_panels(hass, hass_ws_client): diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 31ad70e8aba..a5e9bbc0b82 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -365,8 +365,10 @@ class TestComponentsGroup(unittest.TestCase): 'icon': 'mdi:work', 'view': True, }}}): - group.reload(self.hass) - self.hass.block_till_done() + with patch('homeassistant.config.find_config_file', + return_value=''): + group.reload(self.hass) + self.hass.block_till_done() assert sorted(self.hass.states.entity_ids()) == \ ['group.all_tests', 'group.hello'] diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f67a6cbccec..b1975669731 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -3,8 +3,11 @@ import asyncio import os from unittest.mock import patch, Mock +import pytest + from homeassistant.setup import async_setup_component -from homeassistant.components.hassio import async_check_config +from homeassistant.components.hassio import ( + STORAGE_KEY, async_check_config) from tests.common import mock_coro @@ -15,20 +18,28 @@ MOCK_ENVIRON = { } -@asyncio.coroutine -def test_setup_api_ping(hass, aioclient_mock): - """Test setup with API ping.""" +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock): + """Mock all setup requests.""" + aioclient_mock.post( + "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) + +@asyncio.coroutine +def test_setup_api_ping(hass, aioclient_mock): + """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', {}) assert result - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 assert hass.components.hassio.get_homeassistant_version() == "10.0" assert hass.components.hassio.is_hassio() @@ -36,14 +47,6 @@ def test_setup_api_ping(hass, aioclient_mock): @asyncio.coroutine def test_setup_api_push_api_data(hass, aioclient_mock): """Test setup with API push.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { @@ -64,14 +67,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock): @asyncio.coroutine def test_setup_api_push_api_data_server_host(hass, aioclient_mock): """Test setup with API push with active server host.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { @@ -90,19 +85,12 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): assert not aioclient_mock.mock_calls[1][2]['watchdog'] -@asyncio.coroutine -def test_setup_api_push_api_data_default(hass, aioclient_mock): +async def test_setup_api_push_api_data_default(hass, aioclient_mock, + hass_storage): """Test setup with API push default data.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - - with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component(hass, 'hassio', { + with patch.dict(os.environ, MOCK_ENVIRON), \ + patch('homeassistant.auth.AuthManager.active', return_value=True): + result = await async_setup_component(hass, 'hassio', { 'http': {}, 'hassio': {} }) @@ -112,19 +100,61 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): assert not aioclient_mock.mock_calls[1][2]['ssl'] assert aioclient_mock.mock_calls[1][2]['password'] is None assert aioclient_mock.mock_calls[1][2]['port'] == 8123 + refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token'] + hassio_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]['data']['hassio_user'] + ) + assert hassio_user is not None + assert hassio_user.system_generated + assert refresh_token in hassio_user.refresh_tokens + + +async def test_setup_api_push_api_data_no_auth(hass, aioclient_mock, + hass_storage): + """Test setup with API push default data.""" + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component(hass, 'hassio', { + 'http': {}, + 'hassio': {} + }) + assert result + + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] is None + assert aioclient_mock.mock_calls[1][2]['port'] == 8123 + assert aioclient_mock.mock_calls[1][2]['refresh_token'] is None + + +async def test_setup_api_existing_hassio_user(hass, aioclient_mock, + hass_storage): + """Test setup with API push default data.""" + user = await hass.auth.async_create_system_user('Hass.io test') + token = await hass.auth.async_create_refresh_token(user) + hass_storage[STORAGE_KEY] = { + 'version': 1, + 'data': { + 'hassio_user': user.id + } + } + with patch.dict(os.environ, MOCK_ENVIRON), \ + patch('homeassistant.auth.AuthManager.active', return_value=True): + result = await async_setup_component(hass, 'hassio', { + 'http': {}, + 'hassio': {} + }) + assert result + + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] is None + assert aioclient_mock.mock_calls[1][2]['port'] == 8123 + assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token @asyncio.coroutine def test_setup_core_push_timezone(hass, aioclient_mock): """Test setup with API push default data.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, @@ -134,21 +164,13 @@ def test_setup_core_push_timezone(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 3 - assert aioclient_mock.mock_calls[1][2]['timezone'] == "testzone" + assert aioclient_mock.call_count == 4 + assert aioclient_mock.mock_calls[2][2]['timezone'] == "testzone" @asyncio.coroutine def test_setup_hassio_no_additional_data(hass, aioclient_mock): """Test setup with API push default data.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON), \ patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}): result = yield from async_setup_component(hass, 'hassio', { @@ -156,7 +178,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456" @@ -234,14 +256,14 @@ def test_service_calls(hassio_env, hass, aioclient_mock): 'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'}) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 4 + assert aioclient_mock.call_count == 5 assert aioclient_mock.mock_calls[-1][2] == 'test' yield from hass.services.async_call('hassio', 'host_shutdown', {}) yield from hass.services.async_call('hassio', 'host_reboot', {}) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 yield from hass.services.async_call('hassio', 'snapshot_full', {}) yield from hass.services.async_call('hassio', 'snapshot_partial', { @@ -251,7 +273,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): }) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 8 + assert aioclient_mock.call_count == 9 assert aioclient_mock.mock_calls[-1][2] == { 'addons': ['test'], 'folders': ['ssl'], 'password': "123456"} @@ -267,7 +289,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): }) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 11 assert aioclient_mock.mock_calls[-1][2] == { 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False, 'password': "123456" @@ -289,17 +311,17 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock): yield from hass.services.async_call('homeassistant', 'stop') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 yield from hass.services.async_call('homeassistant', 'check_config') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 yield from hass.services.async_call('homeassistant', 'restart') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 4 + assert aioclient_mock.call_count == 5 @asyncio.coroutine diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 628c5405eaa..ab2e3be11d6 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -12,7 +12,7 @@ from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component) -class TestSetupImageProcessing(object): +class TestSetupImageProcessing: """Test class for setup image processing.""" def setup_method(self): @@ -48,7 +48,7 @@ class TestSetupImageProcessing(object): assert self.hass.services.has_service(ip.DOMAIN, 'scan') -class TestImageProcessing(object): +class TestImageProcessing: """Test class for image processing.""" def setup_method(self): @@ -109,7 +109,7 @@ class TestImageProcessing(object): assert state.state == '0' -class TestImageProcessingAlpr(object): +class TestImageProcessingAlpr: """Test class for alpr image processing.""" def setup_method(self): @@ -211,7 +211,7 @@ class TestImageProcessingAlpr(object): assert event_data[0]['entity_id'] == 'image_processing.demo_alpr' -class TestImageProcessingFace(object): +class TestImageProcessingFace: """Test class for face image processing.""" def setup_method(self): diff --git a/tests/components/image_processing/test_microsoft_face_detect.py b/tests/components/image_processing/test_microsoft_face_detect.py index b743dee9704..acc2519c9b7 100644 --- a/tests/components/image_processing/test_microsoft_face_detect.py +++ b/tests/components/image_processing/test_microsoft_face_detect.py @@ -11,7 +11,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) -class TestMicrosoftFaceDetectSetup(object): +class TestMicrosoftFaceDetectSetup: """Test class for image processing.""" def setup_method(self): @@ -74,7 +74,7 @@ class TestMicrosoftFaceDetectSetup(object): assert self.hass.states.get('image_processing.test_local') -class TestMicrosoftFaceDetect(object): +class TestMicrosoftFaceDetect: """Test class for image processing.""" def setup_method(self): diff --git a/tests/components/image_processing/test_microsoft_face_identify.py b/tests/components/image_processing/test_microsoft_face_identify.py index c2ab5684ed0..8797f661767 100644 --- a/tests/components/image_processing/test_microsoft_face_identify.py +++ b/tests/components/image_processing/test_microsoft_face_identify.py @@ -11,7 +11,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) -class TestMicrosoftFaceIdentifySetup(object): +class TestMicrosoftFaceIdentifySetup: """Test class for image processing.""" def setup_method(self): @@ -75,7 +75,7 @@ class TestMicrosoftFaceIdentifySetup(object): assert self.hass.states.get('image_processing.test_local') -class TestMicrosoftFaceIdentify(object): +class TestMicrosoftFaceIdentify: """Test class for image processing.""" def setup_method(self): diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index 50060e08a4b..65e735a6f7e 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -12,7 +12,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) -class TestOpenAlprCloudSetup(object): +class TestOpenAlprCloudSetup: """Test class for image processing.""" def setup_method(self): @@ -103,7 +103,7 @@ class TestOpenAlprCloudSetup(object): setup_component(self.hass, ip.DOMAIN, config) -class TestOpenAlprCloud(object): +class TestOpenAlprCloud: """Test class for image processing.""" def setup_method(self): diff --git a/tests/components/image_processing/test_openalpr_local.py b/tests/components/image_processing/test_openalpr_local.py index fc40f8e17fb..38e94166c5a 100644 --- a/tests/components/image_processing/test_openalpr_local.py +++ b/tests/components/image_processing/test_openalpr_local.py @@ -26,7 +26,7 @@ def mock_async_subprocess(): return async_popen -class TestOpenAlprLocalSetup(object): +class TestOpenAlprLocalSetup: """Test class for image processing.""" def setup_method(self): @@ -96,7 +96,7 @@ class TestOpenAlprLocalSetup(object): setup_component(self.hass, ip.DOMAIN, config) -class TestOpenAlprLocal(object): +class TestOpenAlprLocal: """Test class for image processing.""" def setup_method(self): diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index 26b949720d9..901535c5465 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -200,21 +200,24 @@ async def test_effect_list(hass): }}) hass.states.async_set('light.test1', 'on', - {'effect_list': ['None', 'Random', 'Colorloop']}) + {'effect_list': ['None', 'Random', 'Colorloop'], + 'supported_features': 4}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert set(state.attributes['effect_list']) == { 'None', 'Random', 'Colorloop'} hass.states.async_set('light.test2', 'on', - {'effect_list': ['None', 'Random', 'Rainbow']}) + {'effect_list': ['None', 'Random', 'Rainbow'], + 'supported_features': 4}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert set(state.attributes['effect_list']) == { 'None', 'Random', 'Colorloop', 'Rainbow'} hass.states.async_set('light.test1', 'off', - {'effect_list': ['None', 'Colorloop', 'Seven']}) + {'effect_list': ['None', 'Colorloop', 'Seven'], + 'supported_features': 4}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert set(state.attributes['effect_list']) == { @@ -229,27 +232,27 @@ async def test_effect(hass): }}) hass.states.async_set('light.test1', 'on', - {'effect': 'None', 'supported_features': 2}) + {'effect': 'None', 'supported_features': 6}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'None' hass.states.async_set('light.test2', 'on', - {'effect': 'None', 'supported_features': 2}) + {'effect': 'None', 'supported_features': 6}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'None' hass.states.async_set('light.test3', 'on', - {'effect': 'Random', 'supported_features': 2}) + {'effect': 'Random', 'supported_features': 6}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'None' hass.states.async_set('light.test1', 'off', - {'effect': 'None', 'supported_features': 2}) + {'effect': 'None', 'supported_features': 6}) hass.states.async_set('light.test2', 'off', - {'effect': 'None', 'supported_features': 2}) + {'effect': 'None', 'supported_features': 6}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'Random' diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index a1e3867f9c3..db8d7e5f1e1 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -182,7 +182,7 @@ def mock_bridge(hass): if path == 'lights': return bridge.mock_light_responses.popleft() - elif path == 'groups': + if path == 'groups': return bridge.mock_group_responses.popleft() return None diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 634e3774b8a..4d779eef461 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,14 +1,16 @@ """The tests for the Light component.""" # pylint: disable=protected-access import unittest +import unittest.mock as mock import os +from io import StringIO -from homeassistant.setup import setup_component -import homeassistant.loader as loader +from homeassistant import core, loader +from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_SUPPORTED_FEATURES) -import homeassistant.components.light as light +from homeassistant.components import light from homeassistant.helpers.intent import IntentHandleError from tests.common import ( @@ -308,6 +310,82 @@ class TestLight(unittest.TestCase): light.ATTR_BRIGHTNESS: 100 }, data) + def test_default_profiles_group(self): + """Test default turn-on light profile for all lights.""" + platform = loader.get_component(self.hass, 'light.test') + platform.init() + + user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) + real_isfile = os.path.isfile + real_open = open + + def _mock_isfile(path): + if path == user_light_file: + return True + return real_isfile(path) + + def _mock_open(path): + if path == user_light_file: + return StringIO(profile_data) + return real_open(path) + + profile_data = "id,x,y,brightness\n" +\ + "group.all_lights.default,.4,.6,99\n" + with mock.patch('os.path.isfile', side_effect=_mock_isfile): + with mock.patch('builtins.open', side_effect=_mock_open): + self.assertTrue(setup_component( + self.hass, light.DOMAIN, + {light.DOMAIN: {CONF_PLATFORM: 'test'}} + )) + + dev, _, _ = platform.DEVICES + light.turn_on(self.hass, dev.entity_id) + self.hass.block_till_done() + _, data = dev.last_call('turn_on') + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 99 + }, data) + + def test_default_profiles_light(self): + """Test default turn-on light profile for a specific light.""" + platform = loader.get_component(self.hass, 'light.test') + platform.init() + + user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) + real_isfile = os.path.isfile + real_open = open + + def _mock_isfile(path): + if path == user_light_file: + return True + return real_isfile(path) + + def _mock_open(path): + if path == user_light_file: + return StringIO(profile_data) + return real_open(path) + + profile_data = "id,x,y,brightness\n" +\ + "group.all_lights.default,.3,.5,200\n" +\ + "light.ceiling_2.default,.6,.6,100\n" + with mock.patch('os.path.isfile', side_effect=_mock_isfile): + with mock.patch('builtins.open', side_effect=_mock_open): + self.assertTrue(setup_component( + self.hass, light.DOMAIN, + {light.DOMAIN: {CONF_PLATFORM: 'test'}} + )) + + dev = next(filter(lambda x: x.entity_id == 'light.ceiling_2', + platform.DEVICES)) + light.turn_on(self.hass, dev.entity_id) + self.hass.block_till_done() + _, data = dev.last_call('turn_on') + self.assertEqual({ + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 100 + }, data) + async def test_intent_set_color(hass): """Test the set color intent.""" @@ -397,3 +475,24 @@ async def test_intent_set_color_and_brightness(hass): assert call.data.get(ATTR_ENTITY_ID) == 'light.hello_2' assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 + + +async def test_light_context(hass): + """Test that light context works.""" + assert await async_setup_component(hass, 'light', { + 'light': { + 'platform': 'test' + } + }) + + state = hass.states.get('light.ceiling') + assert state is not None + + await hass.services.async_call('light', 'toggle', { + 'entity_id': state.entity_id, + }, True, core.Context(user_id='abcd')) + + state2 = hass.states.get('light.ceiling') + assert state2 is not None + assert state.state != state2.state + assert state2.context.user_id == 'abcd' diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 7d6dd65e90a..404d60c0a2e 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -415,6 +415,12 @@ class TestLightMQTT(unittest.TestCase): 'name': 'test', 'state_topic': 'test_light_rgb/status', 'command_topic': 'test_light_rgb/set', + 'brightness_command_topic': 'test_light_rgb/brightness/set', + 'rgb_command_topic': 'test_light_rgb/rgb/set', + 'color_temp_command_topic': 'test_light_rgb/color_temp/set', + 'effect_command_topic': 'test_light_rgb/effect/set', + 'white_value_command_topic': 'test_light_rgb/white_value/set', + 'xy_command_topic': 'test_light_rgb/xy/set', 'brightness_state_topic': 'test_light_rgb/brightness/status', 'color_temp_state_topic': 'test_light_rgb/color_temp/status', 'effect_state_topic': 'test_light_rgb/effect/status', @@ -475,6 +481,7 @@ class TestLightMQTT(unittest.TestCase): 'effect_command_topic': 'test_light_rgb/effect/set', 'white_value_command_topic': 'test_light_rgb/white_value/set', 'xy_command_topic': 'test_light_rgb/xy/set', + 'effect_list': ['colorloop', 'random'], 'qos': 2, 'payload_on': 'on', 'payload_off': 'off' diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index e1c3da50e7e..1cf09f2ccb5 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -228,6 +228,8 @@ class TestLightMQTTTemplate(unittest.TestCase): '{{ green|d }}-' '{{ blue|d }}', 'command_off_template': 'off', + 'effect_list': ['colorloop', 'random'], + 'effect_command_topic': 'test_light_rgb/effect/set', 'qos': 2 } }) diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index 7c85775949c..550bfe88a61 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -25,7 +25,7 @@ class AttrDict(dict): return self[item] -class MockBlackbird(object): +class MockBlackbird: """Mock for pyblackbird object.""" def __init__(self): diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index 399cdc67ca6..14e1769047a 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -27,7 +27,7 @@ class AttrDict(dict): return self[item] -class MockMonoprice(object): +class MockMonoprice: """Mock for pymonoprice object.""" def __init__(self): diff --git a/tests/components/media_player/test_yamaha.py b/tests/components/media_player/test_yamaha.py index e17241485db..980284737a2 100644 --- a/tests/components/media_player/test_yamaha.py +++ b/tests/components/media_player/test_yamaha.py @@ -15,7 +15,7 @@ def _create_zone_mock(name, url): return zone -class FakeYamahaDevice(object): +class FakeYamahaDevice: """A fake Yamaha device.""" def __init__(self, ctrl_url, name, zones=None): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index ed6c77f676c..9e0ef14a3fa 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -124,6 +124,28 @@ def test_discover_climate(hass, mqtt_mock, caplog): assert ('climate', 'bla') in hass.data[ALREADY_DISCOVERED] +@asyncio.coroutine +def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): + """Test discovering an MQTT alarm control panel component.""" + yield from async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "AlarmControlPanelTest",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message( + hass, 'homeassistant/alarm_control_panel/bla/config', data) + yield from hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.AlarmControlPanelTest') + + assert state is not None + assert state.name == 'AlarmControlPanelTest' + assert ('alarm_control_panel', 'bla') in hass.data[ALREADY_DISCOVERED] + + @asyncio.coroutine def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index a847de51142..8e7ef4348f7 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -26,8 +26,7 @@ class TestNotifyGroup(unittest.TestCase): def mock_get_service(hass, config, discovery_info=None): if config['name'] == 'demo1': return self.service1 - else: - return self.service2 + return self.service2 with assert_setup_component(2), \ patch.object(demo, 'get_service', mock_get_service): diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 318f3c7512c..486300679b7 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -65,7 +65,7 @@ async def mock_client(hass, aiohttp_client, registrations=None): return await aiohttp_client(hass.http.app) -class TestHtml5Notify(object): +class TestHtml5Notify: """Tests for HTML5 notify platform.""" def test_get_service_with_no_json(self): diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py index 31ec5ee7ed7..990414d7713 100644 --- a/tests/components/recorder/models_original.py +++ b/tests/components/recorder/models_original.py @@ -157,7 +157,6 @@ def _process_timestamp(ts): """Process a timestamp into datetime object.""" if ts is None: return None - elif ts.tzinfo is None: + if 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/tests/components/sensor/test_command_line.py b/tests/components/sensor/test_command_line.py index bc073a04c47..808f8cff6a1 100644 --- a/tests/components/sensor/test_command_line.py +++ b/tests/components/sensor/test_command_line.py @@ -1,5 +1,6 @@ """The tests for the Command line sensor platform.""" import unittest +from unittest.mock import patch from homeassistant.helpers.template import Template from homeassistant.components.sensor import command_line @@ -17,11 +18,16 @@ class TestCommandSensorSensor(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() + def update_side_effect(self, data): + """Side effect function for mocking CommandSensorData.update().""" + self.commandline.data = data + def test_setup(self): """Test sensor setup.""" config = {'name': 'Test', 'unit_of_measurement': 'in', - 'command': 'echo 5' + 'command': 'echo 5', + 'command_timeout': 15 } devices = [] @@ -41,11 +47,11 @@ class TestCommandSensorSensor(unittest.TestCase): def test_template(self): """Test command sensor with template.""" - data = command_line.CommandSensorData(self.hass, 'echo 50') + data = command_line.CommandSensorData(self.hass, 'echo 50', 15) entity = command_line.CommandSensor( self.hass, data, 'test', 'in', - Template('{{ value | multiply(0.1) }}', self.hass)) + Template('{{ value | multiply(0.1) }}', self.hass), []) entity.update() self.assertEqual(5, float(entity.state)) @@ -55,7 +61,7 @@ class TestCommandSensorSensor(unittest.TestCase): self.hass.states.set('sensor.test_state', 'Works') data = command_line.CommandSensorData( self.hass, - 'echo {{ states.sensor.test_state.state }}' + 'echo {{ states.sensor.test_state.state }}', 15 ) data.update() @@ -63,7 +69,109 @@ class TestCommandSensorSensor(unittest.TestCase): def test_bad_command(self): """Test bad command.""" - data = command_line.CommandSensorData(self.hass, 'asdfasdf') + data = command_line.CommandSensorData(self.hass, 'asdfasdf', 15) data.update() self.assertEqual(None, data.value) + + def test_update_with_json_attrs(self): + """Test attributes get extracted from a JSON result.""" + data = command_line.CommandSensorData( + self.hass, + ('echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ + \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }'), + 15 + ) + + self.sensor = command_line.CommandSensor(self.hass, data, 'test', + None, None, ['key', + 'another_key', + 'key_three']) + self.sensor.update() + self.assertEqual('some_json_value', + self.sensor.device_state_attributes['key']) + self.assertEqual('another_json_value', + self.sensor.device_state_attributes['another_key']) + self.assertEqual('value_three', + self.sensor.device_state_attributes['key_three']) + + @patch('homeassistant.components.sensor.command_line._LOGGER') + def test_update_with_json_attrs_no_data(self, mock_logger): + """Test attributes when no JSON result fetched.""" + data = command_line.CommandSensorData( + self.hass, + 'echo ', 15 + ) + self.sensor = command_line.CommandSensor(self.hass, data, 'test', + None, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + + @patch('homeassistant.components.sensor.command_line._LOGGER') + def test_update_with_json_attrs_not_dict(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + data = command_line.CommandSensorData( + self.hass, + 'echo [1, 2, 3]', 15 + ) + self.sensor = command_line.CommandSensor(self.hass, data, 'test', + None, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + + @patch('homeassistant.components.sensor.command_line._LOGGER') + def test_update_with_json_attrs_bad_JSON(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + data = command_line.CommandSensorData( + self.hass, + 'echo This is text rather than JSON data.', 15 + ) + self.sensor = command_line.CommandSensor(self.hass, data, 'test', + None, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + + def test_update_with_missing_json_attrs(self): + """Test attributes get extracted from a JSON result.""" + data = command_line.CommandSensorData( + self.hass, + ('echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ + \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }'), + 15 + ) + + self.sensor = command_line.CommandSensor(self.hass, data, 'test', + None, None, ['key', + 'another_key', + 'key_three', + 'special_key']) + self.sensor.update() + self.assertEqual('some_json_value', + self.sensor.device_state_attributes['key']) + self.assertEqual('another_json_value', + self.sensor.device_state_attributes['another_key']) + self.assertEqual('value_three', + self.sensor.device_state_attributes['key_three']) + self.assertFalse('special_key' in self.sensor.device_state_attributes) + + def test_update_with_unnecessary_json_attrs(self): + """Test attributes get extracted from a JSON result.""" + data = command_line.CommandSensorData( + self.hass, + ('echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ + \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }'), + 15 + ) + + self.sensor = command_line.CommandSensor(self.hass, data, 'test', + None, None, ['key', + 'another_key']) + self.sensor.update() + self.assertEqual('some_json_value', + self.sensor.device_state_attributes['key']) + self.assertEqual('another_json_value', + self.sensor.device_state_attributes['another_key']) + self.assertFalse('key_three' in self.sensor.device_state_attributes) diff --git a/tests/components/sensor/test_moon.py b/tests/components/sensor/test_moon.py index 334dd9a0bec..9086df6e79b 100644 --- a/tests/components/sensor/test_moon.py +++ b/tests/components/sensor/test_moon.py @@ -37,7 +37,7 @@ class TestMoonSensor(unittest.TestCase): assert setup_component(self.hass, 'sensor', config) state = self.hass.states.get('sensor.moon_day1') - self.assertEqual(state.state, 'Waxing crescent') + self.assertEqual(state.state, 'waxing_crescent') @patch('homeassistant.components.sensor.moon.dt_util.utcnow', return_value=DAY2) @@ -53,4 +53,4 @@ class TestMoonSensor(unittest.TestCase): assert setup_component(self.hass, 'sensor', config) state = self.hass.states.get('sensor.moon_day2') - self.assertEqual(state.state, 'Waning gibbous') + self.assertEqual(state.state, 'waning_gibbous') diff --git a/tests/components/sensor/test_radarr.py b/tests/components/sensor/test_radarr.py index 94eeafad7b1..0d6aca9d0b7 100644 --- a/tests/components/sensor/test_radarr.py +++ b/tests/components/sensor/test_radarr.py @@ -83,7 +83,7 @@ def mocked_requests_get(*args, **kwargs): "id": 12 } ], 200) - elif 'api/command' in url: + if 'api/command' in url: return MockResponse([ { "name": "RescanMovie", @@ -94,7 +94,7 @@ def mocked_requests_get(*args, **kwargs): "id": 24 } ], 200) - elif 'api/movie' in url: + if 'api/movie' in url: return MockResponse([ { "title": "Assassin's Creed", @@ -149,7 +149,7 @@ def mocked_requests_get(*args, **kwargs): "id": 1 } ], 200) - elif 'api/diskspace' in url: + if 'api/diskspace' in url: return MockResponse([ { "path": "/data", @@ -158,7 +158,7 @@ def mocked_requests_get(*args, **kwargs): "totalSpace": 499738734592 } ], 200) - elif 'api/system/status' in url: + if 'api/system/status' in url: return MockResponse({ "version": "0.2.0.210", "buildTime": "2017-01-22T23:12:49Z", @@ -182,10 +182,9 @@ def mocked_requests_get(*args, **kwargs): "(Stable 4.6.1.3/abb06f1 " "Mon Oct 3 07:57:59 UTC 2016)") }, 200) - else: - return MockResponse({ - "error": "Unauthorized" - }, 401) + return MockResponse({ + "error": "Unauthorized" + }, 401) class TestRadarrSetup(unittest.TestCase): diff --git a/tests/components/sensor/test_sonarr.py b/tests/components/sensor/test_sonarr.py index 9e2050e850c..275bb4a1e8b 100644 --- a/tests/components/sensor/test_sonarr.py +++ b/tests/components/sensor/test_sonarr.py @@ -139,7 +139,7 @@ def mocked_requests_get(*args, **kwargs): "id": 14402 } ], 200) - elif 'api/command' in url: + if 'api/command' in url: return MockResponse([ { "name": "RescanSeries", @@ -150,7 +150,7 @@ def mocked_requests_get(*args, **kwargs): "id": 24 } ], 200) - elif 'api/wanted/missing' in url or 'totalRecords' in url: + if 'api/wanted/missing' in url or 'totalRecords' in url: return MockResponse( { "page": 1, @@ -325,7 +325,7 @@ def mocked_requests_get(*args, **kwargs): } ] }, 200) - elif 'api/queue' in url: + if 'api/queue' in url: return MockResponse([ { "series": { @@ -449,7 +449,7 @@ def mocked_requests_get(*args, **kwargs): "id": 1503378561 } ], 200) - elif 'api/series' in url: + if 'api/series' in url: return MockResponse([ { "title": "Marvel's Daredevil", @@ -540,7 +540,7 @@ def mocked_requests_get(*args, **kwargs): "id": 7 } ], 200) - elif 'api/diskspace' in url: + if 'api/diskspace' in url: return MockResponse([ { "path": "/data", @@ -549,7 +549,7 @@ def mocked_requests_get(*args, **kwargs): "totalSpace": 499738734592 } ], 200) - elif 'api/system/status' in url: + if 'api/system/status' in url: return MockResponse({ "version": "2.0.0.1121", "buildTime": "2014-02-08T20:49:36.5560392Z", @@ -568,10 +568,9 @@ def mocked_requests_get(*args, **kwargs): "startOfWeek": 0, "urlBase": "" }, 200) - else: - return MockResponse({ - "error": "Unauthorized" - }, 401) + return MockResponse({ + "error": "Unauthorized" + }, 401) class TestSonarrSetup(unittest.TestCase): diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index 61e665f265c..155ed85dac2 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -92,8 +92,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -134,8 +133,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -181,8 +179,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -228,8 +225,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -276,8 +272,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -323,8 +318,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -374,8 +368,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -426,8 +419,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -477,8 +469,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -528,8 +519,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -579,8 +569,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -627,8 +616,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -677,8 +665,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -739,9 +726,8 @@ class TestSwitchFlux(unittest.TestCase): if event == 'sunrise': print('sunrise {}'.format(sunrise_time)) return sunrise_time - else: - print('sunset {}'.format(sunset_time)) - return sunset_time + print('sunset {}'.format(sunset_time)) + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -793,8 +779,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', @@ -838,8 +823,7 @@ class TestSwitchFlux(unittest.TestCase): def event_date(hass, event, now=None): if event == 'sunrise': return sunrise_time - else: - return sunset_time + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): with patch('homeassistant.helpers.sun.get_astral_event_date', diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index d679aa2c827..55e44299294 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -2,8 +2,8 @@ # pylint: disable=protected-access import unittest -from homeassistant.setup import setup_component -from homeassistant import loader +from homeassistant.setup import setup_component, async_setup_component +from homeassistant import core, loader from homeassistant.components import switch from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM @@ -91,3 +91,24 @@ class TestSwitch(unittest.TestCase): '{} 2'.format(switch.DOMAIN): {CONF_PLATFORM: 'test2'}, } )) + + +async def test_switch_context(hass): + """Test that switch context works.""" + assert await async_setup_component(hass, 'switch', { + 'switch': { + 'platform': 'test' + } + }) + + state = hass.states.get('switch.ac') + assert state is not None + + await hass.services.async_call('switch', 'toggle', { + 'entity_id': state.entity_id, + }, True, core.Context(user_id='abcd')) + + state2 = hass.states.get('switch.ac') + assert state2 is not None + assert state.state != state2.state + assert state2.context.user_id == 'abcd' diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 31f9a729c53..7cd5a42b4a3 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -249,6 +249,37 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) + def test_custom_state_payload(self): + """Test the state payload.""" + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_on': 1, + 'payload_off': 0, + 'state_on': "HIGH", + 'state_off': "LOW", + } + }) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'state-topic', 'HIGH') + self.hass.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + fire_mqtt_message(self.hass, 'state-topic', 'LOW') + self.hass.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + def test_unique_id(self): """Test unique id option only creates one switch per unique_id.""" assert setup_component(self.hass, switch.DOMAIN, { diff --git a/tests/components/test_api.py b/tests/components/test_api.py index f53010ef27f..09dc27e97c1 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -12,6 +12,8 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component +from tests.common import async_mock_service + @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -429,3 +431,58 @@ async def test_api_error_log(hass, aiohttp_client): assert mock_file.mock_calls[0][1][0] == hass.data[DATA_LOGGING] assert resp.status == 200 assert await resp.text() == 'Hello' + + +async def test_api_fire_event_context(hass, mock_api_client, + hass_access_token): + """Test if the API sets right context if we fire an event.""" + test_value = [] + + @ha.callback + def listener(event): + """Helper method that will verify our event got called.""" + test_value.append(event) + + hass.bus.async_listen("test.event", listener) + + await mock_api_client.post( + const.URL_API_EVENTS_EVENT.format("test.event"), + headers={ + 'authorization': 'Bearer {}'.format(hass_access_token.token) + }) + await hass.async_block_till_done() + + assert len(test_value) == 1 + assert test_value[0].context.user_id == \ + hass_access_token.refresh_token.user.id + + +async def test_api_call_service_context(hass, mock_api_client, + hass_access_token): + """Test if the API sets right context if we call a service.""" + calls = async_mock_service(hass, 'test_domain', 'test_service') + + await mock_api_client.post( + '/api/services/test_domain/test_service', + headers={ + 'authorization': 'Bearer {}'.format(hass_access_token.token) + }) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context.user_id == hass_access_token.refresh_token.user.id + + +async def test_api_set_state_context(hass, mock_api_client, hass_access_token): + """Test if the API sets right context if we set state.""" + await mock_api_client.post( + '/api/states/light.kitchen', + json={ + 'state': 'on' + }, + headers={ + 'authorization': 'Bearer {}'.format(hass_access_token.token) + }) + + state = hass.states.get('light.kitchen') + assert state.context.user_id == hass_access_token.refresh_token.user.id diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py index 5a5fdffd5a3..44c3a1dd695 100644 --- a/tests/components/test_ffmpeg.py +++ b/tests/components/test_ffmpeg.py @@ -38,7 +38,7 @@ class MockFFmpegDev(ffmpeg.FFmpegBase): self.called_entities = entity_ids -class TestFFmpegSetup(object): +class TestFFmpegSetup: """Test class for ffmpeg.""" def setup_method(self): diff --git a/tests/components/test_graphite.py b/tests/components/test_graphite.py index 280704fdc31..892fe5b5f4d 100644 --- a/tests/components/test_graphite.py +++ b/tests/components/test_graphite.py @@ -224,13 +224,12 @@ class TestGraphite(unittest.TestCase): def fake_get(): if len(runs) >= 2: return self.gf._quit_object - elif runs: + if runs: runs.append(1) return mock.MagicMock(event_type='somethingelse', data={'new_event': None}) - else: - runs.append(1) - return event + runs.append(1) + return event with mock.patch.object(self.gf, '_queue') as mock_queue: with mock.patch.object(self.gf, '_report_attributes') as mock_r: diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index 370059a0a09..92f840b8033 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -9,7 +9,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component, mock_coro, load_fixture) -class TestMicrosoftFaceSetup(object): +class TestMicrosoftFaceSetup: """Test the microsoft face component.""" def setup_method(self): diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py index 48bc04d46ed..8da1311c87d 100644 --- a/tests/components/test_mqtt_eventstream.py +++ b/tests/components/test_mqtt_eventstream.py @@ -18,7 +18,7 @@ from tests.common import ( ) -class TestMqttEventStream(object): +class TestMqttEventStream: """Test the MQTT eventstream module.""" def setup_method(self): @@ -104,12 +104,14 @@ class TestMqttEventStream(object): "state": "on", "entity_id": e_id, "attributes": {}, - "last_changed": now.isoformat() + "last_changed": now.isoformat(), } event['event_data'] = {"new_state": new_state, "entity_id": e_id} # Verify that the message received was that expected - assert json.loads(msg) == event + result = json.loads(msg) + result['event_data']['new_state'].pop('context') + assert result == event @patch('homeassistant.components.mqtt.async_publish') def test_time_event_does_not_send_message(self, mock_pub): diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index 2ed2f4487ea..4cf79e679cd 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -12,7 +12,7 @@ from tests.common import ( ) -class TestMqttStateStream(object): +class TestMqttStateStream: """Test the MQTT statestream module.""" def setup_method(self): diff --git a/tests/components/test_plant.py b/tests/components/test_plant.py index ee1372509d9..95167dd181b 100644 --- a/tests/components/test_plant.py +++ b/tests/components/test_plant.py @@ -41,7 +41,7 @@ GOOD_CONFIG = { } -class _MockState(object): +class _MockState: def __init__(self, state=None): self.state = state diff --git a/tests/components/test_rest_command.py b/tests/components/test_rest_command.py index 3ddcfae8c01..097fb799d40 100644 --- a/tests/components/test_rest_command.py +++ b/tests/components/test_rest_command.py @@ -10,7 +10,7 @@ from tests.common import ( get_test_home_assistant, assert_setup_component) -class TestRestCommandSetup(object): +class TestRestCommandSetup: """Test the rest command component.""" def setup_method(self): @@ -47,7 +47,7 @@ class TestRestCommandSetup(object): assert self.hass.services.has_service(rc.DOMAIN, 'test_get') -class TestRestCommandComponent(object): +class TestRestCommandComponent: """Test the rest command component.""" def setup_method(self): diff --git a/tests/components/test_script.py b/tests/components/test_script.py index fcb0047c135..c4282cdfbaf 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -199,8 +199,10 @@ class TestScriptComponent(unittest.TestCase): } }] }}}): - script.reload(self.hass) - self.hass.block_till_done() + with patch('homeassistant.config.find_config_file', + return_value=''): + script.reload(self.hass) + self.hass.block_till_done() assert self.hass.states.get(ENTITY_ID) is None assert not self.hass.services.has_service(script.DOMAIN, 'test') diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index 59e99e5c1b5..5d48fd88127 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -28,7 +28,7 @@ async def get_error_log(hass, aiohttp_client, expected_count): def _generate_and_log_exception(exception, log): try: raise Exception(exception) - except: # noqa: E722 # pylint: disable=bare-except + except: # noqa: E722 pylint: disable=bare-except _LOGGER.exception(log) diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index dc1688bae16..1fac1af9f64 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -10,7 +10,7 @@ from homeassistant.core import callback from homeassistant.components import websocket_api as wapi from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.common import mock_coro, async_mock_service API_PASSWORD = 'test1234' @@ -443,3 +443,94 @@ async def test_auth_with_invalid_token(hass, aiohttp_client): auth_msg = await ws.receive_json() assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + +async def test_call_service_context_with_user(hass, aiohttp_client, + hass_access_token): + """Test that the user is set in the service call context.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + calls = async_mock_service(hass, 'domain_test', 'test_service') + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + await ws.send_json({ + 'id': 5, + 'type': wapi.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await ws.receive_json() + assert msg['success'] + + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + assert call.context.user_id == hass_access_token.refresh_token.user.id + + +async def test_call_service_context_no_user(hass, aiohttp_client): + """Test that connection without user sets context.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + calls = async_mock_service(hass, 'domain_test', 'test_service') + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + await ws.send_json({ + 'id': 5, + 'type': wapi.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await ws.receive_json() + assert msg['success'] + + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + assert call.context.user_id is None diff --git a/tests/components/tts/test_google.py b/tests/components/tts/test_google.py index 6a2d2c65035..cf9a7b2db29 100644 --- a/tests/components/tts/test_google.py +++ b/tests/components/tts/test_google.py @@ -15,7 +15,7 @@ from tests.common import ( from .test_init import mutagen_mock # noqa -class TestTTSGooglePlatform(object): +class TestTTSGooglePlatform: """Test the Google speech component.""" def setup_method(self): diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index b6bfa430fd2..e8746ee762f 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -29,7 +29,7 @@ def mutagen_mock(): yield -class TestTTS(object): +class TestTTS: """Test the Google speech component.""" def setup_method(self): diff --git a/tests/components/tts/test_marytts.py b/tests/components/tts/test_marytts.py index b55236c5e8e..7ec2ae39cd6 100644 --- a/tests/components/tts/test_marytts.py +++ b/tests/components/tts/test_marytts.py @@ -14,7 +14,7 @@ from tests.common import ( from .test_init import mutagen_mock # noqa -class TestTTSMaryTTSPlatform(object): +class TestTTSMaryTTSPlatform: """Test the speech component.""" def setup_method(self): diff --git a/tests/components/tts/test_voicerss.py b/tests/components/tts/test_voicerss.py index 2abdc0e69ff..365cf1ff73b 100644 --- a/tests/components/tts/test_voicerss.py +++ b/tests/components/tts/test_voicerss.py @@ -14,7 +14,7 @@ from tests.common import ( from .test_init import mutagen_mock # noqa -class TestTTSVoiceRSSPlatform(object): +class TestTTSVoiceRSSPlatform: """Test the voicerss speech component.""" def setup_method(self): diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py index 5b4ef4dcf53..82d20318928 100644 --- a/tests/components/tts/test_yandextts.py +++ b/tests/components/tts/test_yandextts.py @@ -13,7 +13,7 @@ from tests.common import ( from .test_init import mutagen_mock # noqa -class TestTTSYandexPlatform(object): +class TestTTSYandexPlatform: """Test the speech component.""" def setup_method(self): diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index e608dcccaba..39abf6f588f 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -163,10 +163,10 @@ def test_zwave_ready_wait(hass, mock_openzwave): asyncio_sleep = asyncio.sleep @asyncio.coroutine - def sleep(duration, loop): + def sleep(duration, loop=None): if duration > 0: sleeps.append(duration) - yield from asyncio_sleep(0, loop=loop) + yield from asyncio_sleep(0) with patch('homeassistant.components.zwave.dt_util.utcnow', new=utcnow): with patch('asyncio.sleep', new=sleep): @@ -248,10 +248,10 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): asyncio_sleep = asyncio.sleep - async def sleep(duration, loop): + async def sleep(duration, loop=None): if duration > 0: sleeps.append(duration) - await asyncio_sleep(0, loop=loop) + await asyncio_sleep(0) with patch('homeassistant.components.zwave.dt_util.utcnow', new=utcnow): with patch('asyncio.sleep', new=sleep): diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 066e7386c6e..55e67def2bc 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import ( from tests.common import get_test_home_assistant -class TestHelpersDispatcher(object): +class TestHelpersDispatcher: """Tests for discovery helper methods.""" def setup_method(self, method): diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 4211e3da31b..e24bec489f4 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -71,7 +71,7 @@ def test_async_update_support(hass): assert len(async_update) == 1 -class TestHelpersEntity(object): +class TestHelpersEntity: """Test homeassistant.helpers.entity module.""" def setup_method(self, method): @@ -400,3 +400,15 @@ def test_async_remove_no_platform(hass): assert len(hass.states.async_entity_ids()) == 1 yield from ent.async_remove() assert len(hass.states.async_entity_ids()) == 0 + + +async def test_async_remove_runs_callbacks(hass): + """Test async_remove method when no platform set.""" + result = [] + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = 'test.test' + ent.async_on_remove(lambda: result.append(1)) + await ent.async_remove() + assert len(result) == 1 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 2d2f148189f..b52405aa8be 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -5,6 +5,8 @@ import unittest from unittest.mock import patch, Mock, MagicMock from datetime import timedelta +import pytest + from homeassistant.exceptions import PlatformNotReady import homeassistant.loader as loader from homeassistant.helpers.entity import generate_entity_id @@ -487,7 +489,7 @@ def test_registry_respect_entity_disabled(hass): assert hass.states.async_entity_ids() == [] -async def test_entity_registry_updates(hass): +async def test_entity_registry_updates_name(hass): """Test that updates on the entity registry update platform entities.""" registry = mock_registry(hass, { 'test_domain.world': entity_registry.RegistryEntry( @@ -602,3 +604,75 @@ def test_not_fails_with_adding_empty_entities_(hass): yield from component.async_add_entities([]) assert len(hass.states.async_entity_ids()) == 0 + + +async def test_entity_registry_updates_entity_id(hass): + """Test that updates on the entity registry update platform entities.""" + registry = mock_registry(hass, { + 'test_domain.world': entity_registry.RegistryEntry( + entity_id='test_domain.world', + unique_id='1234', + # Using component.async_add_entities is equal to platform "domain" + platform='test_platform', + name='Some name' + ) + }) + platform = MockEntityPlatform(hass) + entity = MockEntity(unique_id='1234') + await platform.async_add_entities([entity]) + + state = hass.states.get('test_domain.world') + assert state is not None + assert state.name == 'Some name' + + registry.async_update_entity('test_domain.world', + new_entity_id='test_domain.planet') + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get('test_domain.world') is None + assert hass.states.get('test_domain.planet') is not None + + +async def test_entity_registry_updates_invalid_entity_id(hass): + """Test that we can't update to an invalid entity id.""" + registry = mock_registry(hass, { + 'test_domain.world': entity_registry.RegistryEntry( + entity_id='test_domain.world', + unique_id='1234', + # Using component.async_add_entities is equal to platform "domain" + platform='test_platform', + name='Some name' + ), + 'test_domain.existing': entity_registry.RegistryEntry( + entity_id='test_domain.existing', + unique_id='5678', + platform='test_platform', + ), + }) + platform = MockEntityPlatform(hass) + entity = MockEntity(unique_id='1234') + await platform.async_add_entities([entity]) + + state = hass.states.get('test_domain.world') + assert state is not None + assert state.name == 'Some name' + + with pytest.raises(ValueError): + registry.async_update_entity('test_domain.world', + new_entity_id='test_domain.existing') + + with pytest.raises(ValueError): + registry.async_update_entity('test_domain.world', + new_entity_id='invalid_entity_id') + + with pytest.raises(ValueError): + registry.async_update_entity('test_domain.world', + new_entity_id='diff_domain.world') + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get('test_domain.world') is not None + assert hass.states.get('invalid_entity_id') is None + assert hass.states.get('diff_domain.world') is None diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 6808206243f..5a9efd5c041 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -107,7 +107,8 @@ def test_loading_saving_data(hass, registry): # Ensure same order assert list(registry.entities) == list(registry2.entities) new_entry1 = registry.async_get_or_create('light', 'hue', '1234') - new_entry2 = registry.async_get_or_create('light', 'hue', '5678') + new_entry2 = registry.async_get_or_create('light', 'hue', '5678', + config_entry_id='mock-id') assert orig_entry1 == new_entry1 assert orig_entry2 == new_entry2 @@ -191,3 +192,13 @@ def test_async_get_entity_id(registry): assert registry.async_get_entity_id( 'light', 'hue', '1234') == 'light.hue_1234' assert registry.async_get_entity_id('light', 'hue', '123') is None + + +async def test_updating_config_entry_id(registry): + """Test that we update config entry id in registry.""" + entry = registry.async_get_or_create( + 'light', 'hue', '5678', config_entry_id='mock-id-1') + entry2 = registry.async_get_or_create( + 'light', 'hue', '5678', config_entry_id='mock-id-2') + assert entry.entity_id == entry2.entity_id + assert entry2.config_entry_id == 'mock-id-2' diff --git a/tests/test_core.py b/tests/test_core.py index 7633c820d2d..9de801e0bb4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -277,6 +277,10 @@ class TestEvent(unittest.TestCase): 'data': data, 'origin': 'LOCAL', 'time_fired': now, + 'context': { + 'id': event.context.id, + 'user_id': event.context.user_id, + }, } self.assertEqual(expected, event.as_dict()) @@ -598,18 +602,16 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(1, len(events)) -class TestServiceCall(unittest.TestCase): - """Test ServiceCall class.""" +def test_service_call_repr(): + """Test ServiceCall repr.""" + call = ha.ServiceCall('homeassistant', 'start') + assert str(call) == \ + "".format(call.context.id) - def test_repr(self): - """Test repr method.""" - self.assertEqual( - "", - str(ha.ServiceCall('homeassistant', 'start'))) - - self.assertEqual( - "", - str(ha.ServiceCall('homeassistant', 'start', {"fast": "yes"}))) + call2 = ha.ServiceCall('homeassistant', 'start', {'fast': 'yes'}) + assert str(call2) == \ + "".format( + call2.context.id) class TestServiceRegistry(unittest.TestCase): diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 60b0e68ca59..1f43c5a4b49 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -221,7 +221,7 @@ class TestUtil(unittest.TestCase): def test_throttle_per_instance(self): """Test that the throttle method is done per instance of a class.""" - class Tester(object): + class Tester: """A tester class for the throttle.""" @util.Throttle(timedelta(seconds=1)) @@ -234,7 +234,7 @@ class TestUtil(unittest.TestCase): def test_throttle_on_method(self): """Test that throttle works when wrapping a method.""" - class Tester(object): + class Tester: """A tester class for the throttle.""" def hello(self): @@ -249,7 +249,7 @@ class TestUtil(unittest.TestCase): def test_throttle_on_two_method(self): """Test that throttle works when wrapping two methods.""" - class Tester(object): + class Tester: """A test class for the throttle.""" @util.Throttle(timedelta(seconds=1))